From a84778a640b182ec31e27771934dfd0768bef741 Mon Sep 17 00:00:00 2001 From: Anton Volnuhin Date: Thu, 6 Aug 2026 02:39:49 +0300 Subject: [PATCH] fix(kitty): read file permissions correctly where stat is GNU `chezmoi apply` on Linux printed "could not regenerate kitty theme files" every run. GNU stat reads -f as --file-system, so `stat -f '%Lp' file` prints a block of filesystem info to stdout and exits non-zero. The `|| stat -c '%a'` fallback then appended the real answer to that block, and chmod rejected the result, failing the script under set -e. Replace the chain with a file_mode() helper in both scripts: try the GNU form first, since BSD rejects -c cleanly without printing anything, and accept the answer only if it is octal. Covered by a new case in tests/test_theme_mode.sh using a stat stub that reproduces GNU's behaviour; it fails against the old chain. --- .../private_executable_apply-theme-mode.sh | 21 +++++++++--- .../private_executable_repair-auto-themes.sh | 21 +++++++++--- .../private_executable_test_theme_mode.sh | 32 +++++++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/private_dot_config/kitty/private_executable_apply-theme-mode.sh b/private_dot_config/kitty/private_executable_apply-theme-mode.sh index e23f51f..b5283cf 100644 --- a/private_dot_config/kitty/private_executable_apply-theme-mode.sh +++ b/private_dot_config/kitty/private_executable_apply-theme-mode.sh @@ -14,6 +14,22 @@ set -eu +# Permission bits of a file, or 600 if they cannot be read. GNU stat spells this +# `-c %a` while BSD/macOS spells it `-f %Lp`, and each mis-parses the other's +# form -- GNU treats `-f` as --file-system and prints a block of filesystem info +# before failing -- so try one at a time and only accept an octal answer. +file_mode() { + _m=$(stat -c '%a' "$1" 2>/dev/null) || _m='' + case "$_m" in + [0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) printf '%s\n' "$_m"; return 0 ;; + esac + _m=$(stat -f '%Lp' "$1" 2>/dev/null) || _m='' + case "$_m" in + [0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) printf '%s\n' "$_m"; return 0 ;; + esac + printf '600\n' +} + script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" config_dir=${1:-"$script_dir"} canonical_dir="$config_dir/themes" @@ -54,12 +70,9 @@ for appearance in dark light no-preference; do [ -f "$canonical" ] || continue generated="$config_dir/$appearance-theme.auto.conf" - # stat -f is BSD/macOS, stat -c is GNU; this also runs on Linux boxes. mode_bits=600 if [ -f "$generated" ]; then - mode_bits=$(stat -f '%Lp' "$generated" 2>/dev/null \ - || stat -c '%a' "$generated" 2>/dev/null \ - || echo 600) + mode_bits=$(file_mode "$generated") fi tmp=$(mktemp "$generated.tmp.XXXXXX") diff --git a/private_dot_config/kitty/private_executable_repair-auto-themes.sh b/private_dot_config/kitty/private_executable_repair-auto-themes.sh index d12bc99..077c829 100644 --- a/private_dot_config/kitty/private_executable_repair-auto-themes.sh +++ b/private_dot_config/kitty/private_executable_repair-auto-themes.sh @@ -2,6 +2,22 @@ set -eu +# Permission bits of a file, or 600 if they cannot be read. GNU stat spells this +# `-c %a` while BSD/macOS spells it `-f %Lp`, and each mis-parses the other's +# form -- GNU treats `-f` as --file-system and prints a block of filesystem info +# before failing -- so try one at a time and only accept an octal answer. +file_mode() { + _m=$(stat -c '%a' "$1" 2>/dev/null) || _m='' + case "$_m" in + [0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) printf '%s\n' "$_m"; return 0 ;; + esac + _m=$(stat -f '%Lp' "$1" 2>/dev/null) || _m='' + case "$_m" in + [0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) printf '%s\n' "$_m"; return 0 ;; + esac + printf '600\n' +} + config_dir=${1:-"$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"} for appearance in dark light no-preference; do @@ -22,10 +38,7 @@ for appearance in dark light no-preference; do trap 'rm -f "$tmp"' EXIT HUP INT TERM awk '$1 != "macos_titlebar_color"' "$config_file" > "$tmp" printf '\nmacos_titlebar_color background\n' >> "$tmp" - # stat -f is BSD/macOS, stat -c is GNU; this also runs on Linux boxes. - chmod "$(stat -f '%Lp' "$config_file" 2>/dev/null \ - || stat -c '%a' "$config_file" 2>/dev/null \ - || echo 600)" "$tmp" + chmod "$(file_mode "$config_file")" "$tmp" mv -f "$tmp" "$config_file" trap - EXIT HUP INT TERM done diff --git a/private_dot_config/kitty/private_tests/private_executable_test_theme_mode.sh b/private_dot_config/kitty/private_tests/private_executable_test_theme_mode.sh index 3cf01d2..b9b57de 100644 --- a/private_dot_config/kitty/private_tests/private_executable_test_theme_mode.sh +++ b/private_dot_config/kitty/private_tests/private_executable_test_theme_mode.sh @@ -149,4 +149,36 @@ run_fish dark 'theme >/dev/null' "$root/tests/verify_auto_themes.sh" "$config_dir" +# --- the scripts must work where stat is GNU, not BSD --------------------- +# GNU stat reads -f as --file-system, so `stat -f '%Lp' file` prints a block of +# filesystem info to stdout *and* exits non-zero. Chaining it into `|| stat -c` +# concatenates that block with the real answer and chmod then rejects it, which +# broke `chezmoi apply` on Linux with "could not regenerate kitty theme files". +cat >"$fake_bin/stat" <<'EOF' +#!/bin/sh +case "$1" in + -c) shift; [ "$1" = "%a" ] && shift; /usr/bin/stat -f '%Lp' "$1" ;; + -f) shift + echo " File: \"$2\"" + echo "Block size: 4096 Fundamental block size: 4096" + exit 1 ;; + *) exec /usr/bin/stat "$@" ;; +esac +EOF +chmod +x "$fake_bin/stat" + +echo auto >"$config_dir/theme-mode" +if ! PATH="$fake_bin:/usr/bin:/bin" "$root/apply-theme-mode.sh" "$config_dir" \ + >/dev/null 2>&1; then + echo "not ok - apply-theme-mode.sh fails where stat is GNU" >&2 + exit 1 +fi +for appearance in dark light no-preference; do + [ -f "$config_dir/$appearance-theme.auto.conf" ] || { + echo "not ok - GNU stat run did not produce $appearance theme" >&2 + exit 1 + } +done +rm -f "$fake_bin/stat" + echo "PASS: theme mode is remembered, survives kitten themes, and collapses to auto"