feat(quickshell): use native Polkit agent
This commit is contained in:
parent
2de313b94a
commit
195b777b84
12 changed files with 708 additions and 14 deletions
|
|
@ -133,7 +133,6 @@ pipewire-alsa
|
|||
pipewire-jack
|
||||
pipewire-libcamera
|
||||
pipewire-pulse
|
||||
polkit-gnome
|
||||
podman
|
||||
powertop
|
||||
protonmail-bridge
|
||||
|
|
|
|||
BIN
docs/assets/screenshots/quickshell-polkit-demo-empty.png
Normal file
BIN
docs/assets/screenshots/quickshell-polkit-demo-empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
|
|
@ -87,9 +87,9 @@ Le package
|
|||
SwayFX 0.6 est un paquet foreign/local à reconstruire depuis sa source
|
||||
vérifiée ; `scenefx` et `quickshell` sont présents dans la liste Pacman
|
||||
officielle de cette machine. Voir les inventaires `.assets/`.
|
||||
`polkit-gnome` fournit l'agent graphique requis pour authentifier les
|
||||
changements de fuseau effectués par `timedatectl` depuis QuickShell. Le package
|
||||
`sway` le lance une seule fois à l'ouverture de session via `autostart` ;
|
||||
QuickShell fournit l'agent graphique Polkit requis pour authentifier les
|
||||
changements de fuseau effectués par `timedatectl`. Le service utilisateur
|
||||
`quickshell-labfy-sway.service` le lance une seule fois par session ;
|
||||
aucune règle `sudoers` propre au fuseau n'est nécessaire.
|
||||
Le backend batterie exige TLP et `/usr/local/sbin/batlimit-set`, déployé
|
||||
séparément avec les droits root selon `bin/root/README.md`.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ PanelWindow {
|
|||
required property bool startupHost
|
||||
required property bool resizeMode
|
||||
required property var applicationCoordinator
|
||||
required property bool authenticationActive
|
||||
required property var keepAwakeController
|
||||
screen: modelData
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ PanelWindow {
|
|||
(Quickshell.env("HOME") + "/.config")) + "/quickshell/labfy-sway/applications/backend.py"
|
||||
function closeApplications() { applicationCoordinator.applicationsOutput = ""; }
|
||||
function toggleApplications() {
|
||||
if (authenticationActive) return false;
|
||||
if (applicationCoordinator.applicationsOutput !== screen.name) applicationsError = "";
|
||||
applicationCoordinator.toggleApplications(screen.name);
|
||||
return true;
|
||||
|
|
@ -100,6 +102,19 @@ PanelWindow {
|
|||
}
|
||||
Connections {
|
||||
target: bar.applicationCoordinator
|
||||
function onAuthenticationOpening() {
|
||||
bar.closeApplications();
|
||||
// INVARIANT: no Overview screenshot may contain the auth surface.
|
||||
overviewCapture.running = false;
|
||||
overviewCaptureRefresh.stop();
|
||||
if (overviewLoader.item) overviewLoader.item.skipDismissCaptures = true;
|
||||
bar.closeOverview();
|
||||
bar.closeClipboard();
|
||||
controlCenter.visible = false;
|
||||
dateCenter.requestClose();
|
||||
windowStrip.closeMenu();
|
||||
rightStatusArea.closeRemovableMedia();
|
||||
}
|
||||
function onApplicationsOpening(output) {
|
||||
bar.closeOverview();
|
||||
bar.closeClipboard();
|
||||
|
|
@ -145,6 +160,7 @@ PanelWindow {
|
|||
}
|
||||
// CONTRACT: clic et IPC suivent la même exclusion que les panneaux de barre.
|
||||
function toggleClipboard() {
|
||||
if (authenticationActive) return false;
|
||||
if (clipboardLoader.active) {
|
||||
closeClipboard();
|
||||
} else {
|
||||
|
|
@ -166,6 +182,7 @@ PanelWindow {
|
|||
if (overviewLoader.item) overviewLoader.item.dismiss();
|
||||
}
|
||||
function toggleOverview() {
|
||||
if (authenticationActive) return false;
|
||||
if (overviewRequested || overviewLoader.active) { closeOverview(); return true; }
|
||||
closeApplications();
|
||||
closeClipboard();
|
||||
|
|
@ -201,7 +218,7 @@ PanelWindow {
|
|||
id: overviewCapture
|
||||
command: ["python3", "-B", bar.overviewBackend, "capture", "--output", bar.screen.name]
|
||||
onExited: (code, status) => {
|
||||
if (bar.overviewRequested) overviewLoader.active = true;
|
||||
if (bar.overviewRequested && !bar.authenticationActive) overviewLoader.active = true;
|
||||
else if (code !== 0) console.warn("Capture du workspace impossible");
|
||||
}
|
||||
}
|
||||
|
|
@ -217,7 +234,7 @@ PanelWindow {
|
|||
id: overviewCaptureRefresh
|
||||
interval: 350; repeat: false
|
||||
onTriggered: {
|
||||
if (!overviewCapture.running && !overviewLoader.active && !clipboardLoader.active
|
||||
if (!bar.authenticationActive && !overviewCapture.running && !overviewLoader.active && !clipboardLoader.active
|
||||
&& !applicationsLoader.active
|
||||
&& !controlCenter.visible && !dateCenter.visible)
|
||||
overviewCapture.running = true;
|
||||
|
|
@ -228,7 +245,7 @@ PanelWindow {
|
|||
Timer {
|
||||
interval: 15000; repeat: true; running: true; triggeredOnStart: true
|
||||
onTriggered: {
|
||||
if (!overviewCapture.running && !overviewLoader.active && !clipboardLoader.active
|
||||
if (!bar.authenticationActive && !overviewCapture.running && !overviewLoader.active && !clipboardLoader.active
|
||||
&& !applicationsLoader.active
|
||||
&& !controlCenter.visible && !dateCenter.visible)
|
||||
overviewCapture.running = true;
|
||||
|
|
@ -248,6 +265,7 @@ PanelWindow {
|
|||
IpcHandler {
|
||||
target: "appearanceUi-" + bar.screen.name
|
||||
function openPage(page: string): bool {
|
||||
if (bar.authenticationActive) return false;
|
||||
if (controlCenter.pages[page] === undefined) return false;
|
||||
bar.closeApplications();
|
||||
bar.closeClipboard();
|
||||
|
|
@ -258,8 +276,12 @@ PanelWindow {
|
|||
return true;
|
||||
}
|
||||
function close(): bool { controlCenter.visible = false; return true; }
|
||||
function openDateCenter(): bool { bar.toggleDateCenter(); return true; }
|
||||
function openDateCenter(): bool {
|
||||
if (bar.authenticationActive) return false;
|
||||
bar.toggleDateCenter(); return true;
|
||||
}
|
||||
function openWindowMenu(): bool {
|
||||
if (bar.authenticationActive) return false;
|
||||
bar.closeApplications();
|
||||
bar.closeClipboard();
|
||||
controlCenter.visible = false;
|
||||
|
|
@ -296,6 +318,7 @@ PanelWindow {
|
|||
|
||||
// CONTRACT: toute la capsule centrale partage le même popup et la même exclusion XOR.
|
||||
function toggleDateCenter() {
|
||||
if (authenticationActive) return;
|
||||
const opening = !dateCenter.visible && !dateCenter.opening;
|
||||
closeApplications();
|
||||
closeOverview();
|
||||
|
|
|
|||
143
quickshell/.config/quickshell/labfy-sway/PolkitVisualFixture.qml
Normal file
143
quickshell/.config/quickshell/labfy-sway/PolkitVisualFixture.qml
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
//@ pragma UseQApplication
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Polkit
|
||||
import "polkit"
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
property var shownFlow: fakeFlow
|
||||
QtObject { id: account1; property string displayName: "Compte de test"; property string string: "test"
|
||||
property int id: 1000; property bool isGroup: false }
|
||||
QtObject { id: account2; property string displayName: "Autre compte"; property string string: "other"
|
||||
property int id: 1001; property bool isGroup: false }
|
||||
QtObject {
|
||||
id: fakeFlow
|
||||
property string message: "Autoriser une opération de démonstration sans données personnelles ?"
|
||||
property string iconName: ""
|
||||
property string actionId: "org.example.demo"
|
||||
property var identities: [account1]
|
||||
property var selectedIdentity: account1
|
||||
property bool isResponseRequired: true
|
||||
property string inputPrompt: "Réponse de démonstration :"
|
||||
property bool responseVisible: false
|
||||
property string supplementaryMessage: ""
|
||||
property bool supplementaryIsError: false
|
||||
property bool isCompleted: false
|
||||
property bool isCancelled: false
|
||||
property bool failed: false
|
||||
property int submissions: 0
|
||||
signal authenticationFailed()
|
||||
function submit(value) { submissions++; isResponseRequired = false; }
|
||||
function cancelAuthenticationRequest() { isCancelled = true; }
|
||||
}
|
||||
QtObject {
|
||||
id: secondFlow
|
||||
property string message: "Deuxième demande de test"
|
||||
property string iconName: ""
|
||||
property string actionId: "org.example.second"
|
||||
property var identities: [account1]
|
||||
property var selectedIdentity: account1
|
||||
property bool isResponseRequired: true
|
||||
property string inputPrompt: "Réponse visible de test :"
|
||||
property bool responseVisible: true
|
||||
property string supplementaryMessage: ""
|
||||
property bool supplementaryIsError: false
|
||||
property bool isCompleted: false
|
||||
property bool isCancelled: false
|
||||
property bool failed: false
|
||||
property int submissions: 0
|
||||
signal authenticationFailed()
|
||||
function submit(value) { submissions++; isResponseRequired = false; }
|
||||
function cancelAuthenticationRequest() { isCancelled = true; }
|
||||
}
|
||||
IpcHandler {
|
||||
target: "polkitFixture"
|
||||
function state(): string {
|
||||
return JSON.stringify({waiting: fakeFlow.isResponseRequired, cancelled: fakeFlow.isCancelled,
|
||||
submissions: fakeFlow.submissions, screen: Quickshell.screens.length,
|
||||
canSubmit: dialogLoader.item ? dialogLoader.item.card.canSubmit : false});
|
||||
}
|
||||
function scenario(name: string): bool {
|
||||
if (name === "long") fakeFlow.message = "Cette opération de démonstration comporte un message volontairement long afin de contrôler le retour à la ligne et la hauteur du dialogue sur un écran compact. Aucune autorisation réelle ne sera demandée.";
|
||||
else if (name === "multi") fakeFlow.identities = [account1, account2];
|
||||
else if (name === "identitySamePrompt") {
|
||||
fakeFlow.identities = [account1, account2];
|
||||
dialogLoader.item.card.selectIdentity(account2);
|
||||
}
|
||||
else if (name === "visible") fakeFlow.responseVisible = true;
|
||||
else if (name === "error") { fakeFlow.supplementaryMessage = "Échec de test"; fakeFlow.supplementaryIsError = true; fakeFlow.failed = true; fakeFlow.authenticationFailed(); fakeFlow.isResponseRequired = true; }
|
||||
else if (name === "next") { fakeFlow.isResponseRequired = false; fakeFlow.isResponseRequired = true; }
|
||||
else if (name === "cancel") fakeFlow.cancelAuthenticationRequest();
|
||||
else if (name === "reset") {
|
||||
root.shownFlow = fakeFlow;
|
||||
fakeFlow.message = "Autoriser une opération de démonstration sans données personnelles ?";
|
||||
fakeFlow.identities = [account1]; fakeFlow.selectedIdentity = account1;
|
||||
fakeFlow.inputPrompt = "Réponse de démonstration :";
|
||||
fakeFlow.responseVisible = false; fakeFlow.supplementaryMessage = "";
|
||||
fakeFlow.isCancelled = false; fakeFlow.isCompleted = false; fakeFlow.failed = false;
|
||||
fakeFlow.isResponseRequired = true; fakeFlow.submissions = 0;
|
||||
if (dialogLoader.item) dialogLoader.item.card.clearResponse();
|
||||
}
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
function checks(): string {
|
||||
const card = dialogLoader.item.card;
|
||||
function findField(item) {
|
||||
if (item.objectName === "polkitResponse") return item;
|
||||
for (const child of item.children || []) {
|
||||
const found = findField(child);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const field = findField(card);
|
||||
if (!field) return JSON.stringify({error: "champ absent"});
|
||||
fakeFlow.isCancelled = false; fakeFlow.isCompleted = false;
|
||||
fakeFlow.isResponseRequired = true; card.busy = false;
|
||||
secondFlow.submissions = 0;
|
||||
field.clear();
|
||||
const emptySecretBlocked = card.canEdit && !card.canSubmit
|
||||
&& !card.submitResponse() && fakeFlow.submissions === 0;
|
||||
field.text = "réponse fictive";
|
||||
const first = card.submitResponse();
|
||||
const second = card.submitResponse();
|
||||
const oneSubmit = first && !second && fakeFlow.submissions === 1 && field.text === "";
|
||||
fakeFlow.supplementaryMessage = "Échec de test";
|
||||
fakeFlow.supplementaryIsError = true;
|
||||
fakeFlow.authenticationFailed(); fakeFlow.isResponseRequired = true;
|
||||
const retry = card.canEdit && !card.canSubmit && field.text === "";
|
||||
field.text = "ancienne réponse";
|
||||
fakeFlow.inputPrompt = "Nouvelle question :";
|
||||
const successive = field.text === "";
|
||||
fakeFlow.identities = [account1, account2];
|
||||
fakeFlow.isResponseRequired = true; card.busy = false;
|
||||
field.text = "à effacer au changement de compte";
|
||||
card.selectIdentity(account2);
|
||||
fakeFlow.inputPrompt = "Nouvelle identité :";
|
||||
const identity = fakeFlow.selectedIdentity === account2 && field.text === ""
|
||||
&& card.canEdit && !card.canSubmit;
|
||||
root.shownFlow = null;
|
||||
const disappeared = field.text === "" && !card.canSubmit;
|
||||
root.shownFlow = secondFlow;
|
||||
secondFlow.isCancelled = false; secondFlow.isResponseRequired = true;
|
||||
field.text = "réponse fictive 2";
|
||||
const nextFlow = card.submitResponse() && secondFlow.submissions === 1
|
||||
&& fakeFlow.submissions === 1 && field.text === "";
|
||||
root.shownFlow = fakeFlow;
|
||||
fakeFlow.isCancelled = false; fakeFlow.isResponseRequired = true; card.busy = false;
|
||||
field.text = "à effacer";
|
||||
card.cancelRequest();
|
||||
const cancelled = fakeFlow.isCancelled && field.text === "";
|
||||
return JSON.stringify({emptySecretBlocked, oneSubmit, retry,
|
||||
successive, identity, disappeared, nextFlow, cancelled});
|
||||
}
|
||||
}
|
||||
LazyLoader {
|
||||
id: dialogLoader
|
||||
active: Quickshell.screens.length > 0
|
||||
PolkitDialog { requestFlow: root.shownFlow; hostScreen: Quickshell.screens[0] }
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,14 @@ Une sélection propose **Appliquer les deux**, **Fuseau uniquement** et **Positi
|
|||
|
||||
## Privilèges et transaction
|
||||
|
||||
`timedatectl set-timezone` est appelé directement, avec un tableau d'arguments, via l'authentification Polkit standard de la session. L'agent graphique `polkit-gnome` est lancé au démarrage Sway. QuickShell ne reçoit jamais le mot de passe, ne dispose d'aucune permission root permanente et ne modifie jamais `/etc/localtime` directement. Si l'authentification est annulée ou indisponible, l'opération échoue sans écrire la position solaire.
|
||||
`timedatectl set-timezone` est appelé directement, avec un tableau d'arguments,
|
||||
via l'authentification Polkit standard de la session. L'agent graphique natif
|
||||
QuickShell présente la demande locale : sa fenêtre reçoit temporairement la
|
||||
réponse masquée, la transmet à `AuthFlow.submit()` et efface le champ. Elle ne
|
||||
l'écrit ni dans un argument, ni dans un fichier, ni dans un journal. QuickShell
|
||||
ne dispose d'aucune permission root permanente et ne modifie jamais
|
||||
`/etc/localtime` directement. Si l'authentification est annulée ou
|
||||
indisponible, l'opération échoue sans écrire la position solaire.
|
||||
|
||||
L'application combinée valide d'abord les entrées, mémorise l'état précédent, change le fuseau, écrit la position locale, réconcilie Night Light en Auto, puis vérifie le fuseau. Un échec de fuseau laisse la position intacte. Un échec ultérieur restaure la préférence et tente de restaurer le fuseau et le service. Un échec de rollback est affiché comme erreur explicite.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ PanelWindow {
|
|||
property bool refreshPending: false
|
||||
property string actionName: ""
|
||||
property var dirtyCaptures: []
|
||||
property bool skipDismissCaptures: false
|
||||
property var pendingWindowNumbers: []
|
||||
property var activeWindowNumbers: []
|
||||
property int windowCaptureCursor: 0
|
||||
|
|
@ -49,7 +50,7 @@ PanelWindow {
|
|||
visible = false;
|
||||
// WHY: grim inclurait cette surface overlay. Après sa disparition,
|
||||
// seules les sorties visibles dont l'arbre a changé sont recapturées.
|
||||
for (const card of dirtyCaptures) {
|
||||
for (const card of skipDismissCaptures ? [] : dirtyCaptures) {
|
||||
Quickshell.execDetached(["python3", "-B", backend, "capture", "--output", card.output,
|
||||
"--number", String(card.number), "--workspace-id", String(card.workspaceId),
|
||||
"--revision", card.revision,
|
||||
|
|
|
|||
235
quickshell/.config/quickshell/labfy-sway/polkit/PolkitCard.qml
Normal file
235
quickshell/.config/quickshell/labfy-sway/polkit/PolkitCard.qml
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../theme"
|
||||
|
||||
Item {
|
||||
id: card
|
||||
required property var flow
|
||||
property bool busy: false
|
||||
property bool awaitingIdentityPrompt: false
|
||||
property var pendingIdentity: null
|
||||
property bool detailsOpen: false
|
||||
readonly property bool waiting: !!flow && flow.isResponseRequired && !flow.isCompleted && !flow.isCancelled
|
||||
readonly property bool canEdit: waiting && !busy
|
||||
// INVARIANT: une touche Entrée sur un champ secret vide ne doit pas
|
||||
// consommer une tentative PAM. Une invite visible peut admettre une
|
||||
// réponse vide ; Polkit reste maître de sa validation.
|
||||
readonly property bool canSubmit: canEdit && (flow.responseVisible || answer.text.length > 0)
|
||||
implicitWidth: 480
|
||||
implicitHeight: content.implicitHeight + 36
|
||||
signal cancelled()
|
||||
|
||||
// CONTRACT: only the bound native AuthFlow receives the response. The
|
||||
// field is cleared immediately; no answer enters logs, IPC or storage.
|
||||
function submitResponse() {
|
||||
const target = flow;
|
||||
if (!target || target !== card.flow || !canSubmit) return false;
|
||||
busy = true;
|
||||
target.submit(answer.text);
|
||||
answer.clear();
|
||||
return true;
|
||||
}
|
||||
function cancelRequest() {
|
||||
const target = flow;
|
||||
answer.clear();
|
||||
busy = true;
|
||||
if (target && !target.isCompleted && !target.isCancelled)
|
||||
target.cancelAuthenticationRequest();
|
||||
cancelled();
|
||||
}
|
||||
function selectIdentity(identity) {
|
||||
const target = flow;
|
||||
if (!target || !target.identities || target.identities.indexOf(identity) < 0) return;
|
||||
answer.clear();
|
||||
busy = true;
|
||||
awaitingIdentityPrompt = true;
|
||||
pendingIdentity = identity;
|
||||
target.selectedIdentity = identity;
|
||||
}
|
||||
function clearResponse() { answer.clear(); }
|
||||
function identityLabel(identity) {
|
||||
if (!identity) return "";
|
||||
return identity.displayName + " (" + identity.string + " · "
|
||||
+ (identity.isGroup ? "GID " : "UID ") + identity.id + ")";
|
||||
}
|
||||
function focusResponse() { if (waiting) answer.forceActiveFocus(); else cancelButton.forceActiveFocus(); }
|
||||
onFlowChanged: { answer.clear(); busy = false; awaitingIdentityPrompt = false;
|
||||
pendingIdentity = null; detailsOpen = false; }
|
||||
Timer {
|
||||
id: identityPromptFallback
|
||||
interval: 1000
|
||||
running: card.awaitingIdentityPrompt
|
||||
// WHY: QuickShell 0.3.1 does not expose a session generation. When two
|
||||
// identities receive the same prompt, its notify values do not change.
|
||||
// The native setter has already replaced the session synchronously;
|
||||
// this bounded wait permits the new conversation to present its prompt.
|
||||
onTriggered: {
|
||||
if (card.flow && card.flow.selectedIdentity === card.pendingIdentity
|
||||
&& card.flow.isResponseRequired && !card.flow.isCompleted) {
|
||||
card.awaitingIdentityPrompt = false;
|
||||
card.busy = false;
|
||||
card.focusResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
onWaitingChanged: {
|
||||
if (waiting) {
|
||||
if (!awaitingIdentityPrompt) busy = false;
|
||||
answer.clear();
|
||||
Qt.callLater(focusResponse);
|
||||
} else answer.clear();
|
||||
}
|
||||
Connections {
|
||||
target: card.flow
|
||||
function onAuthenticationFailed() { card.busy = false; answer.clear(); }
|
||||
function onSelectedIdentityChanged() { answer.clear(); }
|
||||
function onInputPromptChanged() {
|
||||
answer.clear();
|
||||
if (card.awaitingIdentityPrompt && card.flow && card.flow.isResponseRequired) {
|
||||
card.awaitingIdentityPrompt = false;
|
||||
card.busy = false;
|
||||
Qt.callLater(card.focusResponse);
|
||||
}
|
||||
}
|
||||
function onIsResponseRequiredChanged() {
|
||||
if (card.awaitingIdentityPrompt && card.flow && card.flow.isResponseRequired) {
|
||||
card.awaitingIdentityPrompt = false;
|
||||
card.busy = false;
|
||||
Qt.callLater(card.focusResponse);
|
||||
}
|
||||
}
|
||||
function onIsCompletedChanged() { answer.clear(); }
|
||||
function onIsCancelledChanged() { answer.clear(); }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 18
|
||||
spacing: 12
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
Rectangle {
|
||||
width: 38; height: 38; radius: 4
|
||||
color: Theme.buttonBackground
|
||||
Text { anchors.centerIn: parent; text: ""; color: Theme.accentForeground
|
||||
font.family: "JetBrainsMono Nerd Font"; font.pixelSize: 23 }
|
||||
}
|
||||
Text { Layout.fillWidth: true; text: "Authentification requise"; color: Theme.foreground
|
||||
font.pixelSize: 18; font.bold: true; wrapMode: Text.Wrap }
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
Image { Layout.preferredWidth: 24; Layout.preferredHeight: 24
|
||||
source: card.flow && card.flow.iconName ? Quickshell.iconPath(card.flow.iconName) : ""
|
||||
visible: status === Image.Ready; fillMode: Image.PreserveAspectFit }
|
||||
Text { Layout.fillWidth: true; text: card.flow ? card.flow.message : ""
|
||||
textFormat: Text.PlainText; color: Theme.foreground; wrapMode: Text.Wrap; font.pixelSize: 14 }
|
||||
}
|
||||
Text { Layout.fillWidth: true; text: "Compte d’authentification";
|
||||
color: Theme.secondaryForeground; font.pixelSize: 12 }
|
||||
Text { Layout.fillWidth: true; visible: !!card.flow && card.flow.identities.length === 1
|
||||
text: card.flow && card.flow.selectedIdentity
|
||||
? card.flow.selectedIdentity.displayName + " (" + card.flow.selectedIdentity.string + ")" : ""
|
||||
textFormat: Text.PlainText; color: Theme.foreground; font.pixelSize: 14; elide: Text.ElideRight }
|
||||
ComboBox {
|
||||
id: identityChoice
|
||||
Layout.fillWidth: true
|
||||
visible: !!card.flow && card.flow.identities.length > 1
|
||||
model: card.flow ? card.flow.identities.map(identity => ({ label: card.identityLabel(identity) })) : []
|
||||
textRole: "label"
|
||||
currentIndex: card.flow && card.flow.identities
|
||||
? card.flow.identities.indexOf(card.flow.selectedIdentity) : -1
|
||||
onActivated: index => card.selectIdentity(card.flow.identities[index])
|
||||
background: Rectangle { radius: 4; color: Theme.inputBackground
|
||||
border.color: identityChoice.activeFocus ? Theme.focus : Theme.inputBorder }
|
||||
contentItem: Text { text: identityChoice.displayText; textFormat: Text.PlainText
|
||||
color: Theme.foreground
|
||||
verticalAlignment: Text.AlignVCenter; leftPadding: 10; elide: Text.ElideRight }
|
||||
delegate: ItemDelegate {
|
||||
width: identityChoice.width
|
||||
text: modelData.label
|
||||
contentItem: Text { text: parent.text; textFormat: Text.PlainText
|
||||
color: Theme.foreground
|
||||
verticalAlignment: Text.AlignVCenter; elide: Text.ElideRight }
|
||||
background: Rectangle { color: parent.highlighted ? Theme.buttonHover : Theme.popupBackground }
|
||||
}
|
||||
popup: Popup {
|
||||
y: identityChoice.height
|
||||
width: identityChoice.width
|
||||
implicitHeight: Math.min(contentItem.implicitHeight, 240)
|
||||
contentItem: ListView { clip: true; implicitHeight: contentHeight
|
||||
model: identityChoice.popup.visible ? identityChoice.delegateModel : null
|
||||
currentIndex: identityChoice.highlightedIndex }
|
||||
background: Rectangle { radius: 4; color: Theme.popupBackground
|
||||
border.color: Theme.outline }
|
||||
}
|
||||
}
|
||||
Text { Layout.fillWidth: true
|
||||
text: card.flow && card.flow.inputPrompt ? card.flow.inputPrompt : "En attente de la demande…"
|
||||
textFormat: Text.PlainText; color: Theme.secondaryForeground; wrapMode: Text.Wrap; font.pixelSize: 12 }
|
||||
TextField {
|
||||
id: answer
|
||||
objectName: "polkitResponse"
|
||||
Layout.fillWidth: true
|
||||
enabled: card.canEdit
|
||||
visible: card.waiting
|
||||
echoMode: card.flow && card.flow.responseVisible ? TextInput.Normal : TextInput.Password
|
||||
color: Theme.foreground
|
||||
placeholderTextColor: Theme.secondaryForeground
|
||||
selectionColor: Theme.accent
|
||||
selectedTextColor: Theme.onAccent
|
||||
selectByMouse: true
|
||||
placeholderText: card.flow && card.flow.responseVisible ? "Réponse" : "Réponse confidentielle"
|
||||
Keys.onReturnPressed: event => { card.submitResponse(); event.accepted = true; }
|
||||
Keys.onEnterPressed: event => { card.submitResponse(); event.accepted = true; }
|
||||
background: Rectangle { radius: 4; color: Theme.inputBackground
|
||||
border.color: answer.activeFocus ? Theme.focus : Theme.inputBorder }
|
||||
}
|
||||
Text { Layout.fillWidth: true
|
||||
visible: !!card.flow && (!!card.flow.supplementaryMessage || card.flow.failed)
|
||||
text: card.flow ? (card.flow.supplementaryMessage
|
||||
|| (card.flow.failed ? "Authentification refusée. Réessayez." : "")) : ""
|
||||
textFormat: Text.PlainText
|
||||
color: card.flow && (card.flow.supplementaryMessage
|
||||
? card.flow.supplementaryIsError : card.flow.failed)
|
||||
? Theme.danger : Theme.secondaryForeground
|
||||
wrapMode: Text.Wrap; font.pixelSize: 12 }
|
||||
Text { Layout.fillWidth: true; visible: !card.waiting
|
||||
text: card.flow && card.flow.isCompleted ? "Demande terminée" : "En attente de l’agent d’authentification…"
|
||||
color: Theme.secondaryForeground; font.pixelSize: 12 }
|
||||
Button {
|
||||
id: detailButton
|
||||
text: card.detailsOpen ? "Masquer l’identifiant de l’action" : "Détails de l’action"
|
||||
flat: true
|
||||
onClicked: card.detailsOpen = !card.detailsOpen
|
||||
contentItem: Text { text: detailButton.text; color: Theme.accentForeground
|
||||
font.pixelSize: 12; verticalAlignment: Text.AlignVCenter }
|
||||
}
|
||||
Text { Layout.fillWidth: true; visible: card.detailsOpen
|
||||
text: card.flow ? card.flow.actionId : ""; textFormat: Text.PlainText
|
||||
color: Theme.secondaryForeground; wrapMode: Text.WrapAnywhere; font.pixelSize: 11 }
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Item { Layout.fillWidth: true }
|
||||
Button { id: cancelButton; text: "Annuler"; onClicked: card.cancelRequest()
|
||||
background: Rectangle { radius: 4; color: cancelButton.hovered ? Theme.buttonHover : Theme.buttonBackground
|
||||
border.color: cancelButton.activeFocus ? Theme.focus : Theme.outline }
|
||||
contentItem: Text { text: cancelButton.text; color: Theme.foreground
|
||||
horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } }
|
||||
Button { id: submitButton; text: "Authentifier"; enabled: card.canSubmit
|
||||
onClicked: card.submitResponse()
|
||||
background: Rectangle { radius: 4; color: Theme.accent
|
||||
border.color: submitButton.activeFocus ? Theme.foreground : Theme.accent }
|
||||
contentItem: Text { text: submitButton.text; color: Theme.onAccent
|
||||
horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } }
|
||||
}
|
||||
}
|
||||
Shortcut { sequence: "Escape"; context: Qt.WindowShortcut; onActivated: card.cancelRequest() }
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "../theme"
|
||||
|
||||
PanelWindow {
|
||||
id: window
|
||||
required property var requestFlow
|
||||
required property var hostScreen
|
||||
readonly property var flow: requestFlow
|
||||
readonly property Item card: form
|
||||
screen: hostScreen
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
exclusiveZone: 0
|
||||
aboveWindows: true
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
WlrLayershell.namespace: "labfy-polkit-authentication"
|
||||
color: "transparent"
|
||||
|
||||
// WHY: Sway gives keyboard focus to an exclusive layer surface. The
|
||||
// surface exists only for this flow and never dismisses on outside click.
|
||||
Rectangle {
|
||||
id: frame
|
||||
anchors.centerIn: parent
|
||||
width: Math.max(0, Math.min(480, parent.width - 32))
|
||||
height: Math.max(0, Math.min(form.implicitHeight, parent.height - 32))
|
||||
radius: 4
|
||||
color: Theme.popupBackground
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
clip: true
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
contentWidth: availableWidth
|
||||
PolkitCard {
|
||||
id: form
|
||||
width: parent.width
|
||||
height: implicitHeight
|
||||
flow: window.flow
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer { interval: 120; running: true; onTriggered: form.focusResponse() }
|
||||
Component.onDestruction: form.clearResponse()
|
||||
}
|
||||
152
quickshell/.config/quickshell/labfy-sway/polkit/README.md
Normal file
152
quickshell/.config/quickshell/labfy-sway/polkit/README.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Agent graphique Polkit V1
|
||||
|
||||
État au 9 octobre 2026 : l'agent QuickShell est actif. Une demande réelle a
|
||||
réussi, puis une autorisation `pkexec` a permis de retirer le paquet
|
||||
`polkit-gnome`, désormais inutile. `faillock` est vide après ces deux
|
||||
authentifications. Les fichiers actifs avant la reprise sont sauvegardés dans
|
||||
`/home/fy59/.local/state/labfy-polkit-v2-rollback-20261009/` ; ceux du premier
|
||||
essai restent dans `/home/fy59/.local/state/labfy-polkit-investigation-20261009/`.
|
||||
|
||||
Sway ne lance plus `polkit-gnome-authentication-agent-1`. La session graphique
|
||||
est SwayFX sur Wayland, et `quickshell-labfy-sway.service` lance QuickShell
|
||||
sous l'utilisateur ordinaire. Son `shell.qml` possède l'unique `PolkitAgent` ;
|
||||
le module natif `Quickshell.Services.Polkit` garde la file des demandes.
|
||||
`PolkitDialog.qml` est créé uniquement pour le `flow` courant, sur la sortie du
|
||||
workspace focalisé au début de la demande. Une sortie débranchée déclenche un
|
||||
repli vers une sortie disponible. La surface layer-shell exclusive est détruite
|
||||
après la demande ; un clic extérieur ne fait rien. Les panneaux de barre se
|
||||
ferment et les captures automatiques Overview sont suspendues pendant la demande.
|
||||
|
||||
La palette vient de `Theme.qml`, elle-même alimentée par l'état effectif
|
||||
`AppearanceController` et `theme/catppuccin.json`. Le dialogue suit donc les
|
||||
changements de thème QuickShell. La synchronisation avec les réglages GTK/Qt
|
||||
reste celle du contrôleur d'apparence existant ; aucun thème GTK supplémentaire
|
||||
n'est imposé par l'agent.
|
||||
|
||||
`AuthFlow` fournit le message réel, l'icône, l'identifiant technique, les
|
||||
identités admises, l'invite, `responseVisible`, le message complémentaire et son
|
||||
état d'erreur. Une seule identité est affichée comme texte ; plusieurs passent
|
||||
par un sélecteur dont les valeurs proviennent exclusivement de Polkit.
|
||||
`submit()` reçoit directement le contenu du champ du flow courant lorsque
|
||||
`isResponseRequired` est vrai. La réponse est effacée après soumission, échec,
|
||||
changement d'identité, annulation, disparition ou remplacement du flow.
|
||||
Une réponse secrète est masquée selon `responseVisible`. Le champ ne passe ni
|
||||
par argv, ni par fichier, ni par IPC, ni par journal. Ceci limite les copies et
|
||||
la durée de vie de la chaîne QML sans promettre un effacement physique de la
|
||||
mémoire Qt. Le collage manuel reste disponible. Le succès est décidé par le
|
||||
backend Polkit, jamais par l'appui sur Entrée.
|
||||
|
||||
Limite de l'API 0.3.1 : un changement d'identité vers une conversation ayant
|
||||
exactement le même texte d'invite peut ne produire aucun signal de changement
|
||||
de `inputPrompt` ni de `isResponseRequired`. Le dialogue garde alors la
|
||||
soumission bloquée pendant une seconde, puis réactive le champ seulement si
|
||||
le même flow, la nouvelle identité et `isResponseRequired` sont toujours
|
||||
présents. Cette temporisation reste locale à la présentation et ne crée pas
|
||||
de deuxième conversation ni de file de réponses. Une invite secrète vide ne
|
||||
peut plus être soumise accidentellement : le champ reste modifiable, mais
|
||||
Entrée et le bouton ne transmettent rien tant qu'il est vide.
|
||||
|
||||
L'API du paquet QuickShell 0.3.1 installé a été vérifiée dans
|
||||
`/usr/lib/qt6/qml/Quickshell/Services/Polkit/quickshell-service-polkit.qmltypes`
|
||||
et dans les sources de son paquet debug. `PolkitAgent` expose `isRegistered`,
|
||||
`isActive`, `flow`, `authenticationRequestStarted`. `AuthFlow` expose les
|
||||
propriétés ci-dessus, `submit()` et `cancelAuthenticationRequest()`. Le paquet
|
||||
construit le sujet d'enregistrement depuis le PID QuickShell et annule la
|
||||
requête en cours si l'agent est détruit. Si QuickShell s'arrête pendant une
|
||||
demande, Polkit ne l'autorise pas par défaut : le demandeur reçoit une
|
||||
annulation ou une erreur. Un redémarrage hors demande doit rétablir
|
||||
`isRegistered=true`.
|
||||
|
||||
## Vérifications et démonstration
|
||||
|
||||
`PolkitVisualFixture.qml` est un shell de test autonome : il n'instancie
|
||||
**aucun** `PolkitAgent`. Son IPC `polkitFixture` n'existe pas dans `shell.qml`
|
||||
et ne peut pas autoriser une action Polkit. Il sert à vérifier message court ou
|
||||
long, icône absente, une ou plusieurs identités, réponse visible ou masquée,
|
||||
erreur et nouvelle invite, annulation et double soumission. La capture vide
|
||||
[de démonstration](../../../../../docs/assets/screenshots/quickshell-polkit-demo-empty.png)
|
||||
provient seulement de cette fixture, sans secret réel.
|
||||
|
||||
```sh
|
||||
qmllint ~/.config/quickshell/labfy-sway/{shell.qml,Bar.qml,polkit/*.qml}
|
||||
qs -p ~/.config/quickshell/labfy-sway/PolkitVisualFixture.qml
|
||||
qs ipc -p ~/.config/quickshell/labfy-sway/PolkitVisualFixture.qml call polkitFixture checks
|
||||
qs -c labfy-sway ipc call polkitUi state
|
||||
```
|
||||
|
||||
Pour vérifier une demande réelle sans opération privilégiée, `pkcheck` peut
|
||||
contrôler `org.freedesktop.timedate1.set-timezone` avec
|
||||
`--process PID,START_TIME,UID --allow-user-interaction`, le PID étant celui de
|
||||
Sway dans la session graphique. Ne pas ajouter `--enable-internal-agent`.
|
||||
Un résultat autorisé immédiatement ne valide pas le dialogue. Aucun véritable
|
||||
mot de passe n'est utilisé dans les tests automatisés ; une authentification
|
||||
réelle a été validée séparément dans le dialogue local lors de la reprise.
|
||||
|
||||
### Premier essai
|
||||
|
||||
Lors de la bascule du 9 octobre 2026, la fixture a confirmé soumission unique,
|
||||
erreur puis nouvelle invite, changement d'identité, disparition du flow,
|
||||
deuxième demande et annulation, avec toutes les assertions à `true`. Le cas
|
||||
des deux identités ayant la même invite a maintenu la soumission bloquée puis
|
||||
l'a réactivée après une seconde. Les 22 tests `unittest` ciblés sur le pont
|
||||
d'inhibition, l'annotation de capture et les mises à jour ont passé ;
|
||||
`qmllint`, `git diff --check` et la simulation Stow ont passé. Le contrôle
|
||||
`sway -C` a retourné 0 avec un avertissement EGL d'énumération GPU.
|
||||
|
||||
Le nouvel agent a répondu `registered=true` après l'arrêt ciblé de l'ancien.
|
||||
`pkcheck` sur le sujet graphique complet `21858,320156,1000` a produit
|
||||
`active=true`, `dialog=true` sur `eDP-1`. La disparition du demandeur a fermé
|
||||
la surface (`active=false`, `dialog=false`). Une seconde demande a ouvert le
|
||||
dialogue puis a été rejetée avec le code `3` lors d'un rechargement du shell ;
|
||||
ce rechargement n'a donc accordé aucune autorisation. Un rechargement hors
|
||||
demande a conservé `registered=true` avec un seul processus QuickShell.
|
||||
Pendant les éditions QML suivantes, un autre rechargement hors demande a
|
||||
rencontré l'erreur Wayland `xdg_popup has no parent` ; le service a redémarré
|
||||
QuickShell et l'enregistrement est redevenu `true`, sans deuxième agent.
|
||||
Cette erreur de popup lors d'un rechargement avec des panneaux existants reste
|
||||
une limite observée de la session, sans autorisation implicite.
|
||||
L'annulation par le bouton local reste à confirmer par une interaction
|
||||
utilisateur hors essai d'authentification. Annuler une conversation PAM peut
|
||||
consommer une tentative et ne doit pas être répété pour tester la présentation.
|
||||
|
||||
### Incident de verrouillage et reprise
|
||||
|
||||
Le premier essai a provoqué à 06:55–06:56 deux conversations Polkit échouées
|
||||
sans mot de passe exploitable et une réponse refusée. `pam_faillock` a compté
|
||||
trois échecs et bloqué temporairement Swaylock et `login`. Le mot de passe
|
||||
n'avait pas changé. Le verrouillage a expiré et une session TTY2 a ensuite
|
||||
réussi ; le redémarrage a effacé le compteur volatil. Aucune permission PAM ni
|
||||
règle `sudoers` n'a été modifiée.
|
||||
|
||||
La reprise a utilisé d'abord `PolkitVisualFixture.qml`, sans `PolkitAgent`.
|
||||
Son IPC a confirmé notamment le blocage d'une réponse secrète vide, une seule
|
||||
soumission, le remplacement du flow et l'effacement du champ. Avant la demande
|
||||
réelle, `faillock --user fy59` était vide et QuickShell indiquait
|
||||
`registered=true`. L'utilisateur a saisi le mot de passe une seule fois dans
|
||||
la fenêtre locale pour un `pkcheck` sur
|
||||
`org.freedesktop.timedate1.set-timezone` ; la commande a retourné 0 sans
|
||||
changer le fuseau. Le dialogue s'est fermé et `faillock` est resté vide.
|
||||
Une autre demande Polkit, initiée par `pkexec` pour retirer uniquement le paquet
|
||||
`polkit-gnome`, a également réussi. Aucun secret n'a été transmis à cette
|
||||
conversation, à un argument de commande ou à un journal.
|
||||
|
||||
## Retour arrière ciblé
|
||||
|
||||
Le paquet GNOME a été retiré. Pour revenir à cet agent, le réinstaller depuis
|
||||
un TTY (`sudo pacman -S polkit-gnome`) si l'agent QuickShell ne fonctionne pas.
|
||||
Hors demande d'authentification, la copie persistante créée avant la reprise
|
||||
retire ensuite l'agent QuickShell et rétablit l'autostart GNOME :
|
||||
|
||||
```sh
|
||||
base=/home/fy59/.dotfiles
|
||||
backup=/home/fy59/.local/state/labfy-polkit-v2-rollback-20261009
|
||||
cp "$backup/shell.qml" "$base/quickshell/.config/quickshell/labfy-sway/shell.qml"
|
||||
cp "$backup/Bar.qml" "$base/quickshell/.config/quickshell/labfy-sway/Bar.qml"
|
||||
cp "$backup/WorkspaceOverview.qml" "$base/quickshell/.config/quickshell/labfy-sway/overview/WorkspaceOverview.qml"
|
||||
cp "$backup/autostart" "$base/sway/.config/sway/autostart"
|
||||
systemctl --user restart quickshell-labfy-sway.service
|
||||
swaymsg exec /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
|
||||
```
|
||||
|
||||
Ne pas lancer ce retour arrière pendant une demande active : interrompre une
|
||||
conversation PAM peut consommer une tentative. Ne pas arrêter `polkitd`.
|
||||
|
|
@ -3,12 +3,98 @@ import QtQuick
|
|||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.I3
|
||||
import Quickshell.Services.Polkit
|
||||
import "notifications"
|
||||
import "idlebridge"
|
||||
import "theme"
|
||||
import "polkit"
|
||||
|
||||
ShellRoot {
|
||||
id: shell
|
||||
// CONTRACT: one PolkitAgent owns the native request queue for the entire
|
||||
// user session. A dialog is constructed only for its current flow.
|
||||
PolkitAgent { id: polkitAgent }
|
||||
property var polkitScreen: null
|
||||
readonly property bool authenticationActive: polkitAgent.flow !== null
|
||||
signal authenticationOpening()
|
||||
function choosePolkitScreen(output) {
|
||||
const screens = Quickshell.screens;
|
||||
polkitScreen = screens.find(screen => screen.name === output)
|
||||
|| (screens.length ? screens[0] : null);
|
||||
}
|
||||
function beginPolkitPresentation() {
|
||||
polkitScreen = null;
|
||||
if (!polkitAgent.flow) {
|
||||
polkitOutput.running = false;
|
||||
return;
|
||||
}
|
||||
authenticationOpening();
|
||||
// WHY: the focused Sway workspace identifies the output at request
|
||||
// start. Later focus changes do not move the authentication surface.
|
||||
if (!polkitOutput.running) polkitOutput.running = true;
|
||||
else choosePolkitScreen("");
|
||||
}
|
||||
Connections {
|
||||
target: polkitAgent
|
||||
function onFlowChanged() { shell.beginPolkitPresentation(); }
|
||||
function onAuthenticationRequestStarted() {
|
||||
if (!polkitOutput.running && !shell.polkitScreen)
|
||||
shell.beginPolkitPresentation();
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
// WHY: a missing or disconnected Sway IPC must not leave an active
|
||||
// Polkit request with no visible way to cancel it.
|
||||
interval: 500
|
||||
running: !!polkitAgent.flow && !shell.polkitScreen
|
||||
onTriggered: {
|
||||
shell.choosePolkitScreen("");
|
||||
polkitOutput.running = false;
|
||||
}
|
||||
}
|
||||
Process {
|
||||
id: polkitOutput
|
||||
command: ["swaymsg", "-t", "get_workspaces", "-r"]
|
||||
stdout: StdioCollector { id: polkitOutputData; waitForEnd: true }
|
||||
onExited: (code, status) => {
|
||||
if (!polkitAgent.flow || shell.polkitScreen) return;
|
||||
let output = "";
|
||||
if (code === 0) {
|
||||
try {
|
||||
const focused = JSON.parse(polkitOutputData.text).find(item => item.focused);
|
||||
output = focused ? focused.output : "";
|
||||
} catch (_) {}
|
||||
}
|
||||
shell.choosePolkitScreen(output);
|
||||
}
|
||||
}
|
||||
// INVARIANT: a vanished output moves the same flow to a surviving screen;
|
||||
// it never creates a second PolkitAgent or another request queue.
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (polkitAgent.flow && Quickshell.screens.indexOf(shell.polkitScreen) < 0)
|
||||
shell.choosePolkitScreen("");
|
||||
}
|
||||
}
|
||||
LazyLoader {
|
||||
id: polkitDialog
|
||||
active: !!polkitAgent.flow && !!shell.polkitScreen
|
||||
PolkitDialog { requestFlow: polkitAgent.flow; hostScreen: shell.polkitScreen }
|
||||
}
|
||||
IpcHandler {
|
||||
target: "polkitUi"
|
||||
function state(): string {
|
||||
return JSON.stringify({ registered: polkitAgent.isRegistered,
|
||||
active: polkitAgent.isActive, dialog: polkitDialog.active,
|
||||
screen: shell.polkitScreen ? shell.polkitScreen.name : "",
|
||||
queryRunning: polkitOutput.running });
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
modeSnapshot.running = true;
|
||||
if (polkitAgent.flow) beginPolkitPresentation();
|
||||
}
|
||||
property bool resizeMode: false
|
||||
// CONTRACT: une seule sélection pour toutes les barres. L'inhibition est
|
||||
// volontairement éphémère : un redémarrage de QuickShell la libère.
|
||||
|
|
@ -51,6 +137,7 @@ ShellRoot {
|
|||
property string applicationsOutput: ""
|
||||
signal applicationsOpening(string output)
|
||||
function toggleApplications(output) {
|
||||
if (authenticationActive) return;
|
||||
if (applicationsOutput === output) { applicationsOutput = ""; return; }
|
||||
applicationsOpening(output);
|
||||
applicationsOutput = output;
|
||||
|
|
@ -81,7 +168,6 @@ ShellRoot {
|
|||
catch (error) { console.warn("État mode Sway invalide", error); }
|
||||
}
|
||||
}
|
||||
Component.onCompleted: modeSnapshot.running = true
|
||||
Connections {
|
||||
target: I3
|
||||
function onConnected() { if (!modeSnapshot.running) modeSnapshot.running = true; }
|
||||
|
|
@ -115,6 +201,7 @@ ShellRoot {
|
|||
|
||||
Bar {
|
||||
applicationCoordinator: shell
|
||||
authenticationActive: shell.authenticationActive
|
||||
keepAwakeController: shell
|
||||
resizeMode: shell.resizeMode
|
||||
notificationService: rootNotificationService
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@
|
|||
# Listener IPC démarré une fois par session ; exec ne se rejoue pas au reload.
|
||||
exec python3 ~/.config/sway/scripts/inactive-windows-transparency.py --opacity $inactive_opacity
|
||||
|
||||
# CONTRACT: l'agent Polkit affiche l'authentification standard de timedated.
|
||||
# exec n'est lancé qu'à l'ouverture de session, pas à chaque reload Sway.
|
||||
exec /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
|
||||
# CONTRACT: QuickShell possède l'unique agent graphique Polkit de la session.
|
||||
# Aucun deuxième agent n'est lancé par Sway à la prochaine connexion.
|
||||
|
||||
# =================================================
|
||||
# Règles d'assignation automatique
|
||||
|
|
|
|||
Loading…
Reference in a new issue