quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -0,0 +1,216 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import Quickshell.Wayland
Singleton {
id: root
// true if Hyprland is running, false otherwise
readonly property bool isHyprland: Compositor.require("hyprland")
// reactive Hyprland data, only valid if Hyprland is running
signal stateChanged()
readonly property var toplevels: isHyprland ? Hyprland.toplevels : []
readonly property var workspaces: isHyprland ? Hyprland.workspaces : []
readonly property var monitors: isHyprland ? Hyprland.monitors : []
readonly property Toplevel activeToplevel: isHyprland ? ToplevelManager.activeToplevel : null
readonly property HyprlandWorkspace focusedWorkspace: isHyprland ? Hyprland.focusedWorkspace : null
readonly property HyprlandMonitor focusedMonitor: isHyprland ? Hyprland.focusedMonitor : null
readonly property int focusedWorkspaceId: focusedWorkspace?.id ?? 1
property real screenW: focusedMonitor ? focusedMonitor.width : 0
property real screenH: focusedMonitor ? focusedMonitor.height : 0
property real screenScale: focusedMonitor ? focusedMonitor.scale : 1
// parsed hyprctl data, defaults are empty
property var windowList: []
property var windowByAddress: ({})
property var addresses: []
property var layers: ({})
property var monitorsInfo: []
property var workspacesInfo: []
property var workspaceById: ({})
property var workspaceIds: []
property var activeWorkspaceInfo: null
property string keyboardLayout: "?"
// dispatch a command to Hyprland, no-op if not running
function dispatch(request: string): void {
if (!isHyprland) return
Hyprland.dispatch(request)
}
// switch workspace safely
function changeWorkspace(targetWorkspaceId) {
if (!isHyprland || !targetWorkspaceId) return
root.dispatch("workspace " + targetWorkspaceId)
}
// find most recently focused window in a workspace
function focusedWindowForWorkspace(workspaceId) {
if (!isHyprland) return null
const wsWindows = root.windowList.filter(w => w.workspace.id === workspaceId)
if (wsWindows.length === 0) return null
return wsWindows.reduce((best, win) => {
const bestFocus = best?.focusHistoryID ?? Infinity
const winFocus = win?.focusHistoryID ?? Infinity
return winFocus < bestFocus ? win : best
}, null)
}
// check if a workspace has any windows
function isWorkspaceOccupied(id: int): bool {
if (!isHyprland) return false
return Hyprland.workspaces.values.find(w => w?.id === id)?.lastIpcObject.windows > 0 || false
}
// update all hyprctl processes
function updateAll() {
if (!isHyprland) return
getClients.running = true
getLayers.running = true
getMonitors.running = true
getWorkspaces.running = true
getActiveWorkspace.running = true
}
// largest window in a workspace
function biggestWindowForWorkspace(workspaceId) {
if (!isHyprland) return null
const windowsInThisWorkspace = root.windowList.filter(w => w.workspace.id === workspaceId)
return windowsInThisWorkspace.reduce((maxWin, win) => {
const maxArea = (maxWin?.size?.[0] ?? 0) * (maxWin?.size?.[1] ?? 0)
const winArea = (win?.size?.[0] ?? 0) * (win?.size?.[1] ?? 0)
return winArea > maxArea ? win : maxWin
}, null)
}
// refresh keyboard layout
function refreshKeyboardLayout() {
if (!isHyprland) return
hyprctlDevices.running = true
}
// only create hyprctl processes if Hyprland is running
Component.onCompleted: {
if (isHyprland) {
updateAll()
refreshKeyboardLayout()
}
}
// process to get keyboard layout
Process {
id: hyprctlDevices
running: false
command: ["hyprctl", "devices", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try {
const devices = JSON.parse(this.text)
const keyboard = devices.keyboards.find(k => k.main) || devices.keyboards[0]
root.keyboardLayout = keyboard?.active_keymap?.toUpperCase()?.slice(0, 2) ?? "?"
} catch (err) {
console.error("Failed to parse keyboard layout:", err)
root.keyboardLayout = "?"
}
}
}
}
Process {
id: getClients
running: false
command: ["hyprctl", "clients", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.windowList = JSON.parse(this.text)
let tempWinByAddress = {}
for (let win of root.windowList) tempWinByAddress[win.address] = win
root.windowByAddress = tempWinByAddress
root.addresses = root.windowList.map(w => w.address)
} catch (e) {
console.error("Failed to parse clients:", e)
}
}
}
}
Process {
id: getMonitors
running: false
command: ["hyprctl", "monitors", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try { root.monitorsInfo = JSON.parse(this.text) }
catch (e) { console.error("Failed to parse monitors:", e) }
}
}
}
Process {
id: getLayers
running: false
command: ["hyprctl", "layers", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try { root.layers = JSON.parse(this.text) }
catch (e) { console.error("Failed to parse layers:", e) }
}
}
}
Process {
id: getWorkspaces
running: false
command: ["hyprctl", "workspaces", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.workspacesInfo = JSON.parse(this.text)
let map = {}
for (let ws of root.workspacesInfo) map[ws.id] = ws
root.workspaceById = map
root.workspaceIds = root.workspacesInfo.map(ws => ws.id)
} catch (e) { console.error("Failed to parse workspaces:", e) }
}
}
}
Process {
id: getActiveWorkspace
running: false
command: ["hyprctl", "activeworkspace", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try { root.activeWorkspaceInfo = JSON.parse(this.text) }
catch (e) { console.error("Failed to parse active workspace:", e) }
}
}
}
// only connect to Hyprland events if running
Connections {
target: isHyprland ? Hyprland : null
function onRawEvent(event) {
if (!isHyprland || event.name.endsWith("v2")) return
if (event.name.includes("activelayout"))
refreshKeyboardLayout()
else if (event.name.includes("mon"))
Hyprland.refreshMonitors()
else if (event.name.includes("workspace") || event.name.includes("window"))
Hyprland.refreshWorkspaces()
else
Hyprland.refreshToplevels()
updateAll()
root.stateChanged()
}
}
}