@sebastienrousseau/dotfiles 0.2.511 → 0.2.512
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +2 -2
- package/docs/AI.md +2 -1
- package/docs/COPYRIGHT +1 -1
- package/docs/architecture/AI_COST_OPTIMIZATION.md +1 -0
- package/docs/archive/LEGACY_ROADMAP.md +4 -158
- package/docs/manual/00-introduction.md +1 -1
- package/docs/manual/02-tutorials/02-add-wallpaper.md +6 -6
- package/docs/manual/02-tutorials/03-create-profile.md +1 -1
- package/docs/manual/02-tutorials/05-deploy-fleet.md +1 -1
- package/docs/manual/03-reference/02-config-files.md +1 -1
- package/docs/manual/command-index.md +1 -0
- package/docs/operations/ARCHITECTURE_ROADMAP.md +5 -143
- package/docs/operations/COVERAGE.md +27 -4
- package/docs/operations/ROADMAP.md +3 -159
- package/docs/operations/ROADMAP_2026.md +5 -663
- package/docs/operations/ROADMAP_V0_2_503.md +5 -129
- package/docs/reference/THEMES.md +1 -1
- package/docs/reference/TOOLS.md +1 -0
- package/docs/reference/UTILS.md +1 -0
- package/install.sh +5 -5
- package/package.json +1 -1
- package/scripts/diagnostics/aliases-manifest.sh +33 -6
- package/scripts/diagnostics/benchmark.sh +27 -1
- package/scripts/diagnostics/doctor.sh +18 -11
- package/scripts/diagnostics/health.sh +8 -3
- package/scripts/diagnostics/mcp-doctor.sh +3 -0
- package/scripts/diagnostics/secret-governance.sh +7 -1
- package/scripts/diagnostics/security-score.sh +14 -5
- package/scripts/diagnostics/verify_state.sh +9 -1
- package/scripts/dot/commands/ai.sh +37 -30
- package/scripts/dot/commands/aliases.sh +28 -4
- package/scripts/dot/commands/appearance.sh +16 -0
- package/scripts/dot/commands/core.sh +17 -0
- package/scripts/dot/commands/diagnostics.sh +19 -0
- package/scripts/dot/commands/manual.sh +4 -4
- package/scripts/dot/commands/meta.sh +87 -12
- package/scripts/dot/commands/registry.sh +1 -1
- package/scripts/dot/commands/secrets.sh +17 -0
- package/scripts/dot/commands/security.sh +17 -0
- package/scripts/dot/commands/tools.sh +19 -0
- package/scripts/git-hooks/pre-commit-audit.sh +1 -1
- package/scripts/ops/ai-setup.sh +13 -6
- package/scripts/ops/bundle.sh +31 -5
- package/scripts/ops/chezmoi-apply.sh +5 -0
- package/scripts/ops/release.sh +14 -5
- package/scripts/qa/docs-coverage.sh +1 -1
- package/scripts/qa/reliability-audit.sh +2 -2
- package/scripts/secrets/age-init.sh +15 -4
- package/scripts/theme/apply-gnome-theme.sh +6 -2
- package/scripts/theme/extract-theme.py +97 -33
- package/scripts/theme/rebuild-themes.sh +240 -35
- package/scripts/theme/switch.sh +26 -16
- package/scripts/theme/wallpaper-sync.sh +95 -7
- package/scripts/version-sync.sh +81 -44
|
@@ -54,7 +54,7 @@ check_dot_command_docs() {
|
|
|
54
54
|
check_ai_provider_docs() {
|
|
55
55
|
local provider=""
|
|
56
56
|
|
|
57
|
-
for provider in cl copilot agy kiro sgpt ollama opencode aider; do
|
|
57
|
+
for provider in cl copilot kimi agy kiro sgpt ollama opencode aider; do
|
|
58
58
|
record_doc_check "dot $provider in AI.md" "\`dot $provider\`" "$AI_DOC"
|
|
59
59
|
done
|
|
60
60
|
}
|
|
@@ -65,12 +65,12 @@ shell_syntax() {
|
|
|
65
65
|
|
|
66
66
|
unit_tests() {
|
|
67
67
|
cd "$REPO_ROOT"
|
|
68
|
-
./tests/framework/test_runner.sh
|
|
68
|
+
./tests/framework/test_runner.sh --jobs auto
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
integration_tests() {
|
|
72
72
|
cd "$REPO_ROOT"
|
|
73
|
-
RUN_INTEGRATION=1 ./tests/framework/test_runner.sh -i
|
|
73
|
+
RUN_INTEGRATION=1 ./tests/framework/test_runner.sh --jobs auto -i
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
coverage_gate() {
|
|
@@ -39,7 +39,8 @@ if config_path.exists():
|
|
|
39
39
|
|
|
40
40
|
lines = content.splitlines()
|
|
41
41
|
|
|
42
|
-
# Remove existing encryption
|
|
42
|
+
# Remove existing encryption keys and age blocks regardless of where a previous
|
|
43
|
+
# run placed them.
|
|
43
44
|
out = []
|
|
44
45
|
in_age = False
|
|
45
46
|
for line in lines:
|
|
@@ -57,9 +58,19 @@ for line in lines:
|
|
|
57
58
|
continue
|
|
58
59
|
out.append(line)
|
|
59
60
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
# Keep encryption at top level; chezmoi warns when it is nested under [data].
|
|
62
|
+
insert_at = 0
|
|
63
|
+
for i, line in enumerate(out):
|
|
64
|
+
if line.strip().startswith("["):
|
|
65
|
+
insert_at = i
|
|
66
|
+
break
|
|
67
|
+
else:
|
|
68
|
+
insert_at = len(out)
|
|
69
|
+
|
|
70
|
+
top_level = ["encryption = \"age\"", ""]
|
|
71
|
+
out[insert_at:insert_at] = top_level
|
|
72
|
+
|
|
73
|
+
# Append updated age config (use json.dumps for safe string escaping)
|
|
63
74
|
out.append("")
|
|
64
75
|
out.append("[age]")
|
|
65
76
|
out.append(f"identity = {json.dumps(key_file)}")
|
|
@@ -56,7 +56,7 @@ success() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
# Check if we're running GNOME
|
|
59
|
-
if [ "$XDG_CURRENT_DESKTOP" != "GNOME" ] && [ "$XDG_SESSION_DESKTOP" != "gnome" ]; then
|
|
59
|
+
if [ "${XDG_CURRENT_DESKTOP:-}" != "GNOME" ] && [ "${XDG_SESSION_DESKTOP:-}" != "gnome" ]; then
|
|
60
60
|
warn "Not running GNOME desktop. Some settings may not apply."
|
|
61
61
|
fi
|
|
62
62
|
|
|
@@ -208,7 +208,11 @@ apply_wallpaper() {
|
|
|
208
208
|
# Pick a random wallpaper matching the mode from curated collection
|
|
209
209
|
local wp=""
|
|
210
210
|
if [ -d "$wallpaper_dir" ]; then
|
|
211
|
-
|
|
211
|
+
if command -v shuf >/dev/null 2>&1; then
|
|
212
|
+
wp="$(find "$wallpaper_dir" -maxdepth 1 -type f -iname "*-${mode}.jpg" 2>/dev/null | shuf -n 1 || true)"
|
|
213
|
+
else
|
|
214
|
+
wp="$(find "$wallpaper_dir" -maxdepth 1 -type f -iname "*-${mode}.jpg" 2>/dev/null | sort | head -n 1 || true)"
|
|
215
|
+
fi
|
|
212
216
|
fi
|
|
213
217
|
|
|
214
218
|
# Fallback: check Catppuccin system/user wallpapers
|
|
@@ -339,42 +339,77 @@ def _nvim_from_hue(hue: float, is_dark: bool) -> Tuple[str, str]:
|
|
|
339
339
|
|
|
340
340
|
|
|
341
341
|
def _macos_accent_from_hue(hue: float) -> int:
|
|
342
|
-
"""Map
|
|
343
|
-
|
|
342
|
+
"""Map a CIELAB hue angle to a macOS accent-colour integer.
|
|
343
|
+
|
|
344
|
+
Boundaries are calibrated to the CIELAB hues of Apple's own accent
|
|
345
|
+
swatches (not HSL): red≈36°, orange≈67°, yellow≈87°, green≈144°,
|
|
346
|
+
blue≈287°, purple≈317°. The previous HSL-style cutoffs pushed blue
|
|
347
|
+
accents (~283°) into Purple and purple into Pink.
|
|
348
|
+
"""
|
|
349
|
+
hue = hue % 360
|
|
350
|
+
if hue < 15 or hue >= 345:
|
|
351
|
+
return 6 # Pink / magenta
|
|
352
|
+
if hue < 50:
|
|
344
353
|
return 0 # Red
|
|
345
|
-
if hue <
|
|
354
|
+
if hue < 77:
|
|
346
355
|
return 1 # Orange
|
|
347
|
-
if hue <
|
|
356
|
+
if hue < 110:
|
|
348
357
|
return 2 # Yellow
|
|
349
|
-
if hue <
|
|
358
|
+
if hue < 200:
|
|
350
359
|
return 3 # Green
|
|
351
|
-
if hue < 255:
|
|
352
|
-
return 4 # Blue
|
|
353
360
|
if hue < 300:
|
|
354
|
-
return
|
|
355
|
-
return
|
|
361
|
+
return 4 # Blue (incl. teal-blue and navy)
|
|
362
|
+
return 5 # Purple / indigo (300–345)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _env_hex(key: str, fallback: str) -> str:
|
|
366
|
+
"""A #rrggbb value from env, or the fallback when unset/invalid."""
|
|
367
|
+
v = os.environ.get(key, "")
|
|
368
|
+
if len(v) == 7 and v[0] == "#":
|
|
369
|
+
try:
|
|
370
|
+
hex_to_rgb(v)
|
|
371
|
+
return v
|
|
372
|
+
except ValueError:
|
|
373
|
+
pass
|
|
374
|
+
return fallback
|
|
356
375
|
|
|
357
376
|
|
|
358
377
|
def _compute_bg_fg(clusters, is_dark):
|
|
359
|
-
"""
|
|
360
|
-
|
|
378
|
+
"""Terminal background/foreground — fixed, engineered neutrals per mode.
|
|
379
|
+
|
|
380
|
+
Deliberately NOT derived from the wallpaper: a terminal wants a
|
|
381
|
+
high-contrast, low-eye-strain surface that stays stable across themes.
|
|
382
|
+
Dark mode uses a deep neutral grey with off-white text; light mode a warm
|
|
383
|
+
paper cream with charcoal text. The wallpaper still drives the accent and
|
|
384
|
+
the 16 ANSI colours (which are computed against this bg for contrast).
|
|
385
|
+
Override per mode via DOTFILES_TERM_BG_DARK / _FG_DARK / _BG_LIGHT /
|
|
386
|
+
_FG_LIGHT.
|
|
387
|
+
"""
|
|
388
|
+
_ = clusters # bg/fg no longer wallpaper-derived
|
|
361
389
|
if is_dark:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
fg_lab = (88.0, bg_lab[1] * 0.1, bg_lab[2] * 0.1)
|
|
390
|
+
bg_rgb = hex_to_rgb(_env_hex("DOTFILES_TERM_BG_DARK", "#1e1e2e"))
|
|
391
|
+
fg_rgb = hex_to_rgb(_env_hex("DOTFILES_TERM_FG_DARK", "#d4d4d4"))
|
|
365
392
|
else:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
bg_rgb = lab_to_rgb(*bg_lab)
|
|
370
|
-
fg_rgb = ensure_contrast(lab_to_rgb(*fg_lab), bg_rgb, 7.0, is_dark)
|
|
393
|
+
bg_rgb = hex_to_rgb(_env_hex("DOTFILES_TERM_BG_LIGHT", "#fbf1c7"))
|
|
394
|
+
fg_rgb = hex_to_rgb(_env_hex("DOTFILES_TERM_FG_LIGHT", "#3c3836"))
|
|
395
|
+
bg_lab = rgb_to_lab(*bg_rgb)
|
|
371
396
|
return bg_lab, bg_rgb, fg_rgb
|
|
372
397
|
|
|
373
398
|
|
|
374
399
|
def _compute_accent(clusters, is_dark):
|
|
375
|
-
"""Select
|
|
376
|
-
|
|
377
|
-
|
|
400
|
+
"""Select the accent from the wallpaper's DOMINANT chromatic colour.
|
|
401
|
+
|
|
402
|
+
Rank clusters by population x chroma so the accent follows the *main*
|
|
403
|
+
colour of the wallpaper (a large, colourful region) rather than the
|
|
404
|
+
single most-saturated cluster — which is often a tiny vivid splash
|
|
405
|
+
that doesn't represent the image. Near-neutral clusters score ~0 and
|
|
406
|
+
are skipped; if the whole image is neutral we fall back to the most
|
|
407
|
+
saturated cluster so the accent still carries a hue. The selected
|
|
408
|
+
colour is then AAA-darkened below (ADR-009 contrast requirement)."""
|
|
409
|
+
ranked = sorted(clusters, key=lambda c: c[1] * lab_chroma(*c[0]), reverse=True)
|
|
410
|
+
accent_lab = ranked[0][0]
|
|
411
|
+
if lab_chroma(*accent_lab) < 5.0:
|
|
412
|
+
accent_lab = max(clusters, key=lambda c: lab_chroma(*c[0]))[0]
|
|
378
413
|
if is_dark:
|
|
379
414
|
accent_lab = (max(accent_lab[0], 35.0), accent_lab[1], accent_lab[2])
|
|
380
415
|
else:
|
|
@@ -412,16 +447,30 @@ def _compute_panel_border(bg_lab, bg_rgb, is_dark):
|
|
|
412
447
|
|
|
413
448
|
def _build_ansi_color(base_lab, accent_lab, bg_rgb, is_dark):
|
|
414
449
|
"""Build normal + bright ANSI variant from a base Lab color."""
|
|
450
|
+
normal_L = (
|
|
451
|
+
max(55.0, min(75.0, base_lab[0])) if is_dark
|
|
452
|
+
else max(30.0, min(50.0, base_lab[0]))
|
|
453
|
+
)
|
|
454
|
+
normal = adjust_lightness(base_lab, normal_L)
|
|
415
455
|
if is_dark:
|
|
416
|
-
|
|
417
|
-
|
|
456
|
+
# Dark bg: the bright variant pops by getting lighter.
|
|
457
|
+
bright = adjust_lightness(base_lab, normal_L + 12)
|
|
458
|
+
bright_min = 4.5
|
|
418
459
|
else:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
460
|
+
# Light bg: a lighter bright would wash out against near-white, so
|
|
461
|
+
# brighten by vividness at equal lightness instead — the bright is
|
|
462
|
+
# never darker than the normal (matches Apple's light ANSI ramp,
|
|
463
|
+
# where brights read as more saturated, not muddier).
|
|
464
|
+
bright = (normal[0], normal[1] * 1.25, normal[2] * 1.25)
|
|
465
|
+
bright_min = 7.0
|
|
466
|
+
# WCAG AAA (7:1) for the chromatic slots on a light bg so coloured text
|
|
467
|
+
# (paths, syntax) is unambiguously legible — AA (4.5:1) still read as washed
|
|
468
|
+
# out on cream. Brights stay AAA too and differ from normals by saturation
|
|
469
|
+
# (Apple's light ramp), not lightness. Dark mode keeps its lower floor
|
|
470
|
+
# (light text on dark reads comfortably at a lower ratio).
|
|
471
|
+
normal_min = 3.0 if is_dark else 7.0
|
|
472
|
+
normal_rgb = ensure_contrast(lab_to_rgb(*normal), bg_rgb, normal_min, is_dark)
|
|
473
|
+
bright_rgb = ensure_contrast(lab_to_rgb(*bright), bg_rgb, bright_min, is_dark)
|
|
425
474
|
return normal_rgb, bright_rgb
|
|
426
475
|
|
|
427
476
|
|
|
@@ -455,11 +504,18 @@ def _structural_colors(bg_lab, bg_rgb, is_dark):
|
|
|
455
504
|
ensure_contrast(lab_to_rgb(bg_lab[0] + 25, bg_lab[1], bg_lab[2]), bg_rgb, 2.5, True),
|
|
456
505
|
ensure_contrast(lab_to_rgb(90.0, bg_lab[1] * 0.05, bg_lab[2] * 0.05), bg_rgb, 7.0, True),
|
|
457
506
|
)
|
|
507
|
+
# Light bg: the neutral ramp increases in lightness (c0 < c8 < c7 < c15).
|
|
508
|
+
# c0/c8/c7 stay readable (>= AA); c15 ("bright white") is the LIGHTEST tone,
|
|
509
|
+
# strictly lighter than c7 — bright-white text legibility is gated by fg, not
|
|
510
|
+
# c15, and forcing c15 to the same floor as c7 made them converge (c15 ended
|
|
511
|
+
# a hair darker, breaking the ramp). Start c15 well above c7 with only a mild
|
|
512
|
+
# floor so it stays the lightest. (Terminals render the dark palette; this
|
|
513
|
+
# ramp only feeds non-terminal light-palette consumers.)
|
|
458
514
|
return (
|
|
459
515
|
ensure_contrast(lab_to_rgb(18.0, bg_lab[1] * 0.2, bg_lab[2] * 0.2), bg_rgb, 7.0, False),
|
|
460
|
-
ensure_contrast(lab_to_rgb(
|
|
516
|
+
ensure_contrast(lab_to_rgb(50.0, bg_lab[1] * 0.15, bg_lab[2] * 0.15), bg_rgb, 4.5, False),
|
|
461
517
|
ensure_contrast(lab_to_rgb(35.0, bg_lab[1] * 0.2, bg_lab[2] * 0.2), bg_rgb, 4.5, False),
|
|
462
|
-
ensure_contrast(lab_to_rgb(
|
|
518
|
+
ensure_contrast(lab_to_rgb(64.0, bg_lab[1] * 0.08, bg_lab[2] * 0.08), bg_rgb, 2.5, False),
|
|
463
519
|
)
|
|
464
520
|
|
|
465
521
|
|
|
@@ -664,7 +720,15 @@ def main():
|
|
|
664
720
|
# Generate theme
|
|
665
721
|
theme = generate_theme(clusters, name, is_dark)
|
|
666
722
|
wp_base = args.image.split("[")[0] if "[" in args.image else args.image
|
|
667
|
-
|
|
723
|
+
wp_abs = os.path.abspath(wp_base)
|
|
724
|
+
# Store home-relative with a `~` so the committed themes.toml is not tied to
|
|
725
|
+
# one machine's username. Consumers (wallpaper-sync.sh) expand `~/`. System
|
|
726
|
+
# wallpapers (/System/..., /usr/share/...) stay absolute — identical on
|
|
727
|
+
# every host anyway.
|
|
728
|
+
home = os.path.expanduser("~")
|
|
729
|
+
if wp_abs == home or wp_abs.startswith(home + os.sep):
|
|
730
|
+
wp_abs = "~" + wp_abs[len(home):]
|
|
731
|
+
theme["wallpaper"] = wp_abs
|
|
668
732
|
theme["source"] = args.source
|
|
669
733
|
|
|
670
734
|
# Output
|
|
@@ -124,51 +124,99 @@ discover_linux_system() {
|
|
|
124
124
|
/usr/share/wallpapers
|
|
125
125
|
)
|
|
126
126
|
|
|
127
|
-
local dir file name
|
|
127
|
+
local dir file name variant
|
|
128
128
|
for dir in "${dirs[@]}"; do
|
|
129
129
|
[[ -d "$dir" ]] || continue
|
|
130
130
|
while IFS= read -r file; do
|
|
131
131
|
name="$(basename "$file")"
|
|
132
132
|
name="${name%.*}"
|
|
133
|
-
name="$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr ' ' '
|
|
133
|
+
name="$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr ' _' '--')"
|
|
134
134
|
name="${name//[^a-z0-9-]/}"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
while [[ "$name" == *--* ]]; do name="${name//--/-}"; done
|
|
136
|
+
name="${name#-}"
|
|
137
|
+
name="${name%-}"
|
|
138
|
+
[[ -n "$name" ]] || continue
|
|
139
|
+
# Static system images carry no mode; theme them in both modes so
|
|
140
|
+
# they show as a pair (unless the file is already a -dark/-light).
|
|
141
|
+
if [[ "$name" == *-dark || "$name" == *-light ]]; then
|
|
142
|
+
[[ -z "${WALLPAPERS[$name]+x}" ]] && {
|
|
143
|
+
WALLPAPERS["$name"]="$file"
|
|
144
|
+
WP_SOURCE["$name"]="system"
|
|
145
|
+
}
|
|
146
|
+
continue
|
|
139
147
|
fi
|
|
140
|
-
|
|
148
|
+
local key
|
|
149
|
+
for variant in dark light; do
|
|
150
|
+
key="${name}-${variant}"
|
|
151
|
+
if [[ -z "${WALLPAPERS[$key]+x}" ]]; then
|
|
152
|
+
WALLPAPERS["$key"]="$file"
|
|
153
|
+
WP_SOURCE["$key"]="system"
|
|
154
|
+
fi
|
|
155
|
+
done
|
|
156
|
+
done < <(find "$dir" -maxdepth 3 -type f \
|
|
157
|
+
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \
|
|
158
|
+
-o -iname '*.heic' -o -iname '*.webp' -o -iname '*.tiff' \) 2>/dev/null | sort)
|
|
141
159
|
done
|
|
142
160
|
}
|
|
143
161
|
|
|
144
162
|
discover_custom() {
|
|
145
163
|
[[ -d "$CUSTOM_DIR" ]] || return 0
|
|
146
164
|
|
|
147
|
-
local file name frame_count
|
|
148
|
-
|
|
165
|
+
local file base name ext frame_count
|
|
166
|
+
# Case-insensitive, wide extension match. The old fixed-glob loop
|
|
167
|
+
# (`*.heic *.jpg *.png`) silently dropped `.jpeg`, `.webp`, `.tiff`
|
|
168
|
+
# and every uppercase variant (`.JPG`, `.HEIC`, …), so those
|
|
169
|
+
# wallpapers never made it into the theme table.
|
|
170
|
+
while IFS= read -r file; do
|
|
149
171
|
[[ -f "$file" ]] || continue
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
base="$(basename "$file")"
|
|
173
|
+
base="${base%.*}"
|
|
174
|
+
ext="$(echo "${file##*.}" | tr '[:upper:]' '[:lower:]')"
|
|
175
|
+
|
|
176
|
+
# Normalize to the [a-z0-9-] namespace that themes.toml and the
|
|
177
|
+
# `dot theme list` picker regex both require — a raw name with
|
|
178
|
+
# spaces/uppercase would generate a section the picker can't match.
|
|
179
|
+
name="$(echo "$base" | tr '[:upper:]' '[:lower:]' | tr ' _' '--')"
|
|
180
|
+
name="${name//[^a-z0-9-]/}"
|
|
181
|
+
while [[ "$name" == *--* ]]; do name="${name//--/-}"; done
|
|
182
|
+
name="${name#-}"
|
|
183
|
+
name="${name%-}"
|
|
184
|
+
[[ -n "$name" ]] || continue
|
|
152
185
|
|
|
153
|
-
#
|
|
154
|
-
if [[ "$
|
|
186
|
+
# Dynamic HEIC (multi-frame = light+dark packed in one file).
|
|
187
|
+
if [[ "$ext" == "heic" ]]; then
|
|
155
188
|
frame_count="$(magick identify "$file" 2>/dev/null | wc -l | tr -d ' ')"
|
|
156
189
|
if [[ "$frame_count" -ge 2 && "$name" != *-dark && "$name" != *-light ]]; then
|
|
157
|
-
# Dynamic HEIC: register as both dark and light
|
|
158
190
|
WALLPAPERS["${name}-light"]="${file}[0]"
|
|
159
191
|
WP_SOURCE["${name}-light"]="custom"
|
|
160
192
|
WALLPAPERS["${name}-dark"]="${file}[1]"
|
|
161
193
|
WP_SOURCE["${name}-dark"]="custom"
|
|
162
|
-
# Store the original file path for wallpaper-sync
|
|
194
|
+
# Store the original file path for wallpaper-sync.
|
|
163
195
|
WALLPAPERS["${name}"]="$file"
|
|
164
196
|
WP_SOURCE["${name}"]="custom-dynamic"
|
|
165
197
|
continue
|
|
166
198
|
fi
|
|
167
199
|
fi
|
|
168
200
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
201
|
+
# Explicitly-named single-mode variant (foo-dark.jpg / foo-light.jpg):
|
|
202
|
+
# honour the author's mode, register the one variant as-is.
|
|
203
|
+
if [[ "$name" == *-dark || "$name" == *-light ]]; then
|
|
204
|
+
WALLPAPERS["$name"]="$file"
|
|
205
|
+
WP_SOURCE["$name"]="custom"
|
|
206
|
+
continue
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# Static single image: theme it in BOTH modes (extract-theme forces
|
|
210
|
+
# the mode from the -dark/-light suffix) so it appears as a light/dark
|
|
211
|
+
# pair in `dot theme list`. Previously a static produced only one
|
|
212
|
+
# variant and paired_families() hid it entirely.
|
|
213
|
+
WALLPAPERS["${name}-dark"]="$file"
|
|
214
|
+
WP_SOURCE["${name}-dark"]="custom"
|
|
215
|
+
WALLPAPERS["${name}-light"]="$file"
|
|
216
|
+
WP_SOURCE["${name}-light"]="custom"
|
|
217
|
+
done < <(find "$CUSTOM_DIR" -maxdepth 1 -type f \
|
|
218
|
+
\( -iname '*.heic' -o -iname '*.jpg' -o -iname '*.jpeg' \
|
|
219
|
+
-o -iname '*.png' -o -iname '*.webp' -o -iname '*.tiff' \) 2>/dev/null | sort)
|
|
172
220
|
}
|
|
173
221
|
|
|
174
222
|
# Remove dynamic base entries (keep only the -dark/-light variants for theme gen)
|
|
@@ -181,11 +229,15 @@ cleanup_dynamic_entries() {
|
|
|
181
229
|
done
|
|
182
230
|
}
|
|
183
231
|
|
|
184
|
-
# Discover in order: system first, custom overrides
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
232
|
+
# Discover in order: system first, custom overrides. System (OS-shipped)
|
|
233
|
+
# wallpapers are opt-in — most users only want themes from their own
|
|
234
|
+
# wallpapers. Enable the ~100 built-in ones with DOTFILES_THEME_SYSTEM=1.
|
|
235
|
+
if [[ "${DOTFILES_THEME_SYSTEM:-0}" == "1" ]]; then
|
|
236
|
+
case "$(uname -s)" in
|
|
237
|
+
Darwin) discover_macos_system ;;
|
|
238
|
+
Linux) discover_linux_system ;;
|
|
239
|
+
esac
|
|
240
|
+
fi
|
|
189
241
|
discover_custom
|
|
190
242
|
cleanup_dynamic_entries
|
|
191
243
|
|
|
@@ -238,16 +290,29 @@ done
|
|
|
238
290
|
# Generate themes
|
|
239
291
|
# ---------------------------------------------------------------------------
|
|
240
292
|
|
|
241
|
-
|
|
242
|
-
|
|
293
|
+
# Report distinct WALLPAPERS (families), not theme sections. Every
|
|
294
|
+
# wallpaper — a dynamic HEIC (light+dark frames) or a static image —
|
|
295
|
+
# yields a `-light` and a `-dark` theme, so a raw section count is ~2x
|
|
296
|
+
# the file count and reads as wrong ("74 files → 148 custom"). Collapse
|
|
297
|
+
# -dark/-light back to the family so the numbers match what's on disk
|
|
298
|
+
# and what `dot theme list` shows, and report the theme total separately.
|
|
299
|
+
# Explicit empty init: a `declare -A x` that never gets a key still trips
|
|
300
|
+
# `set -u` on ${#x[@]} (even in bash 5) — happens now that system wallpapers
|
|
301
|
+
# are opt-in and sys_fam can stay empty.
|
|
302
|
+
declare -A sys_fam=() cust_fam=()
|
|
243
303
|
for name in "${!WP_SOURCE[@]}"; do
|
|
304
|
+
family="${name%-dark}"
|
|
305
|
+
family="${family%-light}"
|
|
244
306
|
case "${WP_SOURCE[$name]}" in
|
|
245
|
-
system)
|
|
246
|
-
custom)
|
|
307
|
+
system) sys_fam["$family"]=1 ;;
|
|
308
|
+
custom) cust_fam["$family"]=1 ;;
|
|
247
309
|
esac
|
|
248
310
|
done
|
|
311
|
+
sys_count=${#sys_fam[@]}
|
|
312
|
+
cust_count=${#cust_fam[@]}
|
|
249
313
|
echo "Discovering wallpapers..."
|
|
250
|
-
echo " Found: $sys_count system, $cust_count custom
|
|
314
|
+
echo " Found: $sys_count system, $cust_count custom wallpapers" \
|
|
315
|
+
"($((sys_count + cust_count)) total → ${#WALLPAPERS[@]} light/dark themes)"
|
|
251
316
|
echo ""
|
|
252
317
|
|
|
253
318
|
GENERATED=0
|
|
@@ -297,11 +362,20 @@ if [[ $TOTAL_WORK -gt 0 ]]; then
|
|
|
297
362
|
wait
|
|
298
363
|
fi
|
|
299
364
|
|
|
300
|
-
# Count results
|
|
301
|
-
|
|
302
|
-
FAILED
|
|
365
|
+
# Count results — how many of THIS run's work items produced a cache file.
|
|
366
|
+
# (The old `find -newer "$0"` counted every cache file newer than the script,
|
|
367
|
+
# so when everything was cached FAILED went negative.)
|
|
368
|
+
GENERATED=0
|
|
369
|
+
FAILED=0
|
|
370
|
+
for name in "${WORK[@]}"; do
|
|
371
|
+
if [[ -f "$CACHE_DIR/${name}.toml" ]]; then
|
|
372
|
+
GENERATED=$((GENERATED + 1))
|
|
373
|
+
else
|
|
374
|
+
FAILED=$((FAILED + 1))
|
|
375
|
+
fi
|
|
376
|
+
done
|
|
303
377
|
echo ""
|
|
304
|
-
echo "Results: $
|
|
378
|
+
echo "Results: $GENERATED generated, $CACHED cached, $FAILED failed"
|
|
305
379
|
|
|
306
380
|
# ---------------------------------------------------------------------------
|
|
307
381
|
# Assemble themes.toml
|
|
@@ -326,14 +400,145 @@ echo "Assembling themes.toml..."
|
|
|
326
400
|
|
|
327
401
|
HEADER
|
|
328
402
|
|
|
329
|
-
|
|
403
|
+
# Assemble ONLY the wallpapers discovered this run, not every file left in
|
|
404
|
+
# the cache. This drops themes for wallpapers no longer present (e.g. system
|
|
405
|
+
# wallpapers once DOTFILES_THEME_SYSTEM is turned off) instead of letting
|
|
406
|
+
# stale cache entries pile up in themes.toml.
|
|
407
|
+
for name in $(printf '%s\n' "${!WALLPAPERS[@]}" | sort); do
|
|
408
|
+
cache_file="$CACHE_DIR/${name}.toml"
|
|
330
409
|
[[ -f "$cache_file" ]] || continue
|
|
331
410
|
echo ""
|
|
332
411
|
cat "$cache_file"
|
|
333
412
|
done
|
|
413
|
+
|
|
414
|
+
# Synthetic fallback themes — always emitted, never wallpaper-derived.
|
|
415
|
+
# Every theme template degrades to `fallback-dark` when `.theme` is unset or
|
|
416
|
+
# names a theme absent from this file. Because these are re-emitted on every
|
|
417
|
+
# rebuild, a regeneration that drops any wallpaper theme can never break the
|
|
418
|
+
# fallback (unlike hardcoding a wallpaper theme like the old big-sur-dark).
|
|
419
|
+
# switch.sh hides the `fallback` family from user-facing theme lists.
|
|
420
|
+
cat <<'FALLBACK'
|
|
421
|
+
|
|
422
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
423
|
+
# Synthetic fallback themes (NOT wallpaper-derived, NEVER user-selectable).
|
|
424
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
[themes.fallback-dark]
|
|
427
|
+
mode = "dark"
|
|
428
|
+
family = "fallback"
|
|
429
|
+
macos_accent = 4
|
|
430
|
+
wallpaper = ""
|
|
431
|
+
source = "custom"
|
|
432
|
+
|
|
433
|
+
[themes.fallback-dark.term]
|
|
434
|
+
bg = "#1e1e2e"
|
|
435
|
+
fg = "#d4d4d4"
|
|
436
|
+
cursor = "#455c67"
|
|
437
|
+
cursor_text = "#1e1e2e"
|
|
438
|
+
sel_bg = "#394145"
|
|
439
|
+
sel_fg = "#d4d4d4"
|
|
440
|
+
c0 = "#3c3c4d"
|
|
441
|
+
c1 = "#997d7a"
|
|
442
|
+
c2 = "#479174"
|
|
443
|
+
c3 = "#8a836f"
|
|
444
|
+
c4 = "#5f86b7"
|
|
445
|
+
c5 = "#917e8e"
|
|
446
|
+
c6 = "#5c8d82"
|
|
447
|
+
c7 = "#b9b8bb"
|
|
448
|
+
c8 = "#605f72"
|
|
449
|
+
c9 = "#bf9b97"
|
|
450
|
+
c10 = "#67b293"
|
|
451
|
+
c11 = "#aba389"
|
|
452
|
+
c12 = "#7fa6d9"
|
|
453
|
+
c13 = "#b49cb0"
|
|
454
|
+
c14 = "#7bada1"
|
|
455
|
+
c15 = "#e2e2e3"
|
|
456
|
+
|
|
457
|
+
[themes.fallback-dark.ui]
|
|
458
|
+
accent = "#455c67"
|
|
459
|
+
accent_text = "#ffffff"
|
|
460
|
+
error = "#997d7a"
|
|
461
|
+
warning = "#8a836f"
|
|
462
|
+
success = "#479174"
|
|
463
|
+
info = "#5f86b7"
|
|
464
|
+
panel = "#242435"
|
|
465
|
+
border = "#302f38"
|
|
466
|
+
|
|
467
|
+
[themes.fallback-dark.app]
|
|
468
|
+
nvim = "tokyonight"
|
|
469
|
+
nvim_style = "night"
|
|
470
|
+
lualine = "tokyonight"
|
|
471
|
+
gtk_theme = "Adwaita-dark"
|
|
472
|
+
gtk_icon = "Papirus-Dark"
|
|
473
|
+
gnome_shell = ""
|
|
474
|
+
gnome_gtk = "Adwaita-dark"
|
|
475
|
+
vscode = "Tokyonight Mocha"
|
|
476
|
+
vscode_dark = "Tokyonight Mocha"
|
|
477
|
+
vscode_light = "Tokyonight Latte"
|
|
478
|
+
cat_wallpaper = ""
|
|
479
|
+
starship_palette = "catppuccin_mocha"
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
[themes.fallback-light]
|
|
483
|
+
mode = "light"
|
|
484
|
+
family = "fallback"
|
|
485
|
+
macos_accent = 4
|
|
486
|
+
wallpaper = ""
|
|
487
|
+
source = "custom"
|
|
488
|
+
|
|
489
|
+
[themes.fallback-light.term]
|
|
490
|
+
bg = "#fbf1c7"
|
|
491
|
+
fg = "#3c3836"
|
|
492
|
+
cursor = "#2c5989"
|
|
493
|
+
cursor_text = "#fbf1c7"
|
|
494
|
+
sel_bg = "#c6cfe0"
|
|
495
|
+
sel_fg = "#3c3836"
|
|
496
|
+
c0 = "#2e2c26"
|
|
497
|
+
c1 = "#7d3d39"
|
|
498
|
+
c2 = "#205a48"
|
|
499
|
+
c3 = "#5b501d"
|
|
500
|
+
c4 = "#2b527d"
|
|
501
|
+
c5 = "#6e4069"
|
|
502
|
+
c6 = "#25554c"
|
|
503
|
+
c7 = "#6f6d67"
|
|
504
|
+
c8 = "#54524b"
|
|
505
|
+
c9 = "#853733"
|
|
506
|
+
c10 = "#025c46"
|
|
507
|
+
c11 = "#5b500b"
|
|
508
|
+
c12 = "#0d5388"
|
|
509
|
+
c13 = "#743b6f"
|
|
510
|
+
c14 = "#15564b"
|
|
511
|
+
c15 = "#979693"
|
|
512
|
+
|
|
513
|
+
[themes.fallback-light.ui]
|
|
514
|
+
accent = "#2c5989"
|
|
515
|
+
accent_text = "#ffffff"
|
|
516
|
+
error = "#7d3d39"
|
|
517
|
+
warning = "#5b501d"
|
|
518
|
+
success = "#205a48"
|
|
519
|
+
info = "#2b527d"
|
|
520
|
+
panel = "#f2e8bf"
|
|
521
|
+
border = "#e3e0d3"
|
|
522
|
+
|
|
523
|
+
[themes.fallback-light.app]
|
|
524
|
+
nvim = "catppuccin"
|
|
525
|
+
nvim_style = "latte"
|
|
526
|
+
lualine = "catppuccin"
|
|
527
|
+
gtk_theme = "Adwaita"
|
|
528
|
+
gtk_icon = "Papirus-Light"
|
|
529
|
+
gnome_shell = ""
|
|
530
|
+
gnome_gtk = "Adwaita"
|
|
531
|
+
vscode = "Catppuccin Latte"
|
|
532
|
+
vscode_dark = "Catppuccin Mocha"
|
|
533
|
+
vscode_light = "Catppuccin Latte"
|
|
534
|
+
cat_wallpaper = ""
|
|
535
|
+
starship_palette = "catppuccin_latte"
|
|
536
|
+
FALLBACK
|
|
334
537
|
} >"$THEMES_FILE"
|
|
335
538
|
|
|
336
|
-
|
|
337
|
-
|
|
539
|
+
# Count top-level [themes.NAME] blocks only — not the .term/.ui/.app
|
|
540
|
+
# subsections (which inflated the tally ~4x).
|
|
541
|
+
theme_count=$(grep -cE '^\[themes\.[a-z0-9-]+\]$' "$THEMES_FILE")
|
|
542
|
+
echo " Written: $THEMES_FILE ($theme_count themes)"
|
|
338
543
|
echo ""
|
|
339
544
|
echo "Done. Run 'dot theme list' to see available themes."
|