Compare commits
No commits in common. "91c39ecf9b85ce4a0053dad6b8973a7d856d4f31" and "a556cd56b71e99badd0b1e3abcae9e8dd9bf6092" have entirely different histories.
91c39ecf9b
...
a556cd56b7
|
|
@ -1,137 +0,0 @@
|
|||
# QuickShell Dock Tela — Polish V2
|
||||
|
||||
Livraison isolée du 22 octobre 2026, base `d4e2a001`, branche
|
||||
`quickshell-dock-tela-polish-v2`, worktree
|
||||
`/home/fy59/.dotfiles-worktrees/quickshell-dock-tela-polish-v2`.
|
||||
La configuration du bureau actif n’a pas été modifiée ou rechargée.
|
||||
|
||||
## Cause et correction du hover
|
||||
|
||||
La V1 animait la largeur de chaque case de la `Row` et celle de chaque
|
||||
`ApplicationIcon`. Chaque déplacement du pointeur modifiait donc les positions
|
||||
suivantes, la largeur de la rangée et potentiellement celle de la surface.
|
||||
De plus, `ApplicationIcon` calcule le `sourceSize` de son image depuis sa largeur :
|
||||
la variation de taille demandait de nouvelles résolutions de rasterisation pour
|
||||
les SVG Tela asynchrones. Ces deux chemins constituent les causes structurelles
|
||||
identifiées des saccades et du risque de clignotement. Aucun profilage GPU du
|
||||
bureau physique n’est revendiqué.
|
||||
|
||||
Les cases gardent désormais une largeur de 54 px (plus le séparateur éventuel),
|
||||
un pas de 57 px et des cibles souris fixes. Les images restent à 46 × 46 px,
|
||||
avec une résolution demandée fixe de 92 × 92 px. Seul leur `scale` varie de
|
||||
1 à 1,39, ancré en bas, avec une interpolation OutCubic de 80 ms. La taille
|
||||
visuelle maximale est ainsi d’environ 64 px. Les deux marges de 12 px à
|
||||
l’intérieur du viewport protègent aussi les icônes agrandies aux extrémités,
|
||||
y compris après défilement sur un petit écran.
|
||||
|
||||
L’influence du pointeur est une courbe smoothstep, symétrique, de rayon 76 px.
|
||||
Ce rayon est inférieur à un pas et demi : seules la case la plus proche et ses
|
||||
voisines immédiates peuvent recevoir une cible de grossissement non nulle.
|
||||
Les positions de référence restent indépendantes de la transformation.
|
||||
Les transitions peuvent brièvement finir leur interpolation après la sortie
|
||||
du rayon. Aucun IPC, blur, chargement d’image ni relayout n’est déclenché par
|
||||
l’animation elle-même. Les petits calculs d’influence restent liés au pointeur ;
|
||||
aucun timer de polling ou délai de regroupement supplémentaire n’est nécessaire.
|
||||
|
||||
## Fond flottant
|
||||
|
||||
Le voile principal passe de 89 % à **16 % d’opacité**, sa hauteur de 70 à 58 px,
|
||||
avec une bordure de 1 px à 9 % d’opacité. Une `RectangularShadow` QtQuick.Effects,
|
||||
de géométrie fixe, produit l’ombre diffuse (blur 24, spread 2, noir à 32 %).
|
||||
Elle ne capture ni les icônes ni le bureau. Le mode de contraste élevé conserve
|
||||
un fond plus soutenu à 94 % et une bordure à 50 % pour la lisibilité.
|
||||
|
||||
La règle SwayFX du seul namespace `labfy-sway-dock` désactive le flou du fond.
|
||||
Le compromis retenu est un verre teinté très transparent, sans réfraction ni
|
||||
flou des fenêtres derrière. Le rendu ne dépend donc plus d’un flou compositeur
|
||||
recalculé pendant le hover. Les effets de la barre restent inchangés.
|
||||
`RectangularShadow` nécessite Qt ≥ 6.9 ; l’hôte testé utilise Qt 6.12.0,
|
||||
QuickShell 0.3.2 et SwayFX 0.6 (Sway 1.12.0).
|
||||
|
||||
## Contrats préservés
|
||||
|
||||
La machine auto-hide conserve sa bande de 4 px, son armement de 180 ms,
|
||||
sa grâce de sortie de 450 ms et son animation verticale de 210 ms.
|
||||
`WlrKeyboardFocus.None`, couche Top, `exclusiveZone: 0` et le masque d’entrée
|
||||
restent en place. Aucune place permanente n’est réservée à l’écran.
|
||||
Le menu, `FavoritesService`, la résolution Tela et le format des favoris
|
||||
partagés sont inchangés. Aucun fichier Session V2, Keyring, PAM, greetd ou
|
||||
logique de session n’a été modifié.
|
||||
|
||||
## Validation
|
||||
|
||||
Les tests s’exécutent dans un Sway privé headless, avec HOME, XDG,
|
||||
sockets Wayland/Sway et profil Firefox privés, sans bus de session hérité. Les enfants sont arrêtés par
|
||||
le laboratoire. Les journaux et résultats sont conservés dans
|
||||
[validation/quickshell-dock-tela-polish-v2](validation/quickshell-dock-tela-polish-v2/).
|
||||
|
||||
Commandes reproductibles depuis le worktree :
|
||||
|
||||
```sh
|
||||
python -m unittest discover -s tests/applications -v
|
||||
node tests/applications/test_model.js
|
||||
python -m unittest discover -s tests/dock -v
|
||||
node tests/dock/test_models.js
|
||||
python -m unittest discover -s tests/session_v2 -v
|
||||
python tests/dock/headless.py
|
||||
python tests/dock/shell_load.py
|
||||
python tests/dock/session_compat.py
|
||||
python tests/session_v2/validate_keyring.py --sway /usr/bin/sway
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Résultats : 7 tests applications, 6 tests dock, 93 tests Session V2 backend,
|
||||
les deux suites JavaScript, le chargement du shell complet, le scénario dock
|
||||
et les scénarios Session V2/Keyring avec dock passent. Le validateur Keyring
|
||||
complet termine avec ses onze contrôles à zéro (succès).
|
||||
|
||||
Le test dock vérifie notamment :
|
||||
|
||||
- 73 positions latérales sans changement de géométrie, sans changement d’état
|
||||
des images et avec au plus trois influences non nulles ;
|
||||
- 4,5 secondes de balayage continu, un mouvement souris par frame Qt,
|
||||
sans changement de géométrie ni d’état de chargement des images ;
|
||||
- extrémités non rognées après défilement sur une sortie de 400 px ;
|
||||
- auto-hide, maintien pendant le menu contextuel, drag réel et redémarrage vide ;
|
||||
- synchronisation menu/dock/client externe et réordonnancement durable ;
|
||||
- absence de vol de focus vers le dock, fenêtres Kitty/Firefox privées,
|
||||
reconnexion IPC et changements de résolution ;
|
||||
- navigation clavier du menu et présence de la règle SwayFX sans blur du dock.
|
||||
|
||||
Le dernier passage mesure 281 intervalles : moyenne 16.00 ms,
|
||||
95e percentile 16.46 ms, maximum 16.62 ms.
|
||||
Les intervalles d’animation sont conservés dans `result.json` ; ce sont des
|
||||
mesures de cadence Qt avec rendu logiciel, pas des timestamps de présentation
|
||||
matérielle. Ils établissent la stabilité du laboratoire privé. La perception
|
||||
sur le GPU, le fond d’écran et les sorties physiques de la session active reste
|
||||
à confirmer après intégration, sans prétendre l’avoir observée ici.
|
||||
|
||||
Captures issues de `grim`, sans retouche :
|
||||
|
||||
- [Dock fermé](validation/quickshell-dock-tela-polish-v2/closed.png).
|
||||
- [Dock ouvert](validation/quickshell-dock-tela-polish-v2/open.png).
|
||||
- [Dock survolé](validation/quickshell-dock-tela-polish-v2/zoom.png).
|
||||
- [Survol sur fond clair : ombre et transparence](validation/quickshell-dock-tela-polish-v2/zoom-light.png).
|
||||
|
||||
## Intégration
|
||||
|
||||
Le patch complet, fichiers nouveaux et PNG compris, se trouve à côté du worktree :
|
||||
`/home/fy59/.dotfiles-worktrees/quickshell-dock-tela-polish-v2.patch`.
|
||||
Les changements restent non commités et aucun fichier de favoris n’est livré.
|
||||
|
||||
Après revue, depuis le dépôt principal à la base compatible :
|
||||
|
||||
```sh
|
||||
cd /home/fy59/.dotfiles
|
||||
git apply --check /home/fy59/.dotfiles-worktrees/quickshell-dock-tela-polish-v2.patch
|
||||
git apply /home/fy59/.dotfiles-worktrees/quickshell-dock-tela-polish-v2.patch
|
||||
swaymsg reload
|
||||
systemctl --user restart quickshell-labfy-sway.service
|
||||
```
|
||||
|
||||
La vérification `git apply --check` est effectuée à la livraison. Les trois
|
||||
commandes d’application/rechargement restent à exécuter lors de l’intégration.
|
||||
Le redémarrage QuickShell recharge aussi les autres composants du shell.
|
||||
Après intégration, vérifier visuellement un balayage latéral, l’auto-hide et
|
||||
le menu sur les sorties physiques. Il n’est pas nécessaire de réécrire les
|
||||
favoris ou de redémarrer la session utilisateur.
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
# Dock QuickShell, Tela et favoris partagés V1
|
||||
|
||||
Implémentation dans le worktree `/home/fy59/.dotfiles-worktrees/quickshell-dock-tela`,
|
||||
branche `codex/quickshell-dock-tela`, base `a556cd56`. Aucun commit ni déploiement.
|
||||
|
||||
## Architecture et persistance
|
||||
|
||||
`applications/FavoritesService.qml` est le singleton du processus QuickShell.
|
||||
Le menu et chaque dock lisent exactement ses `ids`, son catalogue et ses chemins
|
||||
d’icônes. `ApplicationIcon.qml` est leur composant visuel commun.
|
||||
Le menu conserve sa navigation clavier, sa recherche, ses catégories et son
|
||||
chemin de lancement UWSM/GIO. Les handlers clavier passent explicitement avant
|
||||
le traitement interne des contrôles (`Keys.BeforeItem`) pour respecter le cycle
|
||||
Tab recherche → catégories → grille → recherche. Le chemin du backend est désormais relatif au module,
|
||||
ce qui permet aussi de tester un worktree sans modifier la configuration active.
|
||||
|
||||
`~/.local/state/labfy-sway/applications-favorites.json` reste le seul stockage,
|
||||
avec le même format version 1. Aucun changement de schéma. Les neuf favoris
|
||||
existants ne sont ni migrés ni réordonnés au démarrage. Une liste vide existante
|
||||
reste vide ; seul un fichier absent autorise le seed historique du menu.
|
||||
Un favori absent du catalogue reste enregistré et apparaît dans le dock comme
|
||||
indisponible, avec possibilité de le désépingler. Une réinstallation le résout
|
||||
à nouveau sans changer son ordre.
|
||||
|
||||
Le backend Python conserve le verrou de répertoire `flock`, le fichier temporaire
|
||||
privé, le remplacement atomique et les `fsync`. Les opérations ajoutées sont :
|
||||
|
||||
- `favorites toggle ID` : bascule atomique, y compris pour deux clics rapides
|
||||
avant réception de la première réponse.
|
||||
- `favorites move ID AVANT_ID` : déplacement avant une cible encore présente.
|
||||
Une chaîne vide comme cible signifie la fin de la liste. Source ou cible
|
||||
disparue : refus sans écriture. Aucun remplacement global d’une liste périmée.
|
||||
- `serve` : protocole JSON par lignes sur stdin/stdout, lecture initiale,
|
||||
mutations sérialisées et surveillance Gio du répertoire. Les remplacements
|
||||
atomiques et les écritures des anciens clients CLI sont observés.
|
||||
|
||||
La réponse publiée relit le stockage. Le QML ne modifie jamais les favoris par
|
||||
anticipation ; un refus publie une erreur et laisse les deux interfaces sur le
|
||||
même état durable. Les autres processus restent protégés par le même verrou.
|
||||
Entrées bornées à 16 Kio, limite existante de 64 favoris conservée.
|
||||
|
||||
## Icônes
|
||||
|
||||
Le réglage unique est `ICON_THEME` dans `applications/shared.py`, actuellement
|
||||
`Tela-circle-purple-dark`. Une instance **privée** de `Gtk.IconTheme` effectue
|
||||
la résolution du `GIcon` fourni par Gio pour chaque `.desktop`, avec l’héritage
|
||||
natif du thème. Les icônes absolues sont traitées comme `GFileIcon` et les URI
|
||||
échappent correctement espaces et caractères spéciaux. Les icônes installées
|
||||
par l’application dans hicolor/pixmaps servent de repli ; ensuite vient
|
||||
`application-x-executable`. Le composant QML dessine un secours local si un
|
||||
fichier est absent ou illisible, sans dépendre d’une police d’icônes.
|
||||
|
||||
Aucun réglage GTK/Qt global n’est écrit. Le processus utilise PyGObject/Gio déjà
|
||||
employé par le backend et le typelib GTK 3 déjà présent sur cet hôte ; aucun
|
||||
paquet n’a été installé. Le résolveur natif GTK évite de réimplémenter les règles
|
||||
d’héritage, là où `Quickshell.iconPath` ne permet pas de choisir un thème privé.
|
||||
Yazi, OrcaSlicer et AppFlowy n’ont pas d’icône correspondante dans Tela installé :
|
||||
leurs icônes hicolor sont volontairement conservées. Aucun nom d’application
|
||||
n’est transformé en chemin d’icône codé en dur.
|
||||
|
||||
## Dock et focus
|
||||
|
||||
Un `dock/Dock.qml` par sortie, centré et sans zone exclusive. Surface layer-shell
|
||||
`Top`, namespace `labfy-sway-dock`, `WlrKeyboardFocus.None`. Les menus contextuels
|
||||
sont dessinés dans cette même surface : ils n’installent aucun grab clavier.
|
||||
Palette et couleurs viennent du thème Catppuccin existant. Icônes au repos à
|
||||
46 px, agrandissement jusqu’à 67 px avec influence décroissante sur les voisines.
|
||||
Les positions au repos servent au calcul pour éviter une boucle taille/position.
|
||||
Les mouvements et le glissement vertical sont animés. Le contenu dépassement
|
||||
reste défilable horizontalement avec une barre de défilement.
|
||||
|
||||
Machine à états pure dans `dock/AutoHide.js` : `hidden`, `arming`, `shown`,
|
||||
`waiting`, `held`. Au démarrage : `hidden`. Bande sensible de 4 px, centrée,
|
||||
limitée à 360 px au repos ; attente de 180 ms avant apparition, grâce de 450 ms
|
||||
avant fermeture. **Un seul Timer** porte l’échéance de la machine. Pression,
|
||||
glisser-déposer et menu contextuel bloquent la fermeture. Le masque d’entrée
|
||||
laisse passer le pointeur hors du corps, du menu et de la bande. Les changements
|
||||
de résolution recalculent la largeur disponible. Polkit et le menu d’applications
|
||||
suppriment temporairement le dock via son propriétaire dans `shell.qml`.
|
||||
|
||||
Le bloc ajouté à `sway/.config/sway/swayfx` limite le flou au namespace du dock
|
||||
et reprend `$bar_blur`. Aucun effet global supplémentaire.
|
||||
|
||||
## Fenêtres et actions
|
||||
|
||||
Un seul `swaymsg -m -t subscribe` pour les événements window/workspace/output,
|
||||
possédé par le singleton. Après coupure, relance à 1,5 s et réconciliation de
|
||||
l’arbre. Les rafales sont regroupées à 60 ms, et un indicateur dirty préserve
|
||||
un événement survenu pendant une requête. Aucune interrogation périodique en
|
||||
fonctionnement normal. Les requêtes Python utilisent directement le socket IPC,
|
||||
un encodage little-endian explicite, une réponse limitée à 16 Mio et un timeout.
|
||||
|
||||
Correspondances par app_id, classe/instance XWayland, StartupWMClass, identifiant
|
||||
desktop et exécutable. Les alias ambigus ne sont pas attribués ; aucun titre
|
||||
n’est utilisé pour reconnaître une application. Les titres servent seulement
|
||||
à afficher le choix entre fenêtres.
|
||||
|
||||
Clic gauche : lancement d’un favori installé sans fenêtre, focus explicite d’une
|
||||
fenêtre, ou choix si plusieurs fenêtres. Les applications ouvertes non épinglées
|
||||
suivent un séparateur. Clic droit : fenêtres, ouvrir, épingler/désépingler, fermer
|
||||
le menu. Glisser un favori puis relâcher sur sa nouvelle position enregistre son
|
||||
ordre. Clic molette : demande de nouvelle instance pour les terminaux dont la
|
||||
politique est connue (`NEW_INSTANCE_IDS`), sans promesse pour une application
|
||||
singleton inconnue.
|
||||
|
||||
`focus` valide le con_id dans l’arbre courant ; un scratchpad caché est montré
|
||||
avant focus. Cette commande n’est appelée que par les handlers de clic. Ni la
|
||||
machine à états, ni le watcher, ni les animations ne l’appellent. Les surfaces
|
||||
layer-shell n’entrent pas dans l’arbre des toplevels et les snapshots Session V2.
|
||||
Aucun fichier du greeter, PAM, Keyring ou moteur Session V2 n’est modifié.
|
||||
|
||||
## Validation reproductible
|
||||
|
||||
Depuis le worktree, sans arrêter la session graphique active :
|
||||
|
||||
```sh
|
||||
python -m unittest discover -s tests/applications -v
|
||||
node tests/applications/test_model.js
|
||||
python -m unittest discover -s tests/dock -v
|
||||
node tests/dock/test_models.js
|
||||
python tests/dock/headless.py
|
||||
python tests/dock/shell_load.py
|
||||
python tests/dock/session_compat.py
|
||||
python tests/session_v2/validate_keyring.py --sway /usr/bin/sway
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Les tests graphiques créent un compositeur SwayFX headless enfant avec HOME,
|
||||
XDG, socket Wayland et socket Sway privés. Le test Firefox utilise son propre
|
||||
profil. `wtype` envoie Tab uniquement au compositeur privé. Le proxy de test coupe
|
||||
les connexions IPC pour exercer la reconnexion réelle. Les enfants du laboratoire
|
||||
sont arrêtés à la sortie. Aucun environnement ni journal Session V2 utilisateur
|
||||
n’est réutilisé.
|
||||
|
||||
Les captures `validation/quickshell-dock-tela/{closed,open,zoom}.png` et le JSON
|
||||
`result.json` proviennent de ce laboratoire. Les scénarios incluent clic droit,
|
||||
choix/pinning, drag souris réel, agrandissement mesuré, entrée/sortie, ordre durable,
|
||||
refus de déplacement, zéro favori après redémarrage, installation/désinstallation
|
||||
d’une entrée de catalogue, client CLI externe, navigation clavier du menu,
|
||||
Firefox, deux Kitty, flottant, scratchpad, fermeture et résolution.
|
||||
|
||||
La suite de non-régression Session V2/Keyring couvre 93 tests backend et ses
|
||||
scénarios graphiques. `session_compat.py` exécute aussi les scénarios chooser et
|
||||
Keyring existants avec le dock présent : prêt, acceptation, annulation, timeout,
|
||||
absence, verrouillage persistant, collection différente, redémarrage pendant
|
||||
l’unlock, succès/échec de restauration, nouvelle session et focus différé réel.
|
||||
|
||||
## Limites et intégration après revue
|
||||
|
||||
- Une application sans identité desktop correspondante reste visible sous son
|
||||
identité fenêtre avec une icône générique, sans épinglage inventé. Les alias
|
||||
réellement nécessaires peuvent être ajoutés après observation de l’app_id ou
|
||||
de StartupWMClass ; le titre ne doit pas devenir une heuristique.
|
||||
- Yazi/Neovim hébergés dans une fenêtre Kitty ordinaire ont l’identité Kitty.
|
||||
Le dock ne déduit pas leur identité du titre ou du processus enfant. Le
|
||||
comportement de lancement terminal existant est conservé.
|
||||
- XWayland est couvert par des arbres de test ; la recette graphique privée
|
||||
désactive XWayland. Une vérification avec une vraie application X11 reste utile.
|
||||
- Le greeter/PAM réels ne sont pas exécutés dans la recette. La compatibilité
|
||||
Keyring et restauration est exercée avec leurs fixtures privées et les vrais
|
||||
composants de production. Un cycle de connexion matériel reste une recette
|
||||
manuelle après revue, jamais déclenchée ici.
|
||||
- Les PNG ne prouvent pas à eux seuls la qualité du flou sur une fenêtre réelle ;
|
||||
la règle SwayFX est chargée dans le laboratoire. Aucun test tactile ou écran
|
||||
à facteur d’échelle fractionnaire n’est revendiqué.
|
||||
|
||||
La livraison reste dans le worktree. Examiner les sources, les tests, les PNG et
|
||||
le patch complet adjacent au worktree. Après validation humaine, appliquer le
|
||||
patch sur la base compatible dans le dépôt principal, sans copier le fichier de
|
||||
favoris. Vérifier d’abord avec `git apply --check CHEMIN_DU_PATCH`, puis appliquer
|
||||
selon votre procédure de revue. Recharger ensuite QuickShell et Sway pour le
|
||||
nouveau module et la règle de flou. Le rechargement n’a pas été exécuté ici.
|
||||
|
||||
## Références des contrats de plateforme
|
||||
|
||||
- [QuickShell WlrLayershell](https://quickshell.org/docs/v0.3.1/types/Quickshell.Wayland/WlrLayershell/) : modes de focus layer-shell, recoupés avec les types installés de 0.3.2.
|
||||
- [Spécification des thèmes d’icônes](https://specifications.freedesktop.org/icon-theme/) : résolution et héritage délégués au toolkit.
|
||||
- [Protocole IPC i3/Sway](https://i3wm.org/docs/ipc.html) : framing et événements, vérifiés avec le SwayFX privé installé.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
test_bad_state_does_not_get_overwritten (test_backend.FavoritesTests.test_bad_state_does_not_get_overwritten) ... ok
|
||||
test_seed_order_atomic_update_and_missing_entry (test_backend.FavoritesTests.test_seed_order_atomic_update_and_missing_entry) ... ok
|
||||
test_graphical_uses_desktop_id_and_uwsm_service (test_backend.LaunchTests.test_graphical_uses_desktop_id_and_uwsm_service) ... ok
|
||||
test_invalid_id_and_missing_entry (test_backend.LaunchTests.test_invalid_id_and_missing_entry) ... ok
|
||||
test_nodisplay_entry_is_refused_before_launch (test_backend.LaunchTests.test_nodisplay_entry_is_refused_before_launch) ... ok
|
||||
test_terminal_entry_already_running_kitty_is_not_wrapped_twice (test_backend.LaunchTests.test_terminal_entry_already_running_kitty_is_not_wrapped_twice) ... ok
|
||||
test_terminal_preserves_desktop_exec_fields_and_kitty (test_backend.LaunchTests.test_terminal_preserves_desktop_exec_fields_and_kitty) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 7 tests in 0.004s
|
||||
|
||||
OK
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
|
@ -1,18 +0,0 @@
|
|||
/tmp/labfy-session-v2-TEST_ONLY-s23v41ci
|
||||
ready PASS
|
||||
accept PASS
|
||||
cancel PASS
|
||||
timeout PASS
|
||||
absent PASS
|
||||
still-locked PASS
|
||||
different PASS
|
||||
quickshell-restart-during-unlock PASS
|
||||
keyring_ui WITH_DOCK PASS
|
||||
success PASS
|
||||
failure PASS
|
||||
invalid PASS
|
||||
new PASS
|
||||
claimed PASS
|
||||
real PASS
|
||||
deferred-real PASS
|
||||
phase3b_ui WITH_DOCK PASS
|
||||
|
|
@ -1 +0,0 @@
|
|||
Dock headless: PASS /tmp/labfy-session-v2-TEST_ONLY-dlggtemr
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
Dock state and window models: PASS
|
||||
ApplicationsModel: PASS
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
............................/usr/lib/python3.14/site-packages/gi/events.py:803: DeprecationWarning: 'asyncio.AbstractEventLoopPolicy' is deprecated and slated for removal in Python 3.16
|
||||
AbstractEventLoopPolicy = getattr(asyncio, "AbstractEventLoopPolicy", object)
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
../usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
./usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
../usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
../usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
..........................................................
|
||||
----------------------------------------------------------------------
|
||||
Ran 93 tests in 8.443s
|
||||
|
||||
OK
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
.............................
|
||||
----------------------------------------------------------------------
|
||||
Ran 29 tests in 0.129s
|
||||
|
||||
OK
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
/tmp/labfy-session-v2-TEST_ONLY-0p63m0if
|
||||
stable-final-focus PASS
|
||||
/tmp/labfy-session-v2-TEST_ONLY-vn3vcup_
|
||||
late-refused PASS
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
/tmp/labfy-session-v2-TEST_ONLY-p4jqsmpt
|
||||
ready PASS
|
||||
accept PASS
|
||||
cancel PASS
|
||||
timeout PASS
|
||||
absent PASS
|
||||
still-locked PASS
|
||||
different PASS
|
||||
quickshell-restart-during-unlock PASS
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
guard-replacement PASS APPLICATION_MEMBERSHIP_CHANGED
|
||||
guard-extra PASS APPLICATION_MEMBERSHIP_CHANGED
|
||||
last-capture-layout PASS FINAL_TREE_MISMATCH
|
||||
last-capture-swap PASS RECOVERY_ORDER_CHANGED
|
||||
guard-finish-layout PASS FINAL_TREE_MISMATCH
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
pendant-guard SUCCÈS : quatre swaps, quatre slots, arbre exact, focus vérifié
|
||||
apres-phase0 SUCCÈS : quatre swaps, quatre slots, arbre exact, focus vérifié
|
||||
deux-frontieres SUCCÈS : quatre swaps, quatre slots, arbre exact, focus vérifié
|
||||
autotiling-guard SUCCÈS : quatre swaps, quatre slots, arbre exact, focus vérifié
|
||||
provider-unaire SUCCÈS : changement externe refusé avant IPC suivant
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
success PASS
|
||||
failure PASS
|
||||
invalid PASS
|
||||
new PASS
|
||||
claimed PASS
|
||||
real PASS
|
||||
deferred-real PASS
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
********* Start testing of qmltestrunner *********
|
||||
Config: Using QtTest library 6.12.0, Qt 6.12.0 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 16.2.1 20260810), arch unknown
|
||||
PASS : qmltestrunner::SessionExitGateV2::initTestCase()
|
||||
PASS : qmltestrunner::SessionExitGateV2::test_backend_reason_is_visible_and_logout_stays_gated()
|
||||
PASS : qmltestrunner::SessionExitGateV2::test_error_or_legacy_success_never_exits()
|
||||
PASS : qmltestrunner::SessionExitGateV2::test_quit_without_saving_requires_explicit_call()
|
||||
PASS : qmltestrunner::SessionExitGateV2::test_saved_releases_only_confirmed_action()
|
||||
PASS : qmltestrunner::SessionExitGateV2::cleanupTestCase()
|
||||
Totals: 6 passed, 0 failed, 0 skipped, 0 blacklisted, 5ms
|
||||
********* Finished testing of qmltestrunner *********
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
...............
|
||||
----------------------------------------------------------------------
|
||||
Ran 15 tests in 20.330s
|
||||
|
||||
OK
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"backend": 0,
|
||||
"focus": 0,
|
||||
"qml-unit": 0,
|
||||
"qml-qt": 0,
|
||||
"keyring-ui": 0,
|
||||
"keyring-runtime": 0,
|
||||
"scratchpad-runtime": 0,
|
||||
"phase3b-ui": 0,
|
||||
"p0-runtime": 0,
|
||||
"path-runtime": 0,
|
||||
"diff-check": 0
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
/tmp/labfy-session-v2-TEST_ONLY-1lfda8ji
|
||||
PASS save/store/restore/show/hide/new-membership/all-hidden/cold-kitty/no-duplicate
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/tmp/labfy-keyring-validation-ibq2bukh
|
||||
backend 0
|
||||
focus 0
|
||||
qml-unit 0
|
||||
qml-qt 0
|
||||
keyring-ui 0
|
||||
keyring-runtime 0
|
||||
scratchpad-runtime 0
|
||||
phase3b-ui 0
|
||||
p0-runtime 0
|
||||
path-runtime 0
|
||||
diff-check 0
|
||||
|
Before Width: | Height: | Size: 29 KiB |
|
|
@ -1,152 +0,0 @@
|
|||
test_absent_last_existing_directory (test_bootstrap.BootstrapTests.test_absent_last_existing_directory) ... ok
|
||||
test_absent_no_creation (test_bootstrap.BootstrapTests.test_absent_no_creation) ... ok
|
||||
test_allowlist_denied (test_bootstrap.BootstrapTests.test_allowlist_denied) ... ok
|
||||
test_checkpoint_created_between_observations (test_bootstrap.BootstrapTests.test_checkpoint_created_between_observations) ... ok
|
||||
test_empty_sessions_with_or_without_automatic_allows_without_creation (test_bootstrap.BootstrapTests.test_empty_sessions_with_or_without_automatic_allows_without_creation) ... ok
|
||||
test_envelope_and_framing_rejected (test_bootstrap.BootstrapTests.test_envelope_and_framing_rejected) ... ok
|
||||
test_home_fallback_matches_canonical_location (test_bootstrap.BootstrapTests.test_home_fallback_matches_canonical_location) ... ok
|
||||
test_interleaved_operations_map_unchanged (test_bootstrap.BootstrapTests.test_interleaved_operations_map_unchanged) ... ok
|
||||
test_last_with_sessions_absent_refuses (test_bootstrap.BootstrapTests.test_last_with_sessions_absent_refuses) ... ok
|
||||
test_named_only_without_automatic_refuses (test_bootstrap.BootstrapTests.test_named_only_without_automatic_refuses) ... ok
|
||||
test_os_error_is_not_absence (test_bootstrap.BootstrapTests.test_os_error_is_not_absence) ... ok
|
||||
test_present_and_corrupt_always_refuse (test_bootstrap.BootstrapTests.test_present_and_corrupt_always_refuse) ... ok
|
||||
test_sessions_any_entry_refuses_without_content_read (test_bootstrap.BootstrapTests.test_sessions_any_entry_refuses_without_content_read) ... ok
|
||||
test_sessions_list_error_refuses (test_bootstrap.BootstrapTests.test_sessions_list_error_refuses) ... ok
|
||||
test_sessions_unsafe_directory_refuses (test_bootstrap.BootstrapTests.test_sessions_unsafe_directory_refuses) ... ok
|
||||
test_socket_authority_failure_refuses_absence (test_bootstrap.BootstrapTests.test_socket_authority_failure_refuses_absence) ... ok
|
||||
test_state_symlink_and_relative_state_refuse (test_bootstrap.BootstrapTests.test_state_symlink_and_relative_state_refuse) ... ok
|
||||
test_symlinks_and_special_last_refuse_without_open (test_bootstrap.BootstrapTests.test_symlinks_and_special_last_refuse_without_open) ... ok
|
||||
test_unreadable_and_nonprivate_directory_refuse (test_bootstrap.BootstrapTests.test_unreadable_and_nonprivate_directory_refuse) ... ok
|
||||
test_valid_store_checkpoint_and_unreadable_file_refuse (test_bootstrap.BootstrapTests.test_valid_store_checkpoint_and_unreadable_file_refuse) ... ok
|
||||
test_equivalent_unary_paths_are_not_identity_changes (test_focus_recovery.FocusRecoveryTests.test_equivalent_unary_paths_are_not_identity_changes) ... ok
|
||||
test_firefox_command_proven_then_kitty_late_conflict_is_preserved (test_focus_recovery.FocusRecoveryTests.test_firefox_command_proven_then_kitty_late_conflict_is_preserved) ... ok
|
||||
test_interrupted_intention_preserves_all_evidence (test_focus_recovery.FocusRecoveryTests.test_interrupted_intention_preserves_all_evidence) ... ok
|
||||
test_layout_change_with_identical_raw_paths_still_refuses (test_focus_recovery.FocusRecoveryTests.test_layout_change_with_identical_raw_paths_still_refuses) ... ok
|
||||
test_path_evidence_limit_refuses_without_overwriting_history (test_focus_recovery.FocusRecoveryTests.test_path_evidence_limit_refuses_without_overwriting_history) ... ok
|
||||
test_real_identity_order_workspace_and_branch_changes_still_refuse (test_focus_recovery.FocusRecoveryTests.test_real_identity_order_workspace_and_branch_changes_still_refuse) ... ok
|
||||
test_prepare_quits_before_resume_and_finish_has_no_delayed_exit (test_guard_protocol.GuardProtocolTests.test_prepare_quits_before_resume_and_finish_has_no_delayed_exit) ... ok
|
||||
test_resume_without_prepare_refuses_and_still_cleans_owned_helper (test_guard_protocol.GuardProtocolTests.test_resume_without_prepare_refuses_and_still_cleans_owned_helper) ... ok
|
||||
test_actual_dbus_states (test_keyring.KeyringTests.test_actual_dbus_states) ... /usr/lib/python3.14/site-packages/gi/events.py:803: DeprecationWarning: 'asyncio.AbstractEventLoopPolicy' is deprecated and slated for removal in Python 3.16
|
||||
AbstractEventLoopPolicy = getattr(asyncio, "AbstractEventLoopPolicy", object)
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
ok
|
||||
test_apply_is_excluded_during_unlock (test_keyring.KeyringTests.test_apply_is_excluded_during_unlock) ... ok
|
||||
test_apply_last_cannot_bypass_gate (test_keyring.KeyringTests.test_apply_last_cannot_bypass_gate) ... /usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
ok
|
||||
test_ready_preserves_executor_result_and_recovery_required (test_keyring.KeyringTests.test_ready_preserves_executor_result_and_recovery_required) ... /usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
ok
|
||||
test_remote_error_text_is_never_logged_or_persisted (test_keyring.KeyringTests.test_remote_error_text_is_never_logged_or_persisted) ... ok
|
||||
test_transaction_prevents_prompt (test_keyring.KeyringTests.test_transaction_prevents_prompt) ... /usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
ok
|
||||
test_two_processes_and_restart (test_keyring.KeyringTests.test_two_processes_and_restart) ... ok
|
||||
test_unlock_never_restores (test_keyring.KeyringTests.test_unlock_never_restores) ... /usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
|
||||
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
|
||||
ok
|
||||
test_complete_refuses_full_identity_placement_and_pending (test_p0_boundaries.BoundaryTests.test_complete_refuses_full_identity_placement_and_pending) ... ok
|
||||
test_extra_after_first_move_refuses_before_second_staging (test_p0_boundaries.BoundaryTests.test_extra_after_first_move_refuses_before_second_staging) ... ok
|
||||
test_guard_replacement_and_extra_refuse_before_any_mutation (test_p0_boundaries.BoundaryTests.test_guard_replacement_and_extra_refuse_before_any_mutation) ... ok
|
||||
test_last_provider_capture_cannot_hide_structure_or_identity_change (test_p0_boundaries.BoundaryTests.test_last_provider_capture_cannot_hide_structure_or_identity_change) ... ok
|
||||
test_layout_change_in_second_split_focus_observation_refuses (test_p0_boundaries.BoundaryTests.test_layout_change_in_second_split_focus_observation_refuses) ... ok
|
||||
test_unary_parent_normalization_preserves_canonical_layout (test_p0_boundaries.BoundaryTests.test_unary_parent_normalization_preserves_canonical_layout) ... ok
|
||||
test_unchanged_final_observation_completes (test_p0_boundaries.BoundaryTests.test_unchanged_final_observation_completes) ... ok
|
||||
test_changement_apres_capture_canonique_refuse_avant_deplacement (test_path_stability.PathTests.test_changement_apres_capture_canonique_refuse_avant_deplacement) ... ok
|
||||
test_guard_refuse_les_divergences_avant_tout_ipc (test_path_stability.PathTests.test_guard_refuse_les_divergences_avant_tout_ipc) ... ok
|
||||
test_ack_requires_verified_current_transaction (test_phase3b.DeploymentTests.test_ack_requires_verified_current_transaction) ... ok
|
||||
test_limusic_never_launched (test_phase3b.DeploymentTests.test_limusic_never_launched) ... ok
|
||||
test_async_mapping_completes_before_apply_and_focus_proof (test_prewarm.PrewarmTests.test_async_mapping_completes_before_apply_and_focus_proof) ... ok
|
||||
test_deferred_marker_and_cli_preserve_cause_for_explicit_retry (test_prewarm.PrewarmTests.test_deferred_marker_and_cli_preserve_cause_for_explicit_retry) ... ok
|
||||
test_existing_required_journal_refuses_before_launch (test_prewarm.PrewarmTests.test_existing_required_journal_refuses_before_launch) ... ok
|
||||
test_focus_change_after_proof_refuses_without_publication (test_prewarm.PrewarmTests.test_focus_change_after_proof_refuses_without_publication) ... ok
|
||||
test_intruder_extra_identity_session_and_owner_refuse_without_publication (test_prewarm.PrewarmTests.test_intruder_extra_identity_session_and_owner_refuse_without_publication) ... ok
|
||||
test_launch_failure_preserves_clients_and_retry_does_not_duplicate (test_prewarm.PrewarmTests.test_launch_failure_preserves_clients_and_retry_does_not_duplicate) ... ok
|
||||
test_missing_managed_autostart_refuses_before_launch (test_prewarm.PrewarmTests.test_missing_managed_autostart_refuses_before_launch) ... ok
|
||||
test_missing_collision_and_live_publisher (test_provider.ProviderTests.test_missing_collision_and_live_publisher) ... ok
|
||||
test_capture_is_deterministic_for_a_stable_tree (test_scratchpad.ScratchpadTests.test_capture_is_deterministic_for_a_stable_tree) ... ok
|
||||
test_capture_store_reload_and_no_internal_workspace (test_scratchpad.ScratchpadTests.test_capture_store_reload_and_no_internal_workspace) ... ok
|
||||
test_change_between_reads_and_ambiguous_focus_refuse (test_scratchpad.ScratchpadTests.test_change_between_reads_and_ambiguous_focus_refuse) ... ok
|
||||
test_empty_focus_workspace_is_persisted (test_scratchpad.ScratchpadTests.test_empty_focus_workspace_is_persisted) ... ok
|
||||
test_group_ancestor_is_not_admitted_by_leaf_identity (test_scratchpad.ScratchpadTests.test_group_ancestor_is_not_admitted_by_leaf_identity) ... ok
|
||||
test_hidden_focus_is_rejected_before_command (test_scratchpad.ScratchpadTests.test_hidden_focus_is_rejected_before_command) ... ok
|
||||
test_initial_scratchpad_add_predicts_source_workspace_focus (test_scratchpad.ScratchpadTests.test_initial_scratchpad_add_predicts_source_workspace_focus) ... ok
|
||||
test_journal_detects_identity_output_scratchpad_and_interruption (test_scratchpad.ScratchpadTests.test_journal_detects_identity_output_scratchpad_and_interruption) ... ok
|
||||
test_layer_shell_can_leave_no_sway_focus (test_scratchpad.ScratchpadTests.test_layer_shell_can_leave_no_sway_focus) ... ok
|
||||
test_legacy_journal_not_reinterpreted (test_scratchpad.ScratchpadTests.test_legacy_journal_not_reinterpreted) ... ok
|
||||
test_multiple_focused_nodes_are_still_refused (test_scratchpad.ScratchpadTests.test_multiple_focused_nodes_are_still_refused) ... ok
|
||||
test_runtime_visibility_assignment_never_exposes_hidden_peer (test_scratchpad.ScratchpadTests.test_runtime_visibility_assignment_never_exposes_hidden_peer) ... ok
|
||||
test_shown_and_multiple_hidden (test_scratchpad.ScratchpadTests.test_shown_and_multiple_hidden) ... ok
|
||||
test_strict_versions_and_scratchpad_references (test_scratchpad.ScratchpadTests.test_strict_versions_and_scratchpad_references) ... ok
|
||||
test_checkpoint_failed_validation_preserves_old (test_unit.BackendTests.test_checkpoint_failed_validation_preserves_old) ... ok
|
||||
test_checkpoint_permissions (test_unit.BackendTests.test_checkpoint_permissions) ... ok
|
||||
test_cli_execute_required (test_unit.BackendTests.test_cli_execute_required) ... ok
|
||||
test_cli_storage_checkpoint_and_startup_mock (test_unit.BackendTests.test_cli_storage_checkpoint_and_startup_mock) ... ok
|
||||
test_coexistence_and_migration (test_unit.BackendTests.test_coexistence_and_migration) ... ok
|
||||
test_command_injection (test_unit.BackendTests.test_command_injection) ... ok
|
||||
test_counts (test_unit.BackendTests.test_counts) ... ok
|
||||
test_exact_inverted (test_unit.BackendTests.test_exact_inverted) ... ok
|
||||
test_interchangeable_best_effort (test_unit.BackendTests.test_interchangeable_best_effort) ... ok
|
||||
test_intermediate_directory_permissions (test_unit.BackendTests.test_intermediate_directory_permissions) ... ok
|
||||
test_json_schema_matches_fixture (test_unit.BackendTests.test_json_schema_matches_fixture) ... ok
|
||||
test_json_strict (test_unit.BackendTests.test_json_strict) ... ok
|
||||
test_launch_policy (test_unit.BackendTests.test_launch_policy) ... ok
|
||||
test_lock (test_unit.BackendTests.test_lock) ... ok
|
||||
test_multi_output_preflight_synthetic (test_unit.BackendTests.test_multi_output_preflight_synthetic) ... ok
|
||||
test_native_strict (test_unit.BackendTests.test_native_strict) ... ok
|
||||
test_paths_links (test_unit.BackendTests.test_paths_links) ... ok
|
||||
test_plan_pure (test_unit.BackendTests.test_plan_pure) ... ok
|
||||
test_references (test_unit.BackendTests.test_references) ... ok
|
||||
test_report_rejects_private_fields_and_bool_version (test_unit.BackendTests.test_report_rejects_private_fields_and_bool_version) ... ok
|
||||
test_resume_attempts_all_owned_helpers (test_unit.BackendTests.test_resume_attempts_all_owned_helpers) ... ok
|
||||
test_schema_exact (test_unit.BackendTests.test_schema_exact) ... ok
|
||||
test_types_version_and_identity (test_unit.BackendTests.test_types_version_and_identity) ... ok
|
||||
test_unsupported_states (test_unit.BackendTests.test_unsupported_states) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 93 tests in 8.405s
|
||||
|
||||
OK
|
||||
|
|
@ -1 +0,0 @@
|
|||
Full shell private-compositor load: PASS /tmp/labfy-session-v2-TEST_ONLY-vx5tlv5i
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
test_inheritance_from_a_private_test_theme (test_shared.Icons.test_inheritance_from_a_private_test_theme) ... ok
|
||||
test_tela_desktop_icon_and_absolute (test_shared.Icons.test_tela_desktop_icon_and_absolute) ... ok
|
||||
test_nine_move_missing_anchor_and_empty (test_shared.Persistence.test_nine_move_missing_anchor_and_empty) ... ok
|
||||
test_real_concurrent_processes_keep_all_additions_during_moves (test_shared.Persistence.test_real_concurrent_processes_keep_all_additions_during_moves) ... ok
|
||||
test_toggle_is_atomic_and_does_not_depend_on_a_stale_ui_list (test_shared.Persistence.test_toggle_is_atomic_and_does_not_depend_on_a_stale_ui_list) ... ok
|
||||
test_wayland_xwayland_scratchpad_and_explicit_focus (test_shared.Sway.test_wayland_xwayland_scratchpad_and_explicit_focus) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 6 tests in 0.735s
|
||||
|
||||
OK
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"status": "PASS",
|
||||
"base": "d4e2a0013e7a9452add80dddc110e036ebe4ee96",
|
||||
"backendTests": {
|
||||
"applications": 7,
|
||||
"dock": 6,
|
||||
"sessionV2": 93
|
||||
},
|
||||
"javascript": [
|
||||
"ApplicationsModel: PASS",
|
||||
"Dock state and window models: PASS"
|
||||
],
|
||||
"graphical": [
|
||||
"private Sway dock scenario",
|
||||
"complete shell load",
|
||||
"Session V2 and Keyring with dock"
|
||||
],
|
||||
"keyring": {
|
||||
"backend": 0,
|
||||
"focus": 0,
|
||||
"qml-unit": 0,
|
||||
"qml-qt": 0,
|
||||
"keyring-ui": 0,
|
||||
"keyring-runtime": 0,
|
||||
"scratchpad-runtime": 0,
|
||||
"phase3b-ui": 0,
|
||||
"p0-runtime": 0,
|
||||
"path-runtime": 0,
|
||||
"diff-check": 0
|
||||
},
|
||||
"finalChecks": [
|
||||
"git diff --check: PASS",
|
||||
"git apply --check against unchanged main checkout: PASS",
|
||||
"no Binding loop, assignment, ReferenceError or TypeError in final dock lab logs",
|
||||
"main checkout git status --short: empty"
|
||||
],
|
||||
"finalShellLab": "/tmp/labfy-session-v2-TEST_ONLY-ywdppzn5",
|
||||
"activeSessionModified": false
|
||||
}
|
||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
|
@ -1 +0,0 @@
|
|||
Dock headless: PASS /tmp/labfy-session-v2-TEST_ONLY-88x0lspm
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
|
@ -1,225 +0,0 @@
|
|||
{
|
||||
"status": "PASS",
|
||||
"initial": {
|
||||
"ready": true,
|
||||
"ids": [
|
||||
"firefox.desktop",
|
||||
"kitty.desktop",
|
||||
"yazi.desktop",
|
||||
"nvim.desktop",
|
||||
"org.darktable.darktable.desktop",
|
||||
"orca-slicer.desktop",
|
||||
"org.freecad.FreeCAD.desktop",
|
||||
"org.kde.digikam.desktop",
|
||||
"AppFlowy.desktop"
|
||||
],
|
||||
"error": "",
|
||||
"phase": "hidden",
|
||||
"revealed": false,
|
||||
"entries": [
|
||||
{
|
||||
"id": "firefox.desktop",
|
||||
"name": "Mozilla Firefox",
|
||||
"genericName": "Web Browser",
|
||||
"keywords": [
|
||||
"Internet",
|
||||
"WWW",
|
||||
"Browser",
|
||||
"Web",
|
||||
"Explorer"
|
||||
],
|
||||
"icon": "firefox",
|
||||
"iconSource": "file:///usr/share/icons/Tela-circle-purple-dark/scalable/apps/firefox.svg",
|
||||
"aliases": [
|
||||
"firefox"
|
||||
],
|
||||
"newInstance": false,
|
||||
"category": "Internet",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
},
|
||||
{
|
||||
"id": "kitty.desktop",
|
||||
"name": "kitty",
|
||||
"genericName": "Terminal emulator",
|
||||
"keywords": [],
|
||||
"icon": "kitty",
|
||||
"iconSource": "file:///usr/share/icons/Tela-circle-purple-dark/scalable/apps/kitty.svg",
|
||||
"aliases": [
|
||||
"kitty"
|
||||
],
|
||||
"newInstance": true,
|
||||
"category": "Syst\u00e8me",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
},
|
||||
{
|
||||
"id": "yazi.desktop",
|
||||
"name": "Yazi File Manager",
|
||||
"genericName": "",
|
||||
"keywords": [
|
||||
"File",
|
||||
"Manager",
|
||||
"Explorer",
|
||||
"Browser",
|
||||
"Launcher"
|
||||
],
|
||||
"icon": "yazi",
|
||||
"iconSource": "file:///usr/share/icons/hicolor/48x48/apps/yazi.png",
|
||||
"aliases": [
|
||||
"yazi"
|
||||
],
|
||||
"newInstance": false,
|
||||
"category": "Syst\u00e8me",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
},
|
||||
{
|
||||
"id": "nvim.desktop",
|
||||
"name": "Neovim",
|
||||
"genericName": "Text Editor",
|
||||
"keywords": [
|
||||
"Text",
|
||||
"editor"
|
||||
],
|
||||
"icon": "nvim",
|
||||
"iconSource": "file:///usr/share/icons/Tela-circle-purple-dark/scalable/apps/nvim.svg",
|
||||
"aliases": [
|
||||
"nvim"
|
||||
],
|
||||
"newInstance": false,
|
||||
"category": "D\u00e9veloppement",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
},
|
||||
{
|
||||
"id": "org.darktable.darktable.desktop",
|
||||
"name": "Darktable Photo Workflow Software",
|
||||
"genericName": "Virtual Lighttable and Darkroom",
|
||||
"keywords": [
|
||||
"graphics",
|
||||
"photography",
|
||||
"raw"
|
||||
],
|
||||
"icon": "darktable",
|
||||
"iconSource": "file:///usr/share/icons/Tela-circle-purple-dark/scalable/apps/darktable.svg",
|
||||
"aliases": [
|
||||
"darktable",
|
||||
"org.darktable.darktable"
|
||||
],
|
||||
"newInstance": false,
|
||||
"category": "Graphisme",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
},
|
||||
{
|
||||
"id": "orca-slicer.desktop",
|
||||
"name": "OrcaSlicer",
|
||||
"genericName": "",
|
||||
"keywords": [],
|
||||
"icon": "OrcaSlicer",
|
||||
"iconSource": "file:///usr/share/icons/hicolor/192x192/apps/OrcaSlicer.png",
|
||||
"aliases": [
|
||||
"apprun",
|
||||
"orca-slicer"
|
||||
],
|
||||
"newInstance": false,
|
||||
"category": "Utilitaires",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
},
|
||||
{
|
||||
"id": "org.freecad.FreeCAD.desktop",
|
||||
"name": "FreeCAD",
|
||||
"genericName": "CAD Application",
|
||||
"keywords": [],
|
||||
"icon": "org.freecad.FreeCAD",
|
||||
"iconSource": "file:///usr/share/icons/Tela-circle-purple-dark/scalable/apps/org.freecad.FreeCAD.svg",
|
||||
"aliases": [
|
||||
"freecad",
|
||||
"org.freecad.freecad"
|
||||
],
|
||||
"newInstance": false,
|
||||
"category": "Graphisme",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
},
|
||||
{
|
||||
"id": "org.kde.digikam.desktop",
|
||||
"name": "digiKam",
|
||||
"genericName": "Photo Management Program",
|
||||
"keywords": [],
|
||||
"icon": "digikam",
|
||||
"iconSource": "file:///usr/share/icons/Tela-circle-purple-dark/scalable/apps/digikam.svg",
|
||||
"aliases": [
|
||||
"digikam",
|
||||
"org.kde.digikam"
|
||||
],
|
||||
"newInstance": false,
|
||||
"category": "Graphisme",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
},
|
||||
{
|
||||
"id": "AppFlowy.desktop",
|
||||
"name": "AppFlowy",
|
||||
"genericName": "",
|
||||
"keywords": [
|
||||
"Notes"
|
||||
],
|
||||
"icon": "appflowy",
|
||||
"iconSource": "file:///usr/share/icons/hicolor/scalable/apps/appflowy.svg",
|
||||
"aliases": [
|
||||
"appflowy"
|
||||
],
|
||||
"newInstance": false,
|
||||
"category": "Internet",
|
||||
"pinned": true,
|
||||
"windows": []
|
||||
}
|
||||
],
|
||||
"theme": "Tela-circle-purple-dark",
|
||||
"menu": null,
|
||||
"searchFocused": false,
|
||||
"navFocused": false,
|
||||
"gridFocused": false,
|
||||
"screenWidth": 1280,
|
||||
"bodyY": 364,
|
||||
"width": 538,
|
||||
"pointerX": -1000,
|
||||
"tileWidth": 54,
|
||||
"context": false
|
||||
},
|
||||
"reordered": [
|
||||
"kitty.desktop",
|
||||
"firefox.desktop",
|
||||
"yazi.desktop",
|
||||
"nvim.desktop",
|
||||
"org.darktable.darktable.desktop",
|
||||
"orca-slicer.desktop",
|
||||
"org.freecad.FreeCAD.desktop",
|
||||
"org.kde.digikam.desktop",
|
||||
"AppFlowy.desktop"
|
||||
],
|
||||
"checks": [
|
||||
"shared favorites",
|
||||
"menu and dock mutations",
|
||||
"durable reorder",
|
||||
"empty restart",
|
||||
"autohide",
|
||||
"context hold",
|
||||
"kitty multi-window",
|
||||
"floating scratchpad explicit focus",
|
||||
"window close",
|
||||
"no hover focus",
|
||||
"real pointer drag",
|
||||
"zoom growth",
|
||||
"IPC reconnect",
|
||||
"Firefox private profile",
|
||||
"desktop install/remove monitoring",
|
||||
"external writer",
|
||||
"keyboard menu navigation",
|
||||
"resolution change",
|
||||
"SwayFX dock blur rule"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
/tmp/labfy-session-v2-TEST_ONLY-43bkifog
|
||||
ready PASS
|
||||
accept PASS
|
||||
cancel PASS
|
||||
timeout PASS
|
||||
absent PASS
|
||||
still-locked PASS
|
||||
different PASS
|
||||
quickshell-restart-during-unlock PASS
|
||||
keyring_ui WITH_DOCK PASS
|
||||
success PASS
|
||||
failure PASS
|
||||
invalid PASS
|
||||
new PASS
|
||||
claimed PASS
|
||||
real PASS
|
||||
deferred-real PASS
|
||||
phase3b_ui WITH_DOCK PASS
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"backend": 0,
|
||||
"focus": 0,
|
||||
"qml-unit": 0,
|
||||
"qml-qt": 0,
|
||||
"keyring-ui": 0,
|
||||
"keyring-runtime": 0,
|
||||
"scratchpad-runtime": 0,
|
||||
"phase3b-ui": 0,
|
||||
"p0-runtime": 0,
|
||||
"path-runtime": 0,
|
||||
"diff-check": 0
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
Full shell private-compositor load: PASS /tmp/labfy-session-v2-TEST_ONLY-rfgtrd4k
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"status": "PASS",
|
||||
"baseline": "a556cd56",
|
||||
"quickshell": "0.3.2",
|
||||
"sway": "SwayFX 0.6 / Sway 1.12.0",
|
||||
"tests": {
|
||||
"existingApplicationsPython": 7,
|
||||
"dockPython": 6,
|
||||
"existingApplicationsJS": "PASS",
|
||||
"dockModelsJS": "PASS",
|
||||
"sessionV2BackendPython": 93,
|
||||
"sessionV2ValidationGroups": 11,
|
||||
"keyringWithDock": "PASS (8 cases)",
|
||||
"restoreUIWithDock": "PASS (7 cases)",
|
||||
"headless": "PASS",
|
||||
"fullShellRelativeImports": "PASS",
|
||||
"diffCheck": "PASS"
|
||||
},
|
||||
"activeSession": "not modified; original nine favorites preserved",
|
||||
"limitations": [
|
||||
"XWayland fixture coverage only",
|
||||
"physical greeter/PAM login not exercised",
|
||||
"terminal-host identity is preserved",
|
||||
"fractional scale and touch not exercised"
|
||||
],
|
||||
"notes": [
|
||||
"Absolute-import fixture emits QuickShell scanner warnings; actual relative-import shell loads.",
|
||||
"Virtual keyboard teardown emits Qt Wayland leave/text-input warnings; keyboard cycle passes.",
|
||||
"Full shell intentionally has no system services in the private compositor."
|
||||
]
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
test_inheritance_from_a_private_test_theme (test_shared.Icons.test_inheritance_from_a_private_test_theme) ... ok
|
||||
test_tela_desktop_icon_and_absolute (test_shared.Icons.test_tela_desktop_icon_and_absolute) ... ok
|
||||
test_nine_move_missing_anchor_and_empty (test_shared.Persistence.test_nine_move_missing_anchor_and_empty) ... ok
|
||||
test_real_concurrent_processes_keep_all_additions_during_moves (test_shared.Persistence.test_real_concurrent_processes_keep_all_additions_during_moves) ... ok
|
||||
test_toggle_is_atomic_and_does_not_depend_on_a_stale_ui_list (test_shared.Persistence.test_toggle_is_atomic_and_does_not_depend_on_a_stale_ui_list) ... ok
|
||||
test_wayland_xwayland_scratchpad_and_explicit_focus (test_shared.Sway.test_wayland_xwayland_scratchpad_and_explicit_focus) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 6 tests in 0.705s
|
||||
|
||||
OK
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
|
@ -103,7 +103,8 @@ PanelWindow {
|
|||
|
||||
property string applicationsError: ""
|
||||
property string queuedApplicationId: ""
|
||||
readonly property string applicationsBackend: FavoritesService.backend
|
||||
readonly property string applicationsBackend: (Quickshell.env("XDG_CONFIG_HOME") ||
|
||||
(Quickshell.env("HOME") + "/.config")) + "/quickshell/labfy-sway/applications/backend.py"
|
||||
function closeApplications() { applicationCoordinator.applicationsOutput = ""; }
|
||||
function toggleApplications() {
|
||||
if (authenticationActive) return false;
|
||||
|
|
|
|||
|
|
@ -389,10 +389,3 @@ relance n’est automatique, y compris après redémarrage QuickShell.
|
|||
Voir [Session Restore V2](../../../../docs/session-restore-v2.md) pour les
|
||||
commandes, les preuves et les limites de l’environnement. La recette réussie
|
||||
du 7 octobre est historique ; elle ne valide pas cette révision au login réel.
|
||||
|
||||
## Dock et favoris partagés
|
||||
|
||||
Le dock à masquage automatique et le menu utilisent `applications/FavoritesService.qml`
|
||||
et `ApplicationIcon.qml`. Le stockage existant reste inchangé. Architecture,
|
||||
interactions, tests headless, limites et procédure d’intégration :
|
||||
[Dock Tela / favoris V1](../../../../docs/quickshell-dock-tela-shared-favorites-v1.md).
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import QtQuick
|
||||
import "../theme"
|
||||
|
||||
Item {
|
||||
property string iconSource: ""
|
||||
Image {
|
||||
id: image
|
||||
anchors.fill: parent
|
||||
source: parent.iconSource
|
||||
sourceSize.width: Math.ceil(width * 2)
|
||||
sourceSize.height: Math.ceil(height * 2)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
}
|
||||
// CONTRACT: a corrupt, removed or unresolvable icon always has a visible
|
||||
// fallback independent of any installed font or desktop icon theme.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: image.status !== Image.Ready
|
||||
radius: width / 2; color: Theme.surface1
|
||||
border.color: Theme.lavender
|
||||
Text { anchors.centerIn: parent; text: "◇"; color: Theme.lavender; font.pixelSize: parent.width * 0.6 }
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,8 @@ PanelWindow {
|
|||
signal menuDismissed()
|
||||
signal launchRequested(string desktopId)
|
||||
property string errorMessage: ""
|
||||
readonly property var allApps: FavoritesService.apps
|
||||
readonly property var favoriteIds: FavoritesService.ids
|
||||
property var allApps: []
|
||||
property var favoriteIds: []
|
||||
property string selectedCategory: "Toutes"
|
||||
property int selectedIndex: 0
|
||||
property int navIndex: 0
|
||||
|
|
@ -44,6 +44,10 @@ PanelWindow {
|
|||
WlrLayershell.namespace: "labfy-applications-menu"
|
||||
color: "transparent"
|
||||
|
||||
function refreshCatalog() {
|
||||
allApps = ApplicationsModel.catalog(DesktopEntries.applications.values);
|
||||
if (!favoriteIds.length && selectedCategory === "Favoris") selectedCategory = "Toutes";
|
||||
}
|
||||
function chooseCategory(key) {
|
||||
selectedCategory = key;
|
||||
navIndex = navigation.findIndex(item => item.key === key);
|
||||
|
|
@ -59,7 +63,10 @@ PanelWindow {
|
|||
if (app) launchRequested(app.id);
|
||||
}
|
||||
function toggleFavorite(id) {
|
||||
FavoritesService.toggle(id);
|
||||
const had = favoriteIds.includes(id);
|
||||
favoriteIds = had ? favoriteIds.filter(value => value !== id) : favoriteIds.concat([id]);
|
||||
Quickshell.execDetached(["python3", "-B", backend, "favorites", had ? "remove" : "add", id]);
|
||||
if (selectedCategory === "Favoris" && favoriteIds.length === 0) chooseCategory("Toutes");
|
||||
}
|
||||
function dismiss() {
|
||||
if (closing) return;
|
||||
|
|
@ -73,10 +80,39 @@ PanelWindow {
|
|||
interval: 150; repeat: false
|
||||
onTriggered: { menu.visible = false; menu.menuDismissed(); }
|
||||
}
|
||||
readonly property string backend: (Quickshell.env("XDG_CONFIG_HOME") ||
|
||||
(Quickshell.env("HOME") + "/.config")) + "/quickshell/labfy-sway/applications/backend.py"
|
||||
|
||||
onVisibleAppsChanged: selectedIndex = Math.min(selectedIndex, Math.max(0, visibleApps.length - 1))
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() { menu.refreshCatalog(); }
|
||||
}
|
||||
Process {
|
||||
id: favoritesReader
|
||||
command: ["python3", "-B", menu.backend, "favorites", "read"]
|
||||
stdout: StdioCollector { id: favoritesOutput; waitForEnd: true }
|
||||
onExited: (code, status) => {
|
||||
if (code !== 0) { menu.errorMessage = "Favoris indisponibles"; return; }
|
||||
try {
|
||||
const data = JSON.parse(favoritesOutput.text);
|
||||
if (data.exists) {
|
||||
menu.favoriteIds = data.ids;
|
||||
} else {
|
||||
const suggested = ApplicationsModel.initialFavorites(menu.allApps);
|
||||
menu.favoriteIds = suggested;
|
||||
Quickshell.execDetached(["python3", "-B", menu.backend, "favorites", "seed"]
|
||||
.concat(suggested));
|
||||
}
|
||||
menu.selectedCategory = menu.favoriteIds.some(id => menu.allApps.some(app => app.id === id))
|
||||
? "Favoris" : "Toutes";
|
||||
menu.navIndex = menu.selectedCategory === "Favoris" ? 0 : 1;
|
||||
} catch (_) { menu.errorMessage = "Favoris invalides"; }
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
selectedCategory = favoriteIds.some(id => allApps.some(app => app.id === id)) ? "Favoris" : "Toutes";
|
||||
navIndex = selectedCategory === "Favoris" ? 0 : 1;
|
||||
refreshCatalog();
|
||||
favoritesReader.running = true;
|
||||
}
|
||||
// The Wayland surface exists after completion; focus at the next frame.
|
||||
Timer { interval: 120; running: true; repeat: false; onTriggered: search.forceActiveFocus() }
|
||||
|
|
@ -130,10 +166,6 @@ PanelWindow {
|
|||
font.family: "JetBrainsMono Nerd Font"; font.pixelSize: 13
|
||||
background: Item {}
|
||||
onTextChanged: { menu.selectedIndex = 0; grid.positionViewAtBeginning(); }
|
||||
// WHY: TextField's built-in Tab navigation consumes the
|
||||
// event before AfterItem handlers. Preserve this menu's
|
||||
// explicit search/sidebar/grid keyboard cycle.
|
||||
Keys.priority: Keys.BeforeItem
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) { menu.dismiss(); event.accepted = true; }
|
||||
else if (event.key === Qt.Key_Backtab ||
|
||||
|
|
@ -150,8 +182,8 @@ PanelWindow {
|
|||
}
|
||||
}
|
||||
Text {
|
||||
visible: menu.errorMessage !== "" || FavoritesService.error !== ""
|
||||
width: parent.width; text: menu.errorMessage || FavoritesService.error
|
||||
visible: menu.errorMessage !== ""
|
||||
width: parent.width; text: menu.errorMessage
|
||||
color: Theme.danger; font.pixelSize: 12; elide: Text.ElideRight
|
||||
}
|
||||
Item {
|
||||
|
|
@ -167,7 +199,6 @@ PanelWindow {
|
|||
anchors.fill: parent; anchors.margins: 5
|
||||
clip: true; spacing: 3
|
||||
model: menu.navigation
|
||||
Keys.priority: Keys.BeforeItem
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
|
||||
|
|
@ -224,7 +255,6 @@ PanelWindow {
|
|||
visible: menu.visibleApps.length > 0
|
||||
clip: true
|
||||
model: menu.visibleApps
|
||||
Keys.priority: Keys.BeforeItem
|
||||
cellWidth: Math.max(80, Math.floor(width / (width >= 420 ? 4 : 3)))
|
||||
cellHeight: 104
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
|
@ -263,12 +293,23 @@ PanelWindow {
|
|||
border.color: menu.selectedIndex === index && grid.activeFocus
|
||||
? Theme.lavender : "transparent"
|
||||
}
|
||||
ApplicationIcon {
|
||||
Image {
|
||||
id: appIcon
|
||||
anchors.top: parent.top; anchors.topMargin: 12
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 46; height: 46
|
||||
iconSource: modelData.iconSource
|
||||
source: modelData.icon.startsWith("/") ? "file://" + modelData.icon
|
||||
: Quickshell.iconPath(modelData.icon || "application-x-executable",
|
||||
"application-x-executable")
|
||||
sourceSize.width: 46; sourceSize.height: 46
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
}
|
||||
Text {
|
||||
visible: appIcon.status === Image.Error
|
||||
anchors.centerIn: appIcon
|
||||
text: ""; color: Theme.lavender
|
||||
font.family: "JetBrainsMono Nerd Font"; font.pixelSize: 32
|
||||
}
|
||||
Text {
|
||||
anchors.top: appIcon.bottom; anchors.topMargin: 5
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ function catalog(entries) {
|
|||
.map(entry => ({ id: entry.id.endsWith(".desktop") ? entry.id : entry.id + ".desktop",
|
||||
name: entry.name || entry.id,
|
||||
genericName: entry.genericName || "", keywords: entry.keywords || [],
|
||||
icon: entry.icon || "", iconSource: entry.iconSource || "",
|
||||
aliases: entry.aliases || [], newInstance: !!entry.newInstance, category: classify(entry) }))
|
||||
icon: entry.icon || "", category: classify(entry) }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, "fr", { sensitivity: "base" }) || a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "ApplicationsModel.js" as Model
|
||||
import "WindowModel.js" as Windows
|
||||
|
||||
Singleton {
|
||||
id: service
|
||||
// CONTRACT: both interfaces bind these backend-authoritative properties.
|
||||
// No UI writes ids; a failed command never leaves an optimistic favorite.
|
||||
property var ids: []
|
||||
property var apps: []
|
||||
property var windows: []
|
||||
property bool ready: false
|
||||
property bool available: false
|
||||
property string error: ""
|
||||
property string iconTheme: ""
|
||||
readonly property string backend: decodeURIComponent(Qt.resolvedUrl("backend.py").toString().replace(/^file:\/\//, ""))
|
||||
readonly property var dockEntries: Windows.entries(apps, ids, windows)
|
||||
function mutate(action, values) {
|
||||
if (!available) { error = "Service de favoris indisponible"; return; }
|
||||
bridge.write(JSON.stringify({action: action, ids: values}) + "\n");
|
||||
}
|
||||
function toggle(id) { mutate("toggle", [id]); }
|
||||
function move(id, before) { mutate("move", [id, before]); }
|
||||
function launch(id) { act("launch", id); }
|
||||
function focusWindow(id) { act("focus", id); }
|
||||
function act(kind, id) {
|
||||
// INVARIANT: callers are user-click handlers only. Neither snapshots,
|
||||
// state transitions nor animations may call this action boundary.
|
||||
if (action.running) { error = "Action en cours"; return; }
|
||||
action.command = ["python3", "-B", backend, kind, id];
|
||||
action.running = true;
|
||||
}
|
||||
Process {
|
||||
id: bridge
|
||||
command: ["python3", "-B", service.backend, "serve"]
|
||||
stdinEnabled: true
|
||||
running: true
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
try {
|
||||
const state = JSON.parse(data);
|
||||
service.error = state.error || "";
|
||||
if (!state.ids) return;
|
||||
service.ids = state.ids;
|
||||
service.apps = Model.catalog(state.apps);
|
||||
service.iconTheme = state.theme;
|
||||
service.ready = true;
|
||||
service.available = true;
|
||||
// WHY: an existing empty file means intentionally empty.
|
||||
// Seed only an absent file; backend repeats that check under lock.
|
||||
if (!state.exists) service.mutate("seed", Model.initialFavorites(service.apps));
|
||||
} catch (_) { service.error = "Réponse des favoris invalide"; }
|
||||
}
|
||||
}
|
||||
onExited: { service.available = false; reconnect.start(); }
|
||||
}
|
||||
Timer { id: reconnect; interval: 1500; onTriggered: bridge.running = true }
|
||||
Process {
|
||||
id: action
|
||||
stdout: StdioCollector { id: actionReply; waitForEnd: true }
|
||||
onExited: (code, status) => {
|
||||
if (code !== 0) {
|
||||
try { service.error = JSON.parse(actionReply.text).error || "Action refusée"; }
|
||||
catch (_) { service.error = "Action refusée"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
// CONTRACT: one native event subscription for all outputs. Reconcile on
|
||||
// connect and after bursts; dirty preserves events received during a query.
|
||||
property bool dirty: false
|
||||
function refreshWindows() { dirty = true; if (!snapshot.running) settle.restart(); }
|
||||
Process {
|
||||
id: subscription
|
||||
command: ["swaymsg", "-m", "-t", "subscribe", '["window","workspace","output"]']
|
||||
running: true
|
||||
stdout: SplitParser { onRead: data => service.refreshWindows() }
|
||||
onStarted: service.refreshWindows()
|
||||
onExited: { service.windows = []; reconnectEvents.restart(); }
|
||||
}
|
||||
// WHY: the native I3 listener in 0.3.2 did not resubscribe after a forced
|
||||
// socket loss in the private-compositor test. This one owned process has
|
||||
// a single restart deadline; there is no steady-state window polling.
|
||||
Timer { id: reconnectEvents; interval: 1500; onTriggered: subscription.running = true }
|
||||
Timer { id: settle; interval: 60; onTriggered: { service.dirty = false; snapshot.running = true; } }
|
||||
Timer { id: retrySnapshot; interval: 1500; onTriggered: service.refreshWindows() }
|
||||
Process {
|
||||
id: snapshot
|
||||
command: ["python3", "-B", service.backend, "windows"]
|
||||
stdout: StdioCollector { id: snapshotReply; waitForEnd: true }
|
||||
onExited: (code, status) => {
|
||||
if (code === 0) {
|
||||
try { service.windows = JSON.parse(snapshotReply.text).windows; }
|
||||
catch (_) { service.windows = []; }
|
||||
} else { service.windows = []; retrySnapshot.restart(); }
|
||||
if (service.dirty) settle.restart();
|
||||
}
|
||||
}
|
||||
Component.onCompleted: refreshWindows()
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
.pragma library
|
||||
|
||||
function match(apps, window) {
|
||||
// CONTRACT: declared identities only, never titles. Exact desktop stem
|
||||
// wins; an ambiguous executable/WM_CLASS alias must remain unknown.
|
||||
const keys = [window.appId, window.wmClass, window.instance]
|
||||
.filter(Boolean).map(value => value.toLowerCase());
|
||||
for (const key of keys) {
|
||||
const exact = apps.filter(app => app.id.slice(0, -8).toLowerCase() === key);
|
||||
if (exact.length === 1) return exact[0];
|
||||
const aliases = apps.filter(app => (app.aliases || []).includes(key));
|
||||
if (aliases.length === 1) return aliases[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function entries(apps, ids, windows) {
|
||||
const byId = new Map(apps.map(app => [app.id, app]));
|
||||
const result = ids.map(id => Object.assign({}, byId.get(id) || {
|
||||
id: id, name: id.replace(/\.desktop$/, "") + " (indisponible)",
|
||||
iconSource: "", newInstance: false, missing: true
|
||||
}, {pinned: true, windows: []}));
|
||||
const groups = new Map(result.map(entry => [entry.id, entry]));
|
||||
for (const window of windows) {
|
||||
const app = match(apps, window);
|
||||
const id = app ? app.id : "window:" + (window.appId || window.wmClass || window.id);
|
||||
if (!groups.has(id)) {
|
||||
const entry = Object.assign({}, app || {id: id, name: window.appId || window.wmClass || "Application",
|
||||
iconSource: "", missing: true, newInstance: false}, {pinned: false, windows: []});
|
||||
groups.set(id, entry); result.push(entry);
|
||||
}
|
||||
groups.get(id).windows.push(window);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -57,31 +57,18 @@ def write_favorites(path, action, ids):
|
|||
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
fcntl.flock(directory, fcntl.LOCK_EX)
|
||||
if (action in ("add", "remove", "toggle") and (len(ids) != 1 or not valid_id(ids[0]))) or (
|
||||
action == "move" and (len(ids) != 2 or not valid_id(ids[0])
|
||||
or (ids[1] and not valid_id(ids[1])))):
|
||||
raise ValueError("Identifiant de favori invalide")
|
||||
exists, current = read_favorites(path)
|
||||
if action == "seed":
|
||||
if exists:
|
||||
return current
|
||||
current = list(dict.fromkeys(ids))[:MAX_FAVORITES]
|
||||
elif action == "add" or (action == "toggle" and ids[0] not in current):
|
||||
elif action == "add":
|
||||
if ids[0] not in current:
|
||||
if len(current) >= MAX_FAVORITES:
|
||||
raise ValueError("Trop de favoris")
|
||||
current.append(ids[0])
|
||||
elif action in ("remove", "toggle"):
|
||||
elif action == "remove":
|
||||
current = [item for item in current if item != ids[0]]
|
||||
elif action == "move":
|
||||
# CONTRACT: move one existing ID before a live anchor (or to the
|
||||
# end). Never replace a client's stale whole list: concurrent adds
|
||||
# and removes survive. A vanished source/anchor rejects atomically.
|
||||
if len(ids) != 2 or ids[0] not in current or (ids[1] and ids[1] not in current):
|
||||
raise ValueError("Favori déplacé ou cible indisponible")
|
||||
if ids[0] != ids[1]:
|
||||
current.remove(ids[0])
|
||||
current.insert(current.index(ids[1]) if ids[1] else len(current), ids[0])
|
||||
else:
|
||||
raise ValueError("Action de favoris invalide")
|
||||
if any(not valid_id(item) for item in current):
|
||||
|
|
@ -168,22 +155,10 @@ def main(argv):
|
|||
if len(ids) > 4 or any(not valid_id(item) for item in ids):
|
||||
raise ValueError("Favoris initiaux invalides")
|
||||
result = {"ids": write_favorites(state_path(), "seed", ids)}
|
||||
elif len(argv) == 3 and argv[:2] in (["favorites", "add"], ["favorites", "remove"], ["favorites", "toggle"]):
|
||||
elif len(argv) == 3 and argv[:2] in (["favorites", "add"], ["favorites", "remove"]):
|
||||
if not valid_id(argv[2]):
|
||||
raise ValueError("Identifiant de favori invalide")
|
||||
result = {"ids": write_favorites(state_path(), argv[1], [argv[2]])}
|
||||
elif len(argv) == 4 and argv[:2] == ["favorites", "move"]:
|
||||
result = {"ids": write_favorites(state_path(), "move", argv[2:])}
|
||||
elif argv == ["serve"]:
|
||||
from shared import serve
|
||||
return serve()
|
||||
elif argv == ["windows"]:
|
||||
from shared import windows, sway_request
|
||||
result = {"windows": windows(sway_request(4, ""))}
|
||||
elif len(argv) == 2 and argv[0] == "focus":
|
||||
from shared import focus
|
||||
focus(argv[1])
|
||||
result = {"ok": True}
|
||||
elif len(argv) == 2 and argv[0] == "launch":
|
||||
launch(argv[1])
|
||||
result = {"ok": True}
|
||||
|
|
@ -191,7 +166,7 @@ def main(argv):
|
|||
raise ValueError("Commande invalide")
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
except (OSError, ValueError, KeyError, GLib.Error, json.JSONDecodeError, subprocess.TimeoutExpired) as error:
|
||||
except (OSError, ValueError, GLib.Error, json.JSONDecodeError, subprocess.TimeoutExpired) as error:
|
||||
print(json.dumps({"error": str(error)}, ensure_ascii=False))
|
||||
return 1
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
singleton FavoritesService 1.0 FavoritesService.qml
|
||||
ApplicationsMenu 1.0 ApplicationsMenu.qml
|
||||
ApplicationIcon 1.0 ApplicationIcon.qml
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
"""Shared application catalog, private icon theme and bounded Sway requests.
|
||||
|
||||
No session/restore state is consulted or changed. The only persistent write
|
||||
remains backend.write_favorites, under its existing directory transaction lock.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gio, GioUnix, GLib, Gtk
|
||||
from backend import state_path, read_favorites, write_favorites, valid_id
|
||||
|
||||
# CONTRACT: private GtkIconTheme instance, never Gtk settings or desktop-wide
|
||||
# theme changes. GTK implements theme inheritance and absolute GIcon lookup.
|
||||
ICON_THEME = 'Tela-circle-purple-dark'
|
||||
# CONTRACT: middle-click is conservative. These terminal launchers create a
|
||||
# new toplevel per invocation. Unknown and single-instance GUI applications
|
||||
# retain their ordinary Open action without promising another instance.
|
||||
NEW_INSTANCE_IDS = frozenset(('kitty.desktop', 'foot.desktop', 'Alacritty.desktop',
|
||||
'org.wezfurlong.wezterm.desktop'))
|
||||
|
||||
|
||||
class IconResolver:
|
||||
def __init__(self, theme=ICON_THEME):
|
||||
self.theme = Gtk.IconTheme.new()
|
||||
self.theme.set_custom_theme(theme)
|
||||
|
||||
def resolve(self, icon):
|
||||
if icon:
|
||||
info = self.theme.lookup_by_gicon(icon, 48, Gtk.IconLookupFlags.FORCE_SIZE)
|
||||
if info and Path(info.get_filename()).is_file():
|
||||
return Path(info.get_filename()).as_uri()
|
||||
info = self.theme.lookup_icon('application-x-executable', 48, 0)
|
||||
return Path(info.get_filename()).as_uri() if info and Path(info.get_filename()).is_file() else ''
|
||||
|
||||
|
||||
def catalog():
|
||||
resolver = IconResolver()
|
||||
result = []
|
||||
for app in Gio.AppInfo.get_all():
|
||||
identifier = app.get_id()
|
||||
if not valid_id(identifier) or not app.should_show() or not isinstance(app, GioUnix.DesktopAppInfo):
|
||||
continue
|
||||
icon = app.get_icon()
|
||||
# WHY: StartupWMClass is the desktop author's declared window identity.
|
||||
# Filename stem and executable basename cover standard Wayland app_ids;
|
||||
# ambiguous aliases are deliberately left unmatched by the UI model.
|
||||
aliases = [identifier[:-8], app.get_startup_wm_class(),
|
||||
os.path.basename(app.get_executable() or '')]
|
||||
result.append(dict(id=identifier, name=app.get_display_name(),
|
||||
genericName=app.get_generic_name() or '',
|
||||
keywords=list(app.get_keywords() or []),
|
||||
categories=(app.get_categories() or '').split(';'),
|
||||
icon=icon.to_string() if icon else '', iconSource=resolver.resolve(icon),
|
||||
aliases=sorted({v.casefold() for v in aliases if v}),
|
||||
# D-Bus activation may only activate an existing window;
|
||||
# do not promise a new instance for these applications.
|
||||
newInstance=identifier in NEW_INSTANCE_IDS and not app.get_boolean('DBusActivatable')))
|
||||
return sorted(result, key=lambda a: a['id'])
|
||||
|
||||
|
||||
def sway_request(kind, payload):
|
||||
# CONTRACT: direct IPC, explicit little-endian widths, bounded reply and
|
||||
# timeout. No shell, no inherited process, no background focus operations.
|
||||
data = payload.encode()
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
||||
deadline = time.monotonic() + 2
|
||||
sock.settimeout(2)
|
||||
sock.connect(os.environ['SWAYSOCK'])
|
||||
sock.sendall(b'i3-ipc' + struct.pack('<II', len(data), kind) + data)
|
||||
def receive(size):
|
||||
parts = bytearray()
|
||||
while len(parts) < size:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError('Délai IPC Sway dépassé')
|
||||
sock.settimeout(remaining)
|
||||
block = sock.recv(size - len(parts))
|
||||
if not block:
|
||||
raise ValueError('IPC Sway interrompu')
|
||||
parts.extend(block)
|
||||
return parts
|
||||
header = receive(14)
|
||||
length, reply_kind = struct.unpack('<II', header[6:])
|
||||
if header[:6] != b'i3-ipc' or reply_kind != kind or length > 16 * 1024 * 1024:
|
||||
raise ValueError('Réponse IPC Sway invalide')
|
||||
return json.loads(receive(length))
|
||||
|
||||
|
||||
def windows(tree):
|
||||
result = []
|
||||
pending = [(tree, '')]
|
||||
while pending:
|
||||
node, workspace = pending.pop()
|
||||
if node.get('type') == 'workspace':
|
||||
workspace = node.get('name', '')
|
||||
pending.extend((child, workspace) for child in
|
||||
node.get('nodes', []) + node.get('floating_nodes', []))
|
||||
props = node.get('window_properties') or {}
|
||||
if not (node.get('app_id') or node.get('window')):
|
||||
continue
|
||||
result.append(dict(id=str(node['id']), title=node.get('name') or 'Fenêtre',
|
||||
appId=node.get('app_id') or '', wmClass=props.get('class') or '',
|
||||
instance=props.get('instance') or '', workspace=workspace,
|
||||
focused=bool(node.get('focused'))))
|
||||
return sorted(result, key=lambda w: int(w['id']))
|
||||
|
||||
|
||||
def focus(identifier):
|
||||
if not isinstance(identifier, str) or not identifier.isascii() or not identifier.isdigit():
|
||||
raise ValueError('Identifiant de fenêtre invalide')
|
||||
target = next((w for w in windows(sway_request(4, '')) if w['id'] == identifier), None)
|
||||
if not target:
|
||||
raise ValueError('Fenêtre fermée')
|
||||
command = f'[con_id={int(identifier)}] '
|
||||
if target['workspace'] == '__i3_scratch':
|
||||
replies = sway_request(0, command + 'scratchpad show')
|
||||
if not all(reply.get('success') for reply in replies):
|
||||
raise ValueError('Scratchpad indisponible')
|
||||
if not all(reply.get('success') for reply in sway_request(0, command + 'focus')):
|
||||
raise ValueError('Focus refusé')
|
||||
|
||||
|
||||
def serve():
|
||||
"""Single stream serializes UI mutations and disk notifications.
|
||||
|
||||
INVARIANT: every published list is re-read from disk. A rejected mutation
|
||||
publishes the unchanged durable state plus an error, never an optimistic
|
||||
list. File monitoring survives atomic replacement by watching the directory.
|
||||
"""
|
||||
path = state_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
apps = catalog()
|
||||
def publish(error=''):
|
||||
try:
|
||||
exists, ids = read_favorites(path)
|
||||
print(json.dumps(dict(ids=ids, exists=exists, apps=apps,
|
||||
theme=ICON_THEME, error=error)), flush=True)
|
||||
except (OSError, ValueError) as exc:
|
||||
print(json.dumps(dict(error=str(exc))), flush=True)
|
||||
return False
|
||||
monitor = Gio.File.new_for_path(str(path.parent)).monitor_directory(Gio.FileMonitorFlags.NONE, None)
|
||||
pending = [0]
|
||||
def changed(*_args):
|
||||
if not pending[0]:
|
||||
pending[0] = GLib.timeout_add(35, refresh)
|
||||
def refresh():
|
||||
pending[0] = 0
|
||||
return publish()
|
||||
monitor.connect('changed', changed)
|
||||
app_monitor = Gio.AppInfoMonitor.get()
|
||||
def apps_changed(*_args):
|
||||
nonlocal apps
|
||||
apps = catalog()
|
||||
publish()
|
||||
app_monitor.connect('changed', apps_changed)
|
||||
loop = GLib.MainLoop()
|
||||
# CONTRACT: newline JSON input is bounded before parsing. GLib owns the
|
||||
# single main thread; EOF ends this helper with its QuickShell owner.
|
||||
buffer = bytearray()
|
||||
def request(fd, condition):
|
||||
chunk = os.read(fd, 4096)
|
||||
if not chunk:
|
||||
loop.quit()
|
||||
return False
|
||||
buffer.extend(chunk)
|
||||
if len(buffer) > 16384:
|
||||
loop.quit()
|
||||
return False
|
||||
while b'\n' in buffer:
|
||||
line, _, rest = buffer.partition(b'\n')
|
||||
buffer[:] = rest
|
||||
try:
|
||||
data = json.loads(line)
|
||||
action = data.get('action')
|
||||
ids = data.get('ids')
|
||||
if action not in ('add', 'remove', 'toggle', 'move', 'seed') or not isinstance(ids, list):
|
||||
raise ValueError('Commande de favoris invalide')
|
||||
if action == 'seed' and (len(ids) > 4 or any(not valid_id(i) for i in ids)):
|
||||
raise ValueError('Favoris initiaux invalides')
|
||||
write_favorites(path, action, ids)
|
||||
publish()
|
||||
except (OSError, ValueError, TypeError) as exc:
|
||||
publish(str(exc))
|
||||
return True
|
||||
GLib.io_add_watch(sys.stdin.fileno(), GLib.IO_IN | GLib.IO_HUP, request)
|
||||
publish()
|
||||
loop.run()
|
||||
monitor.cancel()
|
||||
return 0
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
.pragma library
|
||||
|
||||
// CONTRACT: the sole deadline belongs to arming (edge dwell) or waiting
|
||||
// (exit grace). Holding any interaction cancels it. No animation sends IPC.
|
||||
function next(state, event, input) {
|
||||
if (input.suppressed) return "hidden";
|
||||
if (input.held) return "held";
|
||||
if (input.body) return "shown";
|
||||
if (input.edge) {
|
||||
if (state === "hidden") return "arming";
|
||||
if (state === "arming") return event === "deadline" ? "shown" : "arming";
|
||||
return "shown";
|
||||
}
|
||||
if (state === "hidden" || state === "arming") return "hidden";
|
||||
if (state === "waiting") return event === "deadline" ? "hidden" : "waiting";
|
||||
return "waiting";
|
||||
}
|
||||
|
||||
function influence(distance) {
|
||||
// CONTRACT: 57 px resting pitch; a radius below 1.5 pitches affects
|
||||
// at most the nearest slot and its immediate neighbours. Smoothstep has
|
||||
// zero slope at both ends, avoiding a kink as an icon enters the halo.
|
||||
const t = Math.max(0, 1 - Math.abs(distance) / 76);
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "../applications"
|
||||
import "../theme"
|
||||
import "AutoHide.js" as AutoHide
|
||||
|
||||
PanelWindow {
|
||||
id: dock
|
||||
required property var modelData
|
||||
property bool suppressed: false
|
||||
screen: modelData
|
||||
anchors.bottom: true
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
exclusiveZone: 0
|
||||
aboveWindows: true
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
// CONTRACT: the dock and its in-surface menus never accept keyboard focus.
|
||||
// Layer surfaces are absent from get_tree, hence Session V2 snapshots.
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "labfy-sway-dock"
|
||||
color: "transparent"
|
||||
implicitWidth: Math.max(96, Math.min(screen.width - 24, items.width + 52))
|
||||
implicitHeight: 360
|
||||
property string phase: "hidden"
|
||||
readonly property bool revealed: phase === "shown" || phase === "waiting" || phase === "held"
|
||||
property bool pressed: false
|
||||
property string draggingId: ""
|
||||
property var contextEntry: null
|
||||
property real contextX: 0
|
||||
property real pointerX: -1000
|
||||
readonly property bool held: pressed || draggingId !== "" || contextEntry !== null
|
||||
readonly property var entries: FavoritesService.dockEntries
|
||||
readonly property int pinnedCount: FavoritesService.ids.length
|
||||
property alias contentItemRow: items
|
||||
property alias surfaceBody: body
|
||||
property alias contextCard: context
|
||||
property alias edgeArea: sensor
|
||||
// INVARIANT: only the bottom activation strip receives input while hidden.
|
||||
// Visible input follows the animated body, with a bridge to the edge.
|
||||
mask: Region {
|
||||
item: sensor
|
||||
Region { item: body; intersection: dock.revealed ? Intersection.Combine : Intersection.Subtract }
|
||||
Region { item: context; intersection: context.visible ? Intersection.Combine : Intersection.Subtract }
|
||||
}
|
||||
function reconcile(event) {
|
||||
phase = AutoHide.next(phase, event || "input", {
|
||||
suppressed: suppressed, held: held,
|
||||
body: bodyHover.hovered || contextHover.hovered,
|
||||
edge: sensor.containsMouse
|
||||
});
|
||||
}
|
||||
onHeldChanged: reconcile()
|
||||
onSuppressedChanged: { contextEntry = null; draggingId = ""; pressed = false; reconcile(); }
|
||||
onEntriesChanged: {
|
||||
if (contextEntry) {
|
||||
contextEntry = entries.find(entry => entry.id === contextEntry.id) || null;
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: deadline
|
||||
interval: dock.phase === "arming" ? 180 : 450
|
||||
running: dock.phase === "arming" || dock.phase === "waiting"
|
||||
onTriggered: dock.reconcile("deadline")
|
||||
}
|
||||
function activate(entry) {
|
||||
if (entry.windows.length === 1) FavoritesService.focusWindow(entry.windows[0].id);
|
||||
else if (entry.windows.length > 1) openContext(entry, width / 2);
|
||||
else if (!entry.missing) FavoritesService.launch(entry.id);
|
||||
}
|
||||
function openContext(entry, x) {
|
||||
contextX = x;
|
||||
contextEntry = entry;
|
||||
}
|
||||
function closeContext() { contextEntry = null; }
|
||||
MouseArea {
|
||||
id: sensor
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: dock.revealed ? dock.width : Math.min(360, dock.width)
|
||||
height: 4
|
||||
hoverEnabled: true
|
||||
onEntered: dock.reconcile()
|
||||
onExited: dock.reconcile()
|
||||
onClicked: dock.closeContext()
|
||||
}
|
||||
Item {
|
||||
id: body
|
||||
width: parent.width
|
||||
height: 110
|
||||
y: dock.revealed ? dock.height - height : dock.height + 4
|
||||
Behavior on y { NumberAnimation { duration: 210; easing.type: Easing.OutCubic } }
|
||||
HoverHandler {
|
||||
id: bodyHover
|
||||
onHoveredChanged: { dock.reconcile(); if (!hovered) dock.pointerX = -1000; }
|
||||
onPointChanged: dock.pointerX = point.position.x
|
||||
}
|
||||
// WHY: an analytic shadow needs neither a captured backdrop nor a
|
||||
// moving blur source. Its geometry stays fixed throughout hover.
|
||||
RectangularShadow {
|
||||
x: 16; y: 43; width: parent.width - 32; height: 48
|
||||
radius: 20; blur: 24; spread: 2
|
||||
color: Qt.rgba(0, 0, 0, 0.32)
|
||||
}
|
||||
Rectangle {
|
||||
x: 14; y: 36; width: parent.width - 28; height: 58
|
||||
radius: 20
|
||||
color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, Theme.highContrast ? 0.94 : 0.16)
|
||||
border.color: Qt.rgba(Theme.text.r, Theme.text.g, Theme.text.b, Theme.highContrast ? 0.5 : 0.09)
|
||||
border.width: 1
|
||||
}
|
||||
Flickable {
|
||||
id: viewport
|
||||
x: 12; y: 0; width: parent.width - 24; height: 105
|
||||
// CONTRACT: reserve visual overflow inside the clipped viewport,
|
||||
// including at both scroll limits, for the 1.39x end icons.
|
||||
contentWidth: items.width + 24; contentHeight: height
|
||||
clip: true
|
||||
interactive: dock.draggingId === ""
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
Row {
|
||||
id: items
|
||||
x: 12
|
||||
height: viewport.height
|
||||
spacing: 3
|
||||
move: Transition { NumberAnimation { properties: "x"; duration: 110; easing.type: Easing.OutCubic } }
|
||||
Repeater {
|
||||
id: icons
|
||||
model: dock.entries
|
||||
delegate: Item {
|
||||
id: tile
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property bool separator: index === dock.pinnedCount && dock.pinnedCount > 0
|
||||
readonly property real center: viewport.x + items.x + x + width / 2 - viewport.contentX
|
||||
// INVARIANT: pointer influence uses fixed layout slots.
|
||||
// Only the visual transform changes: Row, hit targets,
|
||||
// window width and SVG raster size never follow hover.
|
||||
readonly property real baseCenter: viewport.x + items.x + index * 57 + 27
|
||||
+ (index >= dock.pinnedCount && dock.pinnedCount > 0 ? 13 : 0) - viewport.contentX
|
||||
readonly property real growth: dock.revealed && bodyHover.hovered
|
||||
? AutoHide.influence(baseCenter - dock.pointerX) : 0
|
||||
width: 54 + (separator ? 13 : 0)
|
||||
height: 105
|
||||
Rectangle {
|
||||
visible: tile.separator
|
||||
x: 2; y: 45; width: 1; height: 32; color: Theme.strongBorder
|
||||
}
|
||||
ApplicationIcon {
|
||||
id: icon
|
||||
// WHY: ApplicationIcon derives sourceSize from width.
|
||||
// Fixed dimensions avoid async SVG reload/fallback flashes.
|
||||
width: 46; height: 46
|
||||
x: (parent.width - width + (tile.separator ? 12 : 0)) / 2
|
||||
y: 37
|
||||
transformOrigin: Item.Bottom
|
||||
scale: 1 + tile.growth * 0.39
|
||||
Behavior on scale { NumberAnimation { duration: 80; easing.type: Easing.OutCubic } }
|
||||
iconSource: tile.modelData.iconSource || ""
|
||||
opacity: dock.draggingId === tile.modelData.id ? 0.5 : 1
|
||||
}
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: icon.horizontalCenter
|
||||
y: 90; width: 4; height: 4; radius: 2
|
||||
color: Theme.lavender; visible: tile.modelData.windows.length > 0
|
||||
}
|
||||
MouseArea {
|
||||
id: pointer
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
preventStealing: tile.modelData.pinned
|
||||
property real startX: 0
|
||||
property real startY: 0
|
||||
property bool moved: false
|
||||
onPressed: mouse => {
|
||||
dock.pressed = true; startX = mouse.x; startY = mouse.y; moved = false;
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed && pressedButtons === Qt.LeftButton && tile.modelData.pinned
|
||||
&& Math.hypot(mouse.x - startX, mouse.y - startY) > 8) {
|
||||
moved = true; dock.draggingId = tile.modelData.id;
|
||||
}
|
||||
}
|
||||
onReleased: mouse => {
|
||||
if (moved) {
|
||||
const p = mapToItem(items, mouse.x, mouse.y);
|
||||
let before = "";
|
||||
for (let i = 0; i < icons.count; ++i) {
|
||||
const candidate = icons.itemAt(i);
|
||||
if (candidate.modelData.pinned && candidate.modelData.id !== dock.draggingId
|
||||
&& p.x < candidate.x + candidate.width / 2) {
|
||||
before = candidate.modelData.id; break;
|
||||
}
|
||||
}
|
||||
FavoritesService.move(dock.draggingId, before);
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
dock.openContext(tile.modelData, tile.center);
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
if (!tile.modelData.missing && tile.modelData.newInstance)
|
||||
FavoritesService.launch(tile.modelData.id);
|
||||
} else {
|
||||
dock.closeContext(); dock.activate(tile.modelData);
|
||||
}
|
||||
dock.draggingId = ""; dock.pressed = false;
|
||||
}
|
||||
onCanceled: { dock.draggingId = ""; dock.pressed = false; }
|
||||
ToolTip.visible: containsMouse && !dock.held
|
||||
ToolTip.text: tile.modelData.name
|
||||
ToolTip.delay: 600
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: context
|
||||
visible: dock.contextEntry !== null
|
||||
width: Math.min(330, dock.width)
|
||||
height: Math.min(230, contextContent.height + 16)
|
||||
x: Math.max(0, Math.min(dock.width - width, dock.contextX - width / 2))
|
||||
y: dock.height - 116 - height
|
||||
radius: 12; color: Theme.popupBackground
|
||||
border.color: Theme.strongBorder
|
||||
HoverHandler { id: contextHover; onHoveredChanged: dock.reconcile() }
|
||||
Flickable {
|
||||
anchors.fill: parent; anchors.margins: 8
|
||||
contentHeight: contextContent.height; clip: true
|
||||
Column {
|
||||
id: contextContent
|
||||
width: parent.width
|
||||
Text {
|
||||
width: parent.width; height: 28
|
||||
text: dock.contextEntry ? dock.contextEntry.name : ""
|
||||
color: Theme.lavender; elide: Text.ElideRight; font.bold: true
|
||||
}
|
||||
Repeater {
|
||||
model: dock.contextEntry ? dock.contextEntry.windows : []
|
||||
delegate: DockAction {
|
||||
required property var modelData
|
||||
width: contextContent.width
|
||||
text: modelData.title + " · " + modelData.workspace
|
||||
onTriggered: { FavoritesService.focusWindow(modelData.id); dock.closeContext(); }
|
||||
}
|
||||
}
|
||||
DockAction {
|
||||
width: parent.width
|
||||
visible: !!dock.contextEntry && !dock.contextEntry.missing
|
||||
text: "Ouvrir"
|
||||
onTriggered: { FavoritesService.launch(dock.contextEntry.id); dock.closeContext(); }
|
||||
}
|
||||
DockAction {
|
||||
width: parent.width
|
||||
visible: !!dock.contextEntry && (dock.contextEntry.pinned || !dock.contextEntry.missing)
|
||||
objectName: "dock-pin-action"
|
||||
text: dock.contextEntry && dock.contextEntry.pinned ? "Désépingler" : "Épingler"
|
||||
onTriggered: { FavoritesService.toggle(dock.contextEntry.id); dock.closeContext(); }
|
||||
}
|
||||
DockAction { width: parent.width; text: "Fermer le menu"; onTriggered: dock.closeContext() }
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
visible: dock.revealed && FavoritesService.error !== "" && !context.visible
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: dock.height - 140
|
||||
text: FavoritesService.error; color: Theme.danger; font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import QtQuick
|
||||
import "../theme"
|
||||
Rectangle {
|
||||
id: action
|
||||
property string text: ""
|
||||
signal triggered()
|
||||
height: visible ? 30 : 0
|
||||
radius: 5
|
||||
color: pointer.containsMouse ? Theme.buttonHover : "transparent"
|
||||
Text { anchors.fill: parent; anchors.margins: 5; text: action.text; color: Theme.foreground
|
||||
elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter; font.pixelSize: 12 }
|
||||
MouseArea { id: pointer; anchors.fill: parent; hoverEnabled: true; onClicked: action.triggered() }
|
||||
}
|
||||
|
|
@ -10,8 +10,6 @@ import "theme"
|
|||
import "polkit"
|
||||
import "scratchpad"
|
||||
import "osd"
|
||||
import "applications"
|
||||
import "dock"
|
||||
|
||||
ShellRoot {
|
||||
id: shell
|
||||
|
|
@ -244,12 +242,6 @@ ShellRoot {
|
|||
return AppearanceController.acceptPublishedNightLight(payload);
|
||||
}
|
||||
}
|
||||
// CONTRACT: shell-owned singleton remains alive while the menu is closed.
|
||||
readonly property bool applicationsReady: FavoritesService.ready
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
Dock { suppressed: shell.authenticationActive || shell.applicationsOutput !== "" }
|
||||
}
|
||||
// Une barre par écran, y compris si les sorties changent pendant la session.
|
||||
// CONTRACT: un seul serveur D-Bus pour toutes les sorties.
|
||||
NotificationService { id: rootNotificationService }
|
||||
|
|
|
|||
|
|
@ -26,14 +26,3 @@ layer_effects "labfy-sway-bar" {
|
|||
shadows enable
|
||||
corner_radius 4
|
||||
}
|
||||
|
||||
# CONTRACT: dock glass uses a light tint and analytic QML shadow. Disabling
|
||||
# backdrop blur avoids compositor blur work on every magnification frame.
|
||||
# No window rule or focus/restore policy is changed.
|
||||
layer_effects "labfy-sway-dock" {
|
||||
blur disable
|
||||
blur_xray disable
|
||||
blur_ignore_transparent enable
|
||||
shadows disable
|
||||
corner_radius 25
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,296 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Private Sway only; production Dock/Menu, real Python bridge and input paths."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO / 'tests/session_v2/fixtures'))
|
||||
from headless import Lab
|
||||
from ipc_proxy import IpcProxy
|
||||
|
||||
SOURCE = REPO / 'quickshell/.config/quickshell/labfy-sway'
|
||||
ARTIFACTS = REPO / 'docs/validation/quickshell-dock-tela-polish-v2'
|
||||
NINE = ['firefox.desktop', 'kitty.desktop', 'yazi.desktop', 'nvim.desktop',
|
||||
'org.darktable.darktable.desktop', 'orca-slicer.desktop', 'org.freecad.FreeCAD.desktop',
|
||||
'org.kde.digikam.desktop', 'AppFlowy.desktop']
|
||||
|
||||
class DockLab(Lab):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.config=self.directory/'dock-sway.conf'
|
||||
base=(REPO/'tests/session_v2/fixtures/headless.conf').read_text()
|
||||
effects=(REPO/'sway/.config/sway/swayfx').read_text().split('layer_effects "labfy-sway-dock"')[1]
|
||||
effects='layer_effects "labfy-sway-dock"'+effects
|
||||
self.config.write_text(base+'\noutput HEADLESS-1 bg #11111b solid_color\nset $bar_blur enable\n'+effects)
|
||||
def start(self, argv):
|
||||
if argv[0]==os.environ.get('LABFY_TEST_SWAY','sway'):
|
||||
argv=[*argv[:-1],str(self.config)]
|
||||
return super().start(argv)
|
||||
|
||||
with DockLab() as lab:
|
||||
proxy=IpcProxy(lab.directory/'dock-ipc.sock',lab.socket)
|
||||
lab.env['SWAYSOCK']=str(lab.directory/'dock-ipc.sock')
|
||||
path = Path(lab.env['XDG_STATE_HOME']) / 'labfy-sway/applications-favorites.json'
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(json.dumps(dict(version=1, ids=NINE)))
|
||||
qml = lab.directory / 'fixture.qml'
|
||||
qml.write_text('''//@ pragma UseQApplication
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "SOURCE/applications" as A
|
||||
import "SOURCE/dock" as D
|
||||
ShellRoot {
|
||||
D.Dock { id: dock; modelData: Quickshell.screens[0] }
|
||||
PanelWindow { id: bar; implicitWidth: 100; implicitHeight: 30; anchors.top: true; color: "transparent" }
|
||||
LazyLoader { id: menu; active: false; A.ApplicationsMenu { barWindow: bar } }
|
||||
// TEST_ONLY: one pointer move per animation frame avoids measuring IPC
|
||||
// process startup instead of continuous production hover. These are Qt
|
||||
// animation intervals, not hardware presentation timestamps.
|
||||
FrameAnimation {
|
||||
id: sweepFrames
|
||||
property var intervals: []
|
||||
property real origin: 0
|
||||
property bool geometryChanged: false
|
||||
property bool imageChanged: false
|
||||
property var baseline: []
|
||||
running: false
|
||||
onTriggered: {
|
||||
if (currentFrame > 1) intervals.push(frameTime * 1000);
|
||||
const cycle = (elapsedTime * 0.7) % 2;
|
||||
input.mouseMove(dock.surfaceBody, 51 + 420 * (cycle <= 1 ? cycle : 2 - cycle), 70);
|
||||
const tiles=Array.from(dock.contentItemRow.children).filter(t => t.modelData);
|
||||
for (let i=0; i<tiles.length; ++i) {
|
||||
const t=tiles[i], b=baseline[i];
|
||||
if (t.x!==b.x || t.width!==b.width || t.children[1].width!==46 || dock.width!==origin)
|
||||
geometryChanged=true;
|
||||
if (t.children[1].children[0].status!==b.status) imageChanged=true;
|
||||
}
|
||||
if (elapsedTime >= 4.5) running=false;
|
||||
}
|
||||
}
|
||||
TestCase { id: input; name: "DockInput"; when: false }
|
||||
IpcHandler {
|
||||
target: "dockTest"
|
||||
function state(): string {
|
||||
return JSON.stringify({ready:A.FavoritesService.ready, ids:A.FavoritesService.ids,
|
||||
error:A.FavoritesService.error, phase:dock.phase, revealed:dock.revealed,
|
||||
entries:dock.entries, theme:A.FavoritesService.iconTheme,
|
||||
menu:menu.active ? menu.item.favoriteIds : null,
|
||||
searchFocused:menu.active && menu.item.searchFocused,
|
||||
navFocused:menu.active && menu.item.navigationFocused,
|
||||
gridFocused:menu.active && menu.item.gridFocused,
|
||||
screenWidth:dock.screen.width,
|
||||
bodyY:dock.surfaceBody.y, width:dock.width, pointerX:dock.pointerX,
|
||||
|
||||
tileWidth:dock.contentItemRow.children[0] ? dock.contentItemRow.children[0].width : 0,
|
||||
tiles:Array.from(dock.contentItemRow.children).filter(t => t.modelData).map(t => ({
|
||||
x:t.x, width:t.width, growth:t.growth, scale:t.children[1].scale,
|
||||
visualLeft:t.children[1].mapToItem(dock.contentItemRow.parent.parent,0,0).x,
|
||||
visualRight:t.children[1].mapToItem(dock.contentItemRow.parent.parent,46,0).x,
|
||||
iconWidth:t.children[1].width, imageStatus:t.children[1].children[0].status})),
|
||||
context:!!dock.contextEntry});
|
||||
}
|
||||
function startFrames(): void {
|
||||
sweepFrames.intervals=[]; sweepFrames.origin=dock.width;
|
||||
sweepFrames.baseline=Array.from(dock.contentItemRow.children).filter(t => t.modelData)
|
||||
.map(t => ({x:t.x,width:t.width,status:t.children[1].children[0].status}));
|
||||
sweepFrames.start();
|
||||
}
|
||||
function frames(): string {
|
||||
return JSON.stringify({running:sweepFrames.running, intervals:sweepFrames.intervals,
|
||||
geometryChanged:sweepFrames.geometryChanged,imageChanged:sweepFrames.imageChanged});
|
||||
}
|
||||
function edge(): void { input.mouseMove(dock.edgeArea, dock.edgeArea.width/2, 2); }
|
||||
function body(): void { input.mouseMove(dock.surfaceBody, 35, 70); }
|
||||
function sweep(x:real): void { input.mouseMove(dock.surfaceBody, x, 70); }
|
||||
function scrollEnd(): void {
|
||||
const v=dock.contentItemRow.parent.parent; v.contentX=Math.max(0,v.contentWidth-v.width);
|
||||
}
|
||||
function scrollStart(): void { dock.contentItemRow.parent.parent.contentX=0; }
|
||||
function leave(): void { input.mouseMove(dock.contentItem, 1, 1); }
|
||||
function menuToggle(id:string): void { if (!menu.active) menu.active=true; menu.item.toggleFavorite(id); }
|
||||
function dockToggle(id:string): void { A.FavoritesService.toggle(id); }
|
||||
function move(id:string, before:string): void { A.FavoritesService.move(id,before); }
|
||||
function closeMenu(): void { menu.active=false; }
|
||||
function tab(): void { input.keyClick(Qt.Key_Tab); }
|
||||
function clickPin(): void {
|
||||
const action=input.findChild(dock.contextCard,"dock-pin-action");
|
||||
input.mouseClick(action,action.width/2,action.height/2);
|
||||
}
|
||||
function rightClick(): void {
|
||||
input.mouseClick(dock.contentItemRow.children[0],27,65,Qt.RightButton);
|
||||
}
|
||||
function dragFirstAfterSecond(): void {
|
||||
const first=dock.contentItemRow.children[0];
|
||||
input.mousePress(first,27,65,Qt.LeftButton);
|
||||
input.mouseMove(first,125,65,100);
|
||||
input.mouseRelease(first,125,65,Qt.LeftButton);
|
||||
}
|
||||
function activateFirst(): void { input.mouseClick(dock.contentItemRow.children[0],27,65); }
|
||||
function context(): void { dock.openContext(dock.entries[0],dock.width/2); }
|
||||
function closeContext(): void { dock.closeContext(); }
|
||||
function suppress(value:bool): void { dock.suppressed=value; }
|
||||
function focus(id:string): void { A.FavoritesService.focusWindow(id); }
|
||||
}
|
||||
}
|
||||
'''.replace('SOURCE', SOURCE.as_uri()))
|
||||
lab.env['QT_QPA_PLATFORM'] = 'wayland'
|
||||
process = lab.start(['quickshell', '-p', str(qml)])
|
||||
def call(name, *args):
|
||||
r = subprocess.run(['quickshell','ipc','-p',str(qml),'call','dockTest',name,*map(str,args)],
|
||||
env=lab.env, text=True, capture_output=True, timeout=8)
|
||||
if r.returncode:
|
||||
logs = '\n'.join(p.read_text()[-9000:] for p in lab.directory.glob('child-*.log'))
|
||||
raise AssertionError((name,r.stdout,r.stderr,logs))
|
||||
return json.loads(r.stdout) if r.stdout.strip() else None
|
||||
def wait(predicate, seconds=8):
|
||||
end = time.monotonic()+seconds
|
||||
while time.monotonic()<end:
|
||||
state=call('state')
|
||||
if predicate(state): return state
|
||||
time.sleep(.08)
|
||||
raise AssertionError(call('state'))
|
||||
time.sleep(1.2)
|
||||
initial=wait(lambda s:s['ready'])
|
||||
assert initial['ids']==NINE and initial['phase']=='hidden', initial
|
||||
ARTIFACTS.mkdir(parents=True,exist_ok=True)
|
||||
def capture(name):
|
||||
subprocess.run(['grim',str(ARTIFACTS / (name+'.png'))],env=lab.env,check=True)
|
||||
capture('closed')
|
||||
call('edge'); wait(lambda s:s['revealed']); time.sleep(.35)
|
||||
capture('open')
|
||||
resting=call('state')
|
||||
call('body'); time.sleep(.3); zoom=call('state'); capture('zoom')
|
||||
lab.command('output HEADLESS-1 bg #aab4c8 solid_color')
|
||||
time.sleep(.2); capture('zoom-light')
|
||||
lab.command('output HEADLESS-1 bg #11111b solid_color')
|
||||
assert zoom['tileWidth']==54 and zoom['tiles'][0]['scale']>1, zoom
|
||||
assert zoom['tiles'][0]['visualLeft']>=0,zoom
|
||||
# Exercise production hover across slot boundaries. Layout and image
|
||||
# readiness must remain stable while visual transforms change.
|
||||
samples=[]
|
||||
for x in range(45, 480, 6):
|
||||
call('sweep',x)
|
||||
state=call('state')
|
||||
assert state['phase']=='shown',state
|
||||
assert state['width']==resting['width'],state
|
||||
assert [(t['x'],t['width'],t['iconWidth']) for t in state['tiles']]==[(t['x'],t['width'],t['iconWidth']) for t in resting['tiles']],state
|
||||
assert sum(t['growth']>0 for t in state['tiles'])<=3,state
|
||||
assert [t['imageStatus'] for t in state['tiles']]==[t['imageStatus'] for t in resting['tiles']],state
|
||||
samples.append({'pointerX':state['pointerX'],'scales':[t['scale'] for t in state['tiles']]})
|
||||
call('startFrames')
|
||||
time.sleep(4.8)
|
||||
frames=call('frames')
|
||||
assert not frames['running'] and len(frames['intervals'])>30,frames
|
||||
assert not frames['geometryChanged'] and not frames['imageChanged'],frames
|
||||
intervals=sorted(frames['intervals'])
|
||||
timing={'frames':len(intervals),'meanMs':sum(intervals)/len(intervals),
|
||||
'p95Ms':intervals[int((len(intervals)-1)*.95)],'maxMs':max(intervals)}
|
||||
call('leave'); wait(lambda s:s['phase']=='hidden')
|
||||
call('edge'); wait(lambda s:s['revealed']); time.sleep(.3)
|
||||
call('rightClick'); wait(lambda s:s['phase']=='held'); time.sleep(.7)
|
||||
assert call('state')['context']
|
||||
call('clickPin'); wait(lambda s:'firefox.desktop' not in s['ids'])
|
||||
call('dockToggle','firefox.desktop'); wait(lambda s:'firefox.desktop' in s['ids'])
|
||||
call('move','firefox.desktop','kitty.desktop'); wait(lambda s:s['ids']==NINE)
|
||||
call('leave'); wait(lambda s:s['phase']=='hidden')
|
||||
# Refused move leaves both interfaces at the same durable list.
|
||||
call('move','kitty.desktop','removed.desktop')
|
||||
wait(lambda s:bool(s['error']))
|
||||
assert call('state')['ids']==NINE
|
||||
call('menuToggle','fixture.desktop'); wait(lambda s:'fixture.desktop' in s['ids'] and s['menu']==s['ids'])
|
||||
# Gio catalog monitoring replaces the placeholder without dropping its ID.
|
||||
desktop=Path(lab.env['HOME'])/'.local/share/applications/fixture.desktop'
|
||||
desktop.parent.mkdir(parents=True,exist_ok=True)
|
||||
desktop.write_text('[Desktop Entry]\nType=Application\nName=Installed Fixture\nExec=/usr/bin/true\nIcon=firefox\n')
|
||||
wait(lambda s:any(e['id']=='fixture.desktop' and not e.get('missing') for e in s['entries']))
|
||||
desktop.unlink()
|
||||
wait(lambda s:any(e['id']=='fixture.desktop' and e.get('missing') for e in s['entries']))
|
||||
# Keep the virtual keyboard alive: the headless seat has no physical
|
||||
# keyboard, so destroying wtype would remove keyboard capabilities and
|
||||
# deactivate the QML focus window before we could sample its focus item.
|
||||
keyboard=lab.start(['wtype','-s','300','-k','Tab','-s','1500','-k','Tab',
|
||||
'-s','1500','-k','Tab','-s','1500'])
|
||||
wait(lambda s:s['navFocused'])
|
||||
wait(lambda s:s['gridFocused'])
|
||||
wait(lambda s:s['searchFocused'])
|
||||
keyboard.wait(timeout=8)
|
||||
# An independent legacy CLI writer is observed by menu and dock live.
|
||||
subprocess.run(['python','-B',str(SOURCE/'applications/backend.py'),'favorites','add','external.desktop'],
|
||||
env=lab.env,check=True,capture_output=True)
|
||||
wait(lambda s:'external.desktop' in s['ids'] and s['menu']==s['ids'])
|
||||
call('dockToggle','external.desktop'); wait(lambda s:'external.desktop' not in s['ids'])
|
||||
call('dockToggle','fixture.desktop'); wait(lambda s:'fixture.desktop' not in s['ids'] and s['menu']==s['ids'])
|
||||
call('move','kitty.desktop','firefox.desktop'); reordered=wait(lambda s:s['ids'][0]=='kitty.desktop')
|
||||
assert json.loads(path.read_text())['ids']==reordered['ids']
|
||||
call('closeMenu')
|
||||
call('edge'); wait(lambda s:s['revealed']); time.sleep(.3)
|
||||
call('dragFirstAfterSecond')
|
||||
dragged=wait(lambda s:s['ids'][0]=='firefox.desktop')
|
||||
assert dragged['ids'][1]=='kitty.desktop',dragged
|
||||
call('leave'); wait(lambda s:s['phase']=='hidden')
|
||||
# No focus changes while the pointer reveals, zooms, hides, or opens menus.
|
||||
win=lab.kitty('kitty')
|
||||
time.sleep(.5)
|
||||
focused=lambda: next(n['id'] for n in lab.windows() if n.get('focused'))
|
||||
assert focused()==win
|
||||
for action in ('edge','body','context','leave','closeContext'):
|
||||
call(action); time.sleep(.25); assert focused()==win, action
|
||||
call('suppress','true'); assert call('state')['phase']=='hidden'
|
||||
call('suppress','false')
|
||||
wait(lambda s:any(e['id']=='kitty.desktop' and len(e['windows'])==1 for e in s['entries']))
|
||||
proxy.disconnect()
|
||||
time.sleep(2)
|
||||
second=lab.kitty('kitty')
|
||||
wait(lambda s:any(e['id']=='kitty.desktop' and len(e['windows'])==2 for e in s['entries']))
|
||||
lab.command(f'[con_id={win}] floating enable')
|
||||
lab.command(f'[con_id={win}] move scratchpad')
|
||||
call('focus',str(win)); time.sleep(.3); assert focused()==win
|
||||
lab.command(f'[con_id={second}] kill')
|
||||
wait(lambda s:any(e['id']=='kitty.desktop' and len(e['windows'])==1 for e in s['entries']))
|
||||
# Firefox runs with the private HOME/profile and no inherited session bus.
|
||||
profile=lab.directory/'firefox-profile'; profile.mkdir()
|
||||
firefox=lab.start(['firefox','--no-remote','--profile',str(profile),'--new-window','about:blank'])
|
||||
firefox_state=wait(lambda s:any(e['id']=='firefox.desktop' and e['windows'] for e in s['entries']),20)
|
||||
firewin=next(e for e in firefox_state['entries'] if e['id']=='firefox.desktop')['windows'][0]['id']
|
||||
call('closeContext'); call('edge'); wait(lambda s:s['revealed']); time.sleep(.3)
|
||||
call('activateFirst'); time.sleep(.3)
|
||||
assert str(focused())==firewin
|
||||
for action in ('edge','body','leave'):
|
||||
call(action); time.sleep(.25); assert str(focused())==firewin
|
||||
# Layer-shell surfaces cannot enter the Session V2 toplevel inventory.
|
||||
assert not any('labfy-sway-dock' in str(n.get('app_id','')) for n in lab.windows())
|
||||
lab.command('output HEADLESS-1 mode 800x600')
|
||||
resized=wait(lambda s:s['screenWidth']==800)
|
||||
assert resized['width']<=776,resized
|
||||
# Magnified end icons keep their visual margin at both scroll limits.
|
||||
lab.command('output HEADLESS-1 mode 400x600')
|
||||
wait(lambda s:s['screenWidth']==400)
|
||||
call('edge'); wait(lambda s:s['revealed']); time.sleep(.3)
|
||||
call('scrollEnd'); call('sweep',325); time.sleep(.15)
|
||||
end=call('state')
|
||||
assert end['tiles'][-1]['scale']>1 and end['tiles'][-1]['visualRight']<=end['width']-24,end
|
||||
call('scrollStart'); call('sweep',51); time.sleep(.15)
|
||||
start=call('state')
|
||||
assert start['tiles'][0]['scale']>1 and start['tiles'][0]['visualLeft']>=0,start
|
||||
lab.command('output HEADLESS-1 mode 1280x720')
|
||||
wait(lambda s:s['screenWidth']==1280)
|
||||
# Existing empty state must stay empty, including a bridge restart.
|
||||
for identifier in list(call('state')['ids']): call('dockToggle',identifier)
|
||||
wait(lambda s:not s['ids'])
|
||||
process.terminate(); process.wait(timeout=5)
|
||||
process=lab.start(['quickshell','-p',str(qml)])
|
||||
time.sleep(1)
|
||||
assert wait(lambda s:s['ready'])['ids']==[]
|
||||
(ARTIFACTS/'result.json').write_text(json.dumps(dict(status='PASS',initial=initial,hoverSamples=samples,animationTiming=timing,frameSweep=frames,
|
||||
reordered=reordered['ids'],checks=['shared favorites','menu and dock mutations','durable reorder',
|
||||
'empty restart','autohide','context hold','kitty multi-window','floating scratchpad explicit focus',
|
||||
'window close','no hover focus','real pointer drag','fixed layout transform hover', 'stable SVG readiness', 'lateral sweep 73 samples','IPC reconnect','Firefox private profile','desktop install/remove monitoring','external writer','keyboard menu navigation','resolution change','SwayFX dock blur disabled']),indent=2)+'\n')
|
||||
proxy.close()
|
||||
print('Dock headless: PASS',lab.directory)
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
"""Test-only disconnectable proxy; targets only a fixture-owned Sway socket."""
|
||||
import select
|
||||
import socket
|
||||
import threading
|
||||
|
||||
|
||||
class IpcProxy:
|
||||
def __init__(self, path, target):
|
||||
self.target = target
|
||||
self.socket = socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
|
||||
self.socket.bind(str(path)); self.socket.listen(8); self.socket.settimeout(.2)
|
||||
self.stopped = threading.Event()
|
||||
self.lock = threading.Lock()
|
||||
self.connections = set()
|
||||
self.thread = threading.Thread(target=self.accept,daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def accept(self):
|
||||
while not self.stopped.is_set():
|
||||
try: client,_ = self.socket.accept()
|
||||
except socket.timeout: continue
|
||||
except OSError: break
|
||||
upstream=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
|
||||
upstream.settimeout(2)
|
||||
try: upstream.connect(self.target)
|
||||
except OSError: client.close(); upstream.close(); continue
|
||||
with self.lock:
|
||||
if len(self.connections)>=16:
|
||||
client.close(); upstream.close(); continue
|
||||
self.connections.add(client); self.connections.add(upstream)
|
||||
threading.Thread(target=self.forward,args=(client,upstream),daemon=True).start()
|
||||
|
||||
def forward(self, client, upstream):
|
||||
try:
|
||||
while not self.stopped.is_set():
|
||||
ready,_,_=select.select([client,upstream],[],[],.2)
|
||||
for source in ready:
|
||||
data=source.recv(65536)
|
||||
if not data: return
|
||||
(upstream if source is client else client).sendall(data)
|
||||
except (OSError,ValueError): pass
|
||||
finally:
|
||||
with self.lock:
|
||||
self.connections.discard(client); self.connections.discard(upstream)
|
||||
client.close(); upstream.close()
|
||||
|
||||
def disconnect(self):
|
||||
with self.lock:
|
||||
for connection in self.connections:
|
||||
try: connection.shutdown(socket.SHUT_RDWR)
|
||||
except OSError: pass
|
||||
|
||||
def close(self):
|
||||
self.stopped.set(); self.disconnect(); self.socket.close(); self.thread.join(2)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Run existing production chooser/restore scenarios with the dock present.
|
||||
|
||||
Only the test harness is extended in memory. No Session V2 source, journal,
|
||||
keyring/PAM configuration or active graphical environment is changed.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0,str(ROOT/'tests/session_v2'))
|
||||
import keyring_ui
|
||||
import phase3b_ui
|
||||
|
||||
source=(ROOT/'quickshell/.config/quickshell/labfy-sway/dock').as_uri()
|
||||
for module in (keyring_ui,phase3b_ui):
|
||||
module.HARNESS=module.HARNESS.replace('import Quickshell\n',
|
||||
'import Quickshell\nimport "'+source+'" as D\n')
|
||||
module.HARNESS=module.HARNESS.replace('ShellRoot {','''ShellRoot {
|
||||
D.Dock { id: compatibilityDock; modelData: Quickshell.screens[0] }
|
||||
Timer { interval: 3500; running: true; onTriggered: {
|
||||
if (compatibilityDock.phase !== "hidden") {
|
||||
console.error("DOCK_COMPAT_FAIL_NOT_HIDDEN"); Qt.quit();
|
||||
}
|
||||
} }
|
||||
''')
|
||||
module.main()
|
||||
print(module.__name__+' WITH_DOCK PASS',flush=True)
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Load the actual shell through its relative imports in a private Sway.
|
||||
|
||||
The session bus is private; the system bus address deliberately points to a
|
||||
missing fixture socket so no real Polkit agent or system service is contacted.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0,str(ROOT/'tests/session_v2/fixtures'))
|
||||
from headless import Lab
|
||||
|
||||
with Lab() as lab:
|
||||
lab.env.update(DBUS_SYSTEM_BUS_ADDRESS='unix:path='+str(lab.directory/'no-system-bus'),QT_QPA_PLATFORM='wayland')
|
||||
shell=ROOT/'quickshell/.config/quickshell/labfy-sway/shell.qml'
|
||||
process=lab.start(['dbus-run-session','--','quickshell','-p',str(shell)])
|
||||
time.sleep(3)
|
||||
log=(lab.directory/'child-1.log').read_text()
|
||||
assert 'Configuration Loaded' in log and process.poll() is None,log
|
||||
assert 'Binding loop' not in log,log
|
||||
print('Full shell private-compositor load: PASS',lab.directory)
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const path = require('node:path');
|
||||
function load(file) {
|
||||
const context=vm.createContext({});
|
||||
vm.runInContext(fs.readFileSync(path.join(__dirname,'../../quickshell/.config/quickshell/labfy-sway',file),'utf8')
|
||||
.replace(/^\.pragma library\s*\n/,''),context);
|
||||
return context;
|
||||
}
|
||||
const hide=load('dock/AutoHide.js');
|
||||
let phase='hidden';
|
||||
const input={suppressed:false,held:false,body:false,edge:false};
|
||||
const event=(name='input') => phase=hide.next(phase,name,input);
|
||||
assert.equal(event(),'hidden');
|
||||
input.edge=true; assert.equal(event(),'arming');
|
||||
input.edge=false; assert.equal(event(),'hidden');
|
||||
input.edge=true; event(); assert.equal(event('deadline'),'shown');
|
||||
input.edge=false; assert.equal(event(),'waiting');
|
||||
input.body=true; assert.equal(event(),'shown');
|
||||
input.body=false; event(); input.held=true; assert.equal(event(),'held');
|
||||
assert.equal(event('deadline'),'held');
|
||||
input.held=false; assert.equal(event(),'waiting');
|
||||
assert.equal(event('deadline'),'hidden');
|
||||
input.held=true; event(); input.suppressed=true; assert.equal(event(),'hidden');
|
||||
assert.equal(hide.influence(0),1);
|
||||
assert.ok(hide.influence(25)>hide.influence(55));
|
||||
assert.equal(hide.influence(76),0);
|
||||
assert.equal(hide.influence(-76),0);
|
||||
assert.equal(hide.influence(-35),hide.influence(35));
|
||||
for (let cursor=0; cursor<570; ++cursor) {
|
||||
const affected=Array.from({length:10},(_,i)=>i).filter(i=>hide.influence(i*57-cursor)>0);
|
||||
assert.ok(affected.length<=3);
|
||||
assert.ok(affected.length===0 || affected.at(-1)-affected[0]<=2);
|
||||
}
|
||||
const model=load('applications/WindowModel.js');
|
||||
const apps=[{id:'kitty.desktop',aliases:['kitty'],iconSource:'file:///tela/kitty.svg'},
|
||||
{id:'org.app.desktop',aliases:['startup-class','executable']},
|
||||
{id:'org.other.desktop',aliases:['executable']}];
|
||||
assert.equal(model.match(apps,{appId:'kitty'}).id,'kitty.desktop');
|
||||
assert.equal(model.match(apps,{wmClass:'Startup-Class'}).id,'org.app.desktop');
|
||||
assert.equal(model.match(apps,{appId:'executable'}),null);
|
||||
assert.equal(model.match(apps,{title:'kitty'}),null);
|
||||
let entries=model.entries(apps,['absent.desktop','kitty.desktop'],[{id:'10',appId:'kitty'},{id:'11',appId:'kitty'},
|
||||
{id:'12',appId:'unknown'}]);
|
||||
assert.equal(entries[0].missing,true);
|
||||
assert.equal(entries[1].windows.length,2);
|
||||
assert.equal(entries[1].iconSource,apps[0].iconSource);
|
||||
assert.equal(entries[2].pinned,false);
|
||||
assert.equal(model.entries(apps,[],[]).length,0);
|
||||
assert.equal(model.entries(apps,[],[{id:'11',appId:'kitty'}])[0].pinned,false);
|
||||
assert.equal(model.entries(apps,['kitty.desktop'],[])[0].windows.length,0);
|
||||
console.log('Dock state and window models: PASS');
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
"""Durable real backend operations and private toolkit icon resolution."""
|
||||
import concurrent.futures
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
APP = ROOT/'quickshell/.config/quickshell/labfy-sway/applications'
|
||||
sys.path.insert(0, str(APP))
|
||||
import backend
|
||||
import shared
|
||||
from gi.repository import Gio
|
||||
|
||||
NINE = ['firefox.desktop','kitty.desktop','yazi.desktop','nvim.desktop',
|
||||
'org.darktable.darktable.desktop','orca-slicer.desktop','org.freecad.FreeCAD.desktop',
|
||||
'org.kde.digikam.desktop','AppFlowy.desktop']
|
||||
|
||||
|
||||
class Persistence(unittest.TestCase):
|
||||
def test_nine_move_missing_anchor_and_empty(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path=Path(temp)/'labfy-sway/applications-favorites.json'
|
||||
path.parent.mkdir()
|
||||
path.write_text(json.dumps(dict(version=1,ids=NINE)))
|
||||
self.assertEqual(backend.read_favorites(path)[1],NINE)
|
||||
expected=[NINE[1],NINE[0]]+NINE[2:]
|
||||
self.assertEqual(backend.write_favorites(path,'move',[NINE[1],NINE[0]]),expected)
|
||||
before=path.read_bytes()
|
||||
for pair in ([NINE[0],'absent.desktop'], ['absent.desktop',NINE[0]], [NINE[0],'../bad']):
|
||||
with self.assertRaises(ValueError): backend.write_favorites(path,'move',pair)
|
||||
self.assertEqual(before,path.read_bytes())
|
||||
for identifier in NINE: backend.write_favorites(path,'remove',[identifier])
|
||||
self.assertEqual(backend.write_favorites(path,'seed',NINE[:4]),[])
|
||||
self.assertEqual(backend.read_favorites(path),(True,[]))
|
||||
|
||||
def test_toggle_is_atomic_and_does_not_depend_on_a_stale_ui_list(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path=Path(temp)/'favorites.json'
|
||||
backend.write_favorites(path,'toggle',['kitty.desktop'])
|
||||
self.assertEqual(backend.write_favorites(path,'toggle',['kitty.desktop']),[])
|
||||
|
||||
def test_real_concurrent_processes_keep_all_additions_during_moves(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
env=dict(os.environ,XDG_STATE_HOME=temp)
|
||||
path=Path(temp)/'labfy-sway/applications-favorites.json'
|
||||
backend.write_favorites(path,'seed',['a.desktop','b.desktop'])
|
||||
def run(args):
|
||||
result=subprocess.run([sys.executable,'-B',str(APP/'backend.py'),'favorites',*args],
|
||||
env=env,capture_output=True,text=True,timeout=10)
|
||||
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
||||
jobs=[['add',f'added{i}.desktop'] for i in range(24)]
|
||||
jobs += [['move','b.desktop','a.desktop'] for _ in range(10)]
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor:
|
||||
list(executor.map(run,jobs))
|
||||
ids=backend.read_favorites(path)[1]
|
||||
self.assertEqual(ids[:2],['b.desktop','a.desktop'])
|
||||
self.assertEqual(len(ids),26)
|
||||
self.assertEqual(set(ids[2:]),{f'added{i}.desktop' for i in range(24)})
|
||||
|
||||
|
||||
class Icons(unittest.TestCase):
|
||||
def test_tela_desktop_icon_and_absolute(self):
|
||||
resolver=shared.IconResolver()
|
||||
self.assertIn('/Tela-circle-purple-dark/',resolver.resolve(Gio.ThemedIcon.new('firefox')))
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path=Path(temp)/'custom icon.svg'
|
||||
path.write_text('<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"/>')
|
||||
self.assertEqual(resolver.resolve(Gio.FileIcon.new(Gio.File.new_for_path(str(path)))),path.as_uri())
|
||||
self.assertTrue(resolver.resolve(Gio.ThemedIcon.new('labfy-icon-does-not-exist')))
|
||||
self.assertTrue(resolver.resolve(Gio.FileIcon.new(Gio.File.new_for_path('/missing/icon.svg'))))
|
||||
|
||||
def test_inheritance_from_a_private_test_theme(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root=Path(temp)
|
||||
for name,parent in [('Child','Parent'),('Parent','hicolor')]:
|
||||
directory=root/name; (directory/'48x48/apps').mkdir(parents=True)
|
||||
(directory/'index.theme').write_text('[Icon Theme]\nName='+name+'\nInherits='+parent+
|
||||
'\nDirectories=48x48/apps\n[48x48/apps]\nSize=48\nType=Fixed\nContext=Applications\n')
|
||||
icon=root/'Parent/48x48/apps/inherited.svg'
|
||||
icon.write_text('<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"/>')
|
||||
resolver=shared.IconResolver('Child')
|
||||
resolver.theme.set_search_path([temp])
|
||||
self.assertEqual(resolver.resolve(Gio.ThemedIcon.new('inherited')),icon.as_uri())
|
||||
|
||||
|
||||
class Sway(unittest.TestCase):
|
||||
def test_wayland_xwayland_scratchpad_and_explicit_focus(self):
|
||||
tree=dict(nodes=[dict(type='workspace',name='__i3_scratch',floating_nodes=[
|
||||
dict(id=42,window=3,window_properties={'class':'Fixture','instance':'fixture'},name='Arbitrary'),
|
||||
dict(id=43,app_id='kitty',name='Completely different title')])])
|
||||
windows=shared.windows(tree)
|
||||
self.assertEqual(windows[0]['wmClass'],'Fixture')
|
||||
self.assertEqual(windows[1]['appId'],'kitty')
|
||||
self.assertEqual(windows[0]['workspace'],'__i3_scratch')
|
||||
with patch.object(shared,'sway_request',side_effect=[tree,[{'success':True}],[{'success':True}]]) as ipc:
|
||||
shared.focus('42')
|
||||
self.assertEqual([c.args for c in ipc.call_args_list],[(4,''),(0,'[con_id=42] scratchpad show'),
|
||||
(0,'[con_id=42] focus')])
|
||||
with patch.object(shared,'sway_request') as ipc:
|
||||
with self.assertRaises(ValueError): shared.focus('42] focus; exec bad')
|
||||
ipc.assert_not_called()
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||