diff --git a/Bar/Bar.qml b/Bar/Bar.qml index 098aa1d..e249d11 100644 --- a/Bar/Bar.qml +++ b/Bar/Bar.qml @@ -102,6 +102,7 @@ Scope { Workspace { id: workspace + screen: modelData anchors.horizontalCenter: barBackground.horizontalCenter anchors.verticalCenter: barBackground.verticalCenter } diff --git a/Bar/Modules/Workspace.qml b/Bar/Modules/Workspace.qml index b944945..7fe2966 100644 --- a/Bar/Modules/Workspace.qml +++ b/Bar/Modules/Workspace.qml @@ -3,19 +3,20 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import Qt5Compat.GraphicalEffects +import Quickshell import Quickshell.Io import qs.Settings import qs.Services Item { id: root - - property ListModel workspaces: ListModel {} + required property ShellScreen screen property bool isDestroying: false property bool hovered: false signal workspaceChanged(int workspaceId, color accentColor) + property ListModel localWorkspaces: ListModel {} property real masterProgress: 0.0 property bool effectsActive: false property color effectColor: Theme.accentPrimary @@ -24,35 +25,62 @@ Item { property int spacingBetweenPills: 8 width: { - let total = 0 - for (let i = 0; i < workspaces.count; i++) { - const ws = workspaces.get(i) - if (ws.isFocused) total += 44 - else if (ws.isActive) total += 28 - else total += 16 + let total = 0; + for (let i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i); + if (ws.isFocused) + total += 44; + else if (ws.isActive) + total += 28; + else + total += 16; } - total += Math.max(workspaces.count - 1, 0) * spacingBetweenPills - total += horizontalPadding * 2 - return total + total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills; + total += horizontalPadding * 2; + return total; } height: 36 - Component.onCompleted: updateWorkspaceList() + Component.onCompleted: { + localWorkspaces.clear(); + for (let i = 0; i < WorkspaceManager.workspaces.count; i++) { + const ws = WorkspaceManager.workspaces.get(i); + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + localWorkspaces.append(ws); + } + } + workspaceRepeater.model = localWorkspaces; + updateWorkspaceFocus(); + } + Connections { - target: Niri - function onWorkspacesChanged() { updateWorkspaceList(); } - function onFocusedWorkspaceIndexChanged() { updateWorkspaceFocus(); } + target: WorkspaceManager + function onWorkspacesChanged() { + localWorkspaces.clear(); + for (let i = 0; i < WorkspaceManager.workspaces.count; i++) { + const ws = WorkspaceManager.workspaces.get(i); + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + localWorkspaces.append(ws); + } + } + workspaceRepeater.model = localWorkspaces; + updateWorkspaceFocus(); + } } function triggerUnifiedWave() { - effectColor = Theme.accentPrimary - masterAnimation.restart() + effectColor = Theme.accentPrimary; + masterAnimation.restart(); } SequentialAnimation { id: masterAnimation - PropertyAction { target: root; property: "effectsActive"; value: true } + PropertyAction { + target: root + property: "effectsActive" + value: true + } NumberAnimation { target: root property: "masterProgress" @@ -61,41 +89,25 @@ Item { duration: 1000 easing.type: Easing.OutQuint } - PropertyAction { target: root; property: "effectsActive"; value: false } - PropertyAction { target: root; property: "masterProgress"; value: 0.0 } - } - - function updateWorkspaceList() { - const newList = Niri.workspaces || [] - workspaces.clear() - for (let i = 0; i < newList.length; i++) { - const ws = newList[i] - workspaces.append({ - id: ws.id, - idx: ws.idx, - name: ws.name || "", - output: ws.output, - isActive: ws.is_active, - isFocused: ws.is_focused, - isUrgent: ws.is_urgent - }) + PropertyAction { + target: root + property: "effectsActive" + value: false + } + PropertyAction { + target: root + property: "masterProgress" + value: 0.0 } - updateWorkspaceFocus() } function updateWorkspaceFocus() { - const focusedId = Niri.workspaces?.[Niri.focusedWorkspaceIndex]?.id ?? -1 - for (let i = 0; i < workspaces.count; i++) { - const ws = workspaces.get(i) - const isFocused = ws.id === focusedId - const isActive = isFocused - if (ws.isFocused !== isFocused || ws.isActive !== isActive) { - workspaces.setProperty(i, "isFocused", isFocused) - workspaces.setProperty(i, "isActive", isActive) - if (isFocused) { - root.triggerUnifiedWave() - root.workspaceChanged(ws.id, Theme.accentPrimary) - } + for (let i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i); + if (ws.isFocused === true) { + root.triggerUnifiedWave(); + root.workspaceChanged(ws.id, Theme.accentPrimary); + break; } } } @@ -128,28 +140,94 @@ Item { width: root.width - horizontalPadding * 2 x: horizontalPadding Repeater { - model: root.workspaces - Rectangle { - id: workspacePill + id: workspaceRepeater + model: localWorkspaces + Item { + id: workspacePillContainer height: 12 width: { - if (model.isFocused) return 44 - else if (model.isActive) return 28 - else return 16 + if (model.isFocused) + return 44; + else if (model.isActive) + return 28; + else + return 16; } - radius: { - if (model.isFocused) return 12 // half of focused height (if you want to animate this too) - else return 6 + + Rectangle { + id: workspacePill + anchors.fill: parent + radius: { + if (model.isFocused) + return 12; + else + // half of focused height (if you want to animate this too) + return 6; + } + color: { + if (model.isFocused) + return Theme.accentPrimary; + if (model.isActive) + return Theme.accentPrimary.lighter(130); + if (model.isUrgent) + return Theme.error; + return Qt.lighter(Theme.surfaceVariant, 1.6); + } + scale: model.isFocused ? 1.0 : 0.9 + z: 0 + + ToolTip.visible: pillMouseArea.containsMouse + ToolTip.text: `${model.output}:${model.idx} (ID: ${model.id})` + ToolTip.delay: 500 + MouseArea { + id: pillMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + WorkspaceManager.switchToWorkspace(model.idx); + } + z: 20 + hoverEnabled: true + } + // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius + Behavior on width { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + Behavior on scale { + NumberAnimation { + duration: 300 + easing.type: Easing.OutBack + } + } + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + Behavior on radius { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } } - color: { - if (model.isFocused) return Theme.accentPrimary - if (model.isActive) return Theme.accentPrimary.lighter(130) - if (model.isUrgent) return Theme.error - return Qt.lighter(Theme.surfaceVariant, 1.6) - } - scale: model.isFocused ? 1.0 : 0.9 - z: 0 - // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius + Behavior on width { NumberAnimation { duration: 350 @@ -162,43 +240,17 @@ Item { easing.type: Easing.OutBack } } - Behavior on scale { - NumberAnimation { - duration: 300 - easing.type: Easing.OutBack - } - } - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - Behavior on opacity { - NumberAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - Behavior on radius { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } // Burst effect overlay for focused pill (smaller outline) Rectangle { id: pillBurst - anchors.centerIn: parent - width: parent.width + 18 * root.masterProgress - height: parent.height + 18 * root.masterProgress + anchors.centerIn: workspacePillContainer + width: workspacePillContainer.width + 18 * root.masterProgress + height: workspacePillContainer.height + 18 * root.masterProgress radius: width / 2 color: "transparent" border.color: root.effectColor border.width: 2 + 6 * (1.0 - root.masterProgress) - opacity: root.effectsActive && model.isFocused - ? (1.0 - root.masterProgress) * 0.7 - : 0 + opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 visible: root.effectsActive && model.isFocused z: 1 } @@ -206,24 +258,7 @@ Item { } } - // MouseArea to open/close Applauncher - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - if (appLauncherPanel && appLauncherPanel.visible) { - appLauncherPanel.hidePanel(); - } else if (appLauncherPanel) { - appLauncherPanel.showAt(); - } - } - z: 1000 // ensure it's above other content - hoverEnabled: true - onEntered: root.hovered = true - onExited: root.hovered = false - } - Component.onDestruction: { - root.isDestroying = true + root.isDestroying = true; } } diff --git a/Services/Niri.qml b/Services/Niri.qml index b8da823..28c823d 100644 --- a/Services/Niri.qml +++ b/Services/Niri.qml @@ -8,10 +8,10 @@ import Quickshell.Io Singleton { id: root - property list workspaces: [] - property int focusedWorkspaceIndex: 0 - property list windows: [] - property int focusedWindowIndex: 0 + property var workspaces: [] + property var windows: [] + property var outputs: [] + property int focusedWindowIndex: -1 property bool inOverview: false // Reactive property for focused window title @@ -25,61 +25,167 @@ Singleton { focusedWindowTitle = "(No active window)"; } } - // Call updateFocusedWindowTitle on changes onWindowsChanged: updateFocusedWindowTitle() onFocusedWindowIndexChanged: updateFocusedWindowTitle() + + Component.onCompleted: { + eventStream.running = true; + outputsProcess.running = true; + } + + Process { + id: outputsProcess + running: false + command: ["niri", "msg", "--json", "outputs"] + + stdout: SplitParser { + onRead: function(line) { + try { + const outputsData = JSON.parse(line); + const outputsList = []; + + // Process each output + for (const [connector, data] of Object.entries(outputsData)) { + const logical = data.logical || {}; + outputsList.push({ + connector: connector, + name: data.name || connector, + make: data.make || "", + model: data.model || "", + x: logical.x || 0, + y: logical.y || 0, + width: logical.width || 1920, + height: logical.height || 1080, + scale: logical.scale || 1.0, + transform: logical.transform || "Normal" + }); + } + + // Sort outputs by position (left to right, top to bottom) + outputsList.sort((a, b) => { + if (a.x !== b.x) return a.x - b.x; + return a.y - b.y; + }); + + root.outputs = outputsList; + } catch (e) { + console.error("Failed to parse outputs:", e, line); + } + } + } + } Process { - command: ["niri", "msg", "-j", "event-stream"] - running: true + id: eventStream + running: false + command: ["niri", "msg", "--json", "event-stream"] stdout: SplitParser { onRead: data => { - const event = JSON.parse(data.trim()); - - if (event.WorkspacesChanged) { - root.workspaces = [...event.WorkspacesChanged.workspaces].sort((a, b) => a.idx - b.idx); - root.focusedWorkspaceIndex = root.workspaces.findIndex(w => w.is_focused); - if (root.focusedWorkspaceIndex < 0) { - root.focusedWorkspaceIndex = 0; - } - } else if (event.WorkspaceActivated) { - root.focusedWorkspaceIndex = root.workspaces.findIndex(w => w.id === event.WorkspaceActivated.id); - if (root.focusedWorkspaceIndex < 0) { - root.focusedWorkspaceIndex = 0; - } - } else if (event.WindowsChanged) { - root.windows = [...event.WindowsChanged.windows].sort((a, b) => a.id - b.id); - //const window = event.WindowOpenedOrChanged.window; -// const index = root.windows.findIndex(w => w.id === window.id); - // if (index >= 0) { - // root.windows[index] = window; - // } else { - // root.windows.push(window); - // root.windows = [...root.windows].sort((a, b) => a.id - b.id); - // if (window.is_focused) { - // root.focusedWindowIndex = root.windows.findIndex(w => w.id === window.id); - // if (root.focusedWindowIndex < 0) { - // root.focusedWindowIndex = 0; - // } - // } - // } - } else if (event.WindowClosed) { - root.windows = [...root.windows.filter(w => w.id !== event.WindowClosed.id)]; - } else if (event.WindowFocusChanged) { - if (event.WindowFocusChanged.id) { - root.focusedWindowIndex = root.windows.findIndex(w => w.id === event.WindowFocusChanged.id); - if (root.focusedWindowIndex < 0) { - root.focusedWindowIndex = 0; + try { + const event = JSON.parse(data.trim()); + + // Handle different event types + if (event.WorkspacesChanged) { + try { + const workspacesData = event.WorkspacesChanged.workspaces; + const workspacesList = []; + + // Process each workspace + for (const ws of workspacesData) { + workspacesList.push({ + id: ws.id, + idx: ws.idx, + name: ws.name || "", + output: ws.output || "", + isFocused: ws.is_focused === true, + isActive: ws.is_active === true, + isUrgent: ws.is_urgent === true, + activeWindowId: ws.active_window_id + }); + } + + // Sort workspaces by output name and then by ID + workspacesList.sort((a, b) => { + if (a.output !== b.output) { + return a.output.localeCompare(b.output); + } + return a.id - b.id; + }); + + root.workspaces = workspacesList; + } catch (e) { + console.error("Error parsing workspaces event:", e); + } + } else if (event.WindowsChanged) { + try { + const windowsData = event.WindowsChanged.windows; + const windowsList = []; + + // Process each window + for (const win of windowsData) { + windowsList.push({ + id: win.id, + title: win.title || "", + appId: win.app_id || "", + workspaceId: win.workspace_id || null, + isFocused: win.is_focused === true + }); + } + + // Sort windows by ID + windowsList.sort((a, b) => a.id - b.id); + + root.windows = windowsList; + + // Find focused window index + for (let i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + root.focusedWindowIndex = i; + break; + } + } + } catch (e) { + console.error("Error parsing windows event:", e); + } + } else if (event.WorkspaceActivated) { + try { + const focusedId = parseInt(event.WorkspaceActivated.id); + + // Update isFocused flag on all workspaces + for (let i = 0; i < root.workspaces.length; i++) { + // Set isFocused to true only for the activated workspace + root.workspaces[i].isFocused = (root.workspaces[i].id === focusedId); + } + + root.workspacesChanged(); + } catch (e) { + console.error("Error parsing workspace activation event:", e); + } + } else if (event.WindowFocusChanged) { + try { + const focusedId = event.WindowFocusChanged.id; + if (focusedId) { + root.focusedWindowIndex = root.windows.findIndex(w => w.id === focusedId); + if (root.focusedWindowIndex < 0) { + root.focusedWindowIndex = 0; + } + } else { + root.focusedWindowIndex = -1; + } + } catch (e) { + console.error("Error parsing window focus event:", e); + } + } else if (event.OverviewOpenedOrClosed) { + try { + root.inOverview = event.OverviewOpenedOrClosed.is_open === true; + } catch (e) { + console.error("Error parsing overview state:", e); } - const focusedWin = root.windows[root.focusedWindowIndex]; - "title:", focusedWin ? `"${focusedWin.title}"` : ""; - } else { - root.focusedWindowIndex = -1; } - } else if (event.OverviewOpenedOrClosed) { - root.inOverview = event.OverviewOpenedOrClosed.is_open; + } catch (e) { + console.error("Error parsing event stream:", e, data); } } } diff --git a/Services/WorkspaceManager.qml b/Services/WorkspaceManager.qml new file mode 100644 index 0000000..aac1883 --- /dev/null +++ b/Services/WorkspaceManager.qml @@ -0,0 +1,194 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import qs.Services + +Singleton { + id: root + + property ListModel workspaces: ListModel {} + property bool isHyprland: false + property bool isNiri: false + property var hlWorkspaces: Hyprland.workspaces.values + // Detect which compositor we're using + Component.onCompleted: { + console.log("WorkspaceManager initializing..."); + detectCompositor(); + } + + function detectCompositor() { + try { + try { + if (Hyprland.eventSocketPath) { + console.log("Detected Hyprland compositor"); + isHyprland = true; + isNiri = false; + initHyprland(); + return; + } + } catch (e) { + console.log("Hyprland not available:", e); + } + + if (typeof Niri !== "undefined") { + console.log("Detected Niri service"); + isHyprland = false; + isNiri = true; + initNiri(); + return; + } + + console.log("No supported compositor detected"); + } catch (e) { + console.error("Error detecting compositor:", e); + } + } + + // Initialize Hyprland integration + function initHyprland() { + try { + Hyprland.refreshWorkspaces(); + hlWorkspaces = Hyprland.workspaces.values; + updateHyprlandWorkspaces(); + return true; + } catch (e) { + console.error("Error initializing Hyprland:", e); + isHyprland = false; + return false; + } + } + + onHlWorkspacesChanged: { + updateHyprlandWorkspaces(); + } + + Connections { + target: Hyprland.workspaces + function onValuesChanged() { + updateHyprlandWorkspaces(); + } + } + + Connections { + target: Hyprland + function onRawEvent(event) { + updateHyprlandWorkspaces(); + } + } + + function updateHyprlandWorkspaces() { + workspaces.clear(); + try { + for (let i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i]; + workspaces.append({ + id: i, + idx: ws.id, + name: ws.name || "", + output: ws.monitor.name || "", + isActive: ws.active === true, + isFocused: ws.focused === true, + isUrgent: ws.urgent === true + }); + } + workspacesChanged(); + } catch (e) { + console.error("Error updating Hyprland workspaces:", e); + } + } + + function initNiri() { + updateNiriWorkspaces(); + } + + Connections { + target: Niri + function onWorkspacesChanged() { + updateNiriWorkspaces(); + } + } + + function updateNiriWorkspaces() { + const niriWorkspaces = Niri.workspaces || []; + workspaces.clear(); + for (let i = 0; i < niriWorkspaces.length; i++) { + const ws = niriWorkspaces[i]; + workspaces.append({ + id: ws.id, + idx: ws.idx || 1, + name: ws.name || "", + output: ws.output || "", + isFocused: ws.isFocused === true, + isActive: ws.isActive === true, + isUrgent: ws.isUrgent === true + }); + } + + const tempArray = []; + for (let i = 0; i < workspaces.count; i++) { + tempArray.push({ + id: workspaces.get(i).id, + idx: workspaces.get(i).idx, + name: workspaces.get(i).name, + output: workspaces.get(i).output, + isActive: workspaces.get(i).isActive, + isFocused: workspaces.get(i).isFocused, + isUrgent: workspaces.get(i).isUrgent + }); + } + + const outputPositions = {}; + if (isNiri && Niri.outputs) { + for (let i = 0; i < Niri.outputs.length; i++) { + const output = Niri.outputs[i]; + outputPositions[output.connector] = output.x; + } + } + + tempArray.sort((a, b) => { + if (a.output !== b.output) { + if (isNiri && Niri.outputs && Niri.outputs.length > 0) { + const outputA = Niri.outputs.find(o => o.connector === a.output); + const outputB = Niri.outputs.find(o => o.connector === b.output); + + if (outputA && outputB) { + return outputA.x - outputB.x; + } + } + + return a.output.localeCompare(b.output); + } + + return a.id - b.id; + }); + + workspaces.clear(); + for (let i = 0; i < tempArray.length; i++) { + const ws = tempArray[i]; + workspaces.append(ws); + } + workspacesChanged(); + } + + function switchToWorkspace(workspaceId) { + if (isHyprland) { + try { + Hyprland.dispatch(`workspace ${workspaceId}`); + } catch (e) { + console.error("Error switching Hyprland workspace:", e); + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]); + } catch (e) { + console.error("Error switching Niri workspace:", e); + } + } else { + console.warn("No supported compositor detected for workspace switching"); + } + } +}