Dotfiles_ArchAP/quickshell/.config/quickshell/labfy-sway/windows/WindowStrip.qml

244 lines
8.1 KiB
QML

import QtQuick
import Quickshell
import Quickshell.I3
import Quickshell.I3._Ipc
import Quickshell.Io
Item {
id: strip
required property var screen
required property int maxWidth
property var tree: null
property bool componentReady: false
property bool refreshRunning: false
property bool refreshPending: false
property var menuAnchor: null
property var menuWindow: null
signal menuOpened()
// monitorFor(screen) peut rester null dans un binding créé avant l'inventaire IPC.
// Le modèle réactif des moniteurs réévalue le workspace dès que Sway l'annonce.
readonly property var monitor: I3.monitors.values.find(item => item.name === screen.name) || null
readonly property var activeWorkspace: monitor ? monitor.activeWorkspace : null
readonly property var windows: windowsForWorkspace(tree, activeWorkspace)
readonly property var activeWindow: windows.find(window => window.focused) || null
readonly property var secondaryWindows: activeWindow
? windows.filter(window => window.id !== activeWindow.id) : []
width: maxWidth
height: 26
clip: true
// L'arbre Sway est la source du workspace : une sortie seule ne distingue pas ses fenêtres.
function windowsForWorkspace(root, workspace) {
if (!root || !workspace) return [];
function findWorkspace(node) {
if (node.type === "workspace" && node.id === workspace.id) return node;
for (const child of (node.nodes || []).concat(node.floating_nodes || [])) {
const found = findWorkspace(child);
if (found) return found;
}
return null;
}
const node = findWorkspace(root);
if (!node) return [];
const found = [];
function collect(container) {
const appId = container.app_id
|| (container.window_properties && container.window_properties.class) || "";
if (container.type === "con" && appId) {
found.push({
id: container.id,
title: container.name || appId,
appId: appId,
appKey: appId.toLowerCase(),
focused: container.focused === true
});
}
for (const child of (container.nodes || []).concat(container.floating_nodes || []))
collect(child);
}
collect(node);
const focusedIndex = found.findIndex(window => window.focused);
if (focusedIndex > 0)
found.unshift(found.splice(focusedIndex, 1)[0]);
return found;
}
// Process.running devient vrai après la demande de lancement : ce verrou ferme aussi cet intervalle.
// Une rafale IPC garde au plus un relevé supplémentaire en attente.
function requestRefresh() {
if (!componentReady) return;
if (refreshRunning) {
refreshPending = true;
return;
}
refreshRunning = true;
treeReader.running = true;
}
// Les titres animés peuvent changer plusieurs fois par seconde : l'événement IPC
// contient déjà le nouveau titre, sans nécessiter un relevé complet de l'arbre.
function applyTitleEvent(event) {
if (!tree || refreshRunning) return false;
let update;
try {
update = JSON.parse(event.data);
} catch (error) {
return false;
}
if (update.change !== "title" || !update.container) return false;
function updateNode(node) {
if (node.id === update.container.id) {
node.name = update.container.name;
return true;
}
for (const child of (node.nodes || []).concat(node.floating_nodes || [])) {
if (updateNode(child)) return true;
}
return false;
}
if (!updateNode(tree)) return false;
tree = Object.assign({}, tree);
return true;
}
function closeMenu() {
windowMenu.visible = false;
}
function openActiveMenu() {
if (!activeWindow) return false;
showMenu(activeButton, activeWindow);
return true;
}
function showMenu(item, windowInfo) {
if (windowMenu.visible && menuWindow && menuWindow.id === windowInfo.id) {
windowMenu.visible = false;
return;
}
windowMenu.visible = false;
menuAnchor = item;
menuWindow = windowInfo;
windowMenu.visible = true;
menuOpened();
}
// Des changements de workspace arrivent pendant la construction QML : démarrer après celle du Process.
Component.onCompleted: {
componentReady = true;
Qt.callLater(requestRefresh);
}
onMonitorChanged: requestRefresh()
onActiveWorkspaceChanged: {
closeMenu();
requestRefresh();
}
onWindowsChanged: {
if (menuWindow && !windows.some(window => window.id === menuWindow.id))
closeMenu();
}
Connections {
target: I3
function onConnected() { strip.requestRefresh(); }
function onRawEvent(event) {
if (event.type === "workspace")
strip.requestRefresh();
}
}
// QuickShell 0.3.1 n'abonne I3.rawEvent qu'à workspace/output.
I3IpcListener {
subscriptions: ["window"]
onIpcEvent: event => {
// Chaque abonnement confirmé, initial ou rétabli, impose un snapshot complet.
if (event.type === "subscribe") {
try {
if (JSON.parse(event.data).success === true)
strip.requestRefresh();
else
console.warn("Abonnement aux fenêtres Sway refusé");
} catch (error) {
console.warn("Réponse d'abonnement Sway illisible :", error);
}
return;
}
if (!strip.applyTitleEvent(event))
strip.requestRefresh();
}
}
Process {
id: treeReader
command: ["swaymsg", "-t", "get_tree", "-r"]
stdout: StdioCollector { id: treeOutput; waitForEnd: true }
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
try {
strip.tree = JSON.parse(treeOutput.text);
} catch (error) {
console.warn("Arbre Sway illisible :", error);
}
} else {
console.warn("Relevé de l'arbre Sway échoué :", exitCode);
}
strip.refreshRunning = false;
if (strip.refreshPending) {
strip.refreshPending = false;
Qt.callLater(strip.requestRefresh);
}
}
}
Row {
id: buttons
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 4
WindowButton {
id: activeButton
visible: strip.activeWindow !== null
windowInfo: strip.activeWindow || ({ id: 0, title: "", appId: "", appKey: "" })
primary: true
maxPrimaryWidth: Math.max(26, Math.min(400,
strip.width - strip.secondaryWindows.length * 30 - 8))
menuOpen: windowMenu.visible && strip.menuWindow
&& strip.menuWindow.id === windowInfo.id
onMenuRequested: (item, windowInfo) => strip.showMenu(item, windowInfo)
}
Repeater {
model: ScriptModel {
values: strip.secondaryWindows
objectProp: "id"
}
delegate: WindowButton {
required property var modelData
windowInfo: modelData
primary: false
maxPrimaryWidth: 26
menuOpen: windowMenu.visible && strip.menuWindow
&& strip.menuWindow.id === windowInfo.id
onMenuRequested: (item, windowInfo) => strip.showMenu(item, windowInfo)
}
}
}
WindowMenu {
id: windowMenu
anchorItem: strip.menuAnchor
selectedWindow: strip.menuWindow
windows: strip.windows
}
}