From 05f9acdc5dc13382aeca1e7b915eedd57eed3599 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 10:19:51 +0200 Subject: [PATCH] Add CompositorService, make Logger look a bit nicer --- Assets/ColorSchemes/Noctalia.json | 20 ++ Commons/Logger.qml | 2 +- Modules/Background/Overview.qml | 4 +- Modules/Bar/ActiveWindow.qml | 14 +- Modules/SidePanel/PowerMenu.qml | 20 +- Services/CompositorService.qml | 387 ++++++++++++++++++++++++++++++ Services/NiriService.qml | 138 ----------- Services/WorkspacesService.qml | 142 ++--------- Widgets/NPanel.qml | 43 +++- 9 files changed, 473 insertions(+), 297 deletions(-) create mode 100644 Assets/ColorSchemes/Noctalia.json create mode 100644 Services/CompositorService.qml delete mode 100644 Services/NiriService.qml diff --git a/Assets/ColorSchemes/Noctalia.json b/Assets/ColorSchemes/Noctalia.json new file mode 100644 index 0000000..1a16c59 --- /dev/null +++ b/Assets/ColorSchemes/Noctalia.json @@ -0,0 +1,20 @@ +{ + "mPrimary": "#c7a1d8", + "mOnPrimary": "#1a151f", + "mSecondary": "#a984c4", + "mOnSecondary": "#f3edf7", + "mTertiary": "#e0b7c9", + "mOnTertiary": "#20161f", + + "mError": "#e9899d", + "mOnError": "#1e1418", + + "mSurface": "#1c1822", + "mOnSurface": "#e9e4f0", + "mSurfaceVariant": "#262130", + "mOnSurfaceVariant": "#a79ab0", + "mOutline": "#4d445a", + "mOutlineVariant": "#342c42", + "mShadow": "#120f18" + } + \ No newline at end of file diff --git a/Commons/Logger.qml b/Commons/Logger.qml index dbdb9c4..22a4726 100644 --- a/Commons/Logger.qml +++ b/Commons/Logger.qml @@ -10,7 +10,7 @@ Singleton { var t = Time.getFormattedTimestamp() if (args.length > 1) { const maxLength = 14 - var module = args.shift().substring(0, maxLength).padStart(maxLength, ".") + var module = args.shift().substring(0, maxLength).padStart(maxLength, " ") return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ") } else { return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ") diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index c3bea3a..ecd6638 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -7,10 +7,10 @@ import qs.Services import qs.Widgets NLoader { - active: WorkspacesService.isNiri + active: CompositorService.isNiri Component.onCompleted: { - if (WorkspacesService.isNiri) { + if (CompositorService.isNiri) { Logger.log("Overview", "Loading Overview component (Niri detected)") } else { Logger.log("Overview", "Skipping Overview component (Niri not detected)") diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index c2ffe6e..47cb42b 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -27,11 +27,11 @@ Row { // Update text when window changes Connections { - target: typeof NiriService !== "undefined" ? NiriService : null - function onFocusedWindowIndexChanged() { + target: CompositorService + function onActiveWindowChanged() { // Check if window actually changed - if (NiriService.focusedWindowIndex !== lastWindowIndex) { - lastWindowIndex = NiriService.focusedWindowIndex + if (CompositorService.focusedWindowIndex !== lastWindowIndex) { + lastWindowIndex = CompositorService.focusedWindowIndex showingFullTitle = true fullTitleTimer.restart() } @@ -39,11 +39,7 @@ Row { } function getFocusedWindow() { - if (typeof NiriService === "undefined" || NiriService.focusedWindowIndex < 0 - || NiriService.focusedWindowIndex >= NiriService.windows.length) { - return null - } - return NiriService.windows[NiriService.focusedWindowIndex] + return CompositorService.getFocusedWindow() } function getTitle() { diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 8a52447..02d6ed9 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -348,13 +348,7 @@ NPanel { // ---------------------------------- // System functions function logout() { - if (WorkspacesService.isNiri) { - logoutProcessNiri.running = true - } else if (WorkspacesService.isHyprland) { - logoutProcessHyprland.running = true - } else { - Logger.warn("PowerMenu", "No supported compositor detected for logout") - } + CompositorService.logout() } function suspend() { @@ -390,19 +384,7 @@ NPanel { running: false } - Process { - id: logoutProcessNiri - command: ["niri", "msg", "action", "quit", "--skip-confirmation"] - running: false - } - - Process { - id: logoutProcessHyprland - - command: ["hyprctl", "dispatch", "exit"] - running: false - } Process { id: logoutProcess diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml new file mode 100644 index 0000000..d58d53b --- /dev/null +++ b/Services/CompositorService.qml @@ -0,0 +1,387 @@ +pragma Singleton + +pragma ComponentBehavior + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import qs.Commons +import qs.Services + +Singleton { + id: root + + // Generic compositor properties + property string compositorType: "unknown" // "hyprland", "niri", or "unknown" + property bool isHyprland: false + property bool isNiri: false + + // Generic workspace and window data + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + property string focusedWindowTitle: "(No active window)" + property bool inOverview: false + + // Generic events + signal workspaceChanged() + signal activeWindowChanged() + signal overviewStateChanged() + signal windowListChanged() + + // Compositor detection + Component.onCompleted: { + detectCompositor() + } + + // Hyprland connections + Connections { + target: Hyprland.workspaces + enabled: isHyprland + function onValuesChanged() { + updateHyprlandWorkspaces() + workspaceChanged() + } + } + + Connections { + target: Hyprland + enabled: isHyprland + function onRawEvent(event) { + updateHyprlandWorkspaces() + workspaceChanged() + } + } + + + + function detectCompositor() { + try { + // Try Hyprland first + if (Hyprland.eventSocketPath) { + compositorType = "hyprland" + isHyprland = true + isNiri = false + initHyprland() + return + } + } catch (e) { + // Hyprland not available + } + + // Try Niri (always available since we handle it directly) + compositorType = "niri" + isHyprland = false + isNiri = true + initNiri() + return + + // No supported compositor found + compositorType = "unknown" + isHyprland = false + isNiri = false + Logger.warn("CompositorService", "No supported compositor detected") + } + + // Hyprland integration + function initHyprland() { + try { + Hyprland.refreshWorkspaces() + updateHyprlandWorkspaces() + setupHyprlandConnections() + Logger.log("CompositorService", "Hyprland initialized successfully") + } catch (e) { + Logger.error("CompositorService", "Error initializing Hyprland:", e) + compositorType = "unknown" + isHyprland = false + } + } + + function setupHyprlandConnections() { + // Connections are set up at the top level, this function just marks that Hyprland is ready + } + + function updateHyprlandWorkspaces() { + if (!isHyprland) return + + workspaces.clear() + try { + const hlWorkspaces = Hyprland.workspaces.values + for (var i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i] + // Only append workspaces with id >= 1 + if (ws.id >= 1) { + 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 + }) + } + } + } catch (e) { + Logger.error("CompositorService", "Error updating Hyprland workspaces:", e) + } + } + + // Niri integration + function initNiri() { + try { + // Start the event stream to receive Niri events + niriEventStream.running = true + // Initial load of workspaces and windows + updateNiriWorkspaces() + updateNiriWindows() + Logger.log("CompositorService", "Niri initialized successfully") + } catch (e) { + Logger.error("CompositorService", "Error initializing Niri:", e) + compositorType = "unknown" + isNiri = false + } + } + + function updateNiriWorkspaces() { + if (!isNiri) return + + // Get workspaces from the Niri process + niriWorkspaceProcess.running = true + } + + function updateNiriWindows() { + if (!isNiri) return + + // Get windows from the Niri process + niriWindowsProcess.running = true + } + + // Niri workspace process + Process { + id: niriWorkspaceProcess + running: false + command: ["niri", "msg", "--json", "workspaces"] + + stdout: SplitParser { + onRead: function (line) { + try { + const workspacesData = JSON.parse(line) + const workspacesList = [] + + 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, + "isOccupied": ws.active_window_id ? true : false + }) + } + + workspacesList.sort((a, b) => { + if (a.output !== b.output) { + return a.output.localeCompare(b.output) + } + return a.id - b.id + }) + + // Update the workspaces ListModel + workspaces.clear() + for (var i = 0; i < workspacesList.length; i++) { + workspaces.append(workspacesList[i]) + } + workspaceChanged() + } catch (e) { + Logger.error("CompositorService", "Failed to parse workspaces:", e, line) + } + } + } + } + + // Niri event stream process + Process { + id: niriEventStream + running: false + command: ["niri", "msg", "--json", "event-stream"] + + stdout: SplitParser { + onRead: data => { + try { + const event = JSON.parse(data.trim()) + + if (event.WorkspacesChanged) { + niriWorkspaceProcess.running = true + } else if (event.WindowsChanged) { + try { + const windowsData = event.WindowsChanged.windows + const windowsList = [] + 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 + }) + } + + windowsList.sort((a, b) => a.id - b.id) + windows = windowsList + windowListChanged() + + // Update focused window index + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + focusedWindowIndex = i + break + } + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("CompositorService", "Error parsing windows event:", e) + } + } else if (event.WorkspaceActivated) { + niriWorkspaceProcess.running = true + } else if (event.WindowFocusChanged) { + try { + const focusedId = event.WindowFocusChanged.id + if (focusedId) { + focusedWindowIndex = windows.findIndex(w => w.id === focusedId) + if (focusedWindowIndex < 0) { + focusedWindowIndex = 0 + } + } else { + focusedWindowIndex = -1 + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("CompositorService", "Error parsing window focus event:", e) + } + } else if (event.OverviewOpenedOrClosed) { + try { + inOverview = event.OverviewOpenedOrClosed.is_open === true + overviewStateChanged() + } catch (e) { + Logger.error("CompositorService", "Error parsing overview state:", e) + } + } + } catch (e) { + Logger.error("CompositorService", "Error parsing event stream:", e, data) + } + } + } + } + + // Niri windows process (for initial load) + Process { + id: niriWindowsProcess + running: false + command: ["niri", "msg", "--json", "windows"] + + stdout: SplitParser { + onRead: function (line) { + try { + const windowsData = JSON.parse(line) + const windowsList = [] + 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 + }) + } + + windowsList.sort((a, b) => a.id - b.id) + windows = windowsList + windowListChanged() + + // Update focused window index + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + focusedWindowIndex = i + break + } + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("CompositorService", "Failed to parse windows:", e, line) + } + } + } + } + + function updateFocusedWindowTitle() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)" + } else { + focusedWindowTitle = "(No active window)" + } + } + + // Generic workspace switching + function switchToWorkspace(workspaceId) { + if (isHyprland) { + try { + Hyprland.dispatch(`workspace ${workspaceId}`) + } catch (e) { + Logger.error("CompositorService", "Error switching Hyprland workspace:", e) + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) + } catch (e) { + Logger.error("CompositorService", "Error switching Niri workspace:", e) + } + } else { + Logger.warn("CompositorService", "No supported compositor detected for workspace switching") + } + } + + // Generic logout/shutdown commands + function logout() { + if (isHyprland) { + try { + Quickshell.execDetached(["hyprctl", "dispatch", "exit"]) + } catch (e) { + Logger.error("CompositorService", "Error logging out from Hyprland:", e) + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"]) + } catch (e) { + Logger.error("CompositorService", "Error logging out from Niri:", e) + } + } else { + Logger.warn("CompositorService", "No supported compositor detected for logout") + } + } + + // Get current workspace + function getCurrentWorkspace() { + for (var i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i) + if (ws.isFocused) { + return ws + } + } + return null + } + + // Get focused window + function getFocusedWindow() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + return windows[focusedWindowIndex] + } + return null + } +} \ No newline at end of file diff --git a/Services/NiriService.qml b/Services/NiriService.qml deleted file mode 100644 index 4a8f403..0000000 --- a/Services/NiriService.qml +++ /dev/null @@ -1,138 +0,0 @@ -pragma Singleton - -pragma ComponentBehavior - -import QtQuick -import Quickshell -import Quickshell.Io - -Singleton { - id: root - - property var workspaces: [] - property var windows: [] - property int focusedWindowIndex: -1 - property bool inOverview: false - property string focusedWindowTitle: "(No active window)" - - function updateFocusedWindowTitle() { - if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { - focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)" - } else { - focusedWindowTitle = "(No active window)" - } - } - - onWindowsChanged: updateFocusedWindowTitle() - onFocusedWindowIndexChanged: updateFocusedWindowTitle() - - Component.onCompleted: { - eventStream.running = true - } - - Process { - id: workspaceProcess - running: false - command: ["niri", "msg", "--json", "workspaces"] - - stdout: SplitParser { - onRead: function (line) { - try { - const workspacesData = JSON.parse(line) - const workspacesList = [] - - 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, - "isOccupied": ws.active_window_id ? true : false - }) - } - - 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) { - Logger.error("NiriService", "Failed to parse workspaces:", e, line) - } - } - } - } - - Process { - id: eventStream - running: false - command: ["niri", "msg", "--json", "event-stream"] - - stdout: SplitParser { - onRead: data => { - try { - const event = JSON.parse(data.trim()) - - if (event.WorkspacesChanged) { - workspaceProcess.running = true - } else if (event.WindowsChanged) { - try { - const windowsData = event.WindowsChanged.windows - const windowsList = [] - 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 - }) - } - - windowsList.sort((a, b) => a.id - b.id) - root.windows = windowsList - for (var i = 0; i < windowsList.length; i++) { - if (windowsList[i].isFocused) { - root.focusedWindowIndex = i - break - } - } - } catch (e) { - Logger.error("NiriService", "Error parsing windows event:", e) - } - } else if (event.WorkspaceActivated) { - workspaceProcess.running = true - } 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) { - Logger.error("NiriService", "Error parsing window focus event:", e) - } - } else if (event.OverviewOpenedOrClosed) { - try { - root.inOverview = event.OverviewOpenedOrClosed.is_open === true - } catch (e) { - Logger.error("NiriService", "Error parsing overview state:", e) - } - } - } catch (e) { - Logger.error("NiriService", "Error parsing event stream:", e, data) - } - } - } - } -} diff --git a/Services/WorkspacesService.qml b/Services/WorkspacesService.qml index 3ac0598..f928eea 100644 --- a/Services/WorkspacesService.qml +++ b/Services/WorkspacesService.qml @@ -4,150 +4,46 @@ pragma ComponentBehavior import QtQuick import Quickshell -import Quickshell.Io -import Quickshell.Hyprland import qs.Commons import qs.Services Singleton { id: root + // Delegate to CompositorService for all workspace operations 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: { - detectCompositor() - } - - function detectCompositor() { - try { - try { - if (Hyprland.eventSocketPath) { - isHyprland = true - isNiri = false - initHyprland() - return - } - } catch (e) { - - } - - if (typeof NiriService !== "undefined") { - isHyprland = false - isNiri = true - initNiri() - return - } - } catch (e) { - Logger.error("WorkspacesService", "Error detecting compositor:", e) - } - } - - // Initialize Hyprland integration - function initHyprland() { - try { - // Fixes the odd workspace issue. - Hyprland.refreshWorkspaces() - // hlWorkspaces = Hyprland.workspaces.values; - // updateHyprlandWorkspaces(); - return true - } catch (e) { - Logger.error("WorkspacesService", "Error initializing Hyprland:", e) - isHyprland = false - return false - } - } - - onHlWorkspacesChanged: { - updateHyprlandWorkspaces() + // Connect to CompositorService workspace changes + CompositorService.workspaceChanged.connect(updateWorkspaces) + // Initial sync + updateWorkspaces() } + // Listen to compositor detection changes Connections { - target: Hyprland.workspaces - function onValuesChanged() { - updateHyprlandWorkspaces() + target: CompositorService + function onIsHyprlandChanged() { + isHyprland = CompositorService.isHyprland + } + function onIsNiriChanged() { + isNiri = CompositorService.isNiri } } - Connections { - target: Hyprland - function onRawEvent(event) { - updateHyprlandWorkspaces() - } - } - - function updateHyprlandWorkspaces() { + function updateWorkspaces() { workspaces.clear() - try { - for (var i = 0; i < hlWorkspaces.length; i++) { - const ws = hlWorkspaces[i] - // Only append workspaces with id >= 1 - if (ws.id >= 1) { - 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) { - Logger.error("WorkspacesService", "Error updating Hyprland workspaces:", e) + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i) + workspaces.append(ws) } - } - - function initNiri() { - updateNiriWorkspaces() - } - - Connections { - target: NiriService - function onWorkspacesChanged() { - updateNiriWorkspaces() - } - } - - function updateNiriWorkspaces() { - const niriWorkspaces = NiriService.workspaces || [] - workspaces.clear() - for (var 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, - "isOccupied": ws.isOccupied === true - }) - } - + // Explicitly trigger the signal to ensure the Workspace module gets notified workspacesChanged() } function switchToWorkspace(workspaceId) { - if (isHyprland) { - try { - Hyprland.dispatch(`workspace ${workspaceId}`) - } catch (e) { - Logger.error("WorkspacesService", "Error switching Hyprland workspace:", e) - } - } else if (isNiri) { - try { - Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) - } catch (e) { - Logger.error("WorkspacesService", "Error switching Niri workspace:", e) - } - } else { - Logger.warn("WorkspacesService", "No supported compositor detected for workspace switching") - } + CompositorService.switchToWorkspace(workspaceId) } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 8e0ce6d..7fb636b 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -11,11 +11,18 @@ PanelWindow { property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling - property color overlayColor: showOverlay ? Color.applyOpacity(Color.mShadow, "AA") : "transparent" + // Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) + property color overlayColor: (showOverlay && (PanelService.openedPanel === root || isTransitioning)) ? Color.applyOpacity(Color.mShadow, "AA") : "transparent" + property bool isTransitioning: false signal dismissed function hide() { - //visible = false + // Clear the panel service when hiding + if (PanelService.openedPanel === root) { + PanelService.openedPanel = null + } + isTransitioning = false + visible = false root.dismissed() } @@ -23,14 +30,21 @@ PanelWindow { // Ensure only one panel is visible at a time using PanelService as ephemeral storage try { if (PanelService.openedPanel && PanelService.openedPanel !== root && PanelService.openedPanel.hide) { + // Mark both panels as transitioning to prevent dimming flicker + isTransitioning = true + PanelService.openedPanel.isTransitioning = true PanelService.openedPanel.hide() + // Small delay to ensure smooth transition + showTimer.start() + return } + // No previous panel, show immediately PanelService.openedPanel = root + visible = true } catch (e) { // ignore } - visible = true } implicitWidth: screen.width @@ -57,6 +71,17 @@ PanelWindow { } } + Timer { + id: showTimer + interval: 50 // Small delay to ensure smooth transition + repeat: false + onTriggered: { + PanelService.openedPanel = root + isTransitioning = false + visible = true + } + } + Component.onDestruction: { try { if (visible && Settings.openPanel === root) @@ -68,8 +93,16 @@ PanelWindow { onVisibleChanged: { try { - if (!visible && Settings.openPanel === root) - Settings.openPanel = null + if (!visible) { + // Clear panel service when panel becomes invisible + if (PanelService.openedPanel === root) { + PanelService.openedPanel = null + } + if (Settings.openPanel === root) { + Settings.openPanel = null + } + isTransitioning = false + } } catch (e) { }