Dotfiles_ArchAP/quickshell/.config/quickshell/labfy-sway/SessionStartupPromptV2.qml

312 lines
15 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import QtQuick
import Quickshell
import Quickshell.Wayland
import "controlcenter"
import "theme"
import "sessionui"
import "sessionui/Protocol.js" as Protocol
PanelWindow {
id: popup
required property var barWindow
required property bool startupHost
property bool interactionArmed: false
property bool failed: false
property bool restored: false
property bool detailsVisible: false
property bool attempted: false
property bool startupChecked: false
property bool releasing: false
property bool inProgress: false
property bool watchingBackend: false
property bool retryAllowed: false
property bool awaitingUserFocus: false
property int releaseTicks: 0
property int releaseClearTicks: 0
property string failureMessage: "Impossible de restaurer complètement la session."
property string technicalDetails: ""
property int windowCount: 0
property int workspaceCount: 0
readonly property bool busy: SessionV2Service.busy || watchingBackend
property bool unlockingRequested: false
readonly property bool keyringReady: SessionV2Service.keyringReady
function unlockRequested() {
if (!interactionArmed || busy || SessionV2Service.keyringBusy || unlockingRequested) return;
// CONTRACT: release exclusive keyboard before presenting the native
// prompt. Keep New session clickable throughout the unlock operation.
unlockingRequested = true;
unlockRelease.start();
}
Timer {
id: unlockRelease
interval: 50; repeat: true
onTriggered: {
if (popup.focusable) return;
stop(); popup.unlockingRequested = false;
SessionV2Service.checkKeyring(true);
}
}
Timer {
interval: 2000; repeat: true
running: popup.visible && SessionV2Service.keyringState === "KEYRING_UNLOCKING" && !popup.releasing && !popup.unlockingRequested
onTriggered: SessionV2Service.checkKeyring(false)
}
function restoreRequested() {
if (!keyringReady || !interactionArmed || busy || releasing || (attempted && !retryAllowed)) return;
if (!SessionV2Service.prepareRestore(popup)) return;
attempted = true;
failed = false;
retryAllowed = false;
releasing = true;
releaseTicks = 0;
releaseClearTicks = 0;
// WHY : visible=false seul ne vaut pas retrait de la backing window.
// CONTRACT : deux observations QML après retrait, bornées à 1,5 s.
// Le backend exige ensuite sa propre preuve Sway avant tout journal.
visible = false;
releasePoll.start();
}
function newSessionRequested() {
if (interactionArmed && !busy) {
unlockRelease.stop(); unlockingRequested = false;
SessionV2Service.cancelKeyring();
SessionV2Service.run("ack-new", "", "", popup);
}
}
function releaseFailed() {
releasePoll.stop();
SessionV2Service.cancelRestore(popup);
releasing = false;
failed = true;
retryAllowed = true;
// Un retrait expiré ne doit pas recréer la capture clavier au retry.
awaitingUserFocus = true;
failureMessage = "Restauration différée : le dialogue n’a pas libéré sa surface. Réessayez ou choisissez Nouvelle session.";
visible = true;
}
function fail(value) {
inProgress = false;
if (value && /^KEYRING_/.test(value.reason)) {
// Admission failed before Executor: preserve the explicit retry
// path without fabricating a restore attempt or clearing Recovery.
attempted = false; failed = false; restored = false;
visible = true; SessionV2Service.checkKeyring(false); return;
}
failed = true;
restored = false;
retryAllowed = !!value && value.status === "DEFERRED";
// WHY : rouvrir une layer exclusive après DEFERRED recrée le focus nul.
// CONTRACT : l'utilisateur active lui-même sa fenêtre/workspace puis
// clique Réessayer ; ce panneau reste sans clavier même pendant le clic.
// INVARIANT : aucune cible IPC inférée, aucun retry automatique ; le
// backend doit encore prouver le focus après retrait de cette surface.
awaitingUserFocus = retryAllowed;
const prewarmDeferred = retryAllowed && (value.reason === "APPLICATION_LAUNCH_FAILED"
|| value.reason === "APPLICATION_WINDOWS_INCOMPLETE");
const providerDeferred = retryAllowed && value.reason === "FIREFOX_IDENTITY_PROVIDER_UNAVAILABLE";
const stagingDeferred = retryAllowed && value.reason === "RESTORE_FOCUS_STAGING_UNPROVEN";
failureMessage = stagingDeferred
? "Restauration différée : une fenêtre qui doit rester en place empêche de préparer l’espace actif. Activez vous-même un autre espace de travail avec vos raccourcis Sway, puis cliquez Réessayer. Aucune fenêtre n’a été déplacée."
: providerDeferred
? "La restauration attend le composant Firefox. Vérifiez que Firefox et l’extension Labfy sont prêts, puis cliquez Réessayer. Les fenêtres ouvertes sont conservées sans déplacement."
: prewarmDeferred
? "Préparation incomplète. Les fenêtres ouvertes sont conservées, sans déplacement. Vérifiez les applications, puis cliquez Réessayer. Aucune application déjà présente ne sera relancée."
: retryAllowed
? "Restauration différée : aucun focus stable n’est prouvé. Ce dialogue ne capte plus le clavier. Activez vous-même une fenêtre ou un espace avec vos raccourcis Sway, puis cliquez Réessayer. Sinon, choisissez Nouvelle session."
: "Impossible de restaurer complètement la session. Consultez les détails ou choisissez Nouvelle session.";
visible = true;
// Détails provient uniquement de Restore Attempt V2, jamais d'un log.
technicalDetails = Protocol.details(value);
if (!retryAllowed) SessionV2Service.run("restore-status", "", "", popup);
}
Timer {
id: releasePoll
interval: 25
repeat: true
onTriggered: {
popup.releaseTicks += 1;
if (popup.visible || popup.focusable) { popup.releaseFailed(); return; }
popup.releaseClearTicks = popup.backingWindowVisible ? 0 : popup.releaseClearTicks + 1;
if (popup.releaseClearTicks >= 2) {
stop();
popup.releasing = false;
if (SessionV2Service.startRestore(popup)) popup.inProgress = true;
else popup.releaseFailed();
} else if (popup.releaseTicks >= 60) popup.releaseFailed();
}
}
Component.onDestruction: SessionV2Service.cancelRestore(popup)
Timer {
interval: 1000
repeat: true
running: popup.watchingBackend
onTriggered: SessionV2Service.run("startup-status", "", "", popup)
}
Timer {
interval: 350
repeat: true
running: popup.startupHost && !popup.startupChecked
// Un owner recréé pendant un Process existant attend son terme ; le
// premier refus busy ne doit pas perdre le seul chemin de diagnostic.
onTriggered: {
if (SessionV2Service.run("startup-status", "", "", popup)) popup.startupChecked = true;
}
}
Timer { id: arm; interval: 750; onTriggered: popup.interactionArmed = popup.visible }
Timer { id: closeSuccess; interval: 600; onTriggered: popup.visible = false }
Connections {
target: SessionV2Service
function onCompleted(owner, kind, value, okay) {
if (owner !== popup) return;
if (kind === "startup-status") {
if (!okay) {
popup.watchingBackend = false;
popup.fail(value);
return;
}
popup.watchingBackend = value.in_progress === true;
popup.inProgress = popup.watchingBackend;
popup.visible = value.eligible === true && value.claimed === true && !popup.watchingBackend;
if (popup.visible && value.deferred === true) {
popup.attempted = true;
popup.fail({status: "DEFERRED", reason: value.deferred_reason});
} else if (popup.visible && value.interrupted === true) {
popup.attempted = true;
popup.failed = true;
popup.failureMessage = "Une restauration précédente a été interrompue ou attend une décision. Consultez les détails ou choisissez Nouvelle session.";
SessionV2Service.run("restore-status", "", "", popup);
}
if (popup.visible) {
SessionV2Service.checkKeyring(false);
popup.windowCount = value.windows || 0;
popup.workspaceCount = value.workspaces || 0;
}
} else if (kind === "apply-last") {
if (okay && Protocol.restoreSucceeded(value))
SessionV2Service.run("ack-restored", "", value.transaction_id, popup);
else popup.fail(value);
} else if (kind === "ack-restored") {
if (okay && value.status === "acknowledged" && value.choice === "restored") {
popup.inProgress = false;
popup.restored = true;
popup.visible = true;
closeSuccess.start();
} else popup.fail();
} else if (kind === "ack-new") {
if (okay && value.status === "acknowledged") popup.visible = false;
else popup.fail(value);
} else if (kind === "restore-status") {
popup.technicalDetails = okay ? Protocol.details(value) : "Rapport de restauration indisponible.";
}
}
}
// Layer-shell, jamais un PopupWindow grabbing au nouveau login.
screen: barWindow.screen
anchors { top: true; bottom: true; left: true; right: true }
exclusionMode: ExclusionMode.Ignore
exclusiveZone: 0
aboveWindows: true
focusable: visible && !unlockingRequested && !SessionV2Service.keyringBusy && SessionV2Service.keyringState !== "KEYRING_UNLOCKING" && !awaitingUserFocus && !releasing && !inProgress && !restored
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "labfy-session-startup-v2"
visible: false
color: "transparent"
onVisibleChanged: {
if (startupHost) SessionV2Service.startupChooserVisible = visible;
interactionArmed = false;
if (visible) arm.restart(); else arm.stop();
}
PanelWindow {
screen: popup.screen
visible: popup.inProgress
focusable: false
exclusionMode: ExclusionMode.Ignore
exclusiveZone: 0
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "labfy-session-progress-v2"
anchors { top: true }
implicitWidth: 340
implicitHeight: 54
color: Theme.popupBackground
Text {
anchors.centerIn: parent
text: "Restauration de la session en cours…"
color: Theme.foreground
}
}
FocusScope {
id: modal
anchors.fill: parent
focus: popup.visible
// Aucun focus par défaut sur Restore. Les touches en transit n'agissent
// pas, et Escape ne vaut jamais une demande de restauration.
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape || event.key === Qt.Key_Enter
|| event.key === Qt.Key_Return || event.key === Qt.Key_Space) event.accepted = true;
}
Rectangle { anchors.fill: parent; color: Qt.rgba(0, 0, 0, 0.65) }
MouseArea { anchors.fill: parent }
Rectangle {
anchors.centerIn: parent
width: Math.min(460, Math.max(280, modal.width - 32))
height: content.implicitHeight + 40
color: Theme.popupBackground
radius: 8
border.color: Theme.lavender
Column {
id: content
anchors { left: parent.left; right: parent.right; top: parent.top; margins: 20 }
spacing: 14
Text { text: "Session précédente"; color: Theme.foreground; font.pixelSize: 19; font.bold: true }
Text {
width: parent.width
text: popup.failed ? popup.failureMessage
: popup.restored ? "Session restaurée"
: popup.busy && popup.attempted ? "Restauration en cours…"
: popup.keyringReady ? "Une session précédente est disponible."
: SessionV2Service.keyringState === "KEYRING_CANCELLED" ? "Déverrouillage annulé. Réessayez ou choisissez Nouvelle session."
: SessionV2Service.keyringState === "KEYRING_UNAVAILABLE" ? "Trousseau indisponible. Réessayez ou choisissez Nouvelle session. (" + SessionV2Service.keyringReason + ")"
: SessionV2Service.keyringState === "KEYRING_UNLOCKING" ? "Déverrouillage du trousseau en cours…"
: "Le trousseau doit être déverrouillé avant la restauration de votre session."
color: Theme.foreground
wrapMode: Text.Wrap
}
Text {
width: parent.width
visible: popup.detailsVisible
text: popup.technicalDetails
color: Theme.secondaryForeground
wrapMode: Text.Wrap
}
Row {
spacing: 10
visible: !popup.restored && (!popup.attempted || popup.failed)
ActionButton {
visible: popup.failed
label: "Détails"
enabled: !popup.busy
onClicked: popup.detailsVisible = !popup.detailsVisible
}
ActionButton {
label: "Nouvelle session"
enabled: popup.interactionArmed && !popup.busy
onClicked: popup.newSessionRequested()
}
ActionButton {
visible: !popup.keyringReady
label: "Déverrouiller le trousseau"
enabled: popup.interactionArmed && !popup.busy && !SessionV2Service.keyringBusy && !popup.unlockingRequested
onClicked: popup.unlockRequested()
}
ActionButton {
visible: popup.keyringReady && (!popup.failed || popup.retryAllowed)
label: popup.retryAllowed ? "Réessayer" : "Restaurer"
enabled: popup.keyringReady && popup.interactionArmed && !popup.busy && (!popup.attempted || popup.retryAllowed)
onClicked: popup.restoreRequested()
}
}
}
}
}
}