From dd72ba36b9f54c32efbe04a0e3466e53acc61429 Mon Sep 17 00:00:00 2001 From: fy59 Date: Thu, 8 Oct 2026 20:57:24 +0200 Subject: [PATCH] feat(desktop): configure Yazi portal and floating borders --- README.md | 5 +- docs/desktop.md | 28 +++++++ docs/installation.md | 7 +- docs/recovery.md | 2 +- .../xdg-desktop-portal-termfilechooser/config | 8 ++ .../yazi-wrapper.sh | 66 +++++++++++++++ .../.config/xdg-desktop-portal/portals.conf | 8 ++ portal/README.md | 84 +++++++++++++++++++ sway/.config/sway/README.md | 3 + sway/.config/sway/rules | 11 +++ .../scripts/inactive-windows-transparency.py | 41 ++++++++- 11 files changed, 254 insertions(+), 9 deletions(-) create mode 100644 portal/.config/xdg-desktop-portal-termfilechooser/config create mode 100755 portal/.config/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh create mode 100644 portal/.config/xdg-desktop-portal/portals.conf create mode 100644 portal/README.md diff --git a/README.md b/README.md index c55dd30fa..2aaf13a47 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ ArchASP └── Dotfiles_ArchAP utilisateur · $HOME · Stow · systemd --user ``` -Les 31 packages Stow couvrent le desktop, le shell, le développement, les +Les 32 packages Stow couvrent le desktop, le shell, le développement, les services utilisateur et l'apparence. `.assets/` contient les inventaires de reconstruction ; `docs/` contient la documentation et n'est jamais un package Stow. Voir le [contrat d'architecture](docs/architecture.md). @@ -90,7 +90,7 @@ git clone --recurse-submodules \ cd "$HOME/.dotfiles" packages=(atuin bat bin btop cliphist eza fastfetch fzf gtk-3.0 gtklock \ - hyprwhspr icons kitty mako nvim opencode qt5ct qt6ct \ + hyprwhspr icons kitty mako nvim opencode portal qt5ct qt6ct \ quickshell starship sway swaylock systemd themes tmux uwsm wallpapers wofi yazi \ ytmusic-tui zsh) @@ -110,6 +110,7 @@ inventaires et validations sont décrits dans le | [Architecture](docs/architecture.md) | Frontières ArchASP, Stow et données locales | | [Installation](docs/installation.md) | Clone neuf, sous-modules, Stow et validations | | [Desktop](docs/desktop.md) | Chaîne greetd/UWSM/Sway et composants Wayland | +| [Sélecteur de fichiers](portal/README.md) | Portail FileChooser, Yazi, Kitty et raccourcis | | [Shell](docs/shell.md) | Zsh, outils interactifs, plugins et scripts | | [systemd](docs/systemd.md) | Services système et unités utilisateur | | [Packages](docs/packages.md) | Inventaires Pacman, AUR, services et outils externes | diff --git a/docs/desktop.md b/docs/desktop.md index 871787767..24dc2edeb 100644 --- a/docs/desktop.md +++ b/docs/desktop.md @@ -24,6 +24,34 @@ ombres validées, flou global désactivé, flou réservé à la couche QuickShell. Le script d'opacité inactive écoute l'IPC Sway au lancement de la session. +## Décoration des fenêtres flottantes + +Les fenêtres flottantes classiques utilisent une bordure serveur `pixel 2`, +sans barre de titre Sway. `sway/.config/sway/style` fixe les valeurs par défaut +pour les nouvelles fenêtres et les couleurs `client.*` : Lavender `#b4befe` +avec le focus, Surface2 `#6c7086` sans focus. Ces couleurs sont communes aux +fenêtres flottantes et en mosaïque ; SwayFX 0.6 ne fournit pas ici une palette +distincte selon le mode. La règle `[floating]` de `sway/.config/sway/rules` +couvre les fenêtres créées flottantes, y compris les dialogues. La règle +`labfy-yazi-filechooser` conserve son centrage et force cette même bordure +pour le sélecteur Yazi. L'abonné IPC existant +`scripts/inactive-windows-transparency.py` écoute aussi `window::floating`. +Il cible l'ID du conteneur devenu flottant, attend 50 ms que son éventuel mode +`csd` se stabilise, puis applique `border pixel 2` si nécessaire. La bordure +antérieure est rétablie lorsqu'il revient en mosaïque. Cela couvre aussi bien +le raccourci `$mod+Ctrl+space` qu'une commande IPC directe `floating enable`, +sans nouveau processus et sans modifier les surfaces layer-shell. + +`corner_radius 4` dans `sway/.config/sway/swayfx` est un réglage global SwayFX : +les fenêtres en mosaïque avaient déjà ce rayon, qui n'est pas modifié par cette +politique. Les valeurs par défaut des fenêtres en mosaïque restent inchangées. +SwayFX 0.6 ne propose pas de règle native réévaluée à chaque transition ; +`for_window` ne traite que l'apparition de la fenêtre. L'abonné IPC mémorise +uniquement les bordures remplacées jusqu'au retour en mosaïque ou à la fermeture +du conteneur. Les surfaces layer-shell de QuickShell, ses notifications et les +menus système ne sont pas des fenêtres classiques ciblées par `[floating]` ; +leurs règles existantes restent en vigueur. + ## Services et outils `labfy-quickshell-updates.timer` déclenche la vérification Arch/AUR sans diff --git a/docs/installation.md b/docs/installation.md index a6d078d8e..d937543cc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -63,14 +63,14 @@ installer. Comparer leur contenu à la machine cible et lire [`packages.md`](packages.md) ainsi que [`external-tools.md`](../.assets/external-tools.md) avant toute installation. -## 5. Définir les 31 packages Stow +## 5. Définir les 32 packages Stow Les commandes suivantes sont destinées à Bash : ```bash packages=( atuin bat bin btop cliphist eza fastfetch fzf gtk-3.0 gtklock - hyprwhspr icons kitty mako nvim opencode qt5ct qt6ct + hyprwhspr icons kitty mako nvim opencode portal qt5ct qt6ct quickshell starship sway swaylock systemd themes tmux uwsm wallpapers wofi yazi ytmusic-tui zsh ) @@ -79,6 +79,9 @@ printf '%s\n' "${packages[@]}" ``` `docs/` et `.assets/` ne doivent pas être ajoutés à ce tableau. Le package +`portal` demande l'installation du backend décrite dans +[`portal/README.md`](../portal/README.md). +Le package `quickshell` déploie `~/.config/quickshell/labfy-sway/` ; une Nerd Font (`ttf-jetbrains-mono-nerd` sur cette machine) fournit les icônes. SwayFX 0.6 est un paquet foreign/local à reconstruire depuis sa source diff --git a/docs/recovery.md b/docs/recovery.md index 6966f84eb..1f33ee6a6 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -50,7 +50,7 @@ exigent une décision spécifique ; aucun helper AUR n'est supposé magique. ## 6. Déployer Stow Suivre le [guide d'installation](installation.md) : définir explicitement les -31 packages, exécuter le dry-run, résoudre les conflits, puis seulement lancer +32 packages, exécuter le dry-run, résoudre les conflits, puis seulement lancer le déploiement réel. `docs/` et `.assets/` ne sont pas déployés. ## 7. Restaurer les services diff --git a/portal/.config/xdg-desktop-portal-termfilechooser/config b/portal/.config/xdg-desktop-portal-termfilechooser/config new file mode 100644 index 000000000..da155f5f5 --- /dev/null +++ b/portal/.config/xdg-desktop-portal-termfilechooser/config @@ -0,0 +1,8 @@ +[filechooser] +cmd=yazi-wrapper.sh +default_dir=$HOME/Downloads +# WHY: le fichier provisoire permet à Yazi de choisir un nouveau nom de sauvegarde. +# CONTRACT: le backend supprime ce fichier si la sélection est annulée. +create_help_file=1 +open_mode = suggested +save_mode = last diff --git a/portal/.config/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh b/portal/.config/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh new file mode 100755 index 000000000..c2ec42c33 --- /dev/null +++ b/portal/.config/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# Adaptation du wrapper Yazi de hunkyburrito/xdg-desktop-portal-termfilechooser 1.4.3. +# CONTRACT: arguments multiple, directory, save, path, out, debug dans cet ordre. +set -eu + +if [ "${1:-}" != '--session' ]; then + # WHY: kitty reçoit des arguments distincts ; aucun chemin n'est réinterprété par un shell. + # INVARIANT: l'app_id est réservé à ce sélecteur, jamais aux terminaux ordinaires. + # CONTRACT: dimension initiale 1000x650, bornée à la sortie SwayFX active. + width=1000 + height=650 + if command -v swaymsg >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then + geometry=$(swaymsg -t get_outputs -r 2>/dev/null | jq -r '[.[] | select(.focused == true and .active == true)] | first | if . then "\(.rect.width) \(.rect.height)" else empty end' 2>/dev/null) || geometry='' + if [ -n "$geometry" ]; then + output_width=${geometry%% *} + output_height=${geometry#* } + case "$output_width:$output_height" in + *[!0-9:]* ) ;; + * ) + [ "$output_width" -le 1040 ] && width=$(( output_width > 80 ? output_width - 40 : 40 )) + [ "$output_height" -le 690 ] && height=$(( output_height > 80 ? output_height - 40 : 40 )) + ;; + esac + fi + fi + exec kitty --class labfy-yazi-filechooser --title 'Yazi · Sélecteur de fichiers' \ + --override remember_window_size=no \ + --override "initial_window_width=${width}" \ + --override "initial_window_height=${height}" \ + -- "$0" --session "$@" +fi +shift + +multiple=${1:?} +directory=${2:?} +save=${3:?} +path=${4:?} +out=${5:?} +debug=${6:-0} + +case "$multiple:$directory:$save:$debug" in + *[!01:]* ) exit 2 ;; +esac + +# CONTRACT: Yazi utilise sa configuration XDG normale ; chooser-file contient +# les chemins sélectionnés, tandis que cwd-file sert au choix du dossier courant. +if [ "$directory" = 1 ]; then + yazi --chooser-file="$out" --cwd-file="$out.1" "$path" || : + if [ ! -s "$out" ] && [ -s "$out.1" ]; then + cat "$out.1" > "$out" + fi + rm -f -- "$out.1" +else + yazi --chooser-file="$out" "$path" || : +fi + +if [ "$save" = 1 ] && [ -s "$out" ]; then + # WHY: le backend peut proposer un fichier existant. La confirmation garde + # l'écrasement potentiel explicite avant de publier le chemin à l'appelant. + printf '\nDestination sélectionnée : %s\nConfirmer ? L’application peut écraser son contenu. [o/N] ' "$(head -n 1 -- "$out")" + IFS= read -r reply "$out" ;; + esac +fi diff --git a/portal/.config/xdg-desktop-portal/portals.conf b/portal/.config/xdg-desktop-portal/portals.conf new file mode 100644 index 000000000..802dd48c9 --- /dev/null +++ b/portal/.config/xdg-desktop-portal/portals.conf @@ -0,0 +1,8 @@ +[preferred] +# WHY: la configuration utilisateur est choisie avant sway-portals.conf ; +# CONTRACT: conserver le routage Sway pour toutes les autres interfaces. +default=gtk +org.freedesktop.impl.portal.FileChooser=termfilechooser +org.freedesktop.impl.portal.ScreenCast=wlr +org.freedesktop.impl.portal.Screenshot=wlr +org.freedesktop.impl.portal.Secret=gnome-keyring diff --git a/portal/README.md b/portal/README.md new file mode 100644 index 000000000..8878e6cad --- /dev/null +++ b/portal/README.md @@ -0,0 +1,84 @@ +# Yazi comme FileChooser du portail + +Package GNU Stow : `stow -d ~/.dotfiles -t ~ portal`. Les fichiers actifs dans +`~/.config/xdg-desktop-portal/` et +`~/.config/xdg-desktop-portal-termfilechooser/` sont des liens Stow. La règle +SwayFX est dans le package `sway`. + +## Dépendances + +Arch Linux : `xdg-desktop-portal`, `xdg-desktop-portal-gtk`, +`xdg-desktop-portal-wlr`, `kitty`, `yazi`, `jq`, `swayfx`, `libinih`, +`libsystemd`, `meson`, `ninja` et `scdoc` pour une compilation complète. Le +backend retenu est +[`hunkyburrito/xdg-desktop-portal-termfilechooser`](https://github.com/hunkyburrito/xdg-desktop-portal-termfilechooser), +version 1.4.3, commit `bcb2387949e4eb38a35390ebf1693f92869e619d` dans +l'installation testée. Le paquet AUR +`xdg-desktop-portal-termfilechooser-hunkyburrito-git` existe aussi. + +Cette machine utilise une installation Meson utilisateur sous `~/.local`, sans +changer les backends système. Les fichiers nécessaires sont +`~/.local/libexec/xdg-desktop-portal-termfilechooser`, +`~/.local/share/dbus-1/services/org.freedesktop.impl.portal.desktop.termfilechooser.service` +et `~/.local/share/xdg-desktop-portal/portals/termfilechooser.portal`. +La procédure de reproduction, conservée dans ce dépôt indépendamment du cache +de construction, est la suivante (sans l'exécuter pendant une simple publication) : + +```sh +git clone https://github.com/hunkyburrito/xdg-desktop-portal-termfilechooser.git +cd xdg-desktop-portal-termfilechooser +git checkout --detach bcb2387949e4eb38a35390ebf1693f92869e619d +meson setup build --prefix="$HOME/.local" +meson compile -C build +meson install -C build +``` + +Contrôler ensuite les trois chemins ci-dessus et la configuration Stow de ce +dossier. Le dépôt source et le répertoire `build` sont temporaires et ne font +pas partie des dotfiles. Cette procédure installe uniquement dans le préfixe +utilisateur ; elle ne remplace aucun paquet système. +Après installation ou mise à jour du backend, recharger seulement +`xdg-desktop-portal.service` après avoir vérifié qu'aucune session de partage +d'écran ou requête portal n'est en cours. + +## Routage et fenêtre + +`FileChooser` utilise `termfilechooser`, `ScreenCast` et `Screenshot` utilisent +`wlr`, `Secret` utilise `gnome-keyring`, et les autres interfaces disponibles +gardent `gtk`. `XDG_CURRENT_DESKTOP=sway:wlroots:swayfx` sélectionne la +configuration utilisateur `portals.conf` avant les fichiers système propres à +Sway. Le wrapper lance Kitty avec `--class labfy-yazi-filechooser` ; SwayFX +flotte et centre uniquement cet `app_id`. Kitty calcule au lancement une taille +de 1000 × 650, bornée à la sortie SwayFX active avec une marge de 40 pixels. +La fenêtre reste redimensionnable et sur le workspace courant. Le thème Kitty +et le thème Yazi Catppuccin Mocha sont ceux de la configuration habituelle. + +Firefox 157 utilise le portail FileChooser en mode automatique quand il est +disponible. Aucun `GTK_USE_PORTAL` global ni préférence de profil n'est +nécessaire dans cette installation. Pour diagnostiquer un profil différent, +vérifier `widget.use-xdg-desktop-portal.file-picker` dans `about:config` : `1` +force le portail, `0` le désactive, `2` suit le mode automatique. + +## Utilisation + +Dans le sélecteur, `o` ou `Entrée` valide le ou les fichiers survolés ou +sélectionnés. `Espace` sélectionne chaque fichier en mode multiple. `q` annule +un choix de fichier ; `Q` annule aussi et ne transmet pas le dossier courant. +Pour un dossier, entrer dedans puis appuyer sur `q` ; `Q` annule. `M` ouvre le +menu des supports, `g` puis `Espace` permet de saisir un chemin. Ouvrir un +fichier via Yazi sans mode chooser n'est pas la même opération : ici, le +wrapper utilise `--chooser-file` pour remettre les chemins à l'application. + +Pour « Enregistrer sous », le backend crée un fichier provisoire annoté. Le +déplacer avec `x`, naviguer vers la destination, le déposer avec `p`, puis le +renommer avec `r` si nécessaire. Valider le fichier avec `o` ou `Entrée`, puis +répondre `o` au prompt de confirmation du wrapper. Le prompt avertit que +l'application peut écraser une destination existante ; répondre autrement +annule. `Q` dans Yazi annule également et le backend supprime le provisoire +resté à son emplacement d'origine. Si le provisoire a été déplacé avant une +annulation, supprimer manuellement ce fichier déplacé. Les essais doivent +utiliser un dossier temporaire et éviter tout écrasement de données réelles. + +Les applications GTK/Qt qui utilisent leurs dialogues natifs plutôt que le +portail conservent leur comportement. Ce réglage ne change aucune association +MIME et ne remplace pas les portails d'autres interfaces. diff --git a/sway/.config/sway/README.md b/sway/.config/sway/README.md index bcf27f77f..5b2059f0e 100644 --- a/sway/.config/sway/README.md +++ b/sway/.config/sway/README.md @@ -23,6 +23,9 @@ QuickShell, lancé par `quickshell-labfy-sway.service`. `autostart` lance une fois le listener IPC `scripts/inactive-windows-transparency.py`, `limusic-app` et `autotiling`. +Ce listener réutilise les événements `window::floating` pour appliquer la +bordure serveur de 2 px lors des bascules IPC, puis restaure la bordure +antérieure au retour en mosaïque. Aucun second abonné permanent n'est lancé. Cliphist, swayidle, wlsunset et QuickShell sont gérés par systemd user ; aucune commande `qs` n'est lancée par Sway. Le fichier `swayfx` fixe les animations à 0, les coins à 4, les ombres validées et le flou uniquement sur diff --git a/sway/.config/sway/rules b/sway/.config/sway/rules index be854a4b0..5ee11d2f7 100644 --- a/sway/.config/sway/rules +++ b/sway/.config/sway/rules @@ -9,6 +9,11 @@ for_window [app_id="blueman-manager"] floating enable, resize set 40ppt 40ppt, move position center for_window [app_id="Proton Pass"] floating enable, resize set 40ppt 40ppt, move position right top +# WHY: le sélecteur portal doit flotter et rester centré. +# CONTRACT: seul l'app_id dédié au wrapper Yazi reçoit ce placement ; +# la bordure serveur garantit la même politique que les autres flottantes. +for_window [app_id="labfy-yazi-filechooser"] floating enable, border pixel 2, move position center + #================================================= # Lardon #================================================= @@ -17,3 +22,9 @@ for_window [app_id="lardon-freecad-log"] floating enable for_window [app_id="lardon-freecad-log"] resize set width 850 px height 360 px for_window [app_id="lardon-freecad-log"] move position center for_window [app_id="lardon-freecad-log"] border pixel 2 + +# WHY: les fenêtres CSD peuvent ignorer default_floating_border. +# CONTRACT: toute fenêtre créée flottante utilise une bordure serveur de 2 px, +# sans barre de titre ; client.* fournit Lavender au focus et Mocha hors focus. +# Les transitions ultérieures sont traitées par l'abonné IPC d'opacité existant. +for_window [floating] border pixel 2 diff --git a/sway/.config/sway/scripts/inactive-windows-transparency.py b/sway/.config/sway/scripts/inactive-windows-transparency.py index e85247096..f52c0589b 100755 --- a/sway/.config/sway/scripts/inactive-windows-transparency.py +++ b/sway/.config/sway/scripts/inactive-windows-transparency.py @@ -2,14 +2,15 @@ # This script requires i3ipc-python package (install it from a system package manager # or pip). -# It makes inactive windows transparent. Use `transparency_val` variable to control -# transparency strength in range of 0…1 or use the command line argument -o. +# It makes inactive windows transparent and corrects borders on floating events. +# Use the command line argument -o to control inactive opacity in range 0…1. import argparse import json import os import signal import sys +import time from functools import partial from pathlib import Path @@ -17,6 +18,10 @@ import i3ipc STATE = Path(os.environ.get('XDG_STATE_HOME', Path.home() / '.local/state')) / 'labfy-appearance/effective.json' +# INVARIANT: seules les bordures de mosaïque remplacées en flottant sont +# mémorisées, puis oubliées au retour en mosaïque ou à la fermeture. +previous_tiled_borders = {} + def inactive_opacity(): # CONTRACT: lecture uniquement sur événement IPC ou SIGUSR1 de la @@ -38,10 +43,38 @@ def apply_all(ipc): def on_window(args, ipc, event): global focused_set - # To get the workspace for a container, we need to have received its - # parents, so fetch the whole tree + # WHY: Sway émet window::floating avant de finir le passage à csd de + # certaines fenêtres. Attendre uniquement cette transition évite qu'une + # bordure corrigée aussitôt soit écrasée dans la même opération. + if event.change == 'floating' and event.container.floating in ('user_on', 'auto_on'): + time.sleep(0.05) + + # L'arbre courant remplace l'instantané trop précoce de l'événement. tree = ipc.get_tree() + # WHY: for_window ne se rejoue pas sur une transition IPC vers floating. + # CONTRACT: la bordure serveur ne s'applique qu'au conteneur devenu + # flottant ; sa décoration précédente revient lors du retour en mosaïque. + # INVARIANT: une bordure déjà correcte ne déclenche aucune commande, donc + # aucune réaction répétée à un éventuel événement causé par border. + if event.change == 'floating': + window = tree.find_by_id(event.container.id) + if window is not None and window.floating in ('user_on', 'auto_on'): + if window.border != 'pixel' or window.current_border_width != 2: + # L'instantané de l'événement garde la bordure d'entrée, + # même si l'arbre courant montre déjà csd en flottant. + previous_tiled_borders[window.id] = ( + event.container.border, event.container.current_border_width) + window.command('border pixel 2') + elif event.container.id in previous_tiled_borders: + border, width = previous_tiled_borders.pop(event.container.id) + if border in ('normal', 'pixel') and isinstance(width, int) and 0 <= width <= 1000: + event.container.command(f'border {border} {width}') + elif border in ('none', 'csd'): + event.container.command(f'border {border}') + elif event.change == 'close': + previous_tiled_borders.pop(event.container.id, None) + args.opacity = inactive_opacity() focused = tree.find_focused() if focused is None: