diff --git a/.gitignore b/.gitignore index b6959d0..429a07d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .rumdl_cache +vaguesyntax_quickshell diff --git a/EXAMPLE.md b/EXAMPLE.md index 41e2767..99b82a1 100644 --- a/EXAMPLE.md +++ b/EXAMPLE.md @@ -50,6 +50,14 @@ Path: `~/.config/overzicht/settings.json` "scale": 0.12, "enable": true, "hideEmptyRows": false, + "useWorkspaceMap": false, + "workspaceMap": [], + "orderRightLeft": false, + "orderBottomUp": false, + "previewsEnabled": true, + "previewMode": "live", + "includeInactiveMonitorPreviews": true, + "previewRecaptureDelayMs": 60, "workspaceSpacing": 5, "backgroundPadding": 10, "workspaceNumberBaseSize": 250 @@ -61,7 +69,8 @@ Path: `~/.config/overzicht/settings.json` "inactiveMonitorOpacity": 0.4 }, "hacks": { - "arbitraryRaceConditionDelay": 150 + "arbitraryRaceConditionDelay": 150, + "hyprlandEventDebounceMs": 40 } } ``` diff --git a/README.md b/README.md index d034df4..aa5b7d7 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,18 @@ Optional setting: `overview.hideEmptyRows` (default `false`) hides workspace rows in the active group that contain no windows, while keeping the current workspace row visible. +Additional optional settings: + +- `overview.useWorkspaceMap` +- `overview.workspaceMap` +- `overview.orderRightLeft` +- `overview.orderBottomUp` +- `overview.previewsEnabled` +- `overview.previewMode` +- `overview.includeInactiveMonitorPreviews` +- `overview.previewRecaptureDelayMs` +- `hacks.hyprlandEventDebounceMs` + For full JSON examples of both runtime files, see [`EXAMPLE.md`](EXAMPLE.md). ## NixOS Module diff --git a/common/Config.qml b/common/Config.qml index 7e26433..b7b5619 100644 --- a/common/Config.qml +++ b/common/Config.qml @@ -102,6 +102,14 @@ Singleton { property real scale: root.optionValue(["overview", "scale"], 0.12) property bool enable: root.optionValue(["overview", "enable"], true) property bool hideEmptyRows: root.optionValue(["overview", "hideEmptyRows"], false) + property bool useWorkspaceMap: root.optionValue(["overview", "useWorkspaceMap"], false) + property var workspaceMap: root.optionValue(["overview", "workspaceMap"], []) + property bool orderRightLeft: root.optionValue(["overview", "orderRightLeft"], false) + property bool orderBottomUp: root.optionValue(["overview", "orderBottomUp"], false) + property bool previewsEnabled: root.optionValue(["overview", "previewsEnabled"], true) + property string previewMode: root.optionValue(["overview", "previewMode"], "live") + property bool includeInactiveMonitorPreviews: root.optionValue(["overview", "includeInactiveMonitorPreviews"], true) + property int previewRecaptureDelayMs: root.optionValue(["overview", "previewRecaptureDelayMs"], 60) property real workspaceSpacing: root.optionValue(["overview", "workspaceSpacing"], 5) property real backgroundPadding: root.optionValue(["overview", "backgroundPadding"], 10) property real workspaceNumberBaseSize: root.optionValue(["overview", "workspaceNumberBaseSize"], 250) @@ -116,6 +124,7 @@ Singleton { property QtObject hacks: QtObject { property int arbitraryRaceConditionDelay: root.optionValue(["hacks", "arbitraryRaceConditionDelay"], 150) + property int hyprlandEventDebounceMs: root.optionValue(["hacks", "hyprlandEventDebounceMs"], 40) } } } diff --git a/modules/overview/Overview.qml b/modules/overview/Overview.qml index b9f674d..d454e8c 100644 --- a/modules/overview/Overview.qml +++ b/modules/overview/Overview.qml @@ -18,7 +18,7 @@ Scope { id: root required property var modelData readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + property bool monitorIsFocused: Hyprland.focusedMonitor?.id == monitor?.id screen: modelData visible: GlobalStates.overviewOpen @@ -52,9 +52,8 @@ Scope { Connections { target: GlobalStates function onOverviewOpenChanged() { - if (GlobalStates.overviewOpen) { + if (GlobalStates.overviewOpen) delayedGrabTimer.start(); - } } } @@ -79,66 +78,82 @@ Scope { focus: GlobalStates.overviewOpen Keys.onPressed: event => { - // close: Escape or Enter if (event.key === Qt.Key_Escape || event.key === Qt.Key_Return) { GlobalStates.overviewOpen = false; event.accepted = true; return; } - // Helper: compute current group bounds const workspacesPerGroup = Config.options.overview.rows * Config.options.overview.columns; const currentId = Hyprland.focusedMonitor?.activeWorkspace?.id ?? 1; - const currentGroup = Math.floor((currentId - 1) / workspacesPerGroup); - const minWorkspaceId = currentGroup * workspacesPerGroup + 1; + const useWorkspaceMap = Config.options.overview.useWorkspaceMap; + const workspaceMap = Config.options.overview.workspaceMap ?? []; + const focusedMonitorId = Hyprland.focusedMonitor?.id ?? root.monitor?.id ?? 0; + const workspaceOffset = useWorkspaceMap ? Number(workspaceMap[focusedMonitorId] ?? 0) : 0; + const currentGroup = Math.floor((currentId - workspaceOffset - 1) / workspacesPerGroup); + const minWorkspaceId = currentGroup * workspacesPerGroup + 1 + workspaceOffset; const maxWorkspaceId = minWorkspaceId + workspacesPerGroup - 1; - const currentRow = Math.floor((currentId - minWorkspaceId) / Config.options.overview.columns); - const rowMinId = minWorkspaceId + currentRow * Config.options.overview.columns; - const rowMaxId = rowMinId + Config.options.overview.columns - 1; + const rows = Config.options.overview.rows; + const columns = Config.options.overview.columns; + const reverseColumns = Config.options.overview.orderRightLeft; + const reverseRows = Config.options.overview.orderBottomUp; + + const clampedIndex = Math.max(0, Math.min(workspacesPerGroup - 1, currentId - minWorkspaceId)); + const currentNormalRow = Math.floor(clampedIndex / columns); + const currentNormalColumn = clampedIndex % columns; + + function toVisualRow(normalRow) { + return reverseRows ? rows - normalRow - 1 : normalRow; + } + + function toVisualColumn(normalColumn) { + return reverseColumns ? columns - normalColumn - 1 : normalColumn; + } + + function toNormalRow(visualRow) { + return reverseRows ? rows - visualRow - 1 : visualRow; + } + + function toNormalColumn(visualColumn) { + return reverseColumns ? columns - visualColumn - 1 : visualColumn; + } + + let targetVisualRow = toVisualRow(currentNormalRow); + let targetVisualColumn = toVisualColumn(currentNormalColumn); let targetId = null; - // Arrow keys and vim-style hjkl if (event.key === Qt.Key_Left || event.key === Qt.Key_H) { - targetId = currentId - 1; - if (Config.options.overview.hideEmptyRows) { - if (targetId < rowMinId) - targetId = rowMaxId; - } else if (targetId < minWorkspaceId) - targetId = maxWorkspaceId; + targetVisualColumn = (targetVisualColumn - 1 + columns) % columns; } else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) { - targetId = currentId + 1; - if (Config.options.overview.hideEmptyRows) { - if (targetId > rowMaxId) - targetId = rowMinId; - } else if (targetId > maxWorkspaceId) - targetId = minWorkspaceId; + targetVisualColumn = (targetVisualColumn + 1) % columns; } else if (event.key === Qt.Key_Up || event.key === Qt.Key_K) { - targetId = currentId - Config.options.overview.columns; - if (targetId < minWorkspaceId) - targetId += workspacesPerGroup; + targetVisualRow = (targetVisualRow - 1 + rows) % rows; } else if (event.key === Qt.Key_Down || event.key === Qt.Key_J) { - targetId = currentId + Config.options.overview.columns; - if (targetId > maxWorkspaceId) - targetId -= workspacesPerGroup; - } else - - // Number keys: jump to workspace within the current group - // 1-9 map to positions 1-9, 0 maps to position 10 - if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) { - const position = event.key - Qt.Key_0; // 1-9 - if (position <= workspacesPerGroup) { + targetVisualRow = (targetVisualRow + 1) % rows; + } else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) { + const position = event.key - Qt.Key_0; + if (position <= workspacesPerGroup) targetId = minWorkspaceId + position - 1; - } } else if (event.key === Qt.Key_0) { - // 0 = 10th workspace in the group (if group has 10+ workspaces) - if (workspacesPerGroup >= 10) { - targetId = minWorkspaceId + 9; // 10th position = offset 9 - } + if (workspacesPerGroup >= 10) + targetId = minWorkspaceId + 9; + } + + if (targetId === null && ( + event.key === Qt.Key_Left || event.key === Qt.Key_H || + event.key === Qt.Key_Right || event.key === Qt.Key_L || + event.key === Qt.Key_Up || event.key === Qt.Key_K || + event.key === Qt.Key_Down || event.key === Qt.Key_J + )) { + const targetNormalRow = toNormalRow(targetVisualRow); + const targetNormalColumn = toNormalColumn(targetVisualColumn); + targetId = minWorkspaceId + targetNormalRow * columns + targetNormalColumn; } if (targetId !== null) { - Hyprland.dispatch("workspace " + targetId); + const clampedTarget = Math.max(minWorkspaceId, Math.min(maxWorkspaceId, targetId)); + Hyprland.dispatch("workspace " + clampedTarget); event.accepted = true; } } diff --git a/modules/overview/OverviewWidget.qml b/modules/overview/OverviewWidget.qml index 9473667..88a38b8 100644 --- a/modules/overview/OverviewWidget.qml +++ b/modules/overview/OverviewWidget.qml @@ -14,9 +14,13 @@ Item { required property var panelWindow readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) readonly property var toplevels: ToplevelManager.toplevels + readonly property int effectiveActiveWorkspaceId: Math.max(1, Math.min(100, monitor?.activeWorkspace?.id ?? 1)) readonly property int workspacesShown: Config.options.overview.rows * Config.options.overview.columns - readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.name == monitor.name) + readonly property bool useWorkspaceMap: Config.options.overview.useWorkspaceMap + readonly property var workspaceMap: Config.options.overview.workspaceMap + readonly property int workspaceOffset: useWorkspaceMap ? Number(workspaceMap[root.monitor?.id] ?? 0) : 0 + readonly property int workspaceGroup: Math.floor((effectiveActiveWorkspaceId - workspaceOffset - 1) / workspacesShown) + property bool monitorIsFocused: Hyprland.focusedMonitor?.name == monitor.name property var windows: HyprlandData.windowList property var windowByAddress: HyprlandData.windowByAddress property var windowAddresses: HyprlandData.addresses @@ -38,23 +42,47 @@ Item { property int draggingTargetWorkspace: -1 property bool hideEmptyRows: Config.options.overview.hideEmptyRows + function getWorkspaceRow(workspaceId) { + if (!Number.isFinite(workspaceId)) + return 0; + + const adjusted = workspaceId - workspaceOffset; + const normalRow = Math.floor((adjusted - 1) / Config.options.overview.columns) % Config.options.overview.rows; + return Config.options.overview.orderBottomUp ? (Config.options.overview.rows - normalRow - 1) : normalRow; + } + + function getWorkspaceColumn(workspaceId) { + if (!Number.isFinite(workspaceId)) + return 0; + + const adjusted = workspaceId - workspaceOffset; + const normalCol = (adjusted - 1) % Config.options.overview.columns; + return Config.options.overview.orderRightLeft ? (Config.options.overview.columns - normalCol - 1) : normalCol; + } + + function getWorkspaceInCell(rowIndex, colIndex) { + const mappedRow = Config.options.overview.orderBottomUp ? (Config.options.overview.rows - rowIndex - 1) : rowIndex; + const mappedCol = Config.options.overview.orderRightLeft ? (Config.options.overview.columns - colIndex - 1) : colIndex; + return workspaceGroup * workspacesShown + mappedRow * Config.options.overview.columns + mappedCol + 1 + workspaceOffset; + } + property var rowsWithContent: { if (!root.hideEmptyRows) return null; let rows = new Set(); - const firstWorkspace = root.workspaceGroup * root.workspacesShown + 1; - const lastWorkspace = (root.workspaceGroup + 1) * root.workspacesShown; + const firstWorkspace = root.workspaceGroup * root.workspacesShown + 1 + workspaceOffset; + const lastWorkspace = (root.workspaceGroup + 1) * root.workspacesShown + workspaceOffset; - const currentWorkspace = monitor.activeWorkspace?.id ?? 1; + const currentWorkspace = effectiveActiveWorkspaceId; if (currentWorkspace >= firstWorkspace && currentWorkspace <= lastWorkspace) - rows.add(Math.floor((currentWorkspace - firstWorkspace) / Config.options.overview.columns)); + rows.add(getWorkspaceRow(currentWorkspace)); for (let addr in windowByAddress) { const win = windowByAddress[addr]; const wsId = win?.workspace?.id; if (wsId >= firstWorkspace && wsId <= lastWorkspace) - rows.add(Math.floor((wsId - firstWorkspace) / Config.options.overview.columns)); + rows.add(getWorkspaceRow(wsId)); } return rows; @@ -113,12 +141,11 @@ Item { height: visible ? implicitHeight : 0 Repeater { - // Workspace repeater model: Config.options.overview.columns Rectangle { // Workspace id: workspace property int colIndex: index - property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * Config.options.overview.columns + colIndex + 1 + property int workspaceValue: root.getWorkspaceInCell(rowIndex, colIndex) property color defaultWorkspaceColor: Appearance.colors.colLayer1 property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1) property color hoveredBorderColor: Appearance.colors.colLayer2Hover @@ -183,33 +210,26 @@ Item { implicitHeight: workspaceColumnLayout.implicitHeight Repeater { - // Window repeater model: ScriptModel { values: { return ToplevelManager.toplevels.values.filter(toplevel => { const address = `0x${toplevel.HyprlandToplevel.address}`; var win = windowByAddress[address]; - const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown); - return inWorkspaceGroup; + const minWorkspace = root.workspaceGroup * root.workspacesShown + 1 + workspaceOffset; + const maxWorkspace = (root.workspaceGroup + 1) * root.workspacesShown + workspaceOffset; + return minWorkspace <= win?.workspace?.id && win?.workspace?.id <= maxWorkspace; }).sort((a, b) => { - // Proper stacking order based on Hyprland's window properties const addrA = `0x${a.HyprlandToplevel.address}`; const addrB = `0x${b.HyprlandToplevel.address}`; const winA = windowByAddress[addrA]; const winB = windowByAddress[addrB]; - // 1. Pinned windows are always on top - if (winA?.pinned !== winB?.pinned) { + if (winA?.pinned !== winB?.pinned) return winA?.pinned ? 1 : -1; - } - // 2. Floating windows above tiled windows - if (winA?.floating !== winB?.floating) { + if (winA?.floating !== winB?.floating) return winA?.floating ? 1 : -1; - } - // 3. Within same category, sort by focus history - // Lower focusHistoryID = more recently focused = higher in stack return (winB?.focusHistoryID ?? 0) - (winA?.focusHistoryID ?? 0); }); } @@ -225,21 +245,19 @@ Item { toplevel: modelData monitorData: monitor - // Calculate scale relative to window's source monitor + // Keep the fork's source-monitor scaling behavior while applying the new workspace mapping logic. property real sourceMonitorWidth: (monitor?.transform % 2 === 1) ? (monitor?.height ?? 1920) / (monitor?.scale ?? 1) - (monitor?.reserved?.[0] ?? 0) - (monitor?.reserved?.[2] ?? 0) : (monitor?.width ?? 1920) / (monitor?.scale ?? 1) - (monitor?.reserved?.[0] ?? 0) - (monitor?.reserved?.[2] ?? 0) property real sourceMonitorHeight: (monitor?.transform % 2 === 1) ? (monitor?.width ?? 1080) / (monitor?.scale ?? 1) - (monitor?.reserved?.[1] ?? 0) - (monitor?.reserved?.[3] ?? 0) : (monitor?.height ?? 1080) / (monitor?.scale ?? 1) - (monitor?.reserved?.[1] ?? 0) - (monitor?.reserved?.[3] ?? 0) - // Scale windows to fit the workspace size, accounting for different monitor sizes scale: Math.min(root.workspaceImplicitWidth / sourceMonitorWidth, root.workspaceImplicitHeight / sourceMonitorHeight) - availableWorkspaceWidth: root.workspaceImplicitWidth availableWorkspaceHeight: root.workspaceImplicitHeight widgetMonitorId: root.monitor.id - property bool atInitPosition: (initX == x && initY == y) + property bool atInitPosition: initX == x && initY == y - property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.columns - property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.columns) + property int workspaceColIndex: root.getWorkspaceColumn(windowData?.workspace.id) + property int workspaceRowIndex: root.getWorkspaceRow(windowData?.workspace.id) property int workspaceVisibleRowIndex: root.mappedRowIndex(workspaceRowIndex) xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceVisibleRowIndex @@ -250,12 +268,12 @@ Item { repeat: false running: false onTriggered: { - window.x = Math.round(Math.max((windowData?.at[0] - (monitor?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset); - window.y = Math.round(Math.max((windowData?.at[1] - (monitor?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset); + window.x = Math.round(Math.max((windowData?.at[0] - (monitor?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * window.scale, 0) + xOffset); + window.y = Math.round(Math.max((windowData?.at[1] - (monitor?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * window.scale, 0) + yOffset); } } - z: atInitPosition ? (root.windowZ + index) : root.windowDraggingZ + z: atInitPosition ? root.windowZ + index : root.windowDraggingZ Drag.hotSpot.x: targetWindowWidth / 2 Drag.hotSpot.y: targetWindowHeight / 2 MouseArea { @@ -312,9 +330,8 @@ Item { Rectangle { // Focused workspace indicator id: focusedWorkspaceIndicator - property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown) - property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns) - property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns + property int activeWorkspaceRowIndex: root.getWorkspaceRow(root.effectiveActiveWorkspaceId) + property int activeWorkspaceColIndex: root.getWorkspaceColumn(root.effectiveActiveWorkspaceId) x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex y: (root.workspaceImplicitHeight + workspaceSpacing) * root.mappedRowIndex(activeWorkspaceRowIndex) z: root.windowZ diff --git a/modules/overview/OverviewWindow.qml b/modules/overview/OverviewWindow.qml index 74bd0e1..1da281e 100644 --- a/modules/overview/OverviewWindow.qml +++ b/modules/overview/OverviewWindow.qml @@ -30,39 +30,67 @@ Item { // Window property var iconToWindowRatio: Config.options.windowPreview.iconToWindowRatio property var xwaylandIndicatorToIconRatio: Config.options.windowPreview.xwaylandIndicatorToIconRatio property var iconToWindowRatioCompact: Config.options.windowPreview.iconToWindowRatioCompact + property bool previewsEnabled: Config.options.overview.previewsEnabled + property bool includeInactiveMonitorPreviews: Config.options.overview.includeInactiveMonitorPreviews + property int previewRecaptureDelayMs: Config.options.overview.previewRecaptureDelayMs + property string previewModeRaw: Config.options.overview.previewMode + property string previewMode: { + const mode = `${previewModeRaw ?? "live"}`.trim().toLowerCase(); + return (mode === "event" || mode === "snapshot") ? "event" : "live"; + } + property bool livePreviewEnabled: previewsEnabled && previewMode === "live" + property bool shouldCapturePreview: { + if (!GlobalStates.overviewOpen || !previewsEnabled || !previewCaptureEnabled) + return false; + if (includeInactiveMonitorPreviews) + return true; + return (windowData?.monitor ?? -1) === widgetMonitorId; + } property var entry: DesktopEntries.heuristicLookup(windowData?.class) - property var iconPath: Quickshell.iconPath(entry?.icon ?? windowData?.class ?? "application-x-executable", "image-missing") + property string iconName: { + const raw = `${entry?.icon ?? ""}`.trim(); + const withoutProviderPrefix = raw.replace(/^image:\/\/icon\//, ""); + const withoutQuery = withoutProviderPrefix.split("?")[0].trim(); + return withoutQuery.length > 0 ? withoutQuery : "application-x-executable"; + } + property var iconPath: Quickshell.iconPath(iconName, "image-missing") property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth property bool indicateXWayland: windowData?.xwayland ?? false property bool previewCaptureEnabled: true + property bool initialized: false x: initX y: initY - width: Math.min((windowData?.size[0] ?? 100) * root.scale, availableWorkspaceWidth) - height: Math.min((windowData?.size[1] ?? 100) * root.scale, availableWorkspaceHeight) + width: Math.min(targetWindowWidth, availableWorkspaceWidth) + height: Math.min(targetWindowHeight, availableWorkspaceHeight) opacity: (windowData?.monitor ?? -1) == widgetMonitorId ? 1 : Config.options.windowPreview.inactiveMonitorOpacity clip: true + Component.onCompleted: Qt.callLater(() => root.initialized = true) Behavior on x { + enabled: root.initialized animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) } Behavior on y { + enabled: root.initialized animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) } Behavior on width { + enabled: root.initialized animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) } Behavior on height { + enabled: root.initialized animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) } ScreencopyView { id: windowPreview anchors.fill: parent - captureSource: GlobalStates.overviewOpen && root.previewCaptureEnabled ? root.toplevel : null - live: true + captureSource: shouldCapturePreview ? root.toplevel : null + live: livePreviewEnabled Rectangle { anchors.fill: parent @@ -100,7 +128,7 @@ Item { // Window } function refreshCapture() { - if (!GlobalStates.overviewOpen) + if (!GlobalStates.overviewOpen || livePreviewEnabled || !previewsEnabled) return; root.previewCaptureEnabled = false; @@ -109,7 +137,7 @@ Item { // Window Timer { id: previewResetTimer - interval: 60 + interval: Math.max(1, previewRecaptureDelayMs) repeat: false onTriggered: root.previewCaptureEnabled = true } diff --git a/services/HyprlandData.qml b/services/HyprlandData.qml index 264f44e..8d1d5a4 100644 --- a/services/HyprlandData.qml +++ b/services/HyprlandData.qml @@ -5,6 +5,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Hyprland +import "../common" /** * Provides access to some Hyprland data not available in Quickshell.Hyprland. @@ -20,6 +21,11 @@ Singleton { property var activeWorkspace: null property var monitors: [] property var layers: ({}) + property bool pendingWindowsUpdate: false + property bool pendingMonitorsUpdate: false + property bool pendingLayersUpdate: false + property bool pendingWorkspacesUpdate: false + property bool pendingActiveWorkspaceUpdate: false function updateWindowList() { getClients.running = true; @@ -39,10 +45,46 @@ Singleton { } function updateAll() { - updateWindowList(); - updateMonitors(); - updateLayers(); - updateWorkspaces(); + scheduleUpdates(true, true, true, true, true); + } + + function scheduleUpdates(windows, monitors, layers, workspaces, activeWorkspace) { + pendingWindowsUpdate = pendingWindowsUpdate || !!windows; + pendingMonitorsUpdate = pendingMonitorsUpdate || !!monitors; + pendingLayersUpdate = pendingLayersUpdate || !!layers; + pendingWorkspacesUpdate = pendingWorkspacesUpdate || !!workspaces; + pendingActiveWorkspaceUpdate = pendingActiveWorkspaceUpdate || !!activeWorkspace; + + const debounceMs = Math.max(0, Config.options.hacks.hyprlandEventDebounceMs); + if (debounceMs === 0) { + flushPendingUpdates(); + } else { + eventDebounceTimer.interval = debounceMs; + eventDebounceTimer.restart(); + } + } + + function flushPendingUpdates() { + if (pendingWindowsUpdate) { + pendingWindowsUpdate = false; + updateWindowList(); + } + if (pendingMonitorsUpdate) { + pendingMonitorsUpdate = false; + updateMonitors(); + } + if (pendingLayersUpdate) { + pendingLayersUpdate = false; + updateLayers(); + } + if (pendingWorkspacesUpdate) { + pendingWorkspacesUpdate = false; + getWorkspaces.running = true; + } + if (pendingActiveWorkspaceUpdate) { + pendingActiveWorkspaceUpdate = false; + getActiveWorkspace.running = true; + } } function biggestWindowForWorkspace(workspaceId) { @@ -55,17 +97,44 @@ Singleton { } Component.onCompleted: { - updateAll(); + scheduleUpdates(true, true, true, true, true); + flushPendingUpdates(); } Connections { target: Hyprland function onRawEvent(event) { - updateAll(); + const eventName = `${event?.name ?? event?.event ?? event?.type ?? ""}`; + if (["openlayer", "closelayer", "screencast"].includes(eventName)) + return; + + if (eventName === "openwindow" || eventName === "closewindow" || eventName === "movewindow" || eventName === "movewindowv2" || eventName === "windowtitle") { + scheduleUpdates(true, false, false, true, false); + return; + } + + if (eventName === "workspace" || eventName === "workspacev2" || eventName === "focusedmon" || eventName === "focusedmonv2" || eventName === "activewindow" || eventName === "activewindowv2") { + scheduleUpdates(false, false, false, true, true); + return; + } + + if (eventName.startsWith("monitor") || eventName === "configreloaded") { + scheduleUpdates(true, true, false, true, true); + return; + } + + scheduleUpdates(true, true, true, true, true); } } + Timer { + id: eventDebounceTimer + interval: Math.max(0, Config.options.hacks.hyprlandEventDebounceMs) + repeat: false + onTriggered: root.flushPendingUpdates() + } + Process { id: getClients command: ["hyprctl", "clients", "-j"] diff --git a/shell.qml b/shell.qml index e92b4eb..22224c9 100644 --- a/shell.qml +++ b/shell.qml @@ -1,5 +1,6 @@ //@ pragma UseQApplication //@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic +//@ pragma Env QT_QPA_PLATFORMTHEME= import "./modules/overview/" import "./services/"