add noctalia and fuzzel

This commit is contained in:
2026-05-23 21:20:58 +02:00
parent 364801f1a3
commit 9a3eceb3ab
599 changed files with 204318 additions and 0 deletions
@@ -0,0 +1,665 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Control
import qs.Services.UI
Singleton {
id: root
// Compositor detection
property bool isHyprland: false
property bool isNiri: false
property bool isSway: false
property bool isMango: false
property bool isLabwc: false
property bool isExtWorkspace: false
property bool isScroll: false
// Generic workspace and window data
property ListModel workspaces: ListModel {}
property ListModel windows: ListModel {}
property int focusedWindowIndex: -1
// Display scale data
property var displayScales: ({})
property bool displayScalesLoaded: false
// Overview state (Niri-specific, defaults to false for other compositors)
property bool overviewActive: false
// Global workspaces flag (workspaces shared across all outputs)
// True for LabWC (stacking compositor), false for tiling WMs with per-output workspaces
property bool globalWorkspaces: false
// Generic events
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
// Backend service loader
property var backend: null
Component.onCompleted: {
// Load display scales from ShellState
Qt.callLater(() => {
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
loadDisplayScalesFromState();
}
});
detectCompositor();
}
Connections {
target: typeof ShellState !== 'undefined' ? ShellState : null
function onIsLoadedChanged() {
if (ShellState.isLoaded) {
loadDisplayScalesFromState();
}
}
}
function detectCompositor() {
const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE");
const niriSocket = Quickshell.env("NIRI_SOCKET");
const swaySock = Quickshell.env("SWAYSOCK");
const currentDesktop = Quickshell.env("XDG_CURRENT_DESKTOP");
const labwcPid = Quickshell.env("LABWC_PID");
// Check for MangoWC using XDG_CURRENT_DESKTOP environment variable
// MangoWC sets XDG_CURRENT_DESKTOP=mango
if (currentDesktop && currentDesktop.toLowerCase().includes("mango")) {
isHyprland = false;
isNiri = false;
isSway = false;
isMango = true;
isLabwc = false;
isExtWorkspace = false;
backendLoader.sourceComponent = mangoComponent;
} else if (labwcPid && labwcPid.length > 0) {
isHyprland = false;
isNiri = false;
isSway = false;
isMango = false;
isLabwc = true;
isExtWorkspace = false;
backendLoader.sourceComponent = labwcComponent;
Logger.i("CompositorService", "Detected LabWC with PID: " + labwcPid);
} else if (niriSocket && niriSocket.length > 0) {
isHyprland = false;
isNiri = true;
isSway = false;
isMango = false;
isLabwc = false;
isExtWorkspace = false;
backendLoader.sourceComponent = niriComponent;
} else if (hyprlandSignature && hyprlandSignature.length > 0) {
isHyprland = true;
isNiri = false;
isSway = false;
isMango = false;
isLabwc = false;
isExtWorkspace = false;
backendLoader.sourceComponent = hyprlandComponent;
} else if (swaySock && swaySock.length > 0) {
isHyprland = false;
isNiri = false;
isSway = true;
isMango = false;
isLabwc = false;
isExtWorkspace = false;
isScroll = currentDesktop && currentDesktop.toLowerCase().includes("scroll");
backendLoader.sourceComponent = swayComponent;
} else {
// Always fallback to ext-workspace-v1
isHyprland = false;
isNiri = false;
isSway = false;
isMango = false;
isLabwc = false;
isExtWorkspace = true;
backendLoader.sourceComponent = extWorkspaceComponent;
Logger.i("CompositorService", "Using generic ext-workspace backend (no recognized compositor env)");
}
}
Loader {
id: backendLoader
onLoaded: {
if (item) {
if (isScroll) {
item.msgCommand = "scrollmsg";
}
root.backend = item;
setupBackendConnections();
backend.initialize();
}
}
}
// Load display scales from ShellState
function loadDisplayScalesFromState() {
try {
const cached = ShellState.getDisplay();
if (cached && Object.keys(cached).length > 0) {
displayScales = cached;
displayScalesLoaded = true;
Logger.d("CompositorService", "Loaded display scales from ShellState");
} else {
// Migration is now handled in Settings.qml
displayScalesLoaded = true;
}
} catch (error) {
Logger.e("CompositorService", "Failed to load display scales:", error);
displayScalesLoaded = true;
}
}
// Hyprland backend component
Component {
id: hyprlandComponent
HyprlandService {
id: hyprlandBackend
}
}
// Niri backend component
Component {
id: niriComponent
NiriService {
id: niriBackend
}
}
// Sway backend component
Component {
id: swayComponent
SwayService {
id: swayBackend
}
}
// Mango backend component
Component {
id: mangoComponent
MangoService {
id: mangoBackend
}
}
// Labwc backend component
Component {
id: labwcComponent
LabwcService {
id: labwcBackend
}
}
// Generic ext-workspace (WindowManager) when compositor env is unknown
Component {
id: extWorkspaceComponent
ExtWorkspaceService {
id: extWorkspaceBackend
}
}
function setupBackendConnections() {
if (!backend)
return;
// Connect backend signals to facade signals
backend.workspaceChanged.connect(() => {
// Sync workspaces when they change
syncWorkspaces();
// Forward the signal
workspaceChanged();
});
backend.activeWindowChanged.connect(() => {
// Only sync focus state, not entire window list
syncFocusedWindow();
// Forward the signal
activeWindowChanged();
});
backend.windowListChanged.connect(() => {
syncWindows();
});
// Property bindings - use automatic property change signal
backend.focusedWindowIndexChanged.connect(() => {
focusedWindowIndex = backend.focusedWindowIndex;
});
// Overview state (Niri-specific)
if (backend.overviewActiveChanged) {
backend.overviewActiveChanged.connect(() => {
overviewActive = backend.overviewActive;
});
}
// Initial sync
syncWorkspaces();
syncWindows();
focusedWindowIndex = backend.focusedWindowIndex;
if (backend.overviewActive !== undefined) {
overviewActive = backend.overviewActive;
}
if (backend.globalWorkspaces !== undefined) {
globalWorkspaces = backend.globalWorkspaces;
}
}
function syncWorkspaces() {
workspaces.clear();
const ws = backend.workspaces;
for (var i = 0; i < ws.count; i++) {
workspaces.append(ws.get(i));
}
// Emit signal to notify listeners that workspace list has been updated
workspacesChanged();
}
function syncWindows() {
windows.clear();
const ws = backend.windows;
for (var i = 0; i < ws.length; i++) {
windows.append(ws[i]);
}
// Emit signal to notify listeners that window list has been updated
windowListChanged();
}
// Sync only the focused window state, not the entire window list
function syncFocusedWindow() {
const newIndex = backend.focusedWindowIndex;
// Update isFocused flags by syncing from backend
for (var i = 0; i < windows.count && i < backend.windows.length; i++) {
const backendFocused = backend.windows[i].isFocused;
if (windows.get(i).isFocused !== backendFocused) {
windows.setProperty(i, "isFocused", backendFocused);
}
}
focusedWindowIndex = newIndex;
}
// Update display scales from backend
function updateDisplayScales() {
if (!backend || !backend.queryDisplayScales) {
Logger.w("CompositorService", "Backend does not support display scale queries");
return;
}
backend.queryDisplayScales();
}
// Called by backend when display scales are ready
function onDisplayScalesUpdated(scales) {
displayScales = scales;
saveDisplayScalesToCache();
Logger.d("CompositorService", "Display scales updated");
}
// Save display scales to cache
function saveDisplayScalesToCache() {
try {
ShellState.setDisplay(displayScales);
Logger.d("CompositorService", "Saved display scales to ShellState");
} catch (error) {
Logger.e("CompositorService", "Failed to save display scales:", error);
}
}
// Public function to get scale for a specific display
function getDisplayScale(displayName) {
if (!displayName || !displayScales[displayName]) {
return 1.0;
}
return displayScales[displayName].scale || 1.0;
}
// Public function to get all display info for a specific display
function getDisplayInfo(displayName) {
if (!displayName || !displayScales[displayName]) {
return null;
}
return displayScales[displayName];
}
// Get focused window
function getFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) {
return windows.get(focusedWindowIndex);
}
return null;
}
// Get focused screen from compositor
function getFocusedScreen() {
if (backend && backend.getFocusedScreen) {
return backend.getFocusedScreen();
}
return null;
}
// Get focused window title
function getFocusedWindowTitle() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) {
var title = windows.get(focusedWindowIndex).title;
if (title !== undefined) {
title = title.replace(/(\r\n|\n|\r)/g, "");
}
return title || "";
}
return "";
}
// Get clean app name from appId
// Extracts the last segment from reverse domain notation (e.g., "org.kde.dolphin" -> "Dolphin")
// Falls back to title if appId is empty
function getCleanAppName(appId, fallbackTitle) {
var name = (appId || "").split(".").pop() || fallbackTitle || "Unknown";
return name.charAt(0).toUpperCase() + name.slice(1);
}
function getWindowsForWorkspace(workspaceId) {
var windowsInWs = [];
for (var i = 0; i < windows.count; i++) {
var window = windows.get(i);
if (window.workspaceId === workspaceId) {
// Snapshot to plain JS object so callers never hold live ListModel
// proxies that become invalid when syncWindows() clears the model.
windowsInWs.push({
id: window.id,
title: window.title,
appId: window.appId,
isFocused: window.isFocused,
workspaceId: window.workspaceId,
handle: window.handle
});
}
}
return windowsInWs;
}
// Generic workspace switching
function switchToWorkspace(workspace) {
if (backend && backend.switchToWorkspace) {
backend.switchToWorkspace(workspace);
} else {
Logger.w("Compositor", "No backend available for workspace switching");
}
}
// Scrollable workspace content (Niri)
function scrollWorkspaceContent(direction) {
if (backend && backend.scrollWorkspaceContent) {
backend.scrollWorkspaceContent(direction);
}
}
// 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 active workspaces
function getActiveWorkspaces() {
const activeWorkspaces = [];
for (var i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i);
if (ws.isActive) {
activeWorkspaces.push(ws);
}
}
return activeWorkspaces;
}
// Set focused window
function focusWindow(window) {
if (backend && backend.focusWindow) {
backend.focusWindow(window);
} else {
Logger.w("Compositor", "No backend available for window focus");
}
}
// Close window
function closeWindow(window) {
if (backend && backend.closeWindow) {
backend.closeWindow(window);
} else {
Logger.w("Compositor", "No backend available for window closing");
}
}
// Spawn command
function spawn(command) {
// Ensure command is a proper JS array (QML lists can behave unexpectedly in some contexts)
const cmdArray = Array.isArray(command) ? command : (command && typeof command === "object" && command.length !== undefined) ? Array.from(command) : [command];
Logger.d("CompositorService", `Spawning: ${cmdArray.join(" ")}`);
if (backend && backend.spawn) {
backend.spawn(cmdArray);
} else {
try {
Quickshell.execDetached(cmdArray);
} catch (e) {
Logger.e("CompositorService", "Failed to execute detached:", e);
}
}
}
// Session management helper for custom commands
function getCustomCommand(action) {
const powerOptions = Settings.data.sessionMenu.powerOptions || [];
for (let i = 0; i < powerOptions.length; i++) {
const option = powerOptions[i];
if (option.action === action && option.enabled && option.command && option.command.trim() !== "") {
return option.command.trim();
}
}
return "";
}
function executeSessionAction(action, defaultCommand) {
const customCommand = getCustomCommand(action);
if (customCommand) {
Logger.i("Compositor", `Executing custom command for action: ${action} Command: ${customCommand}`);
Quickshell.execDetached(["sh", "-c", customCommand]);
return true;
}
return false;
}
// Session management
function logout() {
Logger.i("Compositor", "Logout requested");
if (executeSessionAction("logout"))
return;
if (backend && backend.logout) {
backend.logout();
} else {
Logger.w("Compositor", "No backend available for logout");
}
}
function shutdown() {
Logger.i("Compositor", "Shutdown requested");
if (executeSessionAction("shutdown"))
return;
HooksService.executeSessionHook("shutdown", () => {
Quickshell.execDetached(["sh", "-c", "systemctl poweroff || loginctl poweroff"]);
});
}
function reboot() {
Logger.i("Compositor", "Reboot requested");
if (executeSessionAction("reboot"))
return;
HooksService.executeSessionHook("reboot", () => {
Quickshell.execDetached(["sh", "-c", "systemctl reboot || loginctl reboot"]);
});
}
function userspaceReboot() {
Logger.i("Compositor", "Userspace reboot requested");
if (executeSessionAction("userspaceReboot"))
return;
HooksService.executeSessionHook("userspaceReboot", () => {
Quickshell.execDetached(["sh", "-c", "systemctl soft-reboot"]);
});
}
function rebootToUefi() {
Logger.i("Compositor", "Reboot to UEFI firmware requested requested");
if (executeSessionAction("rebootToUefi"))
return;
HooksService.executeSessionHook("rebootToUefi", () => {
Quickshell.execDetached(["sh", "-c", "systemctl reboot --firmware-setup || loginctl reboot --firmware-setup"]);
});
}
function turnOffMonitors() {
Logger.i("Compositor", "Turn off monitors requested");
if (backend && backend.turnOffMonitors) {
backend.turnOffMonitors();
} else {
Logger.w("Compositor", "No backend available for turnOffMonitors");
}
}
function turnOnMonitors() {
Logger.i("Compositor", "Turn on monitors requested");
if (backend && backend.turnOnMonitors) {
backend.turnOnMonitors();
} else {
Logger.w("Compositor", "No backend available for turnOnMonitors");
}
}
function suspend() {
Logger.i("Compositor", "Suspend requested");
if (executeSessionAction("suspend"))
return;
Quickshell.execDetached(["sh", "-c", "systemctl suspend || loginctl suspend"]);
}
function lock() {
Logger.i("Compositor", "LockScreen requested");
if (executeSessionAction("lock"))
return;
if (PanelService && PanelService.lockScreen) {
PanelService.lockScreen.active = true;
}
}
function hibernate() {
Logger.i("Compositor", "Hibernate requested");
if (executeSessionAction("hibernate"))
return;
Quickshell.execDetached(["sh", "-c", "systemctl hibernate || loginctl hibernate"]);
}
function cycleKeyboardLayout() {
if (backend && backend.cycleKeyboardLayout) {
backend.cycleKeyboardLayout();
}
}
property int lockAndSuspendCheckCount: 0
function lockAndSuspend() {
Logger.i("Compositor", "Lock and suspend requested");
// if a custom lock command exists, execute it and suspend without wait
if (executeSessionAction("lock")) {
suspend();
return;
}
// If already locked, suspend immediately
if (PanelService && PanelService.lockScreen && PanelService.lockScreen.active) {
Logger.i("Compositor", "Screen already locked, suspending");
suspend();
return;
}
// Lock the screen first
try {
if (PanelService && PanelService.lockScreen) {
PanelService.lockScreen.active = true;
lockAndSuspendCheckCount = 0;
// Wait for lock screen to be confirmed active before suspending
lockAndSuspendTimer.start();
} else {
Logger.w("Compositor", "Lock screen not available, suspending without lock");
suspend();
}
} catch (e) {
Logger.w("Compositor", "Failed to activate lock screen before suspend: " + e);
suspend();
}
}
Timer {
id: lockAndSuspendTimer
interval: 100
repeat: true
running: false
onTriggered: {
lockAndSuspendCheckCount++;
// Check if lock screen is now active
if (PanelService && PanelService.lockScreen && PanelService.lockScreen.active) {
// Verify the lock screen component is loaded
if (PanelService.lockScreen.item) {
Logger.i("Compositor", "Lock screen confirmed active, suspending");
stop();
lockAndSuspendCheckCount = 0;
suspend();
} else {
// Lock screen is active but component not loaded yet, wait a bit more
if (lockAndSuspendCheckCount > 20) {
// Max 2 seconds wait
Logger.w("Compositor", "Lock screen active but component not loaded, suspending anyway");
stop();
lockAndSuspendCheckCount = 0;
suspend();
}
}
} else {
// Lock screen not active yet, keep checking
if (lockAndSuspendCheckCount > 30) {
// Max 3 seconds wait
Logger.w("Compositor", "Lock screen failed to activate, suspending anyway");
stop();
lockAndSuspendCheckCount = 0;
suspend();
}
}
}
}
}
@@ -0,0 +1,300 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.WindowManager
import qs.Commons
// Generic ext-workspace-v1 + toplevel handling
Item {
id: root
property ListModel workspaces: ListModel {}
property var windows: []
property int focusedWindowIndex: -1
property var trackedToplevels: new Set()
property bool globalWorkspaces: false
property var nativeWorkspaceMap: ({})
property var connectedWorkspaces: ({})
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
function initialize() {
updateWindows();
connectWorkspaceSignals();
syncWorkspaces();
Logger.i("ExtWorkspaceService", "Service started (generic ext-workspace-v1)");
}
Connections {
target: WindowManager
function onWindowsetsChanged() {
root.connectWorkspaceSignals();
Qt.callLater(root.syncWorkspaces);
}
function onWindowsetProjectionsChanged() {
Qt.callLater(root.syncWorkspaces);
}
}
Timer {
interval: 500
running: true
repeat: false
onTriggered: {
if (WindowManager.windowsets.length > 0) {
root.connectWorkspaceSignals();
root.syncWorkspaces();
}
}
}
function connectWorkspaceSignals() {
const nativeWs = WindowManager.windowsets;
const newConnected = {};
for (const ws of nativeWs) {
const key = ws.id || ws.toString();
newConnected[key] = true;
if (connectedWorkspaces[key])
continue;
ws.activeChanged.connect(() => {
Qt.callLater(root.syncWorkspaces);
});
ws.urgentChanged.connect(() => {
Qt.callLater(root.syncWorkspaces);
});
ws.shouldDisplayChanged.connect(() => {
Qt.callLater(root.syncWorkspaces);
});
ws.nameChanged.connect(() => {
Qt.callLater(root.syncWorkspaces);
});
}
connectedWorkspaces = newConnected;
}
function syncWorkspaces() {
const nativeWs = WindowManager.windowsets;
workspaces.clear();
nativeWorkspaceMap = {};
/* Per-output (projection) index: compositors expose one workspace group per
* monitor with names "1"…"9". A single global idx produced 1018 on the
* second head because all windowsets were merged into one list. */
const perOutputNextIdx = {};
for (const ws of nativeWs) {
if (!ws.shouldDisplay) {
continue;
}
let outputName = "";
if (ws.projection) {
const projScreens = ws.projection.screens;
if (projScreens && projScreens.length > 0) {
outputName = projScreens[0].name || "";
}
}
const groupKey = outputName || "_";
let idx;
const numericName = ws.name && /^\d+$/.test(String(ws.name)) ? parseInt(ws.name, 10) : NaN;
if (!isNaN(numericName) && numericName >= 1) {
idx = numericName;
} else {
if (perOutputNextIdx[groupKey] === undefined) {
perOutputNextIdx[groupKey] = 1;
}
idx = perOutputNextIdx[groupKey]++;
}
const wsEntry = {
"id": ws.id || idx.toString(),
"idx": idx,
"name": ws.name || ("Workspace " + idx),
"output": outputName,
"isFocused": ws.active,
"isActive": true,
"isUrgent": ws.urgent,
"isOccupied": false,
"oid": ws.id || idx.toString()
};
workspaces.append(wsEntry);
nativeWorkspaceMap[wsEntry.id] = ws;
}
updateWindowWorkspaces();
workspaceChanged();
}
function updateWindowWorkspaces() {
let activeId = "";
for (let i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i);
if (ws.isFocused) {
activeId = ws.id;
break;
}
}
for (let i = 0; i < windows.length; i++) {
if (activeId) {
windows[i].workspaceId = activeId;
}
}
windowListChanged();
}
Connections {
target: ToplevelManager.toplevels
function onValuesChanged() {
updateWindows();
}
}
function connectToToplevel(toplevel) {
if (!toplevel)
return;
toplevel.activatedChanged.connect(() => {
Qt.callLater(onToplevelActivationChanged);
});
toplevel.titleChanged.connect(() => {
Qt.callLater(updateWindows);
});
}
function onToplevelActivationChanged() {
updateWindows();
activeWindowChanged();
}
function updateWindows() {
const newWindows = [];
const toplevels = ToplevelManager.toplevels?.values || [];
let focusedIdx = -1;
let idx = 0;
let activeId = "";
for (let i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i);
if (ws.isFocused) {
activeId = ws.id;
break;
}
}
for (const toplevel of toplevels) {
if (!toplevel)
continue;
if (!trackedToplevels.has(toplevel)) {
connectToToplevel(toplevel);
trackedToplevels.add(toplevel);
}
const output = (toplevel.screens && toplevel.screens.length > 0) ? (toplevel.screens[0].name || "") : "";
const windowId = (toplevel.appId || "") + ":" + idx;
newWindows.push({
"id": windowId,
"appId": toplevel.appId || "",
"title": toplevel.title || "",
"output": output,
"workspaceId": activeId || "1",
"isFocused": toplevel.activated || false,
"toplevel": toplevel
});
if (toplevel.activated) {
focusedIdx = idx;
}
idx++;
}
windows = newWindows;
focusedWindowIndex = focusedIdx;
windowListChanged();
}
function focusWindow(window) {
if (window.toplevel && typeof window.toplevel.activate === "function") {
window.toplevel.activate();
}
}
function closeWindow(window) {
if (window.toplevel && typeof window.toplevel.close === "function") {
window.toplevel.close();
}
}
function switchToWorkspace(workspace) {
const nativeWs = nativeWorkspaceMap[workspace.id] || nativeWorkspaceMap[workspace.oid];
if (nativeWs && nativeWs.canActivate) {
nativeWs.activate();
} else {
Logger.w("ExtWorkspaceService", "Cannot activate workspace: " + (workspace.name || workspace.id));
}
}
function turnOffMonitors() {
try {
Quickshell.execDetached(["wlr-randr", "--off"]);
} catch (e) {
Logger.e("ExtWorkspaceService", "Failed to turn off monitors:", e);
}
}
function turnOnMonitors() {
try {
Quickshell.execDetached(["wlr-randr", "--on"]);
} catch (e) {
Logger.e("ExtWorkspaceService", "Failed to turn on monitors:", e);
}
}
function logout() {
const sid = Quickshell.env("XDG_SESSION_ID");
try {
if (sid && sid.length > 0) {
Quickshell.execDetached(["loginctl", "terminate-session", sid]);
} else {
Logger.w("ExtWorkspaceService", "logout: XDG_SESSION_ID unset; use session menu custom command or compositor-specific backend");
}
} catch (e) {
Logger.e("ExtWorkspaceService", "Failed to logout:", e);
}
}
function cycleKeyboardLayout() {
Logger.w("ExtWorkspaceService", "Keyboard layout cycling not supported");
}
function queryDisplayScales() {
Logger.w("ExtWorkspaceService", "Display scale queries not supported via ToplevelManager");
}
function getFocusedScreen() {
return null;
}
}
@@ -0,0 +1,615 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Commons
import qs.Services.Keyboard
Item {
id: root
// Properties that match the facade interface
property ListModel workspaces: ListModel {}
property var windows: []
property int focusedWindowIndex: -1
// Signals that match the facade interface
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// Hyprland-specific properties
property bool initialized: false
property var workspaceCache: ({})
property var windowCache: ({})
// Debounce timer for window updates
Timer {
id: updateTimer
interval: 50
repeat: false
onTriggered: safeUpdate()
}
// Deferred via Qt.callLater to coalesce workspace updates: onRawEvent calls
// refreshWorkspaces() which triggers onValuesChanged synchronously in the
// same call stack — without deferral the ListModel gets cleared+repopulated
// twice per event. Qt.callLater deduplicates by function identity.
function _deferredWorkspaceUpdate() {
safeUpdateWorkspaces();
workspaceChanged();
}
// Initialization
function initialize() {
if (initialized)
return;
try {
Hyprland.refreshWorkspaces();
Hyprland.refreshToplevels();
Qt.callLater(() => {
safeUpdateWorkspaces();
safeUpdateWindows();
queryDisplayScales();
queryKeyboardLayout();
});
initialized = true;
Logger.i("HyprlandService", "Service started");
} catch (e) {
Logger.e("HyprlandService", "Failed to initialize:", e);
}
}
// Query display scales
function queryDisplayScales() {
hyprlandMonitorsProcess.running = true;
}
// Hyprland monitors process for display scale detection
Process {
id: hyprlandMonitorsProcess
running: false
command: ["hyprctl", "monitors", "-j"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
// Accumulate lines instead of parsing each one
hyprlandMonitorsProcess.accumulatedOutput += line;
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.e("HyprlandService", "Failed to query monitors, exit code:", exitCode);
accumulatedOutput = "";
return;
}
try {
const monitorsData = JSON.parse(accumulatedOutput);
const scales = {};
for (const monitor of monitorsData) {
if (monitor.name) {
scales[monitor.name] = {
"name": monitor.name,
"scale": monitor.scale || 1.0,
"width": monitor.width || 0,
"height": monitor.height || 0,
"refresh_rate": monitor.refreshRate || 0,
"x": monitor.x || 0,
"y": monitor.y || 0,
"active_workspace": monitor.activeWorkspace ? monitor.activeWorkspace.id : -1,
"vrr": monitor.vrr || false,
"focused": monitor.focused || false
};
}
}
// Notify CompositorService (it will emit displayScalesChanged)
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scales);
}
} catch (e) {
Logger.e("HyprlandService", "Failed to parse monitors:", e);
} finally {
// Clear accumulated output for next query
accumulatedOutput = "";
}
}
}
function queryKeyboardLayout() {
hyprlandDevicesProcess.running = true;
}
// Hyprland devices process for keyboard layout detection
Process {
id: hyprlandDevicesProcess
running: false
command: ["hyprctl", "devices", "-j"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
// Accumulate lines instead of parsing each one
hyprlandDevicesProcess.accumulatedOutput += line;
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.e("HyprlandService", "Failed to query devices, exit code:", exitCode);
accumulatedOutput = "";
return;
}
try {
const devicesData = JSON.parse(accumulatedOutput);
for (const keyboard of devicesData.keyboards) {
if (keyboard.main) {
const layoutName = keyboard.active_keymap;
KeyboardLayoutService.setCurrentLayout(layoutName);
Logger.d("HyprlandService", "Keyboard layout switched:", layoutName);
}
}
} catch (e) {
Logger.e("HyprlandService", "Failed to parse devices:", e);
} finally {
// Clear accumulated output for next query
accumulatedOutput = "";
}
}
}
// Safe update wrapper
function safeUpdate() {
safeUpdateWindows();
safeUpdateWorkspaces();
workspaceChanged();
windowListChanged();
}
// Safe workspace update
function safeUpdateWorkspaces() {
try {
workspaces.clear();
workspaceCache = {};
if (!Hyprland.workspaces || !Hyprland.workspaces.values) {
return;
}
const hlWorkspaces = Hyprland.workspaces.values;
const occupiedIds = getOccupiedWorkspaceIds();
for (var i = 0; i < hlWorkspaces.length; i++) {
const ws = hlWorkspaces[i];
if (ws.name && ws.name.startsWith("special:"))
continue;
const wsData = {
"id": ws.id,
"idx": ws.id,
"name": ws.name || "",
"output": (ws.monitor && ws.monitor.name) ? ws.monitor.name : "",
"isActive": ws.active === true,
"isFocused": ws.focused === true,
"isUrgent": ws.urgent === true,
"isOccupied": occupiedIds[ws.id] === true
};
workspaceCache[ws.id] = wsData;
workspaces.append(wsData);
}
} catch (e) {
Logger.e("HyprlandService", "Error updating workspaces:", e);
}
}
// Get occupied workspace IDs safely
function getOccupiedWorkspaceIds() {
const occupiedIds = {};
try {
if (!Hyprland.toplevels || !Hyprland.toplevels.values) {
return occupiedIds;
}
const hlToplevels = Hyprland.toplevels.values;
for (var i = 0; i < hlToplevels.length; i++) {
const toplevel = hlToplevels[i];
if (!toplevel)
continue;
try {
const wsId = toplevel.workspace ? toplevel.workspace.id : null;
if (wsId !== null && wsId !== undefined) {
occupiedIds[wsId] = true;
}
} catch (e)
// Ignore individual toplevel errors
{}
}
} catch (e)
// Return empty if we can't determine occupancy
{}
return occupiedIds;
}
// Safe window update
function safeUpdateWindows() {
try {
const windowsList = [];
windowCache = {};
if (!Hyprland.toplevels || !Hyprland.toplevels.values) {
windows = [];
focusedWindowIndex = -1;
return;
}
const hlToplevels = Hyprland.toplevels.values;
let focusedWindowId = null;
// Get active workspaces to filter focus
const activeWorkspaceIds = {};
if (Hyprland.workspaces && Hyprland.workspaces.values) {
const hlWorkspaces = Hyprland.workspaces.values;
for (var j = 0; j < hlWorkspaces.length; j++) {
if (hlWorkspaces[j].active) {
activeWorkspaceIds[hlWorkspaces[j].id] = true;
}
}
}
for (var i = 0; i < hlToplevels.length; i++) {
const toplevel = hlToplevels[i];
if (!toplevel)
continue;
const windowData = extractWindowData(toplevel);
if (windowData) {
// If the window claims to be focused, verify it's on an active workspace
if (windowData.isFocused) {
if (!activeWorkspaceIds[windowData.workspaceId]) {
windowData.isFocused = false;
}
}
// Normalize to a plain, backend-independent window object
const normalized = {
"id": windowData.id ? String(windowData.id) : "",
"title": windowData.title ? String(windowData.title) : "",
"appId": windowData.appId ? String(windowData.appId) : "",
"workspaceId": (typeof windowData.workspaceId === "number" && !isNaN(windowData.workspaceId)) ? windowData.workspaceId : -1,
"isFocused": windowData.isFocused === true,
"output": windowData.output ? String(windowData.output) : "",
"x": (typeof windowData.x === "number" && !isNaN(windowData.x)) ? windowData.x : 0,
"y": (typeof windowData.y === "number" && !isNaN(windowData.y)) ? windowData.y : 0
};
windowsList.push(normalized);
windowCache[normalized.id] = normalized;
if (normalized.isFocused) {
focusedWindowId = normalized.id;
}
}
}
windows = toSortedWindowList(windowsList);
// Resolve focused index from sorted list (order changes after sort)
let newFocusedIndex = -1;
if (focusedWindowId) {
for (let k = 0; k < windows.length; k++) {
if (windows[k].id === focusedWindowId) {
newFocusedIndex = k;
break;
}
}
}
if (newFocusedIndex !== focusedWindowIndex) {
focusedWindowIndex = newFocusedIndex;
activeWindowChanged();
}
} catch (e) {
Logger.e("HyprlandService", "Error updating windows:", e);
}
}
// Extract window data safely from a toplevel
function extractWindowData(toplevel) {
if (!toplevel)
return null;
try {
// Safely extract properties
const windowId = safeGetProperty(toplevel, "address", "");
if (!windowId)
return null;
const appId = getAppId(toplevel);
const title = getAppTitle(toplevel);
const wsId = toplevel.workspace ? toplevel.workspace.id : null;
const focused = toplevel.activated === true;
const output = toplevel.monitor?.name || "";
// Extract position
let x = 0;
let y = 0;
try {
const ipcData = toplevel.lastIpcObject;
if (ipcData && ipcData.at) {
x = ipcData.at[0];
y = ipcData.at[1];
} else if (typeof toplevel.x !== 'undefined') {
x = toplevel.x;
y = toplevel.y;
}
} catch (e) {}
// Normalize coordinates to safe numeric values
const safeX = (typeof x === "number" && !isNaN(x)) ? x : 0;
const safeY = (typeof y === "number" && !isNaN(y)) ? y : 0;
return {
"id": windowId,
"title": title,
"appId": appId,
"workspaceId": wsId || -1,
"isFocused": focused,
"output": output,
"x": safeX,
"y": safeY
};
} catch (e) {
return null;
}
}
function toSortedWindowList(windowList) {
return windowList.sort((a, b) => {
// Sort by workspace first (just in case they are mixed)
if (a.workspaceId !== b.workspaceId) {
return a.workspaceId - b.workspaceId;
}
// Then sort by X position (left to right)
if (a.x !== b.x) {
return a.x - b.x;
}
// Then sort by Y position (top to bottom)
if (a.y !== b.y) {
return a.y - b.y;
}
// Fallback to Window ID mapping
return a.id.localeCompare(b.id);
});
}
function getAppTitle(toplevel) {
try {
var title = toplevel.wayland.title;
if (title)
return title;
} catch (e) {}
return safeGetProperty(toplevel, "title", "");
}
function getAppId(toplevel) {
if (!toplevel)
return "";
var appId = "";
// Try the wayland object first!
// From my (Lemmy) testing it works fine so we could probably get rid of all the other attempts below.
// Leaving them in for now, just in case...
try {
appId = toplevel.wayland.appId;
if (appId)
return appId;
} catch (e) {}
// Try direct properties
appId = safeGetProperty(toplevel, "class", "");
if (appId)
return appId;
appId = safeGetProperty(toplevel, "initialClass", "");
if (appId)
return appId;
appId = safeGetProperty(toplevel, "appId", "");
if (appId)
return appId;
// Try lastIpcObject
try {
const ipcData = toplevel.lastIpcObject;
if (ipcData) {
return String(ipcData.class || ipcData.initialClass || ipcData.appId || ipcData.wm_class || "");
}
} catch (e) {}
return "";
}
// Safe property getter
function safeGetProperty(obj, prop, defaultValue) {
try {
const value = obj[prop];
if (value !== undefined && value !== null) {
return String(value);
}
} catch (e)
// Property access failed
{}
return defaultValue;
}
function handleActiveLayoutEvent(ev) {
try {
let beforeParenthesis;
const parenthesisPos = ev.lastIndexOf('(');
if (parenthesisPos === -1) {
beforeParenthesis = ev;
} else {
beforeParenthesis = ev.substring(0, parenthesisPos);
}
const layoutNameStart = beforeParenthesis.lastIndexOf(',') + 1;
const layoutName = ev.substring(layoutNameStart);
// Ignore bogus "error" layout reported by virtual keyboards (e.g. wtype)
if (layoutName.toLowerCase() === "error") {
Logger.d("HyprlandService", "Ignoring bogus 'error' layout from activelayout event");
return;
}
KeyboardLayoutService.setCurrentLayout(layoutName);
Logger.d("HyprlandService", "Keyboard layout switched:", layoutName);
} catch (e) {
Logger.e("HyprlandService", "Error handling activelayout:", e);
}
}
// Connections to Hyprland
Connections {
target: Hyprland.workspaces
enabled: initialized
function onValuesChanged() {
Qt.callLater(_deferredWorkspaceUpdate);
}
}
Connections {
target: Hyprland.toplevels
enabled: initialized
function onValuesChanged() {
updateTimer.restart();
}
}
Connections {
target: Hyprland
enabled: initialized
function onRawEvent(event) {
Hyprland.refreshWorkspaces();
Hyprland.refreshToplevels();
// Workspace and window updates are deferred — refreshWorkspaces()/
// refreshToplevels() trigger onValuesChanged which also calls
// Qt.callLater, so the deduplication coalesces into one update.
Qt.callLater(_deferredWorkspaceUpdate);
updateTimer.restart();
const monitorsEvents = ["configreloaded", "monitoradded", "monitorremoved", "monitoraddedv2", "monitorremovedv2"];
if (monitorsEvents.includes(event.name)) {
Qt.callLater(queryDisplayScales);
}
if (event.name == "activelayout") {
handleActiveLayoutEvent(event.data);
}
}
}
// Public functions
function switchToWorkspace(workspace) {
try {
if (workspace.name) {
Hyprland.dispatch(`workspace ${workspace.name}`);
return;
}
Hyprland.dispatch(`workspace ${workspace.idx}`);
} catch (e) {
Logger.e("HyprlandService", "Failed to switch workspace:", e);
}
}
function focusWindow(window) {
try {
if (!window || !window.id) {
Logger.w("HyprlandService", "Invalid window object for focus");
return;
}
const windowId = window.id.toString();
Hyprland.dispatch(`focuswindow address:0x${windowId}`);
Hyprland.dispatch(`alterzorder top,address:0x${windowId}`); // Bring the focused window to the top (essential for Float Mode)
} catch (e) {
Logger.e("HyprlandService", "Failed to switch window:", e);
}
}
function closeWindow(window) {
try {
Hyprland.dispatch(`killwindow address:0x${window.id}`);
} catch (e) {
Logger.e("HyprlandService", "Failed to close window:", e);
}
}
function turnOffMonitors() {
try {
Quickshell.execDetached(["hyprctl", "dispatch", "dpms", "off"]);
} catch (e) {
Logger.e("HyprlandService", "Failed to turn off monitors:", e);
}
}
function turnOnMonitors() {
try {
Quickshell.execDetached(["hyprctl", "dispatch", "dpms", "on"]);
} catch (e) {
Logger.e("HyprlandService", "Failed to turn on monitors:", e);
}
}
function logout() {
try {
Quickshell.execDetached(["hyprctl", "dispatch", "exit"]);
} catch (e) {
Logger.e("HyprlandService", "Failed to logout:", e);
}
}
function cycleKeyboardLayout() {
try {
Quickshell.execDetached(["hyprctl", "switchxkblayout", "all", "next"]);
} catch (e) {
Logger.e("HyprlandService", "Failed to cycle keyboard layout:", e);
}
}
function getFocusedScreen() {
const hyprMon = Hyprland.focusedMonitor;
if (hyprMon) {
const monitorName = hyprMon.name;
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === monitorName) {
return Quickshell.screens[i];
}
}
}
return null;
}
function spawn(command) {
try {
Quickshell.execDetached(["hyprctl", "dispatch", "--", "exec"].concat(command));
} catch (e) {
Logger.e("HyprlandService", "Failed to spawn command:", e);
}
}
}
@@ -0,0 +1,300 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.WindowManager
import qs.Commons
Item {
id: root
property ListModel workspaces: ListModel {}
property var windows: []
property int focusedWindowIndex: -1
property var trackedToplevels: new Set()
// LabWC typically has global workspaces (shared across all outputs)
property bool globalWorkspaces: true
// Map from native workspace id to the native Workspace object for activation
property var nativeWorkspaceMap: ({})
// Set of workspace objects we've already connected signals to
property var connectedWorkspaces: ({})
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
function initialize() {
updateWindows();
connectWorkspaceSignals();
syncWorkspaces();
Logger.i("LabwcService", "Service started (ext-workspace-v1)");
}
// Watch for windowsets being added/removed
Connections {
target: WindowManager
function onWindowsetsChanged() {
root.connectWorkspaceSignals();
Qt.callLater(root.syncWorkspaces);
}
function onWindowsetProjectionsChanged() {
Qt.callLater(root.syncWorkspaces);
}
}
// Re-check windowsets after a short delay - the Wayland protocol data
// may arrive after init and the changed signal can be missed
Timer {
interval: 500
running: true
repeat: false
onTriggered: {
if (WindowManager.windowsets.length > 0) {
root.connectWorkspaceSignals();
root.syncWorkspaces();
}
}
}
// Connect to property change signals on each native windowset object
function connectWorkspaceSignals() {
const nativeWs = WindowManager.windowsets;
const newConnected = {};
for (const ws of nativeWs) {
const key = ws.id || ws.toString();
newConnected[key] = true;
if (connectedWorkspaces[key])
continue;
ws.activeChanged.connect(() => {
Qt.callLater(root.syncWorkspaces);
});
ws.urgentChanged.connect(() => {
Qt.callLater(root.syncWorkspaces);
});
ws.shouldDisplayChanged.connect(() => {
Qt.callLater(root.syncWorkspaces);
});
ws.nameChanged.connect(() => {
Qt.callLater(root.syncWorkspaces);
});
}
connectedWorkspaces = newConnected;
}
function syncWorkspaces() {
const nativeWs = WindowManager.windowsets;
workspaces.clear();
nativeWorkspaceMap = {};
let idx = 1;
for (const ws of nativeWs) {
// Skip hidden workspaces (shouldDisplay = false means hidden)
if (!ws.shouldDisplay) {
continue;
}
// Find which outputs this windowset's projection spans
let outputName = "";
if (ws.projection) {
const projScreens = ws.projection.screens;
if (projScreens && projScreens.length > 0) {
outputName = projScreens[0].name || "";
}
}
const wsEntry = {
"id": ws.id || idx.toString(),
"idx": idx,
"name": ws.name || ("Workspace " + idx),
"output": outputName,
"isFocused": ws.active,
"isActive": true,
"isUrgent": ws.urgent,
"isOccupied": false,
"oid": ws.id || idx.toString()
};
workspaces.append(wsEntry);
nativeWorkspaceMap[wsEntry.id] = ws;
idx++;
}
// Update windows with workspace info
updateWindowWorkspaces();
workspaceChanged();
}
function updateWindowWorkspaces() {
// ext-workspace-v1 doesn't provide window-to-workspace mapping
// Assign all windows to the active workspace
let activeId = "";
for (let i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i);
if (ws.isFocused) {
activeId = ws.id;
break;
}
}
for (let i = 0; i < windows.length; i++) {
if (activeId) {
windows[i].workspaceId = activeId;
}
}
windowListChanged();
}
Connections {
target: ToplevelManager.toplevels
function onValuesChanged() {
updateWindows();
}
}
function connectToToplevel(toplevel) {
if (!toplevel)
return;
toplevel.activatedChanged.connect(() => {
Qt.callLater(onToplevelActivationChanged);
});
toplevel.titleChanged.connect(() => {
Qt.callLater(updateWindows);
});
}
function onToplevelActivationChanged() {
updateWindows();
activeWindowChanged();
}
function updateWindows() {
const newWindows = [];
const toplevels = ToplevelManager.toplevels?.values || [];
let focusedIdx = -1;
let idx = 0;
// Find active workspace id
let activeId = "";
for (let i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i);
if (ws.isFocused) {
activeId = ws.id;
break;
}
}
for (const toplevel of toplevels) {
if (!toplevel)
continue;
if (!trackedToplevels.has(toplevel)) {
connectToToplevel(toplevel);
trackedToplevels.add(toplevel);
}
// Get output name from toplevel's screen list
const output = (toplevel.screens && toplevel.screens.length > 0) ? (toplevel.screens[0].name || "") : "";
// Use appId + title as a stable id since Toplevel has no address property
const windowId = (toplevel.appId || "") + ":" + idx;
newWindows.push({
"id": windowId,
"appId": toplevel.appId || "",
"title": toplevel.title || "",
"output": output,
"workspaceId": activeId || "1",
"isFocused": toplevel.activated || false,
"toplevel": toplevel
});
if (toplevel.activated) {
focusedIdx = idx;
}
idx++;
}
windows = newWindows;
focusedWindowIndex = focusedIdx;
windowListChanged();
}
function focusWindow(window) {
if (window.toplevel && typeof window.toplevel.activate === "function") {
window.toplevel.activate();
}
}
function closeWindow(window) {
if (window.toplevel && typeof window.toplevel.close === "function") {
window.toplevel.close();
}
}
function switchToWorkspace(workspace) {
// Find the native Workspace object and activate it directly
const nativeWs = nativeWorkspaceMap[workspace.id] || nativeWorkspaceMap[workspace.oid];
if (nativeWs && nativeWs.canActivate) {
nativeWs.activate();
} else {
Logger.w("LabwcService", "Cannot activate workspace: " + (workspace.name || workspace.id));
}
}
function turnOffMonitors() {
try {
Quickshell.execDetached(["wlr-randr", "--off"]);
} catch (e) {
Logger.e("LabwcService", "Failed to turn off monitors:", e);
}
}
function turnOnMonitors() {
try {
Quickshell.execDetached(["wlr-randr", "--on"]);
} catch (e) {
Logger.e("LabwcService", "Failed to turn on monitors:", e);
}
}
function logout() {
try {
// Exit labwc by sending SIGTERM to $LABWC_PID or using --exit flag
Quickshell.execDetached(["sh", "-c", "labwc --exit || kill -s SIGTERM $LABWC_PID"]);
} catch (e) {
Logger.e("LabwcService", "Failed to logout:", e);
}
}
function cycleKeyboardLayout() {
Logger.w("LabwcService", "Keyboard layout cycling not supported");
}
function queryDisplayScales() {
Logger.w("LabwcService", "Display scale queries not supported via ToplevelManager");
}
function getFocusedScreen() {
// de-activated until proper testing
return null;
}
}
@@ -0,0 +1,491 @@
import QtQuick
import Quickshell
import Quickshell.DWL
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Services.Keyboard
Item {
id: root
// ===== PUBLIC INTERFACE (CompositorService compatibility) =====
property ListModel workspaces: ListModel {}
property var windows: []
property int focusedWindowIndex: -1
property bool initialized: false
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// ===== MANGOSERVICE-SPECIFIC PROPERTIES =====
property string selectedMonitor: ""
property string currentLayoutSymbol: ""
// ===== INTERNAL STATE =====
QtObject {
id: internal
// Window-to-tag persistence: Map<UniqueID, TagID>
property var windowTagMap: ({})
// Window-to-output persistence: Map<UniqueID, OutputName>
property var windowOutputMap: ({})
// Toplevel-to-ID mapping: Map<ToplevelObject, UniqueID>
property var toplevelIdMap: new Map()
property int windowIdCounter: 0
// Output name to index mapping for unique workspace IDs
property var outputIndices: ({})
property int outputCounter: 0
// Monitor scales: Map<OutputName, scale>
property var monitorScales: ({})
// Window signature for change detection
property string lastWindowSignature: ""
// Scale regex
readonly property var scalePattern: /^(\S+)\s+scale_factor\s+(\d+(?:\.\d+)?)$/
// Get all screen names mapped to their DwlIpcOutput
function getOutputMap() {
const map = {};
const screens = Quickshell.screens;
for (let i = 0; i < screens.length; i++) {
const name = screens[i].name;
const dwlOutput = DwlIpc.outputForName(name);
if (dwlOutput) {
map[name] = dwlOutput;
}
}
return map;
}
// ===== REBUILD WORKSPACES FROM DWL =====
function rebuildWorkspaces() {
if (!DwlIpc.available) {
return;
}
const outputMap = getOutputMap();
const workspaceList = [];
for (const outputName in outputMap) {
const output = outputMap[outputName];
// Assign stable index to output
if (internal.outputIndices[outputName] === undefined) {
internal.outputIndices[outputName] = internal.outputCounter++;
}
const outputIdx = internal.outputIndices[outputName];
// Track selected monitor and layout
if (output.active) {
root.selectedMonitor = outputName;
root.currentLayoutSymbol = output.layoutSymbol;
}
const tags = output.tags;
for (let ti = 0; ti < tags.length; ti++) {
const tag = tags[ti];
const tagId = tag.index + 1; // DwlTag.index is zero-based, our IDs are 1-based
const uniqueId = outputIdx * 100 + tagId;
workspaceList.push({
id: uniqueId,
idx: tagId,
name: tagId.toString(),
output: outputName,
isActive: tag.active,
isFocused: tag.active && output.active,
isUrgent: tag.urgent,
isOccupied: tag.clientCount > 0
});
}
}
// Sort by unique ID
workspaceList.sort((a, b) => a.id - b.id);
root.workspaces.clear();
for (let k = 0; k < workspaceList.length; k++) {
root.workspaces.append(workspaceList[k]);
}
root.workspaceChanged();
}
// ===== UPDATE WINDOWS =====
function updateWindows() {
if (!ToplevelManager.toplevels || !DwlIpc.available) {
return;
}
const outputMap = getOutputMap();
const toplevels = ToplevelManager.toplevels.values;
const windowList = [];
let newFocusedIdx = -1;
const currentWindows = new Set();
// Build per-output state from DWL
// Map<outputName, { title, appId, activeTagId }>
// Always populated (activeTagId needed for visible-window inference),
// title/appId may be empty if no window is focused.
const outputState = {};
for (const outputName in outputMap) {
const output = outputMap[outputName];
// Find active tag for this output
let activeTagId = 1;
const tags = output.tags;
for (let ti = 0; ti < tags.length; ti++) {
if (tags[ti].active) {
activeTagId = tags[ti].index + 1;
break;
}
}
outputState[outputName] = {
title: output.title || "",
appId: output.appId || "",
activeTagId: activeTagId
};
// Ensure output index exists
if (internal.outputIndices[outputName] === undefined) {
internal.outputIndices[outputName] = internal.outputCounter++;
}
}
for (let i = 0; i < toplevels.length; i++) {
const toplevel = toplevels[i];
if (!toplevel || toplevel.outliers) {
continue;
}
const appId = toplevel.appId || toplevel.wayland?.appId || "";
const title = toplevel.title || toplevel.wayland?.title || "";
const isFocused = toplevel.activated;
// Get or assign a stable ID
let windowId;
if (internal.toplevelIdMap.has(toplevel)) {
windowId = internal.toplevelIdMap.get(toplevel);
} else {
windowId = `win-${internal.windowIdCounter++}`;
internal.toplevelIdMap.set(toplevel, windowId);
}
currentWindows.add(windowId);
// Determine output
let outputName;
// Priority 1: Focused window matched to DWL output metadata
if (isFocused && (title || appId)) {
for (const oName in outputState) {
const os = outputState[oName];
if ((os.title || os.appId) && title === os.title && appId === os.appId) {
outputName = oName;
internal.windowOutputMap[windowId] = oName;
break;
}
}
}
// Priority 2: Remembered output
if (!outputName && internal.windowOutputMap[windowId]) {
outputName = internal.windowOutputMap[windowId];
}
// Priority 3: toplevel.screens (wlr-foreign-toplevel visible screens)
if (!outputName && toplevel.screens && toplevel.screens.length > 0) {
outputName = toplevel.screens[0].name;
}
// Fallback: selected monitor
if (!outputName) {
outputName = root.selectedMonitor || "DP-1";
}
// Determine tag
let tagId = null;
const os = outputState[outputName];
if (isFocused && os && !os.consumed && (os.title || os.appId) && title === os.title && appId === os.appId) {
// Focused window: assign to the active tag from DWL metadata
tagId = os.activeTagId;
internal.windowTagMap[windowId] = tagId;
// Consume so a second toplevel with identical title+appId cannot also claim focus
os.consumed = true;
} else if (internal.windowTagMap[windowId] !== undefined) {
// Previously seen window: use remembered tag
tagId = internal.windowTagMap[windowId];
}
if (tagId === null) {
// DWL only reports the focused window per output, so we can't
// determine the tag for unfocused windows until they gain focus.
continue;
}
// Convert to unique workspace ID
const outputIdx = internal.outputIndices[outputName];
if (outputIdx === undefined) {
Logger.e("MangoService", "No output index for", outputName);
continue;
}
const workspaceId = outputIdx * 100 + tagId;
windowList.push({
id: `${outputName}:${appId}:${title}:${i}`,
title: title,
appId: appId,
class: appId,
workspaceId: workspaceId,
isFocused: isFocused,
output: outputName,
handle: toplevel,
fullscreen: toplevel.fullscreen || false,
floating: toplevel.maximized === false && toplevel.fullscreen === false
});
if (isFocused) {
newFocusedIdx = windowList.length - 1;
}
}
// Clean up stale window tracking
if (Object.keys(internal.windowTagMap).length > toplevels.length + 20) {
const newTagMap = {};
const newOutputMap = {};
for (const windowId of currentWindows) {
if (internal.windowTagMap[windowId] !== undefined) {
newTagMap[windowId] = internal.windowTagMap[windowId];
}
if (internal.windowOutputMap[windowId] !== undefined) {
newOutputMap[windowId] = internal.windowOutputMap[windowId];
}
}
internal.windowTagMap = newTagMap;
internal.windowOutputMap = newOutputMap;
}
// Check if window list changed
const signature = JSON.stringify(windowList.map(w => w.id + w.workspaceId + w.isFocused));
if (signature !== internal.lastWindowSignature) {
internal.lastWindowSignature = signature;
root.windows = windowList;
root.windowListChanged();
}
if (newFocusedIdx !== root.focusedWindowIndex) {
root.focusedWindowIndex = newFocusedIdx;
root.activeWindowChanged();
}
}
// ===== PROCESS SCALES =====
function processScales(output) {
const lines = output.trim().split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const match = line.match(scalePattern);
if (match) {
const outputName = match[1];
const scale = parseFloat(match[2]);
internal.monitorScales[outputName] = scale;
}
}
const scalesMap = {};
for (const name in internal.monitorScales) {
scalesMap[name] = {
name: name,
scale: internal.monitorScales[name] || 1.0,
width: 0,
height: 0,
x: 0,
y: 0
};
}
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scalesMap);
}
root.displayScalesChanged();
}
}
// ===== DWL CONNECTIONS =====
// React to DWL frame events on each output (atomic state updates)
Instantiator {
model: DwlIpc.outputs
delegate: Connections {
required property DwlIpcOutput modelData
target: modelData
function onFrame() {
internal.rebuildWorkspaces();
internal.updateWindows();
}
function onKbLayoutChanged() {
if (KeyboardLayoutService) {
KeyboardLayoutService.setCurrentLayout(modelData.kbLayout);
}
}
}
}
// ===== PROCESSES =====
// Scale query (mmsg -g -A) - DWL doesn't provide scale info
property QtObject _scaleQuery: Process {
id: scaleQuery
command: ["mmsg", "-g", "-A"]
property string buffer: ""
stdout: SplitParser {
onRead: line => {
scaleQuery.buffer += line + "\n";
}
}
onExited: code => {
if (code === 0) {
internal.processScales(scaleQuery.buffer);
scaleQuery.buffer = "";
}
}
}
// ===== TOPLEVEL MANAGER CONNECTION =====
property QtObject _toplevelConnection: Connections {
target: ToplevelManager.toplevels
function onValuesChanged() {
internal.updateWindows();
}
}
// ===== PUBLIC FUNCTIONS =====
function initialize() {
if (initialized) {
return;
}
Logger.i("MangoService", "Initializing MangoWC/DWL compositor integration (DWL protocol)");
// Query display scales (only thing still needing mmsg)
scaleQuery.running = true;
// Initial build from DWL state
if (DwlIpc.available && DwlIpc.outputs.length > 0) {
internal.rebuildWorkspaces();
internal.updateWindows();
}
initialized = true;
}
function queryDisplayScales() {
scaleQuery.running = true;
}
function switchToWorkspace(workspace) {
const tagId = workspace.idx || workspace.id || 1;
const outputName = workspace.output || root.selectedMonitor || "";
// Use DWL protocol to switch tags
const dwlOutput = DwlIpc.outputForName(outputName);
if (dwlOutput) {
dwlOutput.setTags(1 << (tagId - 1)); // tagId is 1-based, bitmask is 0-based
} else {
// Fallback to mmsg
const cmd = ["mmsg", "-s", "-t", tagId.toString()];
if (outputName && Object.keys(internal.monitorScales).length > 1) {
cmd.push("-o", outputName);
}
Quickshell.execDetached(cmd);
}
}
function focusWindow(window) {
if (window && window.handle) {
window.handle.activate();
} else if (window.workspaceId) {
switchToWorkspace({
id: window.workspaceId,
output: window.output
});
}
}
function closeWindow(window) {
if (window && window.handle) {
window.handle.close();
} else {
Quickshell.execDetached(["mmsg", "-s", "-d", "killclient"]);
}
}
function turnOffMonitors() {
const screens = Quickshell.screens;
const cmds = [];
for (let i = 0; i < screens.length; i++) {
cmds.push("mmsg -s -d disable_monitor," + screens[i].name);
}
if (cmds.length > 0) {
Quickshell.execDetached(["sh", "-c", cmds.join(" && ")]);
}
}
function turnOnMonitors() {
const screens = Quickshell.screens;
const cmds = [];
for (let i = 0; i < screens.length; i++) {
cmds.push("mmsg -s -d enable_monitor," + screens[i].name);
}
if (cmds.length > 0) {
Quickshell.execDetached(["sh", "-c", cmds.join(" && ")]);
}
}
function logout() {
Quickshell.execDetached(["mmsg", "-s", "-q"]);
}
function cycleKeyboardLayout() {
Logger.w("MangoService", "Keyboard layout cycling not supported");
}
function getFocusedScreen() {
return null;
}
function spawn(command) {
try {
Quickshell.execDetached(["mmsg", "-s", "-d", "spawn_shell," + command.join(" ")]);
} catch (e) {
Logger.e("MangoService", "Failed to spawn command:", e);
}
}
}
@@ -0,0 +1,294 @@
import QtQuick
import Quickshell
import Quickshell.Niri
import qs.Commons
import qs.Services.Keyboard
Item {
id: root
property int floatingWindowPosition: Number.MAX_SAFE_INTEGER
property ListModel workspaces: ListModel {}
property var windows: []
property int focusedWindowIndex: -1
property bool overviewActive: false
property var keyboardLayouts: []
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
property var outputCache: ({})
property var workspaceCache: ({})
function initialize() {
Niri.refreshOutputs();
Niri.refreshWorkspaces();
Niri.refreshWindows();
Qt.callLater(() => {
safeUpdateOutputs();
safeUpdateWorkspaces();
safeUpdateWindows();
queryDisplayScales();
});
Logger.i("NiriService", "Service started");
}
// Connections to the C++ Niri IPC module
Connections {
target: Niri
function onWorkspacesUpdated() {
safeUpdateWorkspaces();
workspaceChanged();
}
function onWindowsUpdated() {
safeUpdateWindows();
windowListChanged();
activeWindowChanged();
}
function onOutputsUpdated() {
safeUpdateOutputs();
queryDisplayScales();
}
function onOverviewActiveChanged() {
overviewActive = Niri.overviewActive;
}
function onKeyboardLayoutsChanged() {
keyboardLayouts = Niri.keyboardLayoutNames;
const layoutName = Niri.currentKeyboardLayoutName;
if (layoutName) {
KeyboardLayoutService.setCurrentLayout(layoutName);
}
Logger.d("NiriService", "Keyboard layouts changed:", keyboardLayouts.toString());
}
function onKeyboardLayoutSwitched() {
const layoutName = Niri.currentKeyboardLayoutName;
if (layoutName) {
KeyboardLayoutService.setCurrentLayout(layoutName);
}
Logger.d("NiriService", "Keyboard layout switched:", layoutName);
}
}
function safeUpdateOutputs() {
const niriOutputs = Niri.outputs.values;
outputCache = {};
for (var i = 0; i < niriOutputs.length; i++) {
const output = niriOutputs[i];
outputCache[output.name] = {
"name": output.name,
"connected": output.connected,
"scale": output.scale,
"width": output.width,
"height": output.height,
"x": output.x,
"y": output.y,
"physical_width": output.physicalWidth,
"physical_height": output.physicalHeight,
"refresh_rate": output.refreshRate,
"vrr_supported": output.vrrSupported,
"vrr_enabled": output.vrrEnabled,
"transform": output.transform
};
}
}
function safeUpdateWorkspaces() {
const niriWorkspaces = Niri.workspaces.values;
workspaceCache = {};
const workspacesList = [];
for (var i = 0; i < niriWorkspaces.length; i++) {
const ws = niriWorkspaces[i];
const wsData = {
"id": ws.id,
"idx": ws.idx,
"name": ws.name,
"output": ws.output,
"isFocused": ws.focused,
"isActive": ws.active,
"isUrgent": ws.urgent,
"isOccupied": ws.occupied
};
workspacesList.push(wsData);
workspaceCache[ws.id] = wsData;
}
// Workspaces come pre-sorted from C++ (by output then idx)
workspaces.clear();
for (var j = 0; j < workspacesList.length; j++) {
workspaces.append(workspacesList[j]);
}
}
function getWindowOutput(win) {
for (var i = 0; i < workspaces.count; i++) {
if (workspaces.get(i).id === win.workspaceId) {
return workspaces.get(i).output;
}
}
return null;
}
function toSortedWindowList(windowList) {
return windowList.map(win => {
const workspace = workspaceCache[win.workspaceId];
const output = (workspace && workspace.output) ? outputCache[workspace.output] : null;
return {
window: win,
workspaceIdx: workspace ? workspace.idx : 0,
outputX: output ? output.x : 0,
outputY: output ? output.y : 0
};
}).sort((a, b) => {
// Sort by output position first
if (a.outputX !== b.outputX) {
return a.outputX - b.outputX;
}
if (a.outputY !== b.outputY) {
return a.outputY - b.outputY;
}
// Then by workspace index
if (a.workspaceIdx !== b.workspaceIdx) {
return a.workspaceIdx - b.workspaceIdx;
}
// Then by window position
if (a.window.position.x !== b.window.position.x) {
return a.window.position.x - b.window.position.x;
}
if (a.window.position.y !== b.window.position.y) {
return a.window.position.y - b.window.position.y;
}
// Finally by window ID to ensure consistent ordering
return a.window.id - b.window.id;
}).map(info => info.window);
}
function safeUpdateWindows() {
const niriWindows = Niri.windows.values;
const windowsList = [];
for (var i = 0; i < niriWindows.length; i++) {
const win = niriWindows[i];
windowsList.push({
"id": win.id,
"title": win.title || "",
"appId": win.appId || "",
"workspaceId": win.workspaceId || -1,
"isFocused": win.focused,
"output": win.output || getWindowOutput(win) || "",
"position": {
"x": win.isFloating ? floatingWindowPosition : win.positionX,
"y": win.isFloating ? floatingWindowPosition : win.positionY
}
});
}
windows = toSortedWindowList(windowsList);
safeUpdateFocusedWindow();
}
function safeUpdateFocusedWindow() {
focusedWindowIndex = -1;
for (var i = 0; i < windows.length; i++) {
if (windows[i].isFocused) {
focusedWindowIndex = i;
break;
}
}
}
function queryDisplayScales() {
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(outputCache);
}
}
function switchToWorkspace(workspace) {
try {
Niri.dispatch(["focus-workspace", workspace.idx.toString()]);
} catch (e) {
Logger.e("NiriService", "Failed to switch workspace:", e);
}
}
function scrollWorkspaceContent(direction) {
try {
var action = direction < 0 ? "focus-column-left" : "focus-column-right";
Niri.dispatch([action]);
} catch (e) {
Logger.e("NiriService", "Failed to scroll workspace content:", e);
}
}
function focusWindow(window) {
try {
Niri.dispatch(["focus-window", "--id", window.id.toString()]);
} catch (e) {
Logger.e("NiriService", "Failed to switch window:", e);
}
}
function closeWindow(window) {
try {
Niri.dispatch(["close-window", "--id", window.id.toString()]);
} catch (e) {
Logger.e("NiriService", "Failed to close window:", e);
}
}
function turnOffMonitors() {
try {
Niri.dispatch(["power-off-monitors"]);
} catch (e) {
Logger.e("NiriService", "Failed to turn off monitors:", e);
}
}
function turnOnMonitors() {
try {
Niri.dispatch(["power-on-monitors"]);
} catch (e) {
Logger.e("NiriService", "Failed to turn on monitors:", e);
}
}
function logout() {
try {
Niri.dispatch(["quit", "--skip-confirmation"]);
} catch (e) {
Logger.e("NiriService", "Failed to logout:", e);
}
}
function cycleKeyboardLayout() {
try {
Niri.dispatch(["switch-layout", "next"]);
} catch (e) {
Logger.e("NiriService", "Failed to cycle keyboard layout:", e);
}
}
function getFocusedScreen() {
// On niri the code below only works when you have an actual app selected on that screen.
return null;
}
function spawn(command) {
try {
const niriArgs = ["spawn", "--"].concat(command);
Logger.d("NiriService", "Calling niri spawn: niri msg action " + niriArgs.join(" "));
Niri.dispatch(niriArgs);
} catch (e) {
Logger.e("NiriService", "Failed to spawn command:", e);
}
}
}
@@ -0,0 +1,593 @@
import QtQuick
import Quickshell
import Quickshell.I3
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Services.Keyboard
Item {
id: root
// Configurable IPC command name (overridden to "scrollmsg" for Scroll)
property string msgCommand: "swaymsg"
// Properties that match the facade interface
property ListModel workspaces: ListModel {}
property var windows: []
property int focusedWindowIndex: -1
// Signals that match the facade interface
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// I3-specific properties
property bool initialized: false
// Cache for window-to-workspace mapping
property var windowWorkspaceMap: ({})
// Track window usage counts per workspace to handle duplicates
property var windowUsageCountsPerWorkspace: ({})
// Debounce timer for updates
Timer {
id: updateTimer
interval: 50
repeat: false
onTriggered: safeUpdate()
}
// Initialization
function initialize() {
if (initialized)
return;
try {
I3.refreshWorkspaces();
Qt.callLater(() => {
safeUpdateWorkspaces();
queryWindowWorkspaces();
queryDisplayScales();
queryKeyboardLayout();
});
initialized = true;
Logger.i("SwayService", "Service started");
} catch (e) {
Logger.e("SwayService", "Failed to initialize:", e);
}
}
// Query window-to-workspace mapping via IPC
function queryWindowWorkspaces() {
swayTreeProcess.running = true;
}
// Sway tree process for getting window workspace information
Process {
id: swayTreeProcess
running: false
command: [msgCommand, "-t", "get_tree", "-r"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
swayTreeProcess.accumulatedOutput += line;
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.e("SwayService", "Failed to query tree, exit code:", exitCode);
accumulatedOutput = "";
return;
}
try {
const treeData = JSON.parse(accumulatedOutput);
const newMap = {};
const workspaceWindows = {}; // Track windows per workspace
// Recursively find all windows and their workspaces
function traverseTree(node, workspaceNum) {
if (!node)
return;
// If this is a workspace node, update the workspace number
if (node.type === "workspace" && node.num !== undefined) {
workspaceNum = node.num;
if (!workspaceWindows[workspaceNum]) {
workspaceWindows[workspaceNum] = [];
}
}
// If this is a regular or floating container with app_id/class (i.e., a window)
if ((node.type === "con" || node.type === "floating_con") && (node.app_id || node.window_properties)) {
const appId = node.app_id || (node.window_properties ? node.window_properties.class : null);
const title = node.name || "";
const id = node.id;
if (appId && workspaceNum !== undefined && workspaceNum >= 0) {
// Store window info for this workspace
workspaceWindows[workspaceNum].push({
appId: appId,
title: title,
id: id
});
}
}
// Traverse children
if (node.nodes && node.nodes.length > 0) {
for (const child of node.nodes) {
traverseTree(child, workspaceNum);
}
}
// Traverse floating nodes
if (node.floating_nodes && node.floating_nodes.length > 0) {
for (const child of node.floating_nodes) {
traverseTree(child, workspaceNum);
}
}
}
traverseTree(treeData, -1);
// Now build the map with workspace-specific keys
for (const wsNum in workspaceWindows) {
const windows = workspaceWindows[wsNum];
const appTitleCounts = {}; // Count occurrences of each appId:title in this workspace
for (const win of windows) {
const baseKey = `${win.appId}:${win.title}`;
// Track how many times we've seen this appId:title combo in this workspace
if (!appTitleCounts[baseKey]) {
appTitleCounts[baseKey] = 0;
}
const occurrence = appTitleCounts[baseKey];
appTitleCounts[baseKey]++;
// Create unique key with workspace and occurrence index
const uniqueKey = `ws${wsNum}:${baseKey}[${occurrence}]`;
newMap[uniqueKey] = parseInt(wsNum);
// Also store by ID if available (most reliable)
if (win.id) {
newMap[`id:${win.id}`] = parseInt(wsNum);
}
}
}
windowWorkspaceMap = newMap;
// Update windows with new workspace information
Qt.callLater(safeUpdateWindows);
} catch (e) {
Logger.e("SwayService", "Failed to parse tree:", e);
} finally {
accumulatedOutput = "";
}
}
}
// Query display scales
function queryDisplayScales() {
swayOutputsProcess.running = true;
}
// Sway outputs process for display scale detection
Process {
id: swayOutputsProcess
running: false
command: [msgCommand, "-t", "get_outputs", "-r"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
swayOutputsProcess.accumulatedOutput += line;
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.e("SwayService", "Failed to query outputs, exit code:", exitCode);
accumulatedOutput = "";
return;
}
try {
const outputsData = JSON.parse(accumulatedOutput);
const scales = {};
for (const output of outputsData) {
if (output.name) {
scales[output.name] = {
"name": output.name,
"scale": output.scale || 1.0,
"width": output.current_mode ? output.current_mode.width : 0,
"height": output.current_mode ? output.current_mode.height : 0,
"refresh_rate": output.current_mode ? output.current_mode.refresh : 0,
"x": output.rect ? output.rect.x : 0,
"y": output.rect ? output.rect.y : 0,
"active": output.active || false,
"focused": output.focused || false,
"current_workspace": output.current_workspace || ""
};
}
}
// Notify CompositorService (it will emit displayScalesChanged)
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scales);
}
} catch (e) {
Logger.e("SwayService", "Failed to parse outputs:", e);
} finally {
// Clear accumulated output for next query
accumulatedOutput = "";
}
}
}
function queryKeyboardLayout() {
swayInputsProcess.running = true;
}
// Sway inputs process for keyboard layout detection
Process {
id: swayInputsProcess
running: false
command: [msgCommand, "-t", "get_inputs", "-r"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
// Accumulate lines instead of parsing each one
swayInputsProcess.accumulatedOutput += line;
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.e("SwayService", "Failed to query inputs, exit code:", exitCode);
accumulatedOutput = "";
return;
}
try {
const inputsData = JSON.parse(accumulatedOutput);
for (const input of inputsData) {
if (input.type == "keyboard") {
const layoutName = input.xkb_active_layout_name;
KeyboardLayoutService.setCurrentLayout(layoutName);
Logger.d("SwayService", "Keyboard layout switched:", layoutName);
break;
}
}
} catch (e) {
Logger.e("SwayService", "Failed to parse inputs:", e);
} finally {
// Clear accumulated output for next query
accumulatedOutput = "";
}
}
}
// Safe update wrapper
function safeUpdate() {
queryWindowWorkspaces();
safeUpdateWorkspaces();
}
// Safe workspace update
function safeUpdateWorkspaces() {
try {
workspaces.clear();
if (!I3.workspaces || !I3.workspaces.values) {
return;
}
const hlWorkspaces = I3.workspaces.values;
for (var i = 0; i < hlWorkspaces.length; i++) {
const ws = hlWorkspaces[i];
if (!ws || ws.id < 1)
continue;
const wsData = {
"id": i,
"idx": ws.num,
"name": ws.name || "",
"output": (ws.monitor && ws.monitor.name) ? ws.monitor.name : "",
"isActive": ws.active === true,
"isFocused": ws.focused === true,
"isUrgent": ws.urgent === true,
"isOccupied": true,
"handle": ws
};
workspaces.append(wsData);
}
} catch (e) {
Logger.e("SwayService", "Error updating workspaces:", e);
}
}
// Safe window update
function safeUpdateWindows() {
try {
const windowsList = [];
// Reset usage counts per workspace before processing windows
windowUsageCountsPerWorkspace = {};
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) {
windows = [];
focusedWindowIndex = -1;
windowListChanged();
return;
}
const hlToplevels = ToplevelManager.toplevels.values;
let newFocusedIndex = -1;
for (var i = 0; i < hlToplevels.length; i++) {
const toplevel = hlToplevels[i];
if (!toplevel)
continue;
const windowData = extractWindowData(toplevel);
if (windowData) {
windowsList.push(windowData);
if (windowData.isFocused) {
newFocusedIndex = windowsList.length - 1;
}
}
}
windows = windowsList;
if (newFocusedIndex !== focusedWindowIndex) {
focusedWindowIndex = newFocusedIndex;
activeWindowChanged();
}
windowListChanged();
} catch (e) {
Logger.e("SwayService", "Error updating windows:", e);
}
}
// Extract window data safely from a toplevel
function extractWindowData(toplevel) {
if (!toplevel)
return null;
try {
// Safely extract properties
const appId = getAppId(toplevel);
const title = safeGetProperty(toplevel, "title", "");
const focused = toplevel.activated === true;
// Try to find workspace ID from our cached map by trying all workspaces
let workspaceId = -1;
let foundWorkspaceNum = -1;
// Build base key for this window
const baseKey = `${appId}:${title}`;
// Try to find this window in any workspace
for (var i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i);
if (!ws)
continue;
const wsNum = ws.idx;
// Initialize usage count for this workspace if needed
if (!windowUsageCountsPerWorkspace[wsNum]) {
windowUsageCountsPerWorkspace[wsNum] = {};
}
// Get current usage count for this appId:title in this workspace
if (!windowUsageCountsPerWorkspace[wsNum][baseKey]) {
windowUsageCountsPerWorkspace[wsNum][baseKey] = 0;
}
const occurrence = windowUsageCountsPerWorkspace[wsNum][baseKey];
const uniqueKey = `ws${wsNum}:${baseKey}[${occurrence}]`;
// Check if this key exists in our map
if (windowWorkspaceMap[uniqueKey] !== undefined) {
foundWorkspaceNum = windowWorkspaceMap[uniqueKey];
workspaceId = ws.id;
// Increment the usage count for this workspace
windowUsageCountsPerWorkspace[wsNum][baseKey]++;
break;
}
}
return {
"title": title,
"appId": appId,
"isFocused": focused,
"workspaceId": workspaceId,
"handle": toplevel
};
} catch (e) {
return null;
}
}
function getAppId(toplevel) {
if (!toplevel)
return "";
return toplevel.appId;
}
// Safe property getter
function safeGetProperty(obj, prop, defaultValue) {
try {
const value = obj[prop];
if (value !== undefined && value !== null) {
return String(value);
}
} catch (e)
// Property access failed
{}
return defaultValue;
}
function handleInputEvent(ev) {
try {
const eventData = JSON.parse(ev);
if (eventData.change == "xkb_layout" && eventData.input != null) {
const input = eventData.input;
if (input.type == "keyboard" && input.xkb_active_layout_name != null) {
const layoutName = input.xkb_active_layout_name;
KeyboardLayoutService.setCurrentLayout(layoutName);
Logger.d("SwayService", "Keyboard layout switched:", layoutName);
}
}
} catch (e) {
Logger.e("SwayService", "Error handling input event:", e);
}
}
// Connections to I3
Connections {
target: I3.workspaces
enabled: initialized
function onValuesChanged() {
safeUpdateWorkspaces();
workspaceChanged();
}
}
Connections {
target: ToplevelManager
enabled: initialized
function onActiveToplevelChanged() {
updateTimer.restart();
}
}
// Some programs change title of window dependent on content
Connections {
target: ToplevelManager ? ToplevelManager.activeToplevel : null
enabled: initialized
function onTitleChanged() {
updateTimer.restart();
}
}
Connections {
target: I3
enabled: initialized
function onRawEvent(event) {
safeUpdateWorkspaces();
workspaceChanged();
updateTimer.restart();
if (event.type === "output") {
Qt.callLater(queryDisplayScales);
}
}
}
I3IpcListener {
subscriptions: ["input"]
onIpcEvent: function (event) {
handleInputEvent(event.data);
}
}
// Public functions
function switchToWorkspace(workspace) {
try {
workspace.handle.activate();
} catch (e) {
Logger.e("SwayService", "Failed to switch workspace:", e);
}
}
function focusWindow(window) {
try {
window.handle.activate();
} catch (e) {
Logger.e("SwayService", "Failed to switch window:", e);
}
}
function closeWindow(window) {
try {
window.handle.close();
} catch (e) {
Logger.e("SwayService", "Failed to close window:", e);
}
}
function turnOffMonitors() {
try {
Quickshell.execDetached([msgCommand, "output", "*", "dpms", "off"]);
} catch (e) {
Logger.e("SwayService", "Failed to turn off monitors:", e);
}
}
function turnOnMonitors() {
try {
Quickshell.execDetached([msgCommand, "output", "*", "dpms", "on"]);
} catch (e) {
Logger.e("SwayService", "Failed to turn on monitors:", e);
}
}
function logout() {
try {
Quickshell.execDetached([msgCommand, "exit"]);
} catch (e) {
Logger.e("SwayService", "Failed to logout:", e);
}
}
function cycleKeyboardLayout() {
try {
Quickshell.execDetached([msgCommand, "input", "type:keyboard", "xkb_switch_layout", "next"]);
} catch (e) {
Logger.e("SwayService", "Failed to cycle keyboard layout:", e);
}
}
function getFocusedScreen() {
// de-activated until proper testing
return null;
// const i3Mon = I3.focusedMonitor;
// if (i3Mon) {
// const monitorName = i3Mon.name;
// for (let i = 0; i < Quickshell.screens.length; i++) {
// if (Quickshell.screens[i].name === monitorName) {
// return Quickshell.screens[i];
// }
// }
// }
// return null;
}
function spawn(command) {
try {
Quickshell.execDetached([msgCommand, "exec", "--"].concat(command));
} catch (e) {
Logger.e("SwayService", "Failed to spawn command:", e);
}
}
}