CompositorService: add tons of null checks to perhaps prevent QS crashes

(and add some logging)
ActiveWindow: added debounce for icons
KeyboardLayoutService: remove console logs
This commit is contained in:
Ly-sec 2025-09-11 17:18:25 +02:00
parent 227b0dd962
commit d30e14f611
3 changed files with 237 additions and 114 deletions

View file

@ -38,7 +38,12 @@ RowLayout {
readonly property real maxWidth: minWidth * 2 readonly property real maxWidth: minWidth * 2
function getTitle() { function getTitle() {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" try {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
} catch (e) {
Logger.warn("ActiveWindow", "Error getting title:", e)
return ""
}
} }
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@ -46,25 +51,44 @@ RowLayout {
visible: getTitle() !== "" visible: getTitle() !== ""
function getAppIcon() { function getAppIcon() {
// Try CompositorService first try {
const focusedWindow = CompositorService.getFocusedWindow() // Try CompositorService first
if (focusedWindow && focusedWindow.appId) { const focusedWindow = CompositorService.getFocusedWindow()
const idValue = focusedWindow.appId if (focusedWindow && focusedWindow.appId) {
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue) try {
return AppIcons.iconForAppId(normalizedId.toLowerCase()) const idValue = focusedWindow.appId
} const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase())
// Fallback to ToplevelManager if (iconResult && iconResult !== "") {
if (ToplevelManager && ToplevelManager.activeToplevel) { return iconResult
const activeToplevel = ToplevelManager.activeToplevel }
if (activeToplevel.appId) { } catch (iconError) {
const idValue2 = activeToplevel.appId Logger.warn("ActiveWindow", "Error getting icon from CompositorService:", iconError)
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2) }
return AppIcons.iconForAppId(normalizedId2.toLowerCase())
} }
}
return "" // Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
try {
const activeToplevel = ToplevelManager.activeToplevel
if (activeToplevel.appId) {
const idValue2 = activeToplevel.appId
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase())
if (iconResult2 && iconResult2 !== "") {
return iconResult2
}
}
} catch (fallbackError) {
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
}
}
return ""
} catch (e) {
Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
return ""
}
} }
// A hidden text element to safely measure the full title width // A hidden text element to safely measure the full title width
@ -110,16 +134,28 @@ RowLayout {
asynchronous: true asynchronous: true
smooth: true smooth: true
visible: source !== "" visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
} }
} }
NText { NText {
id: titleText id: titleText
Layout.preferredWidth: { Layout.preferredWidth: {
if (mouseArea.containsMouse) { try {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) if (mouseArea.containsMouse) {
} else { return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) } else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
}
} catch (e) {
Logger.warn("ActiveWindow", "Error calculating width:", e)
return root.minWidth * scaling
} }
} }
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@ -154,10 +190,18 @@ RowLayout {
Connections { Connections {
target: CompositorService target: CompositorService
function onActiveWindowChanged() { function onActiveWindowChanged() {
windowIcon.source = Qt.binding(getAppIcon) try {
windowIcon.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
}
} }
function onWindowListChanged() { function onWindowListChanged() {
windowIcon.source = Qt.binding(getAppIcon) try {
windowIcon.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
}
} }
} }
} }

View file

@ -29,6 +29,21 @@ Singleton {
signal windowListChanged signal windowListChanged
signal windowTitleChanged signal windowTitleChanged
// Debounce timer for updates
property Timer updateTimer: Timer {
interval: 50 // 50ms debounce
repeat: false
onTriggered: {
try {
updateHyprlandWindows()
updateHyprlandWorkspaces()
windowListChanged()
} catch (e) {
Logger.error("Compositor", "Error in debounced update:", e)
}
}
}
// Compositor detection // Compositor detection
Component.onCompleted: { Component.onCompleted: {
detectCompositor() detectCompositor()
@ -39,8 +54,12 @@ Singleton {
target: Hyprland.workspaces target: Hyprland.workspaces
enabled: isHyprland enabled: isHyprland
function onValuesChanged() { function onValuesChanged() {
updateHyprlandWorkspaces() try {
workspaceChanged() updateHyprlandWorkspaces()
workspaceChanged()
} catch (e) {
Logger.error("Compositor", "Error in workspaces onValuesChanged:", e)
}
} }
} }
@ -48,10 +67,12 @@ Singleton {
target: Hyprland.toplevels target: Hyprland.toplevels
enabled: isHyprland enabled: isHyprland
function onValuesChanged() { function onValuesChanged() {
updateHyprlandWindows() try {
// Keep workspace occupancy up to date when windows change // Use debounced update to prevent too frequent calls
updateHyprlandWorkspaces() updateTimer.restart()
windowListChanged() } catch (e) {
Logger.error("Compositor", "Error in toplevels onValuesChanged:", e)
}
} }
} }
@ -59,10 +80,13 @@ Singleton {
target: Hyprland target: Hyprland
enabled: isHyprland enabled: isHyprland
function onRawEvent(event) { function onRawEvent(event) {
updateHyprlandWorkspaces() try {
workspaceChanged() updateHyprlandWorkspaces()
updateHyprlandWindows() workspaceChanged()
windowListChanged() updateTimer.restart()
} catch (e) {
Logger.error("Compositor", "Error in rawEvent:", e)
}
} }
} }
@ -77,7 +101,6 @@ Singleton {
return return
} }
} catch (e) { } catch (e) {
// Hyprland not available // Hyprland not available
} }
@ -111,7 +134,8 @@ Singleton {
} }
} }
function setupHyprlandConnections() {// Connections are set up at the top level, this function just marks that Hyprland is ready function setupHyprlandConnections() {
// Connections are set up at the top level, this function just marks that Hyprland is ready
} }
function updateHyprlandWorkspaces() { function updateHyprlandWorkspaces() {
@ -121,34 +145,50 @@ Singleton {
workspaces.clear() workspaces.clear()
try { try {
const hlWorkspaces = Hyprland.workspaces.values const hlWorkspaces = Hyprland.workspaces.values
// Determine occupied workspace ids from current toplevels // Determine occupied workspace ids from current toplevels
const occupiedIds = {} const occupiedIds = {}
try { try {
const hlToplevels = Hyprland.toplevels.values const hlToplevels = Hyprland.toplevels.values
for (var t = 0; t < hlToplevels.length; t++) { for (var t = 0; t < hlToplevels.length; t++) {
const tws = hlToplevels[t].workspace?.id const toplevel = hlToplevels[t]
if (tws !== undefined && tws !== null) { if (toplevel) {
occupiedIds[tws] = true try {
const tws = toplevel.workspace?.id
if (tws !== undefined && tws !== null) {
occupiedIds[tws] = true
}
} catch (toplevelError) {
// Ignore errors from individual toplevels
continue
}
} }
} }
} catch (e2) { } catch (e2) {
// ignore occupancy errors; fall back to false // ignore occupancy errors; fall back to false
} }
for (var i = 0; i < hlWorkspaces.length; i++) { for (var i = 0; i < hlWorkspaces.length; i++) {
const ws = hlWorkspaces[i] const ws = hlWorkspaces[i]
// Only append workspaces with id >= 1 if (!ws) continue
if (ws.id >= 1) {
workspaces.append({ try {
"id": i, // Only append workspaces with id >= 1
"idx": ws.id, if (ws.id >= 1) {
"name": ws.name || "", workspaces.append({
"output": ws.monitor?.name || "", "id": i,
"isActive": ws.active === true, "idx": ws.id,
"isFocused": ws.focused === true, "name": ws.name || "",
"isUrgent": ws.urgent === true, "output": ws.monitor?.name || "",
"isOccupied": occupiedIds[ws.id] === true "isActive": ws.active === true,
}) "isFocused": ws.focused === true,
"isUrgent": ws.urgent === true,
"isOccupied": occupiedIds[ws.id] === true
})
}
} catch (workspaceError) {
Logger.warn("Compositor", "Error processing workspace at index", i, ":", workspaceError)
continue
} }
} }
} catch (e) { } catch (e) {
@ -167,39 +207,83 @@ Singleton {
for (var i = 0; i < hlToplevels.length; i++) { for (var i = 0; i < hlToplevels.length; i++) {
const toplevel = hlToplevels[i] const toplevel = hlToplevels[i]
// Try to get appId from various sources // Skip if toplevel is null or invalid
let appId = "" if (!toplevel) {
continue
// First try the direct properties
if (toplevel.class) {
appId = toplevel.class
} else if (toplevel.initialClass) {
appId = toplevel.initialClass
} else if (toplevel.appId) {
appId = toplevel.appId
} }
// If still no appId, try to get it from the lastIpcObject try {
if (!appId && toplevel.lastIpcObject) { // Try to get appId from various sources with proper null checks
let appId = ""
// First try the direct properties with null/undefined checks
try { try {
const ipcData = toplevel.lastIpcObject if (toplevel.class !== undefined && toplevel.class !== null) {
// Try different possible property names for the application identifier appId = String(toplevel.class)
appId = ipcData.class || ipcData.initialClass || ipcData.appId || ipcData.wm_class || "" } else if (toplevel.initialClass !== undefined && toplevel.initialClass !== null) {
} catch (e) { appId = String(toplevel.initialClass)
} else if (toplevel.appId !== undefined && toplevel.appId !== null) {
// Ignore errors when accessing lastIpcObject appId = String(toplevel.appId)
}
} catch (propertyError) {
// Ignore property access errors and continue with empty appId
} }
}
windowsList.push({ // If still no appId, try to get it from the lastIpcObject
"id": (toplevel.address !== undefined if (!appId) {
&& toplevel.address !== null) ? String(toplevel.address) : "", try {
"title": (toplevel.title !== undefined && toplevel.title !== null) ? String( const ipcData = toplevel.lastIpcObject
toplevel.title) : "", if (ipcData) {
"appId": (appId !== undefined && appId !== null) ? String(appId) : "", appId = String(ipcData.class || ipcData.initialClass || ipcData.appId || ipcData.wm_class || "")
"workspaceId": toplevel.workspace?.id || null, }
"isFocused": toplevel.activated === true } catch (ipcError) {
}) // Ignore errors when accessing lastIpcObject
}
}
// Safely get other properties with fallbacks
let windowId = ""
let windowTitle = ""
let workspaceId = null
let isActivated = false
try {
windowId = (toplevel.address !== undefined && toplevel.address !== null) ? String(toplevel.address) : ""
} catch (e) {
windowId = ""
}
try {
windowTitle = (toplevel.title !== undefined && toplevel.title !== null) ? String(toplevel.title) : ""
} catch (e) {
windowTitle = ""
}
try {
workspaceId = toplevel.workspace?.id || null
} catch (e) {
workspaceId = null
}
try {
isActivated = toplevel.activated === true
} catch (e) {
isActivated = false
}
windowsList.push({
"id": windowId,
"title": windowTitle,
"appId": appId,
"workspaceId": workspaceId,
"isFocused": isActivated
})
} catch (toplevelError) {
// Log the error but continue processing other toplevels
Logger.warn("Compositor", "Error processing toplevel at index", i, ":", toplevelError)
continue
}
} }
windows = windowsList windows = windowsList
@ -217,6 +301,7 @@ Singleton {
activeWindowChanged() activeWindowChanged()
} catch (e) { } catch (e) {
Logger.error("Compositor", "Error updating Hyprland windows:", e) Logger.error("Compositor", "Error updating Hyprland windows:", e)
// Don't crash, just keep the previous windows list
} }
} }
@ -266,23 +351,23 @@ Singleton {
for (const ws of workspacesData) { for (const ws of workspacesData) {
workspacesList.push({ workspacesList.push({
"id": ws.id, "id": ws.id,
"idx": ws.idx, "idx": ws.idx,
"name": ws.name || "", "name": ws.name || "",
"output": ws.output || "", "output": ws.output || "",
"isFocused": ws.is_focused === true, "isFocused": ws.is_focused === true,
"isActive": ws.is_active === true, "isActive": ws.is_active === true,
"isUrgent": ws.is_urgent === true, "isUrgent": ws.is_urgent === true,
"isOccupied": ws.active_window_id ? true : false "isOccupied": ws.active_window_id ? true : false
}) })
} }
workspacesList.sort((a, b) => { workspacesList.sort((a, b) => {
if (a.output !== b.output) { if (a.output !== b.output) {
return a.output.localeCompare(b.output) return a.output.localeCompare(b.output)
} }
return a.idx - b.idx return a.idx - b.idx
}) })
// Update the workspaces ListModel // Update the workspaces ListModel
workspaces.clear() workspaces.clear()
@ -387,12 +472,12 @@ Singleton {
const windowsList = [] const windowsList = []
for (const win of windowsData) { for (const win of windowsData) {
windowsList.push({ windowsList.push({
"id": win.id, "id": win.id,
"title": win.title || "", "title": win.title || "",
"appId": win.app_id || "", "appId": win.app_id || "",
"workspaceId": win.workspace_id || null, "workspaceId": win.workspace_id || null,
"isFocused": win.is_focused === true "isFocused": win.is_focused === true
}) })
} }
windowsList.sort((a, b) => a.id - b.id) windowsList.sort((a, b) => a.id - b.id)
@ -457,12 +542,12 @@ Singleton {
const windowsList = [] const windowsList = []
for (const win of windowsData) { for (const win of windowsData) {
windowsList.push({ windowsList.push({
"id": win.id, "id": win.id,
"title": win.title || "", "title": win.title || "",
"appId": win.app_id || "", "appId": win.app_id || "",
"workspaceId": win.workspace_id || null, "workspaceId": win.workspace_id || null,
"isFocused": win.is_focused === true "isFocused": win.is_focused === true
}) })
} }
windowsList.sort((a, b) => a.id - b.id) windowsList.sort((a, b) => a.id - b.id)
@ -567,4 +652,4 @@ Singleton {
function suspend() { function suspend() {
Quickshell.execDetached(["systemctl", "suspend"]) Quickshell.execDetached(["systemctl", "suspend"])
} }
} }

View file

@ -35,7 +35,6 @@ Singleton {
const layoutName = data.names[data.current_idx] const layoutName = data.names[data.current_idx]
root.currentLayout = extractLayoutCode(layoutName) root.currentLayout = extractLayoutCode(layoutName)
} catch (e) { } catch (e) {
console.log("Niri layout error:", e)
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} }
} }
@ -59,7 +58,6 @@ Singleton {
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} }
} catch (e) { } catch (e) {
console.log("Hyprland layout error:", e)
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} }
} }
@ -84,7 +82,6 @@ Singleton {
} }
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} catch (e) { } catch (e) {
console.log("X11 layout error:", e)
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} }
} }
@ -118,7 +115,6 @@ Singleton {
} }
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} catch (e) { } catch (e) {
console.log("Localectl error:", e)
root.currentLayout = "Unknown" root.currentLayout = "Unknown"
} }
} }
@ -136,7 +132,6 @@ Singleton {
const currentIndex = parseInt(text.trim()) const currentIndex = parseInt(text.trim())
gsettingsSourcesProcess.running = true gsettingsSourcesProcess.running = true
} catch (e) { } catch (e) {
console.log("Gsettings current error:", e)
fallbackToLocalectl() fallbackToLocalectl()
} }
} }
@ -163,7 +158,6 @@ Singleton {
fallbackToLocalectl() fallbackToLocalectl()
} }
} catch (e) { } catch (e) {
console.log("Gsettings sources error:", e)
fallbackToLocalectl() fallbackToLocalectl()
} }
} }