@sebastienrousseau/dotfiles 0.2.519 → 0.2.520
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 +182 -0
- package/README.md +1163 -163
- package/docs/ARCHITECTURE.md +117 -0
- package/docs/COPYRIGHT +1 -1
- package/docs/ECOSYSTEM.md +220 -0
- package/docs/GOLD-STANDARD-AUDIT.md +352 -0
- package/docs/GOVERNANCE.md +1 -1
- package/docs/MAINTAINERS.md +1 -1
- package/docs/MINIMUM-TOOLCHAIN.md +100 -0
- package/docs/README.md +1 -1
- package/docs/STRUCTURE.md +1 -1
- package/docs/architecture/ARCHITECTURE.md +11 -108
- package/docs/architecture/REPO_LAYOUT.md +3 -3
- package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +121 -0
- package/docs/index.md +3 -3
- package/docs/manual/00-introduction.md +1 -1
- package/docs/manual/03-reference/01-dot-cli.md +28 -3
- package/docs/manual/command-index.md +24 -4
- package/docs/manual/concept-index.md +2 -0
- package/docs/migration/README.md +81 -0
- package/docs/migration/from-bare-git-repo.md +156 -0
- package/docs/migration/from-gnu-stow.md +165 -0
- package/docs/migration/from-plain-chezmoi.md +148 -0
- package/docs/migration/from-yadm.md +187 -0
- package/docs/operations/PERFORMANCE_BUDGETS.md +196 -0
- package/docs/operations/REGISTRY.md +1 -1
- package/docs/operations/RELEASE_PIPELINE.md +4 -4
- package/docs/operations/TESTING.md +3 -3
- package/docs/operations/TRACEABILITY.md +1 -0
- package/docs/packaging.md +222 -0
- package/docs/reference/FEATURE-MATRIX.md +646 -0
- package/docs/reference/TOOLS.md +1 -1
- package/docs/reference/UTILS.md +1 -0
- package/docs/security/COMPLIANCE.md +1 -1
- package/docs/security/DISCLOSURE.md +4 -4
- package/docs/security/FUZZING.md +113 -18
- package/docs/security/KEY_ROTATION.md +1 -1
- package/docs/security/SCORECARD.md +3 -3
- package/docs/security/VERIFY_RELEASE.md +2 -2
- package/install.sh +6 -6
- package/package.json +2 -2
- package/scripts/ci/check-copyright-headers.sh +1 -1
- package/scripts/ci/check-shell-preamble.sh +1 -1
- package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
- package/scripts/demo/record.sh +1 -1
- package/scripts/diagnostics/a2a-conformance.sh +1 -1
- package/scripts/diagnostics/alias-governance.sh +30 -3
- package/scripts/diagnostics/aliases-cheatsheet.sh +1 -1
- package/scripts/diagnostics/aliases-manifest.sh +1 -1
- package/scripts/diagnostics/attest-verify.sh +147 -0
- package/scripts/diagnostics/benchmark.sh +1 -1
- package/scripts/diagnostics/conflicts.sh +1 -1
- package/scripts/diagnostics/doctor-unified.sh +6 -2
- package/scripts/diagnostics/doctor.sh +56 -10
- package/scripts/diagnostics/drift-dashboard.sh +3 -2
- package/scripts/diagnostics/health.sh +43 -10
- package/scripts/diagnostics/history-analysis.sh +1 -1
- package/scripts/diagnostics/mcp-doctor.sh +2 -2
- package/scripts/diagnostics/perf.sh +1 -1
- package/scripts/diagnostics/scorecard.sh +3 -2
- package/scripts/diagnostics/secret-governance.sh +1 -1
- package/scripts/diagnostics/security-score.sh +1 -1
- package/scripts/diagnostics/smoke-test.sh +1 -1
- package/scripts/diagnostics/snapshot.sh +1 -1
- package/scripts/diagnostics/verify.sh +1 -1
- package/scripts/diagnostics/verify_state.sh +1 -1
- package/scripts/diagnostics/version-locks.sh +1 -1
- package/scripts/diagnostics/workstation-attestation.sh +26 -1
- package/scripts/dot/commands/agent.sh +64 -14
- package/scripts/dot/commands/agents.sh +24 -8
- package/scripts/dot/commands/ai.sh +20 -7
- package/scripts/dot/commands/aliases.sh +1 -1
- package/scripts/dot/commands/appearance.sh +1 -1
- package/scripts/dot/commands/completion.sh +42 -5
- package/scripts/dot/commands/core.sh +1 -1
- package/scripts/dot/commands/diagnostics.sh +1 -1
- package/scripts/dot/commands/env-emit.sh +1 -1
- package/scripts/dot/commands/fleet.sh +41 -18
- package/scripts/dot/commands/init.sh +1 -1
- package/scripts/dot/commands/lint.sh +1 -1
- package/scripts/dot/commands/manual.sh +1 -1
- package/scripts/dot/commands/meta.sh +115 -10
- package/scripts/dot/commands/patterns.sh +1 -1
- package/scripts/dot/commands/registry.sh +43 -7
- package/scripts/dot/commands/restore.sh +1 -1
- package/scripts/dot/commands/secrets.sh +1 -1
- package/scripts/dot/commands/security.sh +1 -1
- package/scripts/dot/commands/tools.sh +21 -7
- package/scripts/fonts/install-nerd-fonts.sh +1 -1
- package/scripts/fonts/patch-fonts.sh +1 -1
- package/scripts/git-hooks/install.sh +1 -1
- package/scripts/git-hooks/pre-commit-audit.sh +2 -2
- package/scripts/lib/secrets_provider.sh +19 -4
- package/scripts/nvim/headless-upgrade.lua +81 -0
- package/scripts/ops/ai-setup.sh +1 -1
- package/scripts/ops/bundle.sh +1 -1
- package/scripts/ops/chaos.sh +1 -1
- package/scripts/ops/chezmoi-apply.sh +1 -1
- package/scripts/ops/chezmoi-diff.sh +1 -1
- package/scripts/ops/chezmoi-remove.sh +3 -3
- package/scripts/ops/chezmoi-update.sh +6 -2
- package/scripts/ops/heal-chezmoi.sh +1 -1
- package/scripts/ops/heal-system.sh +1 -1
- package/scripts/ops/heal-tools.sh +1 -1
- package/scripts/ops/heal.sh +1 -1
- package/scripts/ops/post-apply-repair.sh +1 -1
- package/scripts/ops/prewarm.sh +1 -1
- package/scripts/ops/release.sh +1 -1
- package/scripts/ops/rollback.sh +11 -2
- package/scripts/ops/setup.sh +1 -1
- package/scripts/ops/teleport.sh +1 -1
- package/scripts/qa/check-feature-matrix.sh +296 -0
- package/scripts/qa/check-version-consistency.sh +7 -119
- package/scripts/qa/coverage-baseline.sh +1 -1
- package/scripts/qa/docs-coverage.sh +9 -3
- package/scripts/qa/examples-coverage.sh +1 -1
- package/scripts/qa/reliability-audit.sh +1 -1
- package/scripts/qa/scorecard-snapshot.sh +1 -1
- package/scripts/qa/traceability-coverage.sh +9 -2
- package/scripts/qa/validate-examples.sh +66 -3
- package/scripts/qa/wsl-contract.sh +1 -1
- package/scripts/secrets/age-init.sh +1 -1
- package/scripts/secrets/create-secrets-file.sh +1 -1
- package/scripts/secrets/encrypt-ssh-key.sh +1 -1
- package/scripts/security/backup.sh +1 -1
- package/scripts/security/check-disclosure-key-expiry.sh +1 -1
- package/scripts/security/dns-doh.sh +1 -1
- package/scripts/security/encryption-check.sh +1 -1
- package/scripts/security/enforce-policies.sh +315 -98
- package/scripts/security/firewall.sh +1 -1
- package/scripts/security/lock-configs.sh +1 -1
- package/scripts/security/lock-screen.sh +1 -1
- package/scripts/security/manage-secrets.sh +1 -1
- package/scripts/security/ssh-cert.sh +1 -1
- package/scripts/security/telemetry-kill.sh +1 -1
- package/scripts/security/usb-safety.sh +1 -1
- package/scripts/theme/apply-gnome-theme.sh +1 -1
- package/scripts/theme/extract-heic-frames.sh +1 -1
- package/scripts/theme/extract-theme.py +284 -6
- package/scripts/theme/install-boot-logo.sh +1 -1
- package/scripts/theme/install-catppuccin-themes.sh +1 -1
- package/scripts/theme/install-cursors.sh +1 -1
- package/scripts/theme/install-file-icons.sh +1 -1
- package/scripts/theme/install-grub-theme.sh +1 -1
- package/scripts/theme/install-lock-icon.sh +1 -1
- package/scripts/theme/merge-wallpaper.sh +1 -1
- package/scripts/theme/rebuild-themes.sh +64 -5
- package/scripts/theme/switch.sh +46 -19
- package/scripts/theme/wallpaper-rotate.sh +1 -1
- package/scripts/theme/wallpaper-sync.sh +1 -1
- package/scripts/tools/cmatrix.sh +1 -1
- package/scripts/tools/detect-collisions.py +1 -1
- package/scripts/tools/emoji-picker.sh +1 -1
- package/scripts/tools/figlet-banner.sh +1 -1
- package/scripts/tools/log-rotate.sh +1 -1
- package/scripts/tools/lolcat-wrap.sh +1 -1
- package/scripts/tools/pipes.sh +1 -1
- package/scripts/tuning/linux.sh +1 -1
- package/scripts/tuning/macos.sh +1 -1
- package/scripts/uninstall.sh +1 -1
- package/scripts/verify-release-versions +156 -0
- package/scripts/version-sync.sh +63 -3
- package/LICENSE +0 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# extract-theme.py — Generate a terminal theme from wallpaper dominant colors.
|
|
5
5
|
#
|
|
@@ -329,6 +329,28 @@ def ensure_contrast(
|
|
|
329
329
|
return lab_to_rgb(fl, fa, fb)
|
|
330
330
|
|
|
331
331
|
|
|
332
|
+
# VS Code theming — Catppuccin only.
|
|
333
|
+
#
|
|
334
|
+
# The label must match `contributes.themes[].label` in the extension's
|
|
335
|
+
# package.json exactly, and the icon id must match `contributes.iconThemes[].id`.
|
|
336
|
+
# VS Code cannot resolve an unknown name: it keeps whatever is active and logs
|
|
337
|
+
# an error the user never sees, which is how three wrong values here survived.
|
|
338
|
+
#
|
|
339
|
+
# Verified against catppuccin.catppuccin-vsc 3.19.0 and
|
|
340
|
+
# catppuccin.catppuccin-vsc-icons 1.26.0:
|
|
341
|
+
# themes Catppuccin Mocha | Macchiato | Frappe | Latte
|
|
342
|
+
# iconThemes catppuccin-mocha | -macchiato | -frappe | -latte
|
|
343
|
+
# productIconThemes (none — the extension registers none at all)
|
|
344
|
+
#
|
|
345
|
+
# Neovim still picks per-family (tokyonight/everforest/catppuccin) from the
|
|
346
|
+
# wallpaper hue; only the VS Code surface is pinned, because a single editor
|
|
347
|
+
# theme that is always installed beats three that mostly are not.
|
|
348
|
+
VSCODE_DARK = "Catppuccin Mocha"
|
|
349
|
+
VSCODE_LIGHT = "Catppuccin Latte"
|
|
350
|
+
VSCODE_ICONS_DARK = "catppuccin-mocha"
|
|
351
|
+
VSCODE_ICONS_LIGHT = "catppuccin-latte"
|
|
352
|
+
|
|
353
|
+
|
|
332
354
|
def _nvim_from_hue(hue: float, is_dark: bool) -> Tuple[str, str]:
|
|
333
355
|
"""Map accent hue angle to nearest Neovim colorscheme."""
|
|
334
356
|
if 60 <= hue < 150:
|
|
@@ -385,13 +407,41 @@ def _compute_bg_fg(clusters, is_dark):
|
|
|
385
407
|
Override per mode via DOTFILES_TERM_BG_DARK / _FG_DARK / _BG_LIGHT /
|
|
386
408
|
_FG_LIGHT.
|
|
387
409
|
"""
|
|
388
|
-
_ = clusters # bg/fg no longer wallpaper-derived
|
|
389
410
|
if is_dark:
|
|
390
411
|
bg_rgb = hex_to_rgb(_env_hex("DOTFILES_TERM_BG_DARK", "#1e1e2e"))
|
|
391
412
|
fg_rgb = hex_to_rgb(_env_hex("DOTFILES_TERM_FG_DARK", "#d4d4d4"))
|
|
392
413
|
else:
|
|
393
414
|
bg_rgb = hex_to_rgb(_env_hex("DOTFILES_TERM_BG_LIGHT", "#fbf1c7"))
|
|
394
415
|
fg_rgb = hex_to_rgb(_env_hex("DOTFILES_TERM_FG_LIGHT", "#3c3836"))
|
|
416
|
+
|
|
417
|
+
# Tint the engineered neutral toward the wallpaper's dominant hue.
|
|
418
|
+
#
|
|
419
|
+
# The lightness stays exactly where it was — that is what the docstring
|
|
420
|
+
# above is protecting, and it is what keeps contrast and eye strain
|
|
421
|
+
# predictable. Only a*/b* move, and only to TINT_CHROMA, which is small
|
|
422
|
+
# enough to read as "this terminal belongs to that wallpaper" rather than
|
|
423
|
+
# as a coloured background.
|
|
424
|
+
#
|
|
425
|
+
# Without this, term.bg had exactly two values across 228 themes and every
|
|
426
|
+
# theme looked identical apart from one accent. Set DOTFILES_TERM_TINT=0
|
|
427
|
+
# for the previous flat neutrals.
|
|
428
|
+
if _env_hex("DOTFILES_TERM_TINT", "1") != "0":
|
|
429
|
+
ranked = sorted(clusters, key=lambda c: c[1] * lab_chroma(*c[0]), reverse=True)
|
|
430
|
+
src = ranked[0][0] if ranked and lab_chroma(*ranked[0][0]) >= 5.0 else None
|
|
431
|
+
if src is not None:
|
|
432
|
+
hue_c = lab_chroma(*src)
|
|
433
|
+
if hue_c > 0:
|
|
434
|
+
# Unit vector along the wallpaper's hue, scaled to a fixed,
|
|
435
|
+
# deliberately low chroma. Dark surfaces take a touch more:
|
|
436
|
+
# the same chroma reads weaker against a low-lightness ground.
|
|
437
|
+
bg_chroma = 4.5 if is_dark else 3.0
|
|
438
|
+
fg_chroma = 2.5 if is_dark else 2.0
|
|
439
|
+
ua, ub = src[1] / hue_c, src[2] / hue_c
|
|
440
|
+
bl = rgb_to_lab(*bg_rgb)[0]
|
|
441
|
+
fl = rgb_to_lab(*fg_rgb)[0]
|
|
442
|
+
bg_rgb = lab_to_rgb(bl, ua * bg_chroma, ub * bg_chroma)
|
|
443
|
+
fg_rgb = lab_to_rgb(fl, ua * fg_chroma, ub * fg_chroma)
|
|
444
|
+
|
|
395
445
|
bg_lab = rgb_to_lab(*bg_rgb)
|
|
396
446
|
return bg_lab, bg_rgb, fg_rgb
|
|
397
447
|
|
|
@@ -423,6 +473,212 @@ def _compute_accent(clusters, is_dark):
|
|
|
423
473
|
return (al, aa, ab), lab_to_rgb(al, aa, ab)
|
|
424
474
|
|
|
425
475
|
|
|
476
|
+
# A support colour that has to fall below this lightness to satisfy the 7:1
|
|
477
|
+
# white-on-block requirement is not carrying colour any more. See the
|
|
478
|
+
# rejection note in _compute_support_colours.
|
|
479
|
+
SUPPORT_MIN_L = 25.0
|
|
480
|
+
|
|
481
|
+
# Minimum perceptual distance between the three support colours, as dE*ab.
|
|
482
|
+
#
|
|
483
|
+
# This replaced a 12-degree hue-angle check, which let near-identical colours
|
|
484
|
+
# through: hue angle is meaningless at low chroma, so three slate blues at
|
|
485
|
+
# chroma ~13 could sit 12 degrees apart and still be the same colour to look
|
|
486
|
+
# at. forest-dark's accent/secondary/tertiary measured dE 3.6 — against ~2.3
|
|
487
|
+
# for "just noticeable" — while passing the hue rule comfortably.
|
|
488
|
+
#
|
|
489
|
+
# 10 is roughly where two colours read as clearly different rather than as
|
|
490
|
+
# shades of one. Measured over the library, 68 of 228 themes had at least one
|
|
491
|
+
# pair below it; those now reach further down the cluster ranking for a
|
|
492
|
+
# candidate that is actually distinct, and only fall back to a synthetic
|
|
493
|
+
# rotation when the wallpaper genuinely has nothing else to offer.
|
|
494
|
+
SUPPORT_MIN_DE = 10.0
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _aaa_darkened(lab, is_dark):
|
|
498
|
+
"""Clamp lightness per mode, then darken until white sits on it at 7:1.
|
|
499
|
+
|
|
500
|
+
Shared by the selection filter and the final emission so a candidate is
|
|
501
|
+
judged on exactly the colour it will become, not on the one it started
|
|
502
|
+
as — checking the input and emitting the output was how a colour that
|
|
503
|
+
passed the chroma filter still arrived at L*=14.8.
|
|
504
|
+
|
|
505
|
+
A candidate that is ALREADY darker than SUPPORT_MIN_L is then lifted back
|
|
506
|
+
toward it, one step at a time, stopping the moment another step would
|
|
507
|
+
cost the 7:1 guarantee. Two different things can leave a support colour
|
|
508
|
+
too dark: the darkening loop travelling a long way (a low-chroma
|
|
509
|
+
candidate), and the cluster simply starting dark (dune-light's whole
|
|
510
|
+
palette sits near L*17 and holds 14.3:1, so the loop never ran). The
|
|
511
|
+
first is handled by rejecting the candidate; only the second can be
|
|
512
|
+
lifted, and only where there is contrast headroom to pay for it.
|
|
513
|
+
"""
|
|
514
|
+
L = max(lab[0], 35.0) if is_dark else min(lab[0], 45.0)
|
|
515
|
+
a, b = lab[1], lab[2]
|
|
516
|
+
for _ in range(80):
|
|
517
|
+
if contrast_ratio((255, 255, 255), lab_to_rgb(L, a, b)) >= 7.0:
|
|
518
|
+
break
|
|
519
|
+
L = max(0.0, L - 2.0)
|
|
520
|
+
while L < SUPPORT_MIN_L:
|
|
521
|
+
nxt = min(SUPPORT_MIN_L, L + 1.0)
|
|
522
|
+
if contrast_ratio((255, 255, 255), lab_to_rgb(nxt, a, b)) < 7.0:
|
|
523
|
+
break
|
|
524
|
+
L = nxt
|
|
525
|
+
return (L, a, b)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _displayed(lab, is_dark):
|
|
529
|
+
"""The LAB of the colour that will actually be SHOWN.
|
|
530
|
+
|
|
531
|
+
Cluster centroids routinely land outside sRGB — Tang's top three sit at
|
|
532
|
+
chroma 197, 119 and 98, where sRGB tops out near 130 — and `lab_to_rgb`
|
|
533
|
+
clamps them. Comparing the raw LAB values said those three were dE 79 and
|
|
534
|
+
49 apart; after clamping they were #a80000 and #b00000, dE 3.3. The
|
|
535
|
+
distance test has to run on the clamped colour or it is measuring
|
|
536
|
+
something the user never sees.
|
|
537
|
+
"""
|
|
538
|
+
return rgb_to_lab(*lab_to_rgb(*_aaa_darkened(lab, is_dark)))
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _compute_support_colours(clusters, accent_lab, is_dark):
|
|
542
|
+
"""The wallpaper's SECOND and THIRD chromatic colours, as UI accents.
|
|
543
|
+
|
|
544
|
+
`_compute_accent` takes the top of the population x chroma ranking and
|
|
545
|
+
throws the rest away, so a wallpaper contributed exactly one colour to
|
|
546
|
+
the UI and everything else — status bars, separators, inactive states —
|
|
547
|
+
fell back to fixed neutrals. These are the next two ranked clusters,
|
|
548
|
+
put through the same AAA-darkening as the accent so white text sits on
|
|
549
|
+
them at 7:1.
|
|
550
|
+
|
|
551
|
+
Clusters within 12 degrees of a colour already chosen are skipped: two
|
|
552
|
+
near-identical blues give no more information than one, and the point of
|
|
553
|
+
a secondary is that it reads as different.
|
|
554
|
+
"""
|
|
555
|
+
ranked = [c for c in sorted(clusters, key=lambda c: c[1] * lab_chroma(*c[0]), reverse=True)
|
|
556
|
+
if lab_chroma(*c[0]) >= 5.0]
|
|
557
|
+
# Compare the colours as they will be SHOWN, not as the clusters arrived:
|
|
558
|
+
# the AAA-darkening moves lightness, which moves perceptual distance too.
|
|
559
|
+
picked, chosen = [], [rgb_to_lab(*lab_to_rgb(*accent_lab))]
|
|
560
|
+
for lab, _pop in ranked:
|
|
561
|
+
final = _displayed(lab, is_dark)
|
|
562
|
+
if any(lab_distance(final, o) < SUPPORT_MIN_DE for o in chosen):
|
|
563
|
+
continue
|
|
564
|
+
# Reject a candidate that only reaches 7:1 by going almost black.
|
|
565
|
+
#
|
|
566
|
+
# The AAA-darkening below drives lightness down until white sits on
|
|
567
|
+
# the colour at 7:1. A low-chroma candidate has to travel a long way
|
|
568
|
+
# to get there: bauhaus-light's second cluster landed at L*=14.8,
|
|
569
|
+
# which reads as another shade of dark rather than as a colour, even
|
|
570
|
+
# though its chroma of 8.0 passed the filter above.
|
|
571
|
+
#
|
|
572
|
+
# Measured across the library this affected 7 themes, all light
|
|
573
|
+
# variants, where the surface is pale and the colours must darken to
|
|
574
|
+
# sit on it. Raising the chroma floor instead would have cost 34
|
|
575
|
+
# themes their support colours and fixed only one of these seven, so
|
|
576
|
+
# the constraint belongs on the OUTCOME, not on the input.
|
|
577
|
+
if final[0] < SUPPORT_MIN_L:
|
|
578
|
+
continue
|
|
579
|
+
picked.append(lab)
|
|
580
|
+
chosen.append(final)
|
|
581
|
+
if len(picked) == 2:
|
|
582
|
+
break
|
|
583
|
+
# A wallpaper with only one usable hue still needs two support colours, so
|
|
584
|
+
# the accent's hue is rotated to invent them. Rotating blindly does not
|
|
585
|
+
# work: a rotated hue at high chroma often falls outside sRGB, and
|
|
586
|
+
# lab_to_rgb clamps it. Two different angles then clamp to nearly the same
|
|
587
|
+
# colour — tang-light produced #695600 and #685600, dE 0.5 apart, both at
|
|
588
|
+
# chroma 45. The fallback was the one path not checking its own output.
|
|
589
|
+
#
|
|
590
|
+
# So search: walk angles away from the accent, and drop chroma until the
|
|
591
|
+
# result survives a round trip through sRGB (proof it is in gamut) and is
|
|
592
|
+
# far enough from everything already chosen.
|
|
593
|
+
while len(picked) < 2:
|
|
594
|
+
base = picked[-1] if picked else accent_lab
|
|
595
|
+
best = None
|
|
596
|
+
for deg in (120, 90, 150, 60, 180, 40, 210, 30, 240, 270, 300):
|
|
597
|
+
for scale in (1.0, 0.85, 0.7, 0.55, 0.4):
|
|
598
|
+
c = lab_chroma(*base) * scale
|
|
599
|
+
rad = math.radians((lab_hue(*base) + deg) % 360.0)
|
|
600
|
+
cand_lab = (base[0], math.cos(rad) * c, math.sin(rad) * c)
|
|
601
|
+
final = _displayed(cand_lab, is_dark)
|
|
602
|
+
rgb = lab_to_rgb(*_aaa_darkened(cand_lab, is_dark))
|
|
603
|
+
# Round trip: if the colour was out of gamut it was clamped,
|
|
604
|
+
# and the clamped value will not convert back to what we asked
|
|
605
|
+
# for. That is exactly how the duplicates were produced.
|
|
606
|
+
if lab_distance(rgb_to_lab(*rgb), final) > 3.0:
|
|
607
|
+
continue
|
|
608
|
+
if final[0] < SUPPORT_MIN_L:
|
|
609
|
+
continue
|
|
610
|
+
d = min(lab_distance(final, o) for o in chosen)
|
|
611
|
+
if d >= SUPPORT_MIN_DE:
|
|
612
|
+
best = cand_lab
|
|
613
|
+
break
|
|
614
|
+
if best is None or d > min(lab_distance(_displayed(best, is_dark), o) for o in chosen):
|
|
615
|
+
best = cand_lab
|
|
616
|
+
if best is not None and min(
|
|
617
|
+
lab_distance(_displayed(best, is_dark), o) for o in chosen
|
|
618
|
+
) >= SUPPORT_MIN_DE:
|
|
619
|
+
break
|
|
620
|
+
if best is None:
|
|
621
|
+
best = (base[0], -base[1], -base[2]) # last resort: opposite hue
|
|
622
|
+
picked.append(best)
|
|
623
|
+
chosen.append(_displayed(best, is_dark))
|
|
624
|
+
|
|
625
|
+
return (lab_to_rgb(*_aaa_darkened(picked[0], is_dark)),
|
|
626
|
+
lab_to_rgb(*_aaa_darkened(picked[1], is_dark)))
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _on_dark(lab, surfaces, min_ratio=4.5):
|
|
630
|
+
"""The same hue, light enough to be TEXT on a dark surface.
|
|
631
|
+
|
|
632
|
+
`accent`, `secondary` and `tertiary` are darkened until white sits on
|
|
633
|
+
them at 7:1 — they are background colours by construction, and painting
|
|
634
|
+
them as text on the dark status bar measured as low as 1.67:1.
|
|
635
|
+
|
|
636
|
+
This keeps the hue and chroma and walks lightness up instead, until the
|
|
637
|
+
ratio holds against every surface the text can land on. Without it there
|
|
638
|
+
is no way to show a wallpaper colour AS text: the choice would be
|
|
639
|
+
between wallpaper colour and legibility.
|
|
640
|
+
"""
|
|
641
|
+
L, a, b = lab
|
|
642
|
+
for _ in range(120):
|
|
643
|
+
rgb = lab_to_rgb(L, a, b)
|
|
644
|
+
if all(contrast_ratio(rgb, s) >= min_ratio for s in surfaces):
|
|
645
|
+
return rgb
|
|
646
|
+
if L >= 100.0:
|
|
647
|
+
break
|
|
648
|
+
L = min(100.0, L + 1.5)
|
|
649
|
+
return lab_to_rgb(L, a, b)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _muted_text(panel_rgb, border_rgb, bg_lab, is_dark):
|
|
653
|
+
"""Readable de-emphasised text — 4.5:1 against every surface it lands on.
|
|
654
|
+
|
|
655
|
+
Not `term.c8`. c8 is ANSI bright-black and is supposed to be dim; its
|
|
656
|
+
floor is 2.5:1 against bg, and tmux was painting the clock and the
|
|
657
|
+
inactive window names with it at 2.12:1 and 2.45:1. Raising c8 would
|
|
658
|
+
have made every terminal's dim colour less dim in order to fix a status
|
|
659
|
+
bar, so this is a separate slot.
|
|
660
|
+
|
|
661
|
+
Floored against BOTH panel and border, not just panel: the two differ in
|
|
662
|
+
lightness, tmux draws muted text on each of them, and flooring against
|
|
663
|
+
only the friendlier one left the other at 4.40:1 — a near miss is still
|
|
664
|
+
a miss.
|
|
665
|
+
"""
|
|
666
|
+
start = 62.0 if is_dark else 42.0
|
|
667
|
+
seed = lab_to_rgb(start, bg_lab[1] * 0.5, bg_lab[2] * 0.5)
|
|
668
|
+
out = seed
|
|
669
|
+
for surface in (panel_rgb, border_rgb):
|
|
670
|
+
out = ensure_contrast(out, surface, 4.5, is_dark)
|
|
671
|
+
# ensure_contrast against the second surface can walk back toward the
|
|
672
|
+
# first, so confirm rather than assume, and step until both hold.
|
|
673
|
+
for _ in range(80):
|
|
674
|
+
if all(contrast_ratio(out, s) >= 4.5 for s in (panel_rgb, border_rgb)):
|
|
675
|
+
break
|
|
676
|
+
l, a, b = rgb_to_lab(*out)
|
|
677
|
+
l = min(100.0, l + 2.0) if is_dark else max(0.0, l - 2.0)
|
|
678
|
+
out = lab_to_rgb(l, a, b)
|
|
679
|
+
return out
|
|
680
|
+
|
|
681
|
+
|
|
426
682
|
def _compute_panel_border(bg_lab, bg_rgb, is_dark):
|
|
427
683
|
"""Compute panel and border with enforced contrast ranges against bg."""
|
|
428
684
|
if is_dark:
|
|
@@ -538,6 +794,14 @@ def generate_theme(
|
|
|
538
794
|
sel_rgb = lab_to_rgb(*sel_lab)
|
|
539
795
|
|
|
540
796
|
panel_rgb, border_rgb = _compute_panel_border(bg_lab, bg_rgb, is_dark)
|
|
797
|
+
secondary_rgb, tertiary_rgb = _compute_support_colours(clusters, accent_lab, is_dark)
|
|
798
|
+
muted_rgb = _muted_text(panel_rgb, border_rgb, bg_lab, is_dark)
|
|
799
|
+
# Text-safe versions of the three chromatic colours, for consumers that
|
|
800
|
+
# paint them as foreground on panel/border rather than as a block.
|
|
801
|
+
_surfaces = (panel_rgb, border_rgb, bg_rgb)
|
|
802
|
+
accent_on_rgb = _on_dark(rgb_to_lab(*accent_rgb), _surfaces)
|
|
803
|
+
secondary_on_rgb = _on_dark(rgb_to_lab(*secondary_rgb), _surfaces)
|
|
804
|
+
tertiary_on_rgb = _on_dark(rgb_to_lab(*tertiary_rgb), _surfaces)
|
|
541
805
|
ansi = _ansi_palette(clusters, accent_lab, bg_rgb, is_dark)
|
|
542
806
|
c0_rgb, c7_rgb, c8_rgb, c15_rgb = _structural_colors(bg_lab, bg_rgb, is_dark)
|
|
543
807
|
|
|
@@ -587,6 +851,17 @@ def generate_theme(
|
|
|
587
851
|
"info": rgb_to_hex(*ansi["blue"][0]),
|
|
588
852
|
"panel": rgb_to_hex(*panel_rgb),
|
|
589
853
|
"border": rgb_to_hex(*border_rgb),
|
|
854
|
+
# The wallpaper's 2nd and 3rd chromatic colours. White sits on
|
|
855
|
+
# either at 7:1, same as accent.
|
|
856
|
+
"secondary": rgb_to_hex(*secondary_rgb),
|
|
857
|
+
"tertiary": rgb_to_hex(*tertiary_rgb),
|
|
858
|
+
# De-emphasised text that is still text: 4.5:1 against `panel`.
|
|
859
|
+
"text_muted": rgb_to_hex(*muted_rgb),
|
|
860
|
+
# Same hues, lightened until they are legible AS TEXT on panel,
|
|
861
|
+
# border and bg (>= 4.5:1 on all three).
|
|
862
|
+
"accent_on_surface": rgb_to_hex(*accent_on_rgb),
|
|
863
|
+
"secondary_on_surface": rgb_to_hex(*secondary_on_rgb),
|
|
864
|
+
"tertiary_on_surface": rgb_to_hex(*tertiary_on_rgb),
|
|
590
865
|
},
|
|
591
866
|
"app": {
|
|
592
867
|
"nvim": nvim_theme[0],
|
|
@@ -596,9 +871,10 @@ def generate_theme(
|
|
|
596
871
|
"gtk_icon": "Papirus-Dark" if is_dark else "Papirus-Light",
|
|
597
872
|
"gnome_shell": "",
|
|
598
873
|
"gnome_gtk": "Adwaita-dark" if is_dark else "Adwaita",
|
|
599
|
-
"vscode":
|
|
600
|
-
"vscode_dark":
|
|
601
|
-
"vscode_light":
|
|
874
|
+
"vscode": VSCODE_DARK if is_dark else VSCODE_LIGHT,
|
|
875
|
+
"vscode_dark": VSCODE_DARK,
|
|
876
|
+
"vscode_light": VSCODE_LIGHT,
|
|
877
|
+
"vscode_icons": VSCODE_ICONS_DARK if is_dark else VSCODE_ICONS_LIGHT,
|
|
602
878
|
"cat_wallpaper": "",
|
|
603
879
|
"starship_palette": f"catppuccin_{'mocha' if is_dark else 'latte'}",
|
|
604
880
|
},
|
|
@@ -631,7 +907,9 @@ def theme_to_toml(theme: Dict) -> str:
|
|
|
631
907
|
lines.append("")
|
|
632
908
|
|
|
633
909
|
lines.append(f"[themes.{name}.ui]")
|
|
634
|
-
for key in ["accent", "accent_text", "
|
|
910
|
+
for key in ["accent", "accent_text", "secondary", "tertiary", "text_muted",
|
|
911
|
+
"accent_on_surface", "secondary_on_surface", "tertiary_on_surface",
|
|
912
|
+
"error", "warning", "success", "info", "panel", "border"]:
|
|
635
913
|
lines.append(f'{key} = "{theme["ui"][key]}"')
|
|
636
914
|
lines.append("")
|
|
637
915
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# rebuild-themes.sh — Discover wallpapers and generate themes.toml dynamically.
|
|
5
5
|
#
|
|
@@ -277,6 +277,45 @@ fi
|
|
|
277
277
|
|
|
278
278
|
mkdir -p "$CACHE_DIR"
|
|
279
279
|
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Invalidate the cache when the generator itself changes
|
|
282
|
+
#
|
|
283
|
+
# A cache entry is only valid for the extract-theme.py that produced it, but
|
|
284
|
+
# the freshness check below compares the entry against its *wallpaper* alone.
|
|
285
|
+
# So a wallpaper that never changes keeps serving whatever the generator
|
|
286
|
+
# emitted the first time, indefinitely — while every other theme silently
|
|
287
|
+
# moves on.
|
|
288
|
+
#
|
|
289
|
+
# That is not hypothetical: catalina and sonoma shipped in themes.toml with
|
|
290
|
+
# blocks generated in June/July, months after the rest. They carried an
|
|
291
|
+
# absolute /Users/<name>/ wallpaper path (predating the `~/` normalisation)
|
|
292
|
+
# and a c15 of #181818 in a *light* theme (predating the structural-ramp fix),
|
|
293
|
+
# which failed the WCAG AAA gate in CI. Two real defects, both invisible here
|
|
294
|
+
# because those two wallpapers had simply not been touched since.
|
|
295
|
+
#
|
|
296
|
+
# Keyed on the generator's CONTENT, not its mtime: `git checkout` rewrites
|
|
297
|
+
# mtimes without changing behaviour, and mtime alone would force a full
|
|
298
|
+
# rebuild of every theme on each checkout.
|
|
299
|
+
generator_digest() {
|
|
300
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
301
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
302
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
303
|
+
sha256sum "$1" | awk '{print $1}'
|
|
304
|
+
else
|
|
305
|
+
# No digest tool: fall back to always regenerating rather than risk
|
|
306
|
+
# serving stale blocks. Correctness over speed.
|
|
307
|
+
echo "no-digest-tool-$(date +%s)"
|
|
308
|
+
fi
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
GENERATOR_STAMP="$CACHE_DIR/.extract-theme.sha256"
|
|
312
|
+
GENERATOR_HASH="$(generator_digest "$EXTRACT_SCRIPT")"
|
|
313
|
+
GENERATOR_STALE=false
|
|
314
|
+
if [[ ! -f "$GENERATOR_STAMP" || "$(cat "$GENERATOR_STAMP")" != "$GENERATOR_HASH" ]]; then
|
|
315
|
+
GENERATOR_STALE=true
|
|
316
|
+
echo "Generator changed since the cache was written — rebuilding all themes."
|
|
317
|
+
fi
|
|
318
|
+
|
|
280
319
|
# Clean orphaned cache files (wallpapers that no longer exist)
|
|
281
320
|
for cache_file in "$CACHE_DIR"/*.toml; do
|
|
282
321
|
[[ -f "$cache_file" ]] || continue
|
|
@@ -328,7 +367,8 @@ for name in $(printf '%s\n' "${!WALLPAPERS[@]}" | sort); do
|
|
|
328
367
|
wp_path="${WALLPAPERS[$name]}"
|
|
329
368
|
cache_file="$CACHE_DIR/${name}.toml"
|
|
330
369
|
|
|
331
|
-
if [[ "$FORCE" != "true" &&
|
|
370
|
+
if [[ "$FORCE" != "true" && "$GENERATOR_STALE" != "true" &&
|
|
371
|
+
-f "$cache_file" && "$cache_file" -nt "$wp_path" ]]; then
|
|
332
372
|
CACHED=$((CACHED + 1))
|
|
333
373
|
continue
|
|
334
374
|
fi
|
|
@@ -463,6 +503,12 @@ success = "#479174"
|
|
|
463
503
|
info = "#5f86b7"
|
|
464
504
|
panel = "#242435"
|
|
465
505
|
border = "#302f38"
|
|
506
|
+
secondary = "#2d5987"
|
|
507
|
+
tertiary = "#16654b"
|
|
508
|
+
text_muted = "#9898a5"
|
|
509
|
+
accent_on_surface = "#819dab"
|
|
510
|
+
secondary_on_surface = "#7b9bc4"
|
|
511
|
+
tertiary_on_surface = "#53a987"
|
|
466
512
|
|
|
467
513
|
[themes.fallback-dark.app]
|
|
468
514
|
nvim = "tokyonight"
|
|
@@ -472,9 +518,10 @@ gtk_theme = "Adwaita-dark"
|
|
|
472
518
|
gtk_icon = "Papirus-Dark"
|
|
473
519
|
gnome_shell = ""
|
|
474
520
|
gnome_gtk = "Adwaita-dark"
|
|
475
|
-
vscode = "
|
|
476
|
-
vscode_dark = "
|
|
477
|
-
vscode_light = "
|
|
521
|
+
vscode = "Catppuccin Mocha"
|
|
522
|
+
vscode_dark = "Catppuccin Mocha"
|
|
523
|
+
vscode_light = "Catppuccin Latte"
|
|
524
|
+
vscode_icons = "catppuccin-mocha"
|
|
478
525
|
cat_wallpaper = ""
|
|
479
526
|
starship_palette = "catppuccin_mocha"
|
|
480
527
|
|
|
@@ -519,6 +566,12 @@ success = "#205a48"
|
|
|
519
566
|
info = "#2b527d"
|
|
520
567
|
panel = "#f2e8bf"
|
|
521
568
|
border = "#e3e0d3"
|
|
569
|
+
secondary = "#85443e"
|
|
570
|
+
tertiary = "#29623e"
|
|
571
|
+
text_muted = "#686256"
|
|
572
|
+
accent_on_surface = "#2c5989"
|
|
573
|
+
secondary_on_surface = "#2b527d"
|
|
574
|
+
tertiary_on_surface = "#205a48"
|
|
522
575
|
|
|
523
576
|
[themes.fallback-light.app]
|
|
524
577
|
nvim = "catppuccin"
|
|
@@ -531,6 +584,7 @@ gnome_gtk = "Adwaita"
|
|
|
531
584
|
vscode = "Catppuccin Latte"
|
|
532
585
|
vscode_dark = "Catppuccin Mocha"
|
|
533
586
|
vscode_light = "Catppuccin Latte"
|
|
587
|
+
vscode_icons = "catppuccin-latte"
|
|
534
588
|
cat_wallpaper = ""
|
|
535
589
|
starship_palette = "catppuccin_latte"
|
|
536
590
|
FALLBACK
|
|
@@ -540,5 +594,10 @@ FALLBACK
|
|
|
540
594
|
# subsections (which inflated the tally ~4x).
|
|
541
595
|
theme_count=$(grep -cE '^\[themes\.[a-z0-9-]+\]$' "$THEMES_FILE")
|
|
542
596
|
echo " Written: $THEMES_FILE ($theme_count themes)"
|
|
597
|
+
|
|
598
|
+
# Record which generator produced this cache. Written only now, after the file
|
|
599
|
+
# has been assembled: stamping earlier would mark the cache current even if the
|
|
600
|
+
# run died partway, so the next run would trust half-regenerated blocks.
|
|
601
|
+
printf '%s\n' "$GENERATOR_HASH" >"$GENERATOR_STAMP"
|
|
543
602
|
echo ""
|
|
544
603
|
echo "Done. Run 'dot theme list' to see available themes."
|
package/scripts/theme/switch.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
## Theme Switcher — Switch between theme families and light/dark modes.
|
|
5
5
|
##
|
|
@@ -123,30 +123,34 @@ theme_exists() {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
all_theme_names() {
|
|
126
|
-
sed -n 's/^\[themes\.\([a-
|
|
126
|
+
sed -n 's/^\[themes\.\([a-zA-Z0-9-]*\)\]$/\1/p' "$THEMES_FILE" | sort -u
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
# List wallpaper families that have BOTH dark and light variants in themes.toml.
|
|
130
130
|
# Only these are presented to users — unpaired wallpapers are hidden.
|
|
131
|
+
#
|
|
132
|
+
# This was two associative arrays. Those are bash 4 only, and macOS still
|
|
133
|
+
# ships 3.2 as /bin/bash, where `local -A` fails outright ("local: -A:
|
|
134
|
+
# invalid option"). Both arrays then stayed empty, so this function printed
|
|
135
|
+
# nothing and `dot theme list`, `dot theme family` and the interactive picker
|
|
136
|
+
# silently offered no themes at all on a stock macOS shell.
|
|
137
|
+
#
|
|
138
|
+
# Two newline-separated lists plus `comm` behave identically on 3.2 and 4+.
|
|
131
139
|
paired_families() {
|
|
132
|
-
local
|
|
133
|
-
local name family
|
|
140
|
+
local darks="" lights="" name family
|
|
134
141
|
while IFS= read -r name; do
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
family="${name%-light}"
|
|
140
|
-
has_light["$family"]=1
|
|
141
|
-
fi
|
|
142
|
+
case "$name" in
|
|
143
|
+
*-dark) darks="${darks}${name%-dark}"$'\n' ;;
|
|
144
|
+
*-light) lights="${lights}${name%-light}"$'\n' ;;
|
|
145
|
+
esac
|
|
142
146
|
done < <(all_theme_names)
|
|
143
147
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
# `fallback` is a synthetic safety theme (see themes.toml) that templates
|
|
149
|
+
# degrade to when .theme is unset/invalid — never a user-selectable one.
|
|
150
|
+
comm -12 \
|
|
151
|
+
<(printf '%s' "$darks" | sort -u) \
|
|
152
|
+
<(printf '%s' "$lights" | sort -u) |
|
|
153
|
+
grep -vxF 'fallback' || true
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
# Determine source type (system/custom) for a wallpaper family.
|
|
@@ -196,8 +200,17 @@ is_dark_theme() {
|
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
set_theme() {
|
|
199
|
-
local new_theme="$1"
|
|
203
|
+
local new_theme="${1:-}"
|
|
200
204
|
if [ -z "$new_theme" ]; then
|
|
205
|
+
# A picker needs someone to pick. Under DOTFILES_NONINTERACTIVE the
|
|
206
|
+
# selector cannot run, and ui_pick reports "nothing selected" the same
|
|
207
|
+
# way it reports a cancel — so answering with a silent success would
|
|
208
|
+
# make a forgotten argument indistinguishable from a theme change.
|
|
209
|
+
if [ "${DOTFILES_NONINTERACTIVE:-0}" = "1" ]; then
|
|
210
|
+
ui_err "Missing theme name" "no interactive picker in a non-interactive session"
|
|
211
|
+
ui_info "Usage" "dot theme set <name> (or 'dot theme list' to see them)"
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
201
214
|
pick_theme
|
|
202
215
|
return
|
|
203
216
|
fi
|
|
@@ -248,6 +261,11 @@ pick_theme() {
|
|
|
248
261
|
else
|
|
249
262
|
ui_info "Theme" "already on $current"
|
|
250
263
|
fi
|
|
264
|
+
else
|
|
265
|
+
# Cancelled, or no selector could run. Either way nothing changed, and
|
|
266
|
+
# saying so beats exiting mute on a command the user asked to be
|
|
267
|
+
# interactive.
|
|
268
|
+
ui_info "Theme" "no selection — still on $current"
|
|
251
269
|
fi
|
|
252
270
|
}
|
|
253
271
|
|
|
@@ -399,7 +417,16 @@ case "${1:-}" in
|
|
|
399
417
|
;;
|
|
400
418
|
set)
|
|
401
419
|
shift
|
|
402
|
-
|
|
420
|
+
# `"${1:-}"`, not `"$1"`: with no theme name the bare positional aborts
|
|
421
|
+
# the script under `set -u` before set_theme's own empty-string check can
|
|
422
|
+
# open the picker `dot help theme` promises. Worse, the abort is silent
|
|
423
|
+
# about its status on bash 3.2 (macOS /bin/bash): when a script dies on an
|
|
424
|
+
# unbound variable with an EXIT trap installed — switch.sh installs
|
|
425
|
+
# `trap cleanup EXIT` — 3.2 exits 0, and no handler can recover the status
|
|
426
|
+
# because `$?` is already 0 when the handler runs. So on a stock Mac this
|
|
427
|
+
# one missing default turned every `dot theme set` typo into a reported
|
|
428
|
+
# success. Keep every expansion in this script guarded.
|
|
429
|
+
set_theme "${1:-}"
|
|
403
430
|
;;
|
|
404
431
|
toggle)
|
|
405
432
|
toggle_theme
|
package/scripts/tools/cmatrix.sh
CHANGED