Files
noctalia-shell/Services/Compositor/SwayService.qml
T

517 lines
13 KiB
QML

import QtQuick
import Quickshell
import Quickshell.I3
import Quickshell.Io
import Quickshell.Wayland
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
// I3-specific properties
property bool initialized: false
// Cache for window-to-workspace mapping
property var windowWorkspaceMap: ({})
// Debounce timer for updates
Timer {
id: updateTimer
interval: 50
repeat: false
onTriggered: safeUpdate()
}
// Initialization
function initialize() {
if (initialized)
return;
try {
I3.refreshWorkspaces();
I3.dispatch('(["input"])');
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: ["swaymsg", "-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 = {};
// 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 this is a container with app_id or class (i.e., a window)
if (node.type === "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) {
// Create a key based on app_id and title for matching
const key = `${appId}:${title}`;
newMap[key] = workspaceNum;
// Also store by ID if available
if (id) {
newMap[`id:${id}`] = workspaceNum;
}
}
}
// 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);
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: ["swaymsg", "-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 = "";
}
}
}
Timer {
id: keyboardLayoutUpdateTimer
interval: 1000
running: true
repeat: true
onTriggered: {
queryKeyboardLayout();
}
}
function queryKeyboardLayout() {
swayInputsProcess.running = true;
}
// Sway inputs process for keyboard layout detection
Process {
id: swayInputsProcess
running: false
command: ["swaymsg", "-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 = [];
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
let workspaceId = -1;
// Try matching by app_id:title
const key = `${appId}:${title}`;
if (windowWorkspaceMap[key] !== undefined) {
const workspaceNum = windowWorkspaceMap[key];
for (var i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i);
if (ws && ws.idx === workspaceNum) {
workspaceId = ws.id;
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 {
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);
KeyboardLayoutService.setCurrentLayout(layoutName);
Logger.d("HyprlandService", "Keyboard layout switched:", layoutName);
} catch (e) {
Logger.e("HyprlandService", "Error handling activelayout:", e);
}
}
// Connections to I3
Connections {
target: I3.workspaces
enabled: initialized
function onValuesChanged() {
safeUpdateWorkspaces();
workspaceChanged();
}
}
Connections {
target: ToplevelManager
enabled: initialized
function onActiveToplevelChanged() {
updateTimer.restart();
}
}
Connections {
target: I3
enabled: initialized
function onRawEvent(event) {
safeUpdateWorkspaces();
workspaceChanged();
updateTimer.restart();
if (event.type === "output") {
Qt.callLater(queryDisplayScales);
}
if (event.type == "get_inputs") {
handleInputEvent(event.data);
}
// Query window workspaces on relevant events
if (event.type === "window" || event.type === "workspace") {
Qt.callLater(queryWindowWorkspaces);
}
}
}
// 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 logout() {
try {
Quickshell.execDetached(["swaymsg", "exit"]);
} catch (e) {
Logger.e("SwayService", "Failed to logout:", e);
}
}
function cycleKeyboardLayout() {
try {
Quickshell.execDetached(["swaymsg", "input", "type:keyboard", "xkb_switch_layout", "next"]);
} catch (e) {
Logger.e("SwayService", "Failed to cycle keyboard layout:", e);
}
}
function getFocusedScreen() {
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;
}
}