From 078d01666e987cb984ac1e12958d4b59d71ecf85 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Wed, 9 Sep 2026 14:50:26 -0400 Subject: [PATCH] feat(config-settings): replace the ANSI renderer with a curses front-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit config-settings is now a launcher for scripts/config-settings-tui.py, drawn with Python's stdlib curses. The seven fish files that hand-rolled the ANSI renderer are gone, along with the golden harness that had to pin their byte-exact output. The TUI is a child process, so it can neither read the session's global variables nor write them. State goes in as a dump from the new __config_settings_state; the edits come back as a fish script that config-settings sources, which is what lets the Session page's `set -g` land in the caller's shell instead of in a child that is about to exit. Every edit is emitted as a call to __config_settings_apply or __config_settings_set_value, so list splitting, the SCROLLBACK_HISTORY_* export mirror and the shadow-warning suppression all stay in the fish layer that already owned them. The consequence, and the one behaviour change: edits are applied in one batch on exit rather than on each keypress. The status bar shows a pending count. New: `/` filters the current page, and on the Universal and Session pages it reaches into every category's sub-categories, listing hits as "Category › Sub" so a sub-category can be toggled without drilling into its parent first. Also a `?` help overlay, mouse selection, and a drill-down page that leads with the category's own toggle. Gone with the renderer: the four width tiers, the wrap-aware erase arithmetic, the stty/dd/od raw key reader, the panel-height bookkeeping and the hand-written redraw differ. curses owns all of it, and the alt screen plus absolute addressing makes the desync class behind 608b022, 4210f3b, 93fc5e0 and 3c4f720 unreachable. The sub-category taxonomy is NOT duplicated in Python: it travels in the state dump, still sourced from __config_settings_subcats. The category, Sponge and Paths row tables move into Python, consolidating the two copies the fish renderers kept. Dependency: python3 with curses. Stdlib on Arch, Fedora and a full Debian/Ubuntu python3; python3-minimal alone lacks _curses. The launcher checks for both and names what is missing. Called out in the README. Verified: 416/416 assertions, plus a live end-to-end in a sandbox HOME confirming the Universal page writes universal scope only (U1/G0) and the Session page global scope only (U0/G1). --- README.md | 4 +- functions/__config_settings_diff_redraw.fish | 47 - functions/__config_settings_draw.fish | 155 - functions/__config_settings_draw_subcat.fish | 120 - functions/__config_settings_draw_value.fish | 159 - functions/__config_settings_frame.fish | 155 - functions/__config_settings_pagetab.fish | 45 - functions/__config_settings_read_key.fish | 97 - functions/__config_settings_state.fish | 76 + functions/config-settings.fish | 459 +- scripts/config-settings-tui.py | 706 ++- tests/golden/config-settings-render.txt | 4994 ------------------ tests/test-config-settings-render.fish | 285 - tests/test-config-settings-tui.fish | 43 +- tests/test-session.fish | 138 +- 15 files changed, 745 insertions(+), 6738 deletions(-) delete mode 100644 functions/__config_settings_diff_redraw.fish delete mode 100644 functions/__config_settings_draw.fish delete mode 100644 functions/__config_settings_draw_subcat.fish delete mode 100644 functions/__config_settings_draw_value.fish delete mode 100644 functions/__config_settings_frame.fish delete mode 100644 functions/__config_settings_pagetab.fish delete mode 100644 functions/__config_settings_read_key.fish create mode 100644 functions/__config_settings_state.fish delete mode 100644 tests/golden/config-settings-render.txt delete mode 100644 tests/test-config-settings-render.fish diff --git a/README.md b/README.md index 22fdf30..7327b84 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,8 @@ Then open a new Fish shell — Fisher will be installed automatically on first l A [chezmoi](https://www.chezmoi.io/) dotfile manager is also configured — secrets are kept in a private overlay directory (see [Personalization](#personalization)) and excluded from version control. +**External requirements.** Everything degrades gracefully if a tool is missing, with two exceptions worth calling out. `python3` is required by the documentation pipeline and by `config-settings`, whose TUI is drawn with the stdlib `curses` module. That module ships with CPython on Arch, Fedora and a full Debian/Ubuntu `python3`; `python3-minimal` alone does **not** carry `_curses`, so install the complete `python3` package there. `config-settings` checks for both and tells you which is missing rather than failing inside the renderer. + > [!IMPORTANT] > `config.fish` ends with a `return` sentinel guard. Any lines appended **after** it by a tool's setup command will silently have no effect. Many tools (starship, zoxide, mise, etc.) offer a setup command that appends an `init | source` line to your `config.fish` — all integrations are managed through `conf.d/` files instead. If you add a new tool and its shell integration appears to do nothing, check whether its setup command appended an init line to the bottom of `config.fish` and create a `conf.d/.fish` file for it instead. @@ -288,7 +290,7 @@ To opt out, set `__fish_user_dots_symlink` to a falsy value (or toggle **Dots li Everything opinionated in this config — command shadows, startup side-effects, key and environment overrides, terminal integrations, and the first-run greeting — is active by default but can be switched off. Logging (C5) is the exception: it is **opt-in**, off until you explicitly enable it. -> **The easy way — `config-settings`:** Run `config-settings` for an interactive TUI that manages settings across four pages — **Universal** and **Session** (the opinionated category toggles below, persistent or per-shell), **Sponge** (history-scrubbing: delay, exit codes, purge-on-exit, and extra sensitive variable names), and **Paths** (scrollback log dir, max files, and the user-dots path) — without typing a single variable name. Navigate with the arrow keys (or `h`/`j`/`k`/`l`); toggle rows step OFF ← DEFAULT → ON, value rows edit inline with `Enter` and clear with `←`. `Tab`/`Shift-Tab` cycle pages and `q` quits. Changes apply instantly. The panel auto-sizes to your terminal width (four tiers from 52- to 78-wide with a 6-column margin), centers itself horizontally, and redraws within ~0.3 s of a resize. +> **The easy way — `config-settings`:** Run `config-settings` for an interactive TUI that manages settings across four pages — **Universal** and **Session** (the opinionated category toggles below, persistent or per-shell), **Sponge** (history-scrubbing: delay, exit codes, purge-on-exit, and extra sensitive variable names), and **Paths** (scrollback log dir, max files, and the user-dots path) — without typing a single variable name. Navigate with the arrow keys (or `h`/`j`/`k`/`l`); toggle rows step OFF ← DEFAULT → ON, value rows edit inline with `Enter` and reset with `←`. `Enter` on a category opens its sub-categories, `/` filters the page — reaching into sub-categories too, listed as `Category › Sub` — `?` opens a help overlay, `Tab`/`Shift-Tab` cycle pages, and `q` applies your edits and quits. Edits are collected as you make them and written in one batch on exit; the status bar shows the pending count. The panel is drawn with Python's stdlib `curses`, so it resizes with the terminal and never flickers — see [Installation](#installation) for the `python3` requirement. If you'd rather set them by hand, each category is controlled by a universal variable. Six category toggles and one master switch are available: diff --git a/functions/__config_settings_diff_redraw.fish b/functions/__config_settings_diff_redraw.fish deleted file mode 100644 index f5d9621..0000000 --- a/functions/__config_settings_diff_redraw.fish +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (C) 2026 Rootiest -# SPDX-License-Identifier: AGPL-3.0-or-later - -# SYNOPSIS -# __config_settings_diff_redraw -# -# DESCRIPTION -# Rewrites an on-screen panel frame in place, touching only the lines -# that changed. Assumes the cursor is already positioned at the top-left -# of the frame (the caller moves it there with a plain \e[A -- no -# \e[J -- before calling this). Line count in old_joined and new_joined -# must be equal; a caller facing a height or width change should use the -# existing full erase+redraw path instead of calling this. -# -# Unchanged lines advance the cursor with a bare newline, leaving -# whatever is already on screen untouched. Changed lines clear just that -# line (\e[2K), return to its start (\r), print the new content, and -# advance (\n). This is what removes the erase-then-redraw flicker: the -# screen is never blanked, only the handful of lines that actually -# differ are ever touched, and each of those is cleared and rewritten in -# the same breath rather than blanked-then-paused-then-filled. -# -# ARGUMENTS -# old_joined Previous frame, lines joined with \n -# new_joined New frame, lines joined with \n (same line count as old) -# -# EXIT STATUS -# 0 Always -# -# RETURNS -# The ANSI sequence needed to turn the old frame into the new one, -# printed to stdout -# -# EXAMPLE -# __config_settings_diff_redraw (string join \n -- $prev_frame) \ -# (string join \n -- $new_frame) -function __config_settings_diff_redraw - set -l old (string split \n -- $argv[1]) - set -l new (string split \n -- $argv[2]) - for i in (seq (count $new)) - if test "$old[$i]" = "$new[$i]" - printf '\n' - else - printf '\e[2K\r%s\n' $new[$i] - end - end -end diff --git a/functions/__config_settings_draw.fish b/functions/__config_settings_draw.fish deleted file mode 100644 index f4d975b..0000000 --- a/functions/__config_settings_draw.fish +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright (C) 2026 Rootiest -# SPDX-License-Identifier: AGPL-3.0-or-later - -# SYNOPSIS -# __config_settings_draw ... -# -# DESCRIPTION -# Renders the 16-line config-settings TUI panel to stdout. Panel width and -# horizontal position are chosen automatically from $COLUMNS each call, -# so a terminal resize takes effect on the next keypress without any -# extra bookkeeping. Four width tiers with a 6-col buffer per side: -# -# COLUMNS ≥ 90 → 78-wide (IW=76, desc=43 chars) -# COLUMNS ≥ 86 → 74-wide (IW=72, desc=39 chars) -# COLUMNS ≥ 82 → 70-wide (IW=68, desc=35 chars) -# COLUMNS < 82 → 52-wide (IW=50, desc=17 chars) ← default -# -# The box is horizontally centered via a left-padding prefix on every -# output line. \e[16A\e[J erases by line count so the horizontal offset -# does not interfere with the redraw loop. -# -# ARGUMENTS -# cur_row 0–6, the currently highlighted row -# cur_scope "universal" or "session" -# var1–var7 Variable names for rows 0–6 (6 categories + master) -# -# EXIT STATUS -# 0 Always -# -# EXAMPLE -# __config_settings_draw 0 universal \ -# __fish_config_op_aliases __fish_config_op_autoexec \ -# __fish_config_op_overrides __fish_config_op_integrations \ -# __fish_config_op_logging __fish_config_op_greeting \ -# __fish_config_opinionated -function __config_settings_draw - set -l cur_row $argv[1] - set -l cur_scope $argv[2] - set -l vars $argv[3..] - - __fish_palette - - set -l labels Aliases Auto-exec Overrides Integrations Logging Greeting Master - - # ── Width tier ──────────────────────────────────────────────────────── - # The tier thresholds live in __config_settings_frame; this file only - # chooses which hand-authored description set goes with the width. - # IW = inner width (chars between │ │); desc field = IW - 33. - # All four layouts are exactly 16 lines tall — panel_h in caller stays 16. - # Descriptions are authored to fit their field exactly at every tier - # (43/43, 39/39, 35/35, 17/17), which is why the rows below pass `pad` - # and not `cut` -- see the NOTES in __config_settings_frame. - set -l iw (__config_settings_frame width) - set -l descs \ - "cmd shadows" \ - startup \ - "keys/env/prompt" \ - "terminal coupling" \ - scrollback \ - fish_greeting \ - "disable all" - - switch $iw - case 76 - set descs \ - "shadows: ls→eza, cat→bat, cd→z, rm→trash" \ - "Fisher bootstrap, themes, py-venv activate" \ - "vi-mode, bang-bang, PAGER, CDPATH, starship" \ - "Kitty/WezTerm tab/split fns, notifications" \ - "scrollback capture & paru/yay AUR wrappers" \ - "fish_greeting & first-run welcome banner" \ - "master off-switch: overrides all categories" - case 72 - set descs \ - "ls→eza, cat→bat, cd→zoxide, rm→trash" \ - "Fisher bootstrap, themes, py-venv auto" \ - "vi-mode, bang-bang, PAGER, starship" \ - "Kitty/WezTerm fns, done notifications" \ - "scrollback capture & paru/yay wrappers" \ - "fish_greeting: first-run welcome banner" \ - "master off-switch for all categories" - case 68 - set descs \ - "ls→eza, cat→bat, cd→z, rm→trash" \ - "Fisher, themes, py-venv activate" \ - "vi-mode, bang-bang, PAGER, starship" \ - "Kitty/WezTerm, done notifications" \ - "scrollback & paru/yay log wrappers" \ - "fish_greeting & first-run banner" \ - "master disable for all categories" - end - - set -l HBR (string repeat -n $iw '─') - - # ── Center padding ──────────────────────────────────────────────────── - # ponytail: floor division — left margin may be 1 col less than right if gap is odd - set -l p (string repeat -n (math --scale=0 "max(0, ($COLUMNS - ($iw + 2)) / 2)") ' ') - - # ── Top border ──────────────────────────────────────────────────────── - # ┌─ Opinionated Settings (iw-23)×─ ┐ total = iw+2 - __config_settings_frame title $iw $p "$c_head Opinionated Settings $c_reset" - - # ── Page-tab header ─────────────────────────────────────────────────── - set -l active_idx 0 - if test $cur_scope = session - set active_idx 1 - end - printf '%s│%s│\n' $p (__config_settings_pagetab $active_idx $iw) - - # ── Top divider ─────────────────────────────────────────────────────── - printf '%s│%s│\n' $p $HBR - - # ── Category rows 0–5 ───────────────────────────────────────────────── - # Label field 12 wide; the description field falls out of it inside the - # frame (field_w = iw - 21 - label_w = iw - 33). `pad`, not `cut`: these - # descriptions are authored per tier to fit exactly, so truncating them - # would be a silent no-op that discards that property. - for i in (seq 0 5) - set -l idx (math $i + 1) - set -l val (__config_settings_get_val $vars[$idx] $cur_scope) - __config_settings_frame row $iw $p \ - (__config_settings_frame cursor $i $cur_row) \ - $labels[$idx] 12 \ - (__config_settings_frame badge $val) \ - $descs[$idx] pad - end - - # ── Separator before Master ─────────────────────────────────────────── - printf '%s│ %s │\n' $p (string repeat -n (math $iw - 6) '─') - - # ── Master row (index 6) ────────────────────────────────────────────── - set -l val (__config_settings_get_val $vars[7] $cur_scope) - __config_settings_frame row $iw $p \ - (__config_settings_frame cursor 6 $cur_row) \ - Master 12 \ - (__config_settings_frame badge $val) \ - $descs[7] pad - - # ── Filler (Dots Path moved to the Paths page) ──────────────────────── - printf '%s│ %s%s│\n' $p \ - "$c_dim→ Tab for Sponge & Path settings$c_reset" \ - (string repeat -n (math $iw - 34) ' ') - printf '%s│%s│\n' $p (string repeat -n $iw ' ') - - # ── Bottom divider ──────────────────────────────────────────────────── - printf '%s│%s│\n' $p $HBR - - # ── Keybind hint ────────────────────────────────────────────────────── - # string pad is width-aware (arrows count as 1 column) - set -l hint " ↑↓/kj move ←→/hl set Enter sub-cats Tab pg q quit" - printf '%s│%s%s%s│\n' $p $c_dim (string pad -r -w $iw -- $hint) $c_reset - - # ── Bottom border ───────────────────────────────────────────────────── - printf '%s└%s┘\n' $p $HBR -end diff --git a/functions/__config_settings_draw_subcat.fish b/functions/__config_settings_draw_subcat.fish deleted file mode 100644 index 6920ece..0000000 --- a/functions/__config_settings_draw_subcat.fish +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (C) 2026 Rootiest -# SPDX-License-Identifier: AGPL-3.0-or-later - -# SYNOPSIS -# __config_settings_draw_subcat -# -# DESCRIPTION -# Renders the sub-category drill-down page for one C1-C6 category: -# the category's own toggle at the top (still meaningful as the cascade -# default for its sub-categories), then one row per sub-category from -# __config_settings_subcats, sized dynamically instead of the fixed -# 6-row layout __config_settings_draw uses for the category list. -# Follows the same width-tier and center-padding conventions as -# __config_settings_draw so the panel doesn't visibly jump between the -# two pages. -# -# Label and description fields are defensively truncated to their field -# width before padding (string pad only ever grows a string, never -# shrinks it) -- sub-category labels/descriptions are static data from -# __config_settings_subcats, not authored per width-tier the way -# __config_settings_draw's own category descriptions are, so a couple of -# them are longer than the narrower tiers' fields (e.g. "Notifications" -# is 13 chars against a 12-char label field; several descriptions run -# well past the 17-char field at the narrowest tier). Truncating keeps -# the box perfectly rectangular in every case instead of only in the -# cases the static text happens to fit. -# -# ARGUMENTS -# cur_row 0-based highlighted row (0 = the category's own toggle; -# 1..N = sub-category rows) -# cur_scope "universal" or "session" -# category_var One of the six __fish_config_op_ names -# -# EXIT STATUS -# 0 Always -# -# EXAMPLE -# __config_settings_draw_subcat 1 universal __fish_config_op_aliases -function __config_settings_draw_subcat - set -l cur_row $argv[1] - set -l cur_scope $argv[2] - set -l category_var $argv[3] - - __fish_palette - - set -l rows (__config_settings_subcats $category_var) - set -l n (count $rows) - - # ── Width tier: matches __config_settings_draw's 6-col-per-side steps ── - set -l iw (__config_settings_frame width) - set -l HBR (string repeat -n $iw '─') - set -l p (string repeat -n (math --scale=0 "max(0, ($COLUMNS - ($iw + 2)) / 2)") ' ') - - # Label field is 13 wide (one wider than __config_settings_draw's 12) -- - # the longest real sub-category label ("Notifications") is 13 chars. The - # description field absorbs the difference inside the frame - # (field_w = iw - 21 - label_w = iw - 34), so every row still totals iw+2. - - set -l cat_label (string replace -r '^__fish_config_op_' '' -- $category_var) - # Scope indicator: toggling a row on this page writes -U (Universal, - # persistent) or -g (Session, this-shell-only) -- the title must say - # which, since it isn't otherwise visible anywhere on the page. - set -l scope_label Universal - test "$cur_scope" = session; and set scope_label Session - # Title layout is "┌─ Sub-categories: