feat(screenshot): add annotations following system GTK theme

This commit is contained in:
fy59 2026-10-09 05:14:44 +02:00
parent c9a2545656
commit 335bd62552
5 changed files with 456 additions and 4 deletions

View file

@ -0,0 +1,75 @@
# Capture annotée SwayFX V1
`Print` conserve la capture de zone rapide vers `wl-copy image/png`.
`Shift+Print` conserve la capture plein écran rapide. `Ctrl+Print` lance
`screenshot-region --annotate` : sélectionner une zone, puis annoter dans
Swappy. Échap pendant la sélection annule avant `grim` ; fermer Swappy annule
sans copie ni sauvegarde.
Avant `slurp`, les panneaux QuickShell Clipboard, Applications et Overview
ouverts sur la sortie focalisée sont fermés via leurs IPC, puis leur disparition
est attendue. Aucun workspace ni fenêtre utilisateur n'est déplacé.
Swappy 1.8.0 fournit flèche, rectangle, texte, pinceau, annulation et
rétablissement. Le panneau est ouvert au démarrage. `Ctrl+C` copie la version
annotée en `image/png` avec l'action native de Swappy ; `Ctrl+S` enregistre.
`early_exit=true` ferme l'éditeur après l'une de ces actions. Échap ou
`Ctrl+W` ferment sans sauvegarde. Pour masquer une information sensible,
utiliser un **rectangle plein opaque** (`f` pour remplir, transparence
désactivée), puis vérifier le résultat. Le flou n'est pas une garantie
d'effacement d'un secret.
La capture brute reste dans un répertoire privé `0700` propre à l'invocation
sous `XDG_RUNTIME_DIR`. Aucun `wl-copy` ne reçoit l'image brute dans ce mode.
Un verrou consultatif sur le répertoire runtime sérialise l'ouverture de
Swappy, application GTK à instance unique, lorsque deux raccourcis arrivent
presque ensemble ; il ne crée pas de fichier persistant.
Swappy reçoit le PNG directement et son bouton Copier appelle `wl-copy` avec
le type `image/png`. Les collecteurs cliphist restent inchangés ; une copie
Wayland réussie ne garantit pas l'archivage d'un PNG de plus de 5 000 000
d'octets dans la version installée de cliphist.
Le bouton Enregistrer écrit d'abord dans un dossier privé de cette invocation.
Après fermeture, le script vérifie le PNG et le publie sous un nom exclusif
`Screenshot-YYYYMMDD-HHMMSS-XXXXXXXX.png` dans `Screenshots` du répertoire
Images XDG. La configuration actuelle pointe vers `~/Pictures/Screenshots`.
Ce dossier n'est créé qu'après un enregistrement explicite. Le script nettoie
ses seuls fichiers temporaires à la fermeture, à l'annulation, en erreur et
sur interruption. `auto_save=false` et l'absence de `--output-file` sont
essentiels : Swappy 1.8.0 écrit cette sortie même lors d'une fermeture.
Dépendances : Swappy 1.8.0 (`sudo pacman -S swappy` sur Arch), GTK3, Cairo,
Pango, `wl-clipboard`, `grim`, `jq`, `flock` (util-linux), Python 3 et
`python-pillow`. Le script
utilise **`~/.local/libexec/labfy-slurp`** et non `/usr/bin/slurp` ; le
construire avec `tools/build-slurp-keymap-guard` si nécessaire. La règle de
fenêtre cible exclusivement `me.jtheoof.swappy`, identifiant déclaré dans le
code de Swappy 1.8.0. La bordure et les coins viennent de SwayFX ; le thème
GTK courant n'est pas modifié.
## Thème système
Le thème de Swappy suit la configuration GTK de la session, sans forcer
un nom de thème ni une variante sombre. Le répertoire `XDG_CONFIG_HOME`
temporaire reste propre à Swappy pour ses options d'annotation et de sauvegarde,
mais ses sous-répertoires `dconf` et `gtk-3.0` sont des liens symboliques vers
la configuration utilisateur originale (`$XDG_CONFIG_HOME`, sinon
`$HOME/.config`). Le thème, les icônes, la police et le CSS GTK restent ainsi
ceux du système ; ils ne sont pas copiés dans un instantané figé.
La suppression du répertoire temporaire retire uniquement ces liens, pas
les réglages utilisateur. Les chemins XDG avec espaces et l'absence de
réglages GTK explicites sont pris en charge. Aucune valeur `GTK_THEME`
n'est ajoutée : une éventuelle valeur déjà fournie par la session est
conservée, comme pour les autres applications GTK.
Le diagnostic a reproduit un retour à Adwaita clair sans ces liens, puis
retrouvé le même Catppuccin Mocha, les mêmes icônes et la même police que GTK
lancé normalement. Les tests isolés couvrent deux thèmes factices successifs,
un répertoire XDG personnalisé et la conservation des fichiers originaux.
Le test isolé `python3 -B -m unittest tests/test_screenshot_annotation.py -v`
utilise une image synthétique et de faux exécutables. Une vérification
graphique après installation de Swappy reste nécessaire : flèche, cadre,
texte, annuler/rétablir, collage de la copie après fermeture, enregistrement
explicite et fermeture sans enregistrement. Le test du presse-papiers réel ne
doit être lancé qu'avec l'accord de l'utilisateur, car il remplace son contenu.

View file

@ -235,6 +235,9 @@ bindsym Shift+Print exec grim - | wl-copy -t image/png
# Print = zone -> presse-papier
bindsym Print exec ~/.config/sway/scripts/screenshot-region
# Ctrl+Print = zone -> annotation ; l'éditeur décide Copier/Enregistrer.
bindsym Ctrl+Print exec ~/.config/sway/scripts/screenshot-region --annotate
###############################################
#

View file

@ -14,6 +14,12 @@ for_window [app_id="Proton Pass"] floating enable, resize set 40ppt 40ppt, move
# 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
# WHY: l'identifiant GTK de Swappy 1.8.0 est me.jtheoof.swappy (source
# application.c). La règle ne cible aucune autre fenêtre GTK.
# CONTRACT: garder l'éditeur accessible sur la sortie active, redimensionnable,
# avec la bordure serveur et les coins SwayFX déjà configurés globalement.
for_window [app_id="me.jtheoof.swappy"] floating enable, resize set 70ppt 70ppt, move position center, border pixel 2
#=================================================
# Lardon
#=================================================

View file

@ -1,6 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
# CONTRACT: sans argument, le parcours Print historique reste inchangé.
annotate=false
if [[ $# -ne 0 ]]; then
[[ $# -eq 1 && $1 == --annotate ]] || { echo "Usage: screenshot-region [--annotate]" >&2; exit 2; }
annotate=true
command -v swappy >/dev/null || { echo "Swappy absent : installer le paquet Arch swappy (sudo pacman -S swappy)." >&2; exit 1; }
fi
# WHY: la surface plein écran du presse-papiers prend le focus exclusif et
# peut faire échouer slurp sur SwayFX. La détruire avant de créer le sélecteur.
output="$(swaymsg -t get_outputs -r | jq -r '.[] | select(.focused == true) | .name' | head -n 1)"
@ -21,15 +29,59 @@ if [[ -n "$output" ]]; then
fi
fi
if $annotate && [[ -n $output ]]; then
# WHY: Applications et Overview possèdent aussi un focus layer-shell
# exclusif. Leurs IPC existants libèrent ce focus avant la sélection.
# CONTRACT: ne toucher ni au workspace ni aux fenêtres utilisateur.
for panel in applicationsUi overviewUi; do
target="$panel-$output"
state="$(quickshell ipc -c labfy-sway call "$target" state 2>/dev/null || true)"
if [[ -n $state ]] && jq -e '.loaded == true or .requested == true' >/dev/null 2>&1 <<<"$state"; then
quickshell ipc -c labfy-sway call "$target" toggle >/dev/null
for ((attempt = 0; attempt < 40; attempt++)); do
state="$(quickshell ipc -c labfy-sway call "$target" state 2>/dev/null || true)"
if [[ -n $state ]] && jq -e '.loaded == false and (.requested != true)' >/dev/null 2>&1 <<<"$state"; then
break
fi
sleep 0.05
done
[[ -n $state ]] && jq -e '.loaded == false and (.requested != true)' >/dev/null 2>&1 <<<"$state" || exit 1
fi
done
fi
# INVARIANT: Échap ou une géométrie vide ne lance ni grim ni wl-copy.
selector="$HOME/.local/libexec/labfy-slurp"
[[ -x "$selector" ]] || { echo "Sélecteur slurp corrigé absent : exécuter tools/build-slurp-keymap-guard" >&2; exit 1; }
geometry="$("$selector")" || exit 0
[[ -n "$geometry" ]] || exit 0
if $annotate; then
# CONTRACT: seules des coordonnées et des dimensions positives atteignent grim.
[[ $geometry =~ ^-?[0-9]+,-?[0-9]+[[:space:]][1-9][0-9]*x[1-9][0-9]*$ ]] || { echo "Géométrie slurp invalide" >&2; exit 1; }
fi
runtime="${XDG_RUNTIME_DIR:?}"
image="$(mktemp "$runtime/labfy-screenshot-XXXXXXXX.png")"
chmod 600 "$image"
trap 'rm -f -- "$image"' EXIT
if $annotate; then
# INVARIANT: chaque invocation possède exclusivement son répertoire 0700.
umask 077
workdir="$(mktemp -d "$runtime/labfy-screenshot-annotate-XXXXXXXX")"
editor_pid=""
cleanup_annotation() {
if [[ -n $editor_pid ]]; then
kill "$editor_pid" 2>/dev/null || true
wait "$editor_pid" 2>/dev/null || true
fi
rm -rf -- "$workdir"
}
trap cleanup_annotation EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
image="$workdir/capture.png"
else
image="$(mktemp "$runtime/labfy-screenshot-XXXXXXXX.png")"
chmod 600 "$image"
trap 'rm -f -- "$image"' EXIT
fi
grim -g "$geometry" "$image"
# CONTRACT: seul un PNG complet et non vide peut remplacer le presse-papiers.
[[ -s "$image" ]] || exit 1
@ -39,4 +91,89 @@ with open(sys.argv[1], 'rb') as image:
if image.read(8) != b'\x89PNG\r\n\x1a\n':
raise SystemExit(1)
PY
wl-copy --type image/png < "$image"
if ! $annotate; then
wl-copy --type image/png < "$image"
exit
fi
# CONTRACT: une signature seule ne prouve pas qu'un PNG est décodable.
# Ce contrôle supplémentaire appartient au mode annotation ; Print garde son
# traitement historique. Pillow est déjà présent sur cette machine.
python3 - "$image" <<'PY'
import sys
from PIL import Image
with Image.open(sys.argv[1]) as image:
if image.format != "PNG" or image.width < 1 or image.height < 1:
raise SystemExit(1)
image.verify()
PY
mkdir -m 700 -- "$workdir/config" "$workdir/config/swappy" "$workdir/saved"
# WHY: isoler XDG_CONFIG_HOME pour Swappy masquait aussi dconf et GTK,
# qui revenaient aux valeurs Adwaita claires au lieu du thème système.
# CONTRACT: partager les répertoires de réglages, jamais une copie figée ni
# un GTK_THEME forcé. Les liens suivent les changements de configuration ;
# leur suppression au nettoyage ne supprime pas les données utilisateur.
user_config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
for shared_config in dconf gtk-3.0; do
ln -s -- "$user_config_home/$shared_config" "$workdir/config/$shared_config"
done
# WHY: Swappy 1.8.0 écrit le dossier au démarrage et son nom horodaté peut
# écraser un autre fichier. Le dossier privé isole le clic Enregistrer ; seul
# un fichier issu de ce clic sera publié ensuite avec un nom exclusif.
cat > "$workdir/config/swappy/config" <<'CONFIG'
[Default]
save_dir="$LABFY_SWAPPY_SAVE_DIR"
save_filename_format=swappy-%Y%m%d-%H%M%S.png
show_panel=true
paint_mode=arrow
early_exit=true
auto_save=false
fill_shape=false
transparent=false
CONFIG
export LABFY_SWAPPY_SAVE_DIR="$workdir/saved"
# WHY: Swappy est une GtkApplication à instance unique. Deux captures proches
# doivent conserver chacune leur fichier et attendre leur propre édition.
# Le verrou advisory porte sur le répertoire runtime privé, sans fichier
# partagé à laisser ou à nettoyer. Le client ferme ce descripteur hérité.
exec {editor_lock}< "$runtime"
flock "$editor_lock"
XDG_CONFIG_HOME="$workdir/config" swappy --file "$image" {editor_lock}<&- &
editor_pid=$!
wait "$editor_pid" || exit 1
editor_pid=""
exec {editor_lock}<&-
# CONTRACT: Copier est entièrement natif (wl-copy image/png dans Swappy).
# Fermer ne crée aucun fichier enregistré. Un code de sortie 0 ne prouve
# aucune action ; seul le fichier issu du bouton Enregistrer est publié.
shopt -s nullglob
saved=("$workdir/saved"/*)
[[ ${#saved[@]} -eq 0 ]] && exit 0
[[ ${#saved[@]} -eq 1 && -f ${saved[0]} && -s ${saved[0]} ]] || exit 1
python3 - "${saved[0]}" <<'PY'
import sys
from PIL import Image
with Image.open(sys.argv[1]) as image:
if image.format != "PNG" or image.width < 1 or image.height < 1:
raise SystemExit(1)
image.verify()
PY
# CONTRACT: XDG_PICTURES_DIR est lu depuis la configuration utilisateur ;
# aucun dossier Desktop/Bureau n'est créé. Le chemin peut contenir des espaces.
pictures_dir="${XDG_PICTURES_DIR:-}"
if [[ -z $pictures_dir && -f ${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs ]]; then
source "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
pictures_dir="${XDG_PICTURES_DIR:-}"
fi
pictures_dir="${pictures_dir:-$HOME/Pictures}"
save_dir="$pictures_dir/Screenshots"
mkdir -p -- "$save_dir"
destination="$(mktemp "$save_dir/Screenshot-$(date +%Y%m%d-%H%M%S)-XXXXXXXX.png")"
chmod 600 "$destination"
if ! cp -- "${saved[0]}" "$destination"; then
rm -f -- "$destination"
exit 1
fi

View file

@ -0,0 +1,231 @@
"""Parcours annotation isolé : aucun vrai écran ni presse-papiers n'est lu."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "sway/.config/sway/scripts/screenshot-region"
class ScreenshotAnnotationTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix="annotation test ")
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.home = self.root / "home with spaces"
self.bin = self.root / "fake tools"
self.runtime = self.root / "runtime with spaces"
self.bin.mkdir()
self.runtime.mkdir()
(self.home / ".local/libexec").mkdir(parents=True)
(self.home / ".config").mkdir()
self.pictures = self.root / "Images de test"
(self.home / ".config/user-dirs.dirs").write_text(
f'XDG_PICTURES_DIR="{self.pictures}"\n')
self.png = self.root / "synthetic.png"
Image.new("RGB", (4, 3), (7, 13, 29)).save(self.png)
self.log = self.root / "calls.log"
self.make_tool("swaymsg", "#!/bin/sh\necho '[{\"focused\":true,\"name\":\"eDP-1\"}]'\n")
self.make_tool("quickshell", """#!/bin/sh
if [ "${TEST_PANELS:-}" = yes ]; then
marker="$TEST_PANEL_DIR/$5"
if [ "$6" = toggle ]; then
touch "$marker"
echo true
elif [ -e "$marker" ]; then
echo '{"loaded":false,"requested":false}'
else
echo '{"loaded":true,"requested":true}'
fi
else
echo '{"loaded":false}'
fi
""")
self.make_tool("grim", "#!/bin/sh\necho \"$0\" >> \"$TEST_LOG\"\n[ \"$TEST_CASE\" = grim_fail ] && exit 1\n[ \"$TEST_CASE\" = bad_png ] && { printf 'not a png' > \"$3\"; exit 0; }\ncp \"$TEST_PNG\" \"$3\"\n")
self.make_tool("wl-copy", "#!/bin/sh\necho \"$0\" >> \"$TEST_LOG\"\n")
self.make_tool("swappy", """#!/usr/bin/python3
import os, pathlib, sys
p = pathlib.Path(os.environ['TEST_LOG'])
with p.open('a') as out: out.write(sys.argv[0] + '\\n')
assert sys.argv[1] == '--file'
assert pathlib.Path(sys.argv[2]).read_bytes() == pathlib.Path(os.environ['TEST_PNG']).read_bytes()
config_root = pathlib.Path(os.environ['XDG_CONFIG_HOME'])
source_root = pathlib.Path(os.environ['TEST_SOURCE_CONFIG'])
assert config_root != source_root
for shared in ['dconf', 'gtk-3.0']:
link = config_root / shared
assert link.is_symlink()
assert os.readlink(link) == str(source_root / shared)
for source in (source_root / shared).rglob('*'):
if source.is_file():
assert (link / source.relative_to(source_root / shared)).read_bytes() == source.read_bytes()
assert os.environ.get('GTK_THEME') == os.environ.get('TEST_EXPECT_GTK_THEME')
config = config_root / 'swappy/config'
content = config.read_text()
assert 'show_panel=true' in content and 'early_exit=true' in content
assert 'auto_save=false' in content and 'save_dir="$LABFY_SWAPPY_SAVE_DIR"' in content
assert len(sys.argv) == 3
if os.environ['TEST_CASE'] == 'save':
dest = pathlib.Path(os.environ['LABFY_SWAPPY_SAVE_DIR']) / 'swappy-test.png'
dest.write_bytes(pathlib.Path(os.environ['TEST_PNG']).read_bytes())
if os.environ['TEST_CASE'] == 'save_slow':
import time
time.sleep(0.25)
dest = pathlib.Path(os.environ['LABFY_SWAPPY_SAVE_DIR']) / 'swappy-test.png'
dest.write_bytes(pathlib.Path(os.environ['TEST_PNG']).read_bytes())
""")
selector = self.home / ".local/libexec/labfy-slurp"
selector.write_text("#!/bin/sh\necho \"$0\" >> \"$TEST_LOG\"\nif [ \"${TEST_PANELS:-}\" = yes ]; then\n [ -e \"$TEST_PANEL_DIR/clipboardUi-eDP-1\" ] && [ -e \"$TEST_PANEL_DIR/applicationsUi-eDP-1\" ] && [ -e \"$TEST_PANEL_DIR/overviewUi-eDP-1\" ] || exit 17\nfi\ncase \"$TEST_CASE\" in cancel) exit 1;; empty) exit 0;; invalid) echo bogus;; *) echo '10,20 4x3';; esac\n")
selector.chmod(0o755)
def make_tool(self, name, body):
path = self.bin / name
path.write_text(body)
path.chmod(0o755)
def case_env(self, name):
env = dict(os.environ, HOME=str(self.home), XDG_CONFIG_HOME=str(self.home / ".config"),
XDG_RUNTIME_DIR=str(self.runtime),
PATH=f"{self.bin}:{os.environ['PATH']}", TEST_CASE=name,
TEST_LOG=str(self.log), TEST_PNG=str(self.png),
TEST_SOURCE_CONFIG=str(self.home / ".config"))
env.pop("GTK_THEME", None)
env.pop("TEST_EXPECT_GTK_THEME", None)
env.pop("XDG_PICTURES_DIR", None)
return env
def run_case(self, name, annotate=True):
env = self.case_env(name)
return subprocess.run([str(SCRIPT)] + (["--annotate"] if annotate else []), env=env,
capture_output=True, text=True, timeout=10)
def calls(self):
return self.log.read_text().splitlines() if self.log.exists() else []
def expected(self, *names):
return [str(self.home / ".local/libexec/labfy-slurp") if name == "slurp"
else str(self.bin / name) for name in names]
def assert_clean(self):
self.assertEqual(list(self.runtime.iterdir()), [])
def test_cancel_and_invalid_geometry(self):
for case, expected in (("cancel", 0), ("empty", 0), ("invalid", 1)):
with self.subTest(case=case):
self.log.unlink(missing_ok=True)
self.assertEqual(self.run_case(case).returncode, expected)
self.assertEqual(self.calls(), self.expected("slurp"))
self.assert_clean()
def test_grim_failure(self):
self.assertNotEqual(self.run_case("grim_fail").returncode, 0)
self.assertEqual(self.calls(), self.expected("slurp", "grim"))
self.assert_clean()
def test_invalid_png_never_opens_editor(self):
self.assertNotEqual(self.run_case("bad_png").returncode, 0)
self.assertEqual(self.calls(), self.expected("slurp", "grim"))
self.assert_clean()
def test_exclusive_panels_close_before_selector(self):
panel_dir = self.root / "panels"
panel_dir.mkdir()
env = self.case_env("close")
env.update(TEST_PANELS="yes", TEST_PANEL_DIR=str(panel_dir))
result = subprocess.run([str(SCRIPT), "--annotate"], env=env,
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(len(list(panel_dir.iterdir())), 3)
self.assertEqual(self.calls(), self.expected("slurp", "grim", "swappy"))
self.assert_clean()
def test_close_does_not_publish_or_copy(self):
self.assertEqual(self.run_case("close").returncode, 0)
self.assertEqual(self.calls(), self.expected("slurp", "grim", "swappy"))
self.assertFalse(self.pictures.exists())
self.assert_clean()
def test_explicit_save_and_two_invocations(self):
for _ in range(2):
self.assertEqual(self.run_case("save").returncode, 0)
self.assert_clean()
images = list((self.pictures / "Screenshots").glob("Screenshot-*.png"))
self.assertEqual(len(images), 2)
self.assertEqual(len({p.name for p in images}), 2)
self.assertTrue(all(p.read_bytes() == self.png.read_bytes() for p in images))
self.assertNotIn(str(self.bin / "wl-copy"), self.calls())
def test_overlapping_invocations_have_distinct_outputs(self):
env = self.case_env("save_slow")
processes = [subprocess.Popen([str(SCRIPT), "--annotate"], env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for _ in range(2)]
for process in processes:
_, stderr = process.communicate(timeout=10)
self.assertEqual(process.returncode, 0, stderr.decode())
images = list((self.pictures / "Screenshots").glob("Screenshot-*.png"))
self.assertEqual(len(images), 2)
self.assertEqual(len({p.name for p in images}), 2)
self.assert_clean()
def test_system_theme_changes_are_shared_without_copy_or_override(self):
config = self.home / ".config"
for shared in ("dconf", "gtk-3.0"):
(config / shared).mkdir()
for theme in ("Fixture-Dark", "Fixture-Light"):
with self.subTest(theme=theme):
# Données factices : aucune base dconf personnelle n'est lue.
database = config / "dconf/user"
settings = config / "gtk-3.0/settings.ini"
css = config / "gtk-3.0/gtk.css"
database.write_bytes(("fixture:" + theme).encode())
settings.write_text("[Settings]\ngtk-theme-name=" + theme + "\n")
css.write_text("/* CSS système factice " + theme + " */\n")
before = {p: p.read_bytes() for p in (database, settings, css)}
result = self.run_case("close")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual({p: p.read_bytes() for p in before}, before)
self.assert_clean()
def test_custom_xdg_config_and_inherited_theme_are_preserved(self):
config = self.root / "custom config with spaces"
(config / "dconf").mkdir(parents=True)
(config / "gtk-3.0").mkdir()
settings = config / "gtk-3.0/settings.ini"
settings.write_text("[Settings]\ngtk-theme-name=Fixture-User\n")
env = self.case_env("close")
env.update(XDG_CONFIG_HOME=str(config), TEST_SOURCE_CONFIG=str(config),
GTK_THEME="Fixture-Session", TEST_EXPECT_GTK_THEME="Fixture-Session")
result = subprocess.run([str(SCRIPT), "--annotate"], env=env,
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(settings.read_text(), "[Settings]\ngtk-theme-name=Fixture-User\n")
self.assert_clean()
def test_unset_xdg_config_and_missing_gtk_settings_are_supported(self):
env = self.case_env("close")
env.pop("XDG_CONFIG_HOME")
result = subprocess.run([str(SCRIPT), "--annotate"], env=env,
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse((self.home / ".config/gtk-3.0").exists())
self.assertFalse((self.home / ".config/dconf").exists())
self.assert_clean()
def test_print_default_and_shift_binding_remain(self):
self.assertEqual(self.run_case("close", annotate=False).returncode, 0)
self.assertEqual(self.calls(), self.expected("slurp", "grim", "wl-copy"))
self.assert_clean()
binds = (ROOT / "sway/.config/sway/bind").read_text()
self.assertIn("bindsym Shift+Print exec grim - | wl-copy -t image/png", binds)
self.assertIn("bindsym Print exec ~/.config/sway/scripts/screenshot-region\n", binds)
if __name__ == "__main__":
unittest.main()