1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-11 07:52:50 -05:00

compositor service & use toplevels instead of niri data

This commit is contained in:
bbedward
2025-08-20 17:31:10 -04:00
parent 835d46a7af
commit be4c09e56d
13 changed files with 236 additions and 537 deletions

View File

@@ -0,0 +1,84 @@
pragma Singleton
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Singleton {
id: root
// Compositor detection
property bool isHyprland: false
property bool isNiri: false
property string compositor: "unknown"
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
property bool useNiriSorting: isNiri && NiriService
// Unified sorted toplevels - automatically chooses sorting based on compositor
property var sortedToplevels: {
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) {
return []
}
// Only use niri sorting when both compositor is niri AND niri service is ready
if (useNiriSorting) {
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
}
// For non-niri compositors or when niri isn't ready yet, return unsorted toplevels
return ToplevelManager.toplevels.values
}
Component.onCompleted: {
console.log("CompositorService: Starting detection...")
detectCompositor()
}
function detectCompositor() {
// Check for Hyprland first
if (hyprlandSignature && hyprlandSignature.length > 0) {
isHyprland = true
isNiri = false
compositor = "hyprland"
console.log("CompositorService: Detected Hyprland with signature:", hyprlandSignature)
return
}
// Check for Niri
if (niriSocket && niriSocket.length > 0) {
// Verify the socket actually exists
niriSocketCheck.running = true
} else {
// No compositor detected, default to Niri
isHyprland = false
isNiri = false
compositor = "unknown"
console.warn("CompositorService: No compositor detected")
}
}
Process {
id: niriSocketCheck
command: ["test", "-S", root.niriSocket]
onExited: exitCode => {
if (exitCode === 0) {
root.isNiri = true
root.isHyprland = false
root.compositor = "niri"
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
} else {
root.isHyprland = false
root.isNiri = true
root.compositor = "niri"
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
}
}
}
}

View File

@@ -5,6 +5,7 @@ pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Singleton {
id: root
@@ -22,45 +23,19 @@ Singleton {
// Window management
property var windows: []
property int focusedWindowIndex: -1
property string focusedWindowTitle: "(No active window)"
property string focusedWindowId: ""
// Overview state
property bool inOverview: false
// Config validation
// Internal state (not exposed to external components)
property string configValidationOutput: ""
property bool hasInitialConnection: false
signal windowOpenedOrChanged(var windowData)
// Feature availability
property bool niriAvailable: false
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
Component.onCompleted: checkNiriAvailability()
Process {
id: niriCheck
command: ["test", "-S", root.socketPath]
onExited: exitCode => {
root.niriAvailable = exitCode === 0
if (root.niriAvailable) {
eventStreamSocket.connected = true
fetchOutputs()
}
}
}
function checkNiriAvailability() {
niriCheck.running = true
}
function fetchOutputs() {
if (niriAvailable) {
if (CompositorService.isNiri) {
outputsProcess.running = true
}
}
@@ -98,7 +73,7 @@ Singleton {
Socket {
id: eventStreamSocket
path: root.socketPath
connected: false
connected: CompositorService.isNiri
onConnectionStateChanged: {
if (connected) {
@@ -121,7 +96,7 @@ Singleton {
Socket {
id: requestSocket
path: root.socketPath
connected: root.niriAvailable
connected: CompositorService.isNiri
}
function sortWindowsByLayout(windowList) {
@@ -203,8 +178,6 @@ Singleton {
handleWindowsChanged(event.WindowsChanged)
} else if (event.WindowClosed) {
handleWindowClosed(event.WindowClosed)
} else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged)
} else if (event.WindowOpenedOrChanged) {
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
} else if (event.WindowLayoutsChanged) {
@@ -279,10 +252,6 @@ Singleton {
// This is crucial for handling floating window close scenarios
if (data.active_window_id !== null
&& data.active_window_id !== undefined) {
focusedWindowId = String(data.active_window_id)
focusedWindowIndex = windows.findIndex(
w => w.id == data.active_window_id)
// Create new windows array with updated focus states to trigger property change
let updatedWindows = []
for (var i = 0; i < windows.length; i++) {
@@ -295,13 +264,8 @@ Singleton {
updatedWindows.push(updatedWindow)
}
windows = updatedWindows
updateFocusedWindow()
} else {
// No active window in this workspace
focusedWindowId = ""
focusedWindowIndex = -1
// Create new windows array with cleared focus states for this workspace
let updatedWindows = []
for (var i = 0; i < windows.length; i++) {
@@ -315,42 +279,15 @@ Singleton {
updatedWindows.push(updatedWindow)
}
windows = updatedWindows
updateFocusedWindow()
}
}
function handleWindowsChanged(data) {
windows = sortWindowsByLayout(data.windows)
// Extract focused window from initial state
var focusedWindow = windows.find(w => w.is_focused)
if (focusedWindow) {
focusedWindowId = String(focusedWindow.id)
focusedWindowIndex = windows.findIndex(
w => w.id === focusedWindow.id)
} else {
focusedWindowId = ""
focusedWindowIndex = -1
}
updateFocusedWindow()
}
function handleWindowClosed(data) {
windows = windows.filter(w => w.id !== data.id)
updateFocusedWindow()
}
function handleWindowFocusChanged(data) {
if (data.id) {
focusedWindowId = data.id
focusedWindowIndex = windows.findIndex(w => w.id === data.id)
} else {
focusedWindowId = ""
focusedWindowIndex = -1
}
updateFocusedWindow()
}
function handleWindowOpenedOrChanged(data) {
@@ -368,14 +305,6 @@ Singleton {
windows = sortWindowsByLayout([...windows, window])
}
if (window.is_focused) {
focusedWindowId = window.id
focusedWindowIndex = windows.findIndex(w => w.id === window.id)
}
updateFocusedWindow()
windowOpenedOrChanged(window)
}
function handleWindowLayoutsChanged(data) {
@@ -477,17 +406,8 @@ Singleton {
currentOutputWorkspaces = outputWs
}
function updateFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
var focusedWin = windows[focusedWindowIndex]
focusedWindowTitle = focusedWin.title || "(Unnamed window)"
} else {
focusedWindowTitle = "(No active window)"
}
}
function send(request) {
if (!niriAvailable || !requestSocket.connected)
if (!CompositorService.isNiri || !requestSocket.connected)
return false
requestSocket.write(JSON.stringify(request) + "\n")
return true
@@ -518,25 +438,7 @@ Singleton {
return 1
}
function focusWindow(windowId) {
return send({
"Action": {
"FocusWindow": {
"id": windowId
}
}
})
}
function closeWindow(windowId) {
return send({
"Action": {
"CloseWindow": {
"id": windowId
}
}
})
}
function quit() {
return send({
@@ -548,58 +450,66 @@ Singleton {
})
}
function getWindowsByAppId(appId) {
if (!appId)
return []
return windows.filter(w => w.app_id && w.app_id.toLowerCase(
) === appId.toLowerCase())
function sortToplevels(toplevels) {
if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) {
return [...toplevels]
}
// Create a map to match toplevels to niri windows
// We'll match by appId and title since toplevels don't have numeric IDs
var toplevelToNiriMap = {}
for (var i = 0; i < toplevels.length; i++) {
var toplevel = toplevels[i]
if (!toplevel.appId) continue
// Find matching niri window by appId and optionally title
for (var j = 0; j < windows.length; j++) {
var niriWindow = windows[j]
// Match by appId
if (niriWindow.app_id === toplevel.appId) {
// If title also matches or niri window has no title, use this match
if (!niriWindow.title || niriWindow.title === toplevel.title) {
toplevelToNiriMap[i] = {
niriIndex: j,
niriWindow: niriWindow
}
break
}
// If we found appId match but no title match yet, store as fallback
if (!(i in toplevelToNiriMap)) {
toplevelToNiriMap[i] = {
niriIndex: j,
niriWindow: niriWindow
}
}
}
}
}
// Sort toplevels using niri's ordering
return [...toplevels].sort((a, b) => {
var aIndex = toplevels.indexOf(a)
var bIndex = toplevels.indexOf(b)
var aNiri = toplevelToNiriMap[aIndex]
var bNiri = toplevelToNiriMap[bIndex]
// If both have niri data, use niri ordering
if (aNiri && bNiri) {
return aNiri.niriIndex - bNiri.niriIndex
}
// If only one has niri data, prioritize it
if (aNiri && !bNiri) return -1
if (!aNiri && bNiri) return 1
// If neither has niri data, keep original toplevel order
return 0
})
}
function getRunningAppIds() {
var appIds = new Set()
windows.forEach(w => {
if (w.app_id) {
appIds.add(w.app_id.toLowerCase())
}
})
return Array.from(appIds)
}
function getRunningAppIdsOrdered() {
// Get unique app IDs in order they appear in the Niri layout
// Windows are sorted by workspace and then by position within the workspace
var sortedWindows = [...windows].sort((a, b) => {
// If both have layout info, sort by position
if (a.layout && b.layout
&& a.layout.pos_in_scrolling_layout
&& b.layout.pos_in_scrolling_layout) {
var aPos = a.layout.pos_in_scrolling_layout
var bPos = b.layout.pos_in_scrolling_layout
// First compare workspace index
if (aPos[0] !== bPos[0]) {
return aPos[0] - bPos[0]
}
// Then compare position within workspace
return aPos[1] - bPos[1]
}
// Fallback to window ID if no layout info
return a.id - b.id
})
var appIds = []
var seenApps = new Set()
sortedWindows.forEach(w => {
if (w.app_id) {
var lowerAppId = w.app_id.toLowerCase()
if (!seenApps.has(lowerAppId)) {
appIds.push(lowerAppId)
seenApps.add(lowerAppId)
}
}
})
return appIds
}
}