Add CompositorService, make Logger look a bit nicer

This commit is contained in:
Ly-sec 2025-08-17 10:19:51 +02:00
parent c991ac85b4
commit 05f9acdc5d
9 changed files with 473 additions and 297 deletions

View file

@ -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"
}

View file

@ -10,7 +10,7 @@ Singleton {
var t = Time.getFormattedTimestamp() var t = Time.getFormattedTimestamp()
if (args.length > 1) { if (args.length > 1) {
const maxLength = 14 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(" ") return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ")
} else { } else {
return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ") return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ")

View file

@ -7,10 +7,10 @@ import qs.Services
import qs.Widgets import qs.Widgets
NLoader { NLoader {
active: WorkspacesService.isNiri active: CompositorService.isNiri
Component.onCompleted: { Component.onCompleted: {
if (WorkspacesService.isNiri) { if (CompositorService.isNiri) {
Logger.log("Overview", "Loading Overview component (Niri detected)") Logger.log("Overview", "Loading Overview component (Niri detected)")
} else { } else {
Logger.log("Overview", "Skipping Overview component (Niri not detected)") Logger.log("Overview", "Skipping Overview component (Niri not detected)")

View file

@ -27,11 +27,11 @@ Row {
// Update text when window changes // Update text when window changes
Connections { Connections {
target: typeof NiriService !== "undefined" ? NiriService : null target: CompositorService
function onFocusedWindowIndexChanged() { function onActiveWindowChanged() {
// Check if window actually changed // Check if window actually changed
if (NiriService.focusedWindowIndex !== lastWindowIndex) { if (CompositorService.focusedWindowIndex !== lastWindowIndex) {
lastWindowIndex = NiriService.focusedWindowIndex lastWindowIndex = CompositorService.focusedWindowIndex
showingFullTitle = true showingFullTitle = true
fullTitleTimer.restart() fullTitleTimer.restart()
} }
@ -39,11 +39,7 @@ Row {
} }
function getFocusedWindow() { function getFocusedWindow() {
if (typeof NiriService === "undefined" || NiriService.focusedWindowIndex < 0 return CompositorService.getFocusedWindow()
|| NiriService.focusedWindowIndex >= NiriService.windows.length) {
return null
}
return NiriService.windows[NiriService.focusedWindowIndex]
} }
function getTitle() { function getTitle() {

View file

@ -348,13 +348,7 @@ NPanel {
// ---------------------------------- // ----------------------------------
// System functions // System functions
function logout() { function logout() {
if (WorkspacesService.isNiri) { CompositorService.logout()
logoutProcessNiri.running = true
} else if (WorkspacesService.isHyprland) {
logoutProcessHyprland.running = true
} else {
Logger.warn("PowerMenu", "No supported compositor detected for logout")
}
} }
function suspend() { function suspend() {
@ -390,19 +384,7 @@ NPanel {
running: false running: false
} }
Process {
id: logoutProcessNiri
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
running: false
}
Process {
id: logoutProcessHyprland
command: ["hyprctl", "dispatch", "exit"]
running: false
}
Process { Process {
id: logoutProcess id: logoutProcess

View file

@ -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
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -4,150 +4,46 @@ pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
Singleton { Singleton {
id: root id: root
// Delegate to CompositorService for all workspace operations
property ListModel workspaces: ListModel {} property ListModel workspaces: ListModel {}
property bool isHyprland: false property bool isHyprland: false
property bool isNiri: false property bool isNiri: false
property var hlWorkspaces: Hyprland.workspaces.values
// Detect which compositor we're using
Component.onCompleted: { Component.onCompleted: {
detectCompositor() // Connect to CompositorService workspace changes
} CompositorService.workspaceChanged.connect(updateWorkspaces)
// Initial sync
function detectCompositor() { updateWorkspaces()
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()
} }
// Listen to compositor detection changes
Connections { Connections {
target: Hyprland.workspaces target: CompositorService
function onValuesChanged() { function onIsHyprlandChanged() {
updateHyprlandWorkspaces() isHyprland = CompositorService.isHyprland
}
function onIsNiriChanged() {
isNiri = CompositorService.isNiri
} }
} }
Connections { function updateWorkspaces() {
target: Hyprland
function onRawEvent(event) {
updateHyprlandWorkspaces()
}
}
function updateHyprlandWorkspaces() {
workspaces.clear() workspaces.clear()
try { for (var i = 0; i < CompositorService.workspaces.count; i++) {
for (var i = 0; i < hlWorkspaces.length; i++) { const ws = CompositorService.workspaces.get(i)
const ws = hlWorkspaces[i] workspaces.append(ws)
// 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)
} }
} // Explicitly trigger the signal to ensure the Workspace module gets notified
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
})
}
workspacesChanged() workspacesChanged()
} }
function switchToWorkspace(workspaceId) { function switchToWorkspace(workspaceId) {
if (isHyprland) { CompositorService.switchToWorkspace(workspaceId)
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")
}
} }
} }

View file

@ -11,11 +11,18 @@ PanelWindow {
property bool showOverlay: Settings.data.general.dimDesktop property bool showOverlay: Settings.data.general.dimDesktop
property int topMargin: Style.barHeight * scaling 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 signal dismissed
function hide() { function hide() {
//visible = false // Clear the panel service when hiding
if (PanelService.openedPanel === root) {
PanelService.openedPanel = null
}
isTransitioning = false
visible = false
root.dismissed() root.dismissed()
} }
@ -23,14 +30,21 @@ PanelWindow {
// Ensure only one panel is visible at a time using PanelService as ephemeral storage // Ensure only one panel is visible at a time using PanelService as ephemeral storage
try { try {
if (PanelService.openedPanel && PanelService.openedPanel !== root && PanelService.openedPanel.hide) { 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() PanelService.openedPanel.hide()
// Small delay to ensure smooth transition
showTimer.start()
return
} }
// No previous panel, show immediately
PanelService.openedPanel = root PanelService.openedPanel = root
visible = true
} catch (e) { } catch (e) {
// ignore // ignore
} }
visible = true
} }
implicitWidth: screen.width 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: { Component.onDestruction: {
try { try {
if (visible && Settings.openPanel === root) if (visible && Settings.openPanel === root)
@ -68,8 +93,16 @@ PanelWindow {
onVisibleChanged: { onVisibleChanged: {
try { try {
if (!visible && Settings.openPanel === root) if (!visible) {
Settings.openPanel = null // 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) { } catch (e) {
} }