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

393 lines
11 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
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
// 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
// Generic events
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
// Backend service loader
property var backend: null
// Cache file path
property string displayCachePath: ""
Component.onCompleted: {
// Setup cache path (needs Settings to be available)
Qt.callLater(() => {
if (typeof Settings !== 'undefined' && Settings.cacheDir) {
displayCachePath = Settings.cacheDir + "display.json";
displayCacheFileView.path = displayCachePath;
}
});
detectCompositor();
}
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");
// 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;
backendLoader.sourceComponent = mangoComponent;
} else if (niriSocket && niriSocket.length > 0) {
isHyprland = false;
isNiri = true;
isSway = false;
isMango = false;
backendLoader.sourceComponent = niriComponent;
} else if (hyprlandSignature && hyprlandSignature.length > 0) {
isHyprland = true;
isNiri = false;
isSway = false;
isMango = false;
backendLoader.sourceComponent = hyprlandComponent;
} else if (swaySock && swaySock.length > 0) {
isHyprland = false;
isNiri = false;
isSway = true;
isMango = false;
backendLoader.sourceComponent = swayComponent;
} else {
// Always fallback to Niri
isHyprland = false;
isNiri = true;
isSway = false;
isMango = false;
backendLoader.sourceComponent = niriComponent;
}
}
Loader {
id: backendLoader
onLoaded: {
if (item) {
root.backend = item;
setupBackendConnections();
backend.initialize();
}
}
}
// Cache FileView for display scales
FileView {
id: displayCacheFileView
printErrors: false
watchChanges: false
adapter: JsonAdapter {
id: displayCacheAdapter
property var displays: ({})
}
onLoaded: {
// Load cached display scales
displayScales = displayCacheAdapter.displays || {};
displayScalesLoaded = true;
// Logger.i("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales))
}
onLoadFailed: {
// Cache doesn't exist yet, will be created on first update
displayScalesLoaded = true;
// Logger.i("CompositorService", "No display cache found, will create on first update")
}
}
// 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
}
}
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(() => {
// Sync active window when it changes
// TODO: Avoid re-syncing all windows
syncWindows();
// Forward the signal
activeWindowChanged();
});
backend.windowListChanged.connect(() => {
// Sync windows when they change
syncWindows();
// Forward the signal
windowListChanged();
});
// Property bindings - use automatic property change signal
backend.focusedWindowIndexChanged.connect(() => {
focusedWindowIndex = backend.focusedWindowIndex;
});
// Initial sync
syncWorkspaces();
syncWindows();
focusedWindowIndex = backend.focusedWindowIndex;
}
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 workspace list has been updated
windowListChanged();
}
// 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();
displayScalesChanged();
Logger.i("CompositorService", "Display scales updated");
}
// Save display scales to cache
function saveDisplayScalesToCache() {
if (!displayCachePath) {
return;
}
displayCacheAdapter.displays = displayScales;
displayCacheFileView.writeAdapter();
}
// 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 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) {
windowsInWs.push(window);
}
}
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");
}
}
// 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");
}
}
// Session management
function logout() {
if (backend && backend.logout) {
Logger.i("Compositor", "Logout requested");
backend.logout();
} else {
Logger.w("Compositor", "No backend available for logout");
}
}
function shutdown() {
Logger.i("Compositor", "Shutdown requested");
Quickshell.execDetached(["sh", "-c", "systemctl poweroff || loginctl poweroff"]);
}
function reboot() {
Logger.i("Compositor", "Reboot requested");
Quickshell.execDetached(["sh", "-c", "systemctl reboot || loginctl reboot"]);
}
function suspend() {
Logger.i("Compositor", "Suspend requested");
Quickshell.execDetached(["sh", "-c", "systemctl suspend || loginctl suspend"]);
}
function hibernate() {
Logger.i("Compositor", "Hibernate requested");
Quickshell.execDetached(["sh", "-c", "systemctl hibernate || loginctl hibernate"]);
}
function lockAndSuspend() {
Logger.i("Compositor", "Lock and suspend requested");
try {
if (PanelService && PanelService.lockScreen && !PanelService.lockScreen.active) {
PanelService.lockScreen.active = true;
}
} catch (e) {
Logger.w("Compositor", "Failed to activate lock screen before suspend: " + e);
}
// Queue suspend to the next event loop cycle to allow lock UI to render
Qt.callLater(suspend);
}
}