feat(config-settings): replace the ANSI renderer with a curses front-end
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 behind608b022,4210f3b,93fc5e0and3c4f720unreachable. 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).
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_diff_redraw <old_joined> <new_joined>
|
||||
#
|
||||
# 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[<n>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
|
||||
@@ -1,155 +0,0 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_draw <cur_row> <cur_scope> <var1> ... <var7>
|
||||
#
|
||||
# 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
|
||||
@@ -1,120 +0,0 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_draw_subcat <cur_row> <cur_scope> <category_var>
|
||||
#
|
||||
# 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_<category> 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: <label> (<scope>) ───┐"; the
|
||||
# dash count absorbs every visible char added around cat_label so the
|
||||
# line still totals iw+2, matching the surrounding box exactly. The
|
||||
# frame derives it from the segment's visible width, so it no longer
|
||||
# has to be hand-verified here.
|
||||
__config_settings_frame title $iw $p \
|
||||
"$c_head Sub-categories: $cat_label ($scope_label)$c_reset "
|
||||
|
||||
printf '%s│%s│\n' $p $HBR
|
||||
|
||||
# Row 0: the category's own toggle, still meaningful as the cascade
|
||||
# default any DEFAULT-valued sub-category below falls back to.
|
||||
set -l cat_val (__config_settings_get_val $category_var $cur_scope)
|
||||
set -l cat_desc "cascade default"
|
||||
if test $iw -ge 68
|
||||
set cat_desc "default for all sub-cats below"
|
||||
end
|
||||
if test $iw -ge 72
|
||||
set cat_desc "default for all sub-categories below"
|
||||
end
|
||||
# `cut` for the same reason as the sub-category rows below. It also
|
||||
# truncates the label, which the old code did not -- provably inert here,
|
||||
# since "(category)" is a 10-char literal against a 13-wide field.
|
||||
__config_settings_frame row $iw $p \
|
||||
(__config_settings_frame cursor 0 $cur_row) \
|
||||
"(category)" 13 \
|
||||
(__config_settings_frame badge $cat_val) \
|
||||
$cat_desc cut
|
||||
|
||||
printf '%s│ %s │\n' $p (string repeat -n (math $iw - 6) '─')
|
||||
|
||||
for i in (seq 1 $n)
|
||||
set -l fields (string split -- \t $rows[$i])
|
||||
set -l label $fields[2]
|
||||
set -l desc $fields[3]
|
||||
set -l subcat_var "$category_var"_(string replace -a -- '-' '_' $fields[1])
|
||||
set -l val (__config_settings_get_val $subcat_var $cur_scope)
|
||||
# `cut`: these labels and descriptions are static data from
|
||||
# __config_settings_subcats, not authored per width tier the way
|
||||
# __config_settings_draw's are, and several run well past the
|
||||
# narrower tiers' fields. `string pad` only ever grows a string, so
|
||||
# they must be truncated before padding or the box stops being
|
||||
# rectangular. This is the divergence the DESCRIPTION block above
|
||||
# documents -- do not "simplify" it to `pad`.
|
||||
__config_settings_frame row $iw $p \
|
||||
(__config_settings_frame cursor $i $cur_row) \
|
||||
$label 13 \
|
||||
(__config_settings_frame badge $val) \
|
||||
$desc cut
|
||||
end
|
||||
|
||||
printf '%s│%s│\n' $p $HBR
|
||||
set -l hint " ↑↓/kj move ←→/hl set Esc back q quit"
|
||||
printf '%s│%s%s%s│\n' $p $c_dim (string pad -r -w $iw -- $hint) $c_reset
|
||||
printf '%s└%s┘\n' $p $HBR
|
||||
end
|
||||
@@ -1,159 +0,0 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_draw_value <cur_row> <page>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Renders a config-settings value page (Sponge or Paths) as exactly 16
|
||||
# lines, matching the box geometry of the opinionated toggle page so the
|
||||
# caller's wrap-aware erase (panel_h=16) is unchanged. Each row shows a
|
||||
# label, a badge, and the variable's current value (or its default hint).
|
||||
# Toggle-type rows (the two sponge booleans) reuse the ON/OFF/DEFAULT badge;
|
||||
# value rows (path/int/list/string) show a type badge and the live value.
|
||||
#
|
||||
# Page width follows the same $COLUMNS tiers as the toggle page for a
|
||||
# consistent look; exact width is not required for the erase (the erase
|
||||
# over-clears to end of screen using the 78-col worst case).
|
||||
#
|
||||
# ARGUMENTS
|
||||
# cur_row 0-based highlighted row within the page
|
||||
# page "sponge" or "paths"
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Always
|
||||
#
|
||||
# EXAMPLE
|
||||
# __config_settings_draw_value 0 sponge
|
||||
function __config_settings_draw_value
|
||||
set -l cur_row $argv[1]
|
||||
set -l page $argv[2]
|
||||
# Inline-edit state: when argv[3] is "edit", the cur_row field renders the
|
||||
# live input buffer (argv[4]) with a caret instead of its stored value.
|
||||
set -l edit_mode $argv[3]
|
||||
set -l edit_buf $argv[4]
|
||||
|
||||
__fish_palette
|
||||
|
||||
# ── Page row metadata (parallel lists) ────────────────────────────────
|
||||
set -l title
|
||||
set -l vars
|
||||
set -l labels
|
||||
set -l types
|
||||
set -l hints # default hint shown when unset
|
||||
set -l active_idx
|
||||
if test $page = sponge
|
||||
set title "Sponge Settings"
|
||||
set active_idx 2
|
||||
set vars sponge_delay sponge_purge_only_on_exit sponge_allow_previously_successful sponge_successful_exit_codes __fish_sponge_extra_sensitive
|
||||
set labels Delay "Purge@exit" "Allow prev" "OK codes" "Extra secret"
|
||||
set types int bool bool list list
|
||||
set hints 2 false true 0 "(none)"
|
||||
else
|
||||
set title "Path Settings"
|
||||
set active_idx 3
|
||||
set vars __fish_scrollback_history_dir __fish_scrollback_history_max_files __fish_user_dots_path __fish_user_dots_symlink
|
||||
set labels "Log dir" "Log max" "Dots path" "Dots link"
|
||||
set types path int path bool
|
||||
set hints "~/.terminal_history" 100 "(default)" on
|
||||
end
|
||||
set -l nrows (count $vars)
|
||||
|
||||
# ── Width tier (same thresholds as the toggle page) ───────────────────
|
||||
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)") ' ')
|
||||
|
||||
# ── Line 1: top border with title ─────────────────────────────────────
|
||||
__config_settings_frame title $iw $p "$c_head $title$c_reset "
|
||||
|
||||
# ── Line 2: page-tab header ───────────────────────────────────────────
|
||||
printf '%s│%s│\n' $p (__config_settings_pagetab $active_idx $iw)
|
||||
|
||||
# ── Line 3: divider ───────────────────────────────────────────────────
|
||||
printf '%s│%s│\n' $p $HBR
|
||||
|
||||
# ── Value rows ────────────────────────────────────────────────────────
|
||||
for i in (seq 0 (math $nrows - 1))
|
||||
set -l idx (math $i + 1)
|
||||
set -l var $vars[$idx]
|
||||
set -l label $labels[$idx]
|
||||
set -l type $types[$idx]
|
||||
set -l hint $hints[$idx]
|
||||
|
||||
# Badge (7 visible cols) + value field
|
||||
set -l badge
|
||||
set -l field
|
||||
if test $type = bool
|
||||
# Booleans store true/false (sponge convention); unset = DEFAULT.
|
||||
# The frame is told this page's vocabulary rather than merging
|
||||
# true/on: a hand-set "on" here must keep rendering DEFAULT.
|
||||
set badge (__config_settings_frame badge (__config_settings_get_raw $var) true false)
|
||||
set field "default: $hint"
|
||||
else
|
||||
set -l raw (__config_settings_get_raw $var)
|
||||
if test "$raw" = DEFAULT
|
||||
set badge "$c_dim""DEFAULT$c_reset"
|
||||
set field "$hint"
|
||||
else
|
||||
switch $type
|
||||
case path
|
||||
set badge "$c_ok"" PATH $c_reset"
|
||||
case int
|
||||
set badge "$c_ok"" INT $c_reset"
|
||||
case list
|
||||
set badge "$c_ok"" LIST $c_reset"
|
||||
case '*'
|
||||
set badge "$c_ok"" STR $c_reset"
|
||||
end
|
||||
set field "$raw"
|
||||
end
|
||||
end
|
||||
|
||||
# Inline edit: render the active row's field as the live buffer with a
|
||||
# block caret, tail-anchored so the caret stays visible as text grows.
|
||||
if test "$edit_mode" = edit -a $i -eq $cur_row
|
||||
set -l fw (math $iw - 33)
|
||||
set -l avail (math $fw - 1)
|
||||
set -l shown "$edit_buf"
|
||||
set -l blen (string length -- "$edit_buf")
|
||||
if test $blen -gt $avail
|
||||
set shown (string sub -s (math $blen - $avail + 1) -- "$edit_buf")
|
||||
end
|
||||
set badge "$c_head"" EDIT $c_reset"
|
||||
set field "$shown"(set_color --reverse)" "(set_color normal)
|
||||
end
|
||||
|
||||
# `shorten` ellipsises the value: unlike the toggle page's per-tier
|
||||
# descriptions, these fields hold arbitrary user values. The edit row
|
||||
# is the exception -- its field is already length-constrained above
|
||||
# and carries a reverse-video caret whose escapes `string shorten`
|
||||
# miscounts, so it pads directly.
|
||||
set -l fit shorten
|
||||
if test "$edit_mode" = edit -a $i -eq $cur_row
|
||||
set fit pad
|
||||
end
|
||||
__config_settings_frame row $iw $p \
|
||||
(__config_settings_frame cursor $i $cur_row) \
|
||||
$label 12 $badge $field $fit
|
||||
end
|
||||
|
||||
# ── Pad blank rows so chrome(6) + nrows + blanks = 16 ─────────────────
|
||||
set -l blanks (math 10 - $nrows)
|
||||
for i in (seq 1 $blanks)
|
||||
printf '%s│%s│\n' $p (string repeat -n $iw ' ')
|
||||
end
|
||||
|
||||
# ── Bottom divider ────────────────────────────────────────────────────
|
||||
printf '%s│%s│\n' $p $HBR
|
||||
|
||||
# ── Hint line (changes while editing) ─────────────────────────────────
|
||||
set -l hint_line " ↑↓ move Enter edit ←/h clear Tab page q quit"
|
||||
if test "$edit_mode" = edit
|
||||
set hint_line " type value Enter save Esc cancel ⌫ delete"
|
||||
end
|
||||
printf '%s│%s%s%s│\n' $p $c_dim (string pad -r -w $iw -- $hint_line) $c_reset
|
||||
|
||||
# ── Bottom border ─────────────────────────────────────────────────────
|
||||
printf '%s└%s┘\n' $p $HBR
|
||||
end
|
||||
@@ -1,155 +0,0 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_frame width
|
||||
# __config_settings_frame title <iw> <p> <segment>
|
||||
# __config_settings_frame badge <value> [<on_word> <off_word>]
|
||||
# __config_settings_frame cursor <row> <cur_row>
|
||||
# __config_settings_frame row <iw> <p> <curs> <label> <label_w> <badge> <field> <fit>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# The pieces of the config-settings panel that every page draws identically:
|
||||
# the width tier, the title border, the ON/OFF/DEFAULT badge, the cursor cell
|
||||
# and one table row. __config_settings_draw, __config_settings_draw_subcat
|
||||
# and __config_settings_draw_value each keep their own sequence of lines and
|
||||
# their own per-tier text; only these shared computations live here.
|
||||
#
|
||||
# Deliberately owns no page height. Every verb prints exactly one line or one
|
||||
# fragment, so how tall a page is remains a property of its caller's line
|
||||
# sequence: __config_settings_draw and __config_settings_draw_value are a
|
||||
# fixed 16 lines, __config_settings_draw_subcat is 7 + <sub-category count>,
|
||||
# and config-settings.fish's wrap-aware erase (\e[<N>A\e[J, sized from
|
||||
# panel_h) depends on that difference surviving exactly as it is.
|
||||
#
|
||||
# Row geometry. A row is
|
||||
# │ + 2 spaces + cursor(2) + label(label_w) + " [ " + badge(7) + " ] "
|
||||
# + field(field_w) + 3 spaces + │
|
||||
# which totals 23 + label_w + field_w and must equal iw + 2. So
|
||||
# field_w = iw - 21 - label_w
|
||||
# and that single sum reproduces both hand-maintained constants: label_w 12
|
||||
# gives iw-33 (the toggle and value pages), label_w 13 gives iw-34 (the
|
||||
# sub-category page, whose description field absorbs its wider label).
|
||||
#
|
||||
# Title geometry. "┌─" + segment + dashes + "┐" totals iw + 2, so
|
||||
# dashes = iw - visible(segment) - 1
|
||||
# The segment arrives already coloured because the three pages place their
|
||||
# set_color reset at different byte offsets around the same visible text
|
||||
# (__config_settings_draw resets after the trailing space, the other two
|
||||
# before it). That difference is invisible on screen and visible to cmp, so
|
||||
# each page keeps its own bytes while sharing the arithmetic; --visible
|
||||
# discounts the escapes.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# width No arguments. Prints the inner width for the current $COLUMNS:
|
||||
# >= 90 -> 76, >= 86 -> 72, >= 82 -> 68, otherwise 50.
|
||||
# title iw, the centering prefix, and the pre-coloured title segment.
|
||||
# badge The stored value, then optionally the words this page uses for true
|
||||
# and false (default on/off; the value pages store true/false).
|
||||
# Anything else renders DEFAULT.
|
||||
# cursor This row's index and the highlighted row's index.
|
||||
# row iw, centering prefix, cursor cell, label, label field width,
|
||||
# rendered badge, field text, and the fit policy:
|
||||
# pad pad label and field, never shrink either
|
||||
# cut truncate both to their field width, then pad
|
||||
# shorten pad the label, ellipsise the field with string shorten
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Verb recognised
|
||||
# 1 Unknown verb
|
||||
#
|
||||
# RETURNS
|
||||
# The requested panel line or fragment, printed to stdout
|
||||
#
|
||||
# NOTES
|
||||
# `string pad` only ever grows a string, never shrinks it. That is the whole
|
||||
# reason `cut` and `shorten` exist: a caller whose text can exceed its field
|
||||
# must shrink it first, or the box stops being rectangular.
|
||||
#
|
||||
# `pad` is not "the default when you don't care". __config_settings_draw's
|
||||
# descriptions are authored per width tier to fit their field exactly -- at
|
||||
# every tier the longest is precisely the field width -- so applying `cut`
|
||||
# there would be a byte-for-byte no-op that silently discards that property
|
||||
# and passes the render golden. Which policy a page uses is a real decision,
|
||||
# named at each call site. See tests/config-settings-render.fish.
|
||||
#
|
||||
# EXAMPLE
|
||||
# set -l iw (__config_settings_frame width)
|
||||
# __config_settings_frame row $iw $p (__config_settings_frame cursor 0 0) \
|
||||
# Aliases 12 (__config_settings_frame badge on) "cmd shadows" pad
|
||||
function __config_settings_frame
|
||||
switch $argv[1]
|
||||
# ── Width tier: 6-col buffer per side before stepping up ──────────
|
||||
case width
|
||||
if test "$COLUMNS" -ge 90
|
||||
echo 76
|
||||
else if test "$COLUMNS" -ge 86
|
||||
echo 72
|
||||
else if test "$COLUMNS" -ge 82
|
||||
echo 68
|
||||
else
|
||||
echo 50
|
||||
end
|
||||
|
||||
# ── Title border: ┌─<segment><dashes>┐ ────────────────────────────
|
||||
case title
|
||||
set -l iw $argv[2]
|
||||
set -l vis (string length --visible -- "$argv[4]")
|
||||
set -l dashes (math "max(0, $iw - $vis - 1)")
|
||||
printf '%s┌─%s%s┐\n' $argv[3] "$argv[4]" (string repeat -n $dashes '─')
|
||||
|
||||
# ── Badge: 7 visible columns, coloured ────────────────────────────
|
||||
# The truthy/falsy words are arguments rather than one merged
|
||||
# vocabulary: merging would make a hand-set "on" in a true/false
|
||||
# variable render as ON where it renders DEFAULT today.
|
||||
case badge
|
||||
set -l on_word on
|
||||
set -l off_word off
|
||||
if test (count $argv) -ge 4
|
||||
set on_word $argv[3]
|
||||
set off_word $argv[4]
|
||||
end
|
||||
switch "$argv[2]"
|
||||
case $on_word
|
||||
printf '%s ON%s' (set_color green) (set_color normal)
|
||||
case $off_word
|
||||
printf '%sOFF %s' (set_color red) (set_color normal)
|
||||
case '*'
|
||||
printf '%sDEFAULT%s' (set_color brblack) (set_color normal)
|
||||
end
|
||||
|
||||
# ── Cursor cell: 2 visible columns ────────────────────────────────
|
||||
case cursor
|
||||
if test $argv[2] -eq $argv[3]
|
||||
printf '%s▶%s ' (set_color --bold magenta) (set_color normal)
|
||||
else
|
||||
printf ' '
|
||||
end
|
||||
|
||||
# ── One table row ─────────────────────────────────────────────────
|
||||
case row
|
||||
set -l iw $argv[2]
|
||||
set -l p $argv[3]
|
||||
set -l curs $argv[4]
|
||||
set -l label "$argv[5]"
|
||||
set -l label_w $argv[6]
|
||||
set -l badge $argv[7]
|
||||
set -l field "$argv[8]"
|
||||
set -l fit $argv[9]
|
||||
set -l field_w (math $iw - 21 - $label_w)
|
||||
switch $fit
|
||||
case cut
|
||||
set label (string sub -l $label_w -- "$label")
|
||||
set field (string sub -l $field_w -- "$field")
|
||||
case shorten
|
||||
set field (string shorten -m $field_w -- "$field")
|
||||
end
|
||||
printf '%s│ %s%s [ %s ] %s │\n' $p $curs \
|
||||
(string pad -r -w $label_w -- "$label") $badge \
|
||||
(string pad -r -w $field_w -- "$field")
|
||||
|
||||
case '*'
|
||||
echo "__config_settings_frame: unknown verb '$argv[1]'" >&2
|
||||
return 1
|
||||
end
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_pagetab <active_idx> <iw>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Renders the four-page tab strip used as the header line of every
|
||||
# config-settings page. Pages: 0 Universal, 1 Session, 2 Sponge, 3 Paths.
|
||||
# The active page is marked with a filled bullet and bold text; the others
|
||||
# with a hollow bullet. The returned string is padded to exactly <iw>
|
||||
# printable columns (the caller adds the │ │ border and center offset).
|
||||
#
|
||||
# ARGUMENTS
|
||||
# active_idx 0–3, the active page index
|
||||
# iw inner width in columns to pad the strip to
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Always
|
||||
#
|
||||
# RETURNS
|
||||
# The rendered tab strip, printed to stdout (no trailing newline beyond printf's)
|
||||
#
|
||||
# EXAMPLE
|
||||
# set strip (__config_settings_pagetab 2 76)
|
||||
function __config_settings_pagetab
|
||||
set -l active $argv[1]
|
||||
set -l iw $argv[2]
|
||||
|
||||
__fish_palette
|
||||
set -l names Universal Session Sponge Paths
|
||||
|
||||
set -l strip ' '
|
||||
for i in (seq 0 3)
|
||||
set -l idx (math $i + 1)
|
||||
if test $i -eq $active
|
||||
set strip "$strip$c_hi●$names[$idx]$c_reset "
|
||||
else
|
||||
set strip "$strip○$names[$idx] "
|
||||
end
|
||||
end
|
||||
# string pad is width-aware: color escapes count as 0 columns, the bullets
|
||||
# and letters as their printable width.
|
||||
string pad -r -w $iw -- $strip
|
||||
end
|
||||
@@ -1,97 +0,0 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_read_key
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Reads a single keypress directly from the controlling terminal in raw
|
||||
# mode and echoes a normalized token naming the key. Bypasses fish's
|
||||
# read builtin, whose interactive line editor swallows Tab and arrow
|
||||
# keys (and prints a "read> " prompt) — none of which is usable for a TUI.
|
||||
#
|
||||
# The terminal is put into raw, no-echo mode with a 0.1s inter-byte timer
|
||||
# (stty raw -echo min 1 time 1) so a multi-byte escape sequence (e.g.
|
||||
# an arrow key, ESC [ A) is captured in one read while a lone key returns
|
||||
# promptly. Original terminal settings are always restored before return.
|
||||
#
|
||||
# In raw mode Ctrl-C does not raise SIGINT; it arrives as byte 3 (ETX),
|
||||
# which is reported as the token "quit".
|
||||
#
|
||||
# ARGUMENTS
|
||||
# (none)
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 A key was read
|
||||
# 1 The terminal could not be put into raw mode (stdin is not a TTY)
|
||||
#
|
||||
# RETURNS
|
||||
# One token, printed to stdout:
|
||||
# up down left right arrow keys
|
||||
# space tab backtab enter escape backspace
|
||||
# quit Ctrl-C (byte 3) in raw mode
|
||||
# <char> any other single printable character
|
||||
# "" nothing decodable was read
|
||||
#
|
||||
# EXAMPLE
|
||||
# set -l key (__config_settings_read_key)
|
||||
# or return # not a TTY — bail
|
||||
# switch $key
|
||||
# case up; echo "moved up"
|
||||
# case space; echo "toggled"
|
||||
# end
|
||||
function __config_settings_read_key
|
||||
# Snapshot current terminal settings; failure means stdin is not a TTY.
|
||||
set -l saved (stty -g </dev/tty 2>/dev/null)
|
||||
or return 1
|
||||
|
||||
# Raw, no-echo. min 0 / time 3: return after 0.3s even with no bytes (poll
|
||||
# interval for resize detection), or immediately when any bytes arrive.
|
||||
# Escape sequences (e.g. arrow keys) arrive fast enough to land in one read.
|
||||
stty raw -echo min 0 time 3 </dev/tty 2>/dev/null
|
||||
|
||||
# One read() of up to 3 bytes — covers ESC [ A style sequences. od emits
|
||||
# the bytes as space-separated decimal codes.
|
||||
set -l codes (dd if=/dev/tty bs=3 count=1 2>/dev/null \
|
||||
| od -An -tu1 2>/dev/null | string trim | string split -n ' ')
|
||||
|
||||
# Restore the terminal before doing anything else.
|
||||
stty $saved </dev/tty 2>/dev/null
|
||||
|
||||
switch (string join ' ' $codes)
|
||||
case '27 91 65'
|
||||
echo up
|
||||
case '27 91 66'
|
||||
echo down
|
||||
case '27 91 67'
|
||||
echo right
|
||||
case '27 91 68'
|
||||
echo left
|
||||
case '27 91 90'
|
||||
echo backtab
|
||||
case 27
|
||||
echo escape
|
||||
case 9
|
||||
echo tab
|
||||
case 32
|
||||
echo space
|
||||
case 10 13
|
||||
echo enter
|
||||
case 8 127
|
||||
echo backspace
|
||||
case 3
|
||||
echo quit
|
||||
case ''
|
||||
echo ''
|
||||
case '*'
|
||||
# Single printable byte → emit its character; ignore stray
|
||||
# multi-byte sequences we do not recognise. The two-step octal
|
||||
# form avoids fish mangling a one-shot '\\%03o' format string.
|
||||
if test (count $codes) -eq 1
|
||||
set -l oct (printf '%03o' $codes[1])
|
||||
printf '%b\n' "\\$oct"
|
||||
else
|
||||
echo ''
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# DEPENDENCIES
|
||||
# __config_settings_get_val, __config_settings_get_raw,
|
||||
# __config_settings_subcats
|
||||
#
|
||||
# SYNOPSIS
|
||||
# __config_settings_state
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Dumps everything scripts/config-settings-tui.py needs to render the
|
||||
# settings TUI: the current value of every opinionated-component,
|
||||
# sponge, scrollback and user-dots variable, plus the sub-category
|
||||
# taxonomy from __config_settings_subcats.
|
||||
#
|
||||
# Two record types, separated by RS (0x1e), fields separated by US
|
||||
# (0x1f). Both are ASCII control characters, so no value these variables
|
||||
# can hold needs escaping on the way through:
|
||||
#
|
||||
# var<US><scope><US><name><US><value>
|
||||
# sub<US><category_var><US><slug><US><label><US><description>
|
||||
#
|
||||
# Only variables that are actually set are emitted; the TUI renders an
|
||||
# absent variable as DEFAULT. That is what keeps the variable list out of
|
||||
# this function -- names are discovered with `set --names` and filtered by
|
||||
# prefix, so a new sub-category needs no edit here, only in
|
||||
# __config_settings_subcats.
|
||||
#
|
||||
# Toggle variables are dumped once per scope (universal and session), since
|
||||
# the TUI edits those scopes independently. Sponge and Paths variables are
|
||||
# universal-only and dumped as the value the running shell resolves,
|
||||
# space-joined for list variables.
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Always
|
||||
#
|
||||
# RETURNS
|
||||
# The RS/US-separated state dump, printed to stdout
|
||||
#
|
||||
# EXAMPLE
|
||||
# __config_settings_state | string split \x1e
|
||||
function __config_settings_state --description 'Dump config-settings state for the curses TUI'
|
||||
# ── Variable values ───────────────────────────────────────────────────
|
||||
for name in (set --names)
|
||||
switch $name
|
||||
case '__fish_config_op_registry_*'
|
||||
# conf.d data table, not a setting -- shares the op_ prefix.
|
||||
continue
|
||||
case '__fish_config_op_*' __fish_config_opinionated
|
||||
# Toggles: both scopes, independently editable.
|
||||
for scope in universal session
|
||||
set -l val (__config_settings_get_val $name $scope)
|
||||
test "$val" = DEFAULT; and continue
|
||||
printf '%s\x1f%s\x1f%s\x1f%s\x1e' var $scope $name $val
|
||||
end
|
||||
case 'sponge_*' __fish_sponge_extra_sensitive \
|
||||
'__fish_scrollback_history_*' __fish_user_dots_path \
|
||||
__fish_user_dots_symlink
|
||||
# Value rows: universal-only, list variables space-joined.
|
||||
set -l val (__config_settings_get_raw $name)
|
||||
test "$val" = DEFAULT; and continue
|
||||
printf '%s\x1f%s\x1f%s\x1f%s\x1e' var universal $name $val
|
||||
end
|
||||
end
|
||||
|
||||
# ── Sub-category taxonomy ─────────────────────────────────────────────
|
||||
for cvar in __fish_config_op_aliases __fish_config_op_autoexec \
|
||||
__fish_config_op_overrides __fish_config_op_integrations \
|
||||
__fish_config_op_logging __fish_config_op_greeting
|
||||
for row in (__config_settings_subcats $cvar)
|
||||
set -l f (string split -- \t $row)
|
||||
printf '%s\x1f%s\x1f%s\x1f%s\x1f%s\x1e' sub $cvar $f[1] $f[2] $f[3]
|
||||
end
|
||||
end
|
||||
end
|
||||
+82
-377
@@ -4,6 +4,10 @@
|
||||
# CATEGORY
|
||||
# 14-miscellaneous
|
||||
#
|
||||
# DEPENDENCIES
|
||||
# __fish_palette, __config_settings_state, __config_settings_apply,
|
||||
# __config_settings_set_value, python3
|
||||
#
|
||||
# SYNOPSIS
|
||||
# config-settings [-h | --help]
|
||||
#
|
||||
@@ -22,49 +26,55 @@
|
||||
# Toggle rows use ← / → (or h / l) to step OFF ← DEFAULT → ON; DEFAULT erases
|
||||
# the variable so the master switch / built-in default applies. On the
|
||||
# Universal/Session pages, Enter on a category row (C1–C6) opens that
|
||||
# category's sub-category drill-down page for finer-grained toggles;
|
||||
# Escape backs out to the category list. Value rows
|
||||
# (Sponge, Paths) use Enter to edit inline; ← / h clears to default. List rows
|
||||
# (e.g. Extra secret, OK codes) accept values separated by commas and/or
|
||||
# whitespace — "A, B", "A,B" and "A B" all yield the same two entries.
|
||||
# Tab / Shift-Tab cycle forward / backward through pages.
|
||||
# Changes apply immediately — no confirm step. Always available regardless of
|
||||
# __fish_config_opinionated state.
|
||||
# category's sub-category drill-down page, which leads with the category's
|
||||
# own toggle; Escape backs out. Value rows (Sponge, Paths) use Enter to edit
|
||||
# inline and ← / h to reset to the row default; committing a blank edit does
|
||||
# the same. List rows (e.g. Extra secret, OK codes) accept values separated
|
||||
# by commas and/or whitespace — "A, B", "A,B" and "A B" all yield the same
|
||||
# two entries. Tab / Shift-Tab cycle through pages.
|
||||
#
|
||||
# `/` filters the current page on label and description. On the Universal and
|
||||
# Session pages the filter also 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.
|
||||
#
|
||||
# Edits are collected while the TUI runs and applied in one batch when it
|
||||
# exits, via __config_settings_apply and __config_settings_set_value. The
|
||||
# status bar shows a pending count. This is a deliberate consequence of the
|
||||
# renderer being a child process: a child cannot reach into its parent shell
|
||||
# to `set -g`, so the Session page's edits come back as a fish script the
|
||||
# function sources on exit, and the Universal page rides the same path for
|
||||
# consistency. Always available regardless of __fish_config_opinionated state.
|
||||
#
|
||||
# The Sponge and Paths pages always write universal variables — these are
|
||||
# persistent, set-and-forget settings with no per-session scope. Editing a
|
||||
# scrollback row updates both the __fish_scrollback_history_* source-of-truth
|
||||
# variables and the exported SCROLLBACK_HISTORY_* mirrors, so the AUR/tmux/
|
||||
# zellij log wrappers (which read the exported names) see the change in the
|
||||
# running session.
|
||||
# running session. Editing Dots link re-runs __fish_user_dots_link.
|
||||
#
|
||||
# The panel adapts to the terminal width automatically, selecting from four
|
||||
# layout tiers (with a 6-column buffer on each side before stepping up to the
|
||||
# next tier) and horizontally centering the box. The panel redraws within
|
||||
# ~0.3 s of a terminal resize with no keypress required.
|
||||
#
|
||||
# COLUMNS >= 90 → 78-wide panel (most detail)
|
||||
# COLUMNS >= 86 → 74-wide panel
|
||||
# COLUMNS >= 82 → 70-wide panel
|
||||
# COLUMNS < 82 → 52-wide panel (default)
|
||||
# The panel is drawn by scripts/config-settings-tui.py using Python's stdlib
|
||||
# `curses`, which owns the cell arithmetic, the alternate screen and the
|
||||
# redraw diffing. It resizes with the terminal and needs no width tiers.
|
||||
#
|
||||
# Navigation:
|
||||
# ↑ ↓ / k j Move cursor
|
||||
# ← → / h l Toggle rows: OFF ← DEFAULT → ON
|
||||
# ← / h Value rows: clear to default
|
||||
# Enter Category rows (Universal/Session): open sub-category
|
||||
# drill-down page. Value rows: edit inline (Sponge /
|
||||
# Paths pages)
|
||||
# Escape Sub-category page: back out to the category list
|
||||
# ← / h Value rows: reset to default
|
||||
# Enter Category rows: open the sub-category page.
|
||||
# Value rows: edit inline
|
||||
# / Filter, sub-categories included
|
||||
# Escape Back out of a sub-category page, or clear the filter
|
||||
# Tab / S-Tab Next / previous page
|
||||
# q / Escape Exit
|
||||
# ? Help overlay
|
||||
# q Apply pending edits and exit
|
||||
#
|
||||
# ARGUMENTS
|
||||
# -h, --help Print usage and exit
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Exited normally (q or Escape pressed)
|
||||
# 1 Unknown flag passed
|
||||
# 0 Exited normally
|
||||
# 1 Unknown flag, no TTY, or python3/curses unavailable
|
||||
#
|
||||
# EXAMPLE
|
||||
# config-settings
|
||||
@@ -78,16 +88,19 @@ function config-settings --description 'Interactive TUI for managing fish config
|
||||
echo "$c_head""Usage:$c_reset $c_cmd""config-settings$c_reset $c_flag""[-h]$c_reset"
|
||||
echo
|
||||
echo " Interactive TUI for managing fish config settings."
|
||||
echo " Changes apply immediately — no confirm step required."
|
||||
echo " Edits are applied in one batch when the TUI exits."
|
||||
echo
|
||||
echo "$c_head""Navigation:$c_reset"
|
||||
echo " $c_flag↑ ↓$c_reset or $c_flag""k j$c_reset Move cursor up / down"
|
||||
echo " $c_flag← →$c_reset or $c_flag""h l$c_reset Toggle pages: OFF ← DEFAULT → ON"
|
||||
echo " $c_flag← →$c_reset or $c_flag""h l$c_reset Toggle rows: OFF ← DEFAULT → ON"
|
||||
echo " $c_flag""Enter$c_reset Open sub-category page (Universal / Session);"
|
||||
echo " edit value (Sponge / Paths pages)"
|
||||
echo " $c_flag← / h$c_reset Clear value to default (value rows)"
|
||||
echo " $c_flag← / h$c_reset Reset value to default (value rows)"
|
||||
echo " $c_flag/$c_reset Filter rows, sub-categories included"
|
||||
echo " $c_flag""Tab / S-Tab$c_reset Next / previous page"
|
||||
echo " $c_flag""q$c_reset / $c_flag""Esc$c_reset Exit"
|
||||
echo " $c_flag""Esc$c_reset Leave sub-category page / clear filter"
|
||||
echo " $c_flag?$c_reset Help overlay"
|
||||
echo " $c_flag""q$c_reset Apply pending edits and exit"
|
||||
echo
|
||||
echo "$c_head""Pages:$c_reset"
|
||||
echo " $c_flag""Universal$c_reset Toggles, persistent ($c_dim""set -U$c_reset)"
|
||||
@@ -102,354 +115,46 @@ function config-settings --description 'Interactive TUI for managing fish config
|
||||
end
|
||||
end
|
||||
|
||||
# ── Toggle-page variables (rows 0–6: 6 categories + master) ───────────
|
||||
set -l toggle_vars \
|
||||
__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
|
||||
|
||||
# ── Drill-down navigation state (Universal/Session pages only) ────────
|
||||
# in_subcat: 1 while viewing a category's sub-category page (Enter to
|
||||
# open, Escape to back out). subcat_row: cursor row within that page.
|
||||
set -l in_subcat 0
|
||||
set -l subcat_row 0
|
||||
|
||||
# ── Value-page row metadata (parallel: var / type) ────────────────────
|
||||
set -l sponge_vars sponge_delay sponge_purge_only_on_exit sponge_allow_previously_successful sponge_successful_exit_codes __fish_sponge_extra_sensitive
|
||||
set -l sponge_types int bool bool list list
|
||||
set -l sponge_labels Delay "Purge@exit" "Allow prev" "OK codes" "Extra secret"
|
||||
set -l paths_vars __fish_scrollback_history_dir __fish_scrollback_history_max_files __fish_user_dots_path __fish_user_dots_symlink
|
||||
set -l paths_types path int path bool
|
||||
set -l paths_labels "Log dir" "Log max" "Dots path" "Dots link"
|
||||
|
||||
# Reset/blank-edit target for each value row. A non-empty entry is written
|
||||
# verbatim (sponge reads sponge_delay / sponge_successful_exit_codes with no
|
||||
# fallback, so they must never be left unset); an empty entry erases the var
|
||||
# so its own built-in default applies (scrollback/dots paths and the
|
||||
# extra-sensitive list all tolerate being unset). Bool rows are not reset
|
||||
# through this path — they are a 2-state true/false with no unset state.
|
||||
set -l sponge_defaults 2 '' '' 0 ''
|
||||
set -l paths_defaults '' '' '' ''
|
||||
|
||||
# Rows per page index 0..3
|
||||
set -l page_rows 7 7 5 4
|
||||
|
||||
set -l cur_page 0 # 0=Universal 1=Session 2=Sponge 3=Paths
|
||||
set -l cur_row 0
|
||||
set -l panel_h 0 # real value set by the first dispatch call below
|
||||
set -l new_frame # captured by __cs_dispatch_draw
|
||||
set -l last_cols $COLUMNS
|
||||
|
||||
# ── Terminal setup ────────────────────────────────────
|
||||
printf '\e[?25l' # hide cursor
|
||||
trap 'printf "\e[?25h"; set -g __config_settings_exit 1' INT
|
||||
|
||||
# ── Draw dispatch (page 0/1 = toggle table; 2/3 = value page) ─────────
|
||||
# Captures the page's rendered lines into new_frame and derives panel_h
|
||||
# from their count. panel_h is never hand-set again: the sub-category
|
||||
# page is n+7 lines (2-6 sub-categories: 9-13 lines), never the
|
||||
# category list's fixed 16, and deriving it from the real output means
|
||||
# that fact can no longer drift out of sync with what got drawn.
|
||||
function __cs_dispatch_draw --no-scope-shadowing
|
||||
switch $cur_page
|
||||
case 0
|
||||
if test $in_subcat -eq 1
|
||||
set new_frame (__config_settings_draw_subcat $subcat_row universal $toggle_vars[(math $cur_row + 1)])
|
||||
else
|
||||
set new_frame (__config_settings_draw $cur_row universal $toggle_vars)
|
||||
end
|
||||
case 1
|
||||
if test $in_subcat -eq 1
|
||||
set new_frame (__config_settings_draw_subcat $subcat_row session $toggle_vars[(math $cur_row + 1)])
|
||||
else
|
||||
set new_frame (__config_settings_draw $cur_row session $toggle_vars)
|
||||
end
|
||||
case 2
|
||||
set new_frame (__config_settings_draw_value $cur_row sponge)
|
||||
case 3
|
||||
set new_frame (__config_settings_draw_value $cur_row paths)
|
||||
end
|
||||
set panel_h (count $new_frame)
|
||||
# ── Dependencies ──────────────────────────────────────
|
||||
# python3 with the curses module. That is stdlib on Arch, Fedora and a
|
||||
# full Debian/Ubuntu python3; python3-minimal alone does not carry
|
||||
# _curses, so both are checked rather than assumed from `type -q`.
|
||||
if not type -q python3
|
||||
echo "$c_err""config-settings requires python3.$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
__cs_dispatch_draw
|
||||
printf '%s\n' $new_frame
|
||||
|
||||
# ── Event loop ────────────────────────────────────────
|
||||
# __config_settings_read_key reads a single keypress from /dev/tty in raw
|
||||
# mode and returns a normalized token (up/down/tab/space/escape/quit or a
|
||||
# literal char). It bypasses fish's `read`, whose line editor swallows Tab
|
||||
# and arrow keys and prints a `read> ` prompt — unusable for a TUI.
|
||||
while true
|
||||
# Check for Ctrl-C signal (trap sets this flag during redraw, when the
|
||||
# terminal is briefly back in cooked mode and SIGINT can fire).
|
||||
if set -q __config_settings_exit
|
||||
set -eg __config_settings_exit
|
||||
break
|
||||
end
|
||||
|
||||
set -l key (__config_settings_read_key)
|
||||
or break # not a TTY — exit instead of spinning
|
||||
|
||||
set -l did_redraw 0
|
||||
|
||||
switch $key
|
||||
case up k
|
||||
if test $in_subcat -eq 1
|
||||
set subcat_row (math "max(0, $subcat_row - 1)")
|
||||
else
|
||||
set cur_row (math "max(0, $cur_row - 1)")
|
||||
end
|
||||
case down j
|
||||
if test $in_subcat -eq 1
|
||||
set -l n (count (__config_settings_subcats $toggle_vars[(math $cur_row + 1)]))
|
||||
set subcat_row (math "min($n, $subcat_row + 1)")
|
||||
else
|
||||
# Hoist the page index: fish cannot expand a command-substitution
|
||||
# index inside a quoted math string.
|
||||
set -l pidx (math $cur_page + 1)
|
||||
set cur_row (math "min($page_rows[$pidx] - 1, $cur_row + 1)")
|
||||
end
|
||||
case tab
|
||||
set cur_page (math "($cur_page + 1) % 4")
|
||||
set cur_row 0
|
||||
set in_subcat 0
|
||||
case backtab
|
||||
set cur_page (math "($cur_page + 3) % 4")
|
||||
set cur_row 0
|
||||
set in_subcat 0
|
||||
case right l
|
||||
if test $cur_page -le 1
|
||||
# Toggle page: step toward ON
|
||||
set -l scope universal
|
||||
test $cur_page -eq 1; and set scope session
|
||||
# Default: the category variable itself -- correct both
|
||||
# when not in a sub-category page at all, and when in
|
||||
# one but sitting on its row 0 (the category's own
|
||||
# toggle). Only row >= 1 of a sub-category page
|
||||
# resolves to a different, sub-category variable.
|
||||
# Hoist the category variable into a plain local first --
|
||||
# fish cannot expand a command-substitution index
|
||||
# ("$toggle_vars[(math ...)]") inside a quoted string
|
||||
# (same reason the down/j case above hoists $pidx).
|
||||
set -l cvar $toggle_vars[(math $cur_row + 1)]
|
||||
set -l varname $cvar
|
||||
if test $in_subcat -eq 1 -a $subcat_row -ne 0
|
||||
set -l rows (__config_settings_subcats $cvar)
|
||||
set -l fields (string split -- \t $rows[$subcat_row])
|
||||
set varname "$cvar"_(string replace -a -- '-' '_' $fields[1])
|
||||
end
|
||||
set -l cur_val (__config_settings_get_val $varname $scope)
|
||||
set -l next_val on
|
||||
test "$cur_val" = off; and set next_val DEFAULT
|
||||
__config_settings_apply $varname $scope $next_val
|
||||
else
|
||||
# Value pages: bool rows are 2-state (true/false). → sets
|
||||
# true. Sponge reads its bools with no fallback; the Paths
|
||||
# "Dots link" bool drives __fish_user_dots_link on change.
|
||||
set -l v_vars $sponge_vars
|
||||
set -l v_types $sponge_types
|
||||
if test $cur_page -eq 3
|
||||
set v_vars $paths_vars
|
||||
set v_types $paths_types
|
||||
end
|
||||
set -l ridx (math $cur_row + 1)
|
||||
if test "$v_types[$ridx]" = bool
|
||||
set -U $v_vars[$ridx] true 2>/dev/null
|
||||
test "$v_vars[$ridx]" = __fish_user_dots_symlink
|
||||
and __fish_user_dots_link
|
||||
end
|
||||
end
|
||||
case left h
|
||||
if test $cur_page -le 1
|
||||
set -l scope universal
|
||||
test $cur_page -eq 1; and set scope session
|
||||
# Same varname resolution as the right/l case above
|
||||
# (hoisted local -- see the comment there for why the
|
||||
# command-substitution index can't be inlined into the
|
||||
# quoted string directly).
|
||||
set -l cvar $toggle_vars[(math $cur_row + 1)]
|
||||
set -l varname $cvar
|
||||
if test $in_subcat -eq 1 -a $subcat_row -ne 0
|
||||
set -l rows (__config_settings_subcats $cvar)
|
||||
set -l fields (string split -- \t $rows[$subcat_row])
|
||||
set varname "$cvar"_(string replace -a -- '-' '_' $fields[1])
|
||||
end
|
||||
set -l cur_val (__config_settings_get_val $varname $scope)
|
||||
set -l next_val off
|
||||
test "$cur_val" = on; and set next_val DEFAULT
|
||||
__config_settings_apply $varname $scope $next_val
|
||||
else
|
||||
# Value pages: bool rows set false; other value rows reset to
|
||||
# their default (a literal value, or erase when the var
|
||||
# tolerates being unset — see sponge_defaults/paths_defaults).
|
||||
set -l v_vars $sponge_vars
|
||||
set -l v_types $sponge_types
|
||||
set -l v_defaults $sponge_defaults
|
||||
if test $cur_page -eq 3
|
||||
set v_vars $paths_vars
|
||||
set v_types $paths_types
|
||||
set v_defaults $paths_defaults
|
||||
end
|
||||
set -l ridx (math $cur_row + 1)
|
||||
set -l varname $v_vars[$ridx]
|
||||
set -l vtype $v_types[$ridx]
|
||||
if test "$vtype" = bool
|
||||
set -U $varname false 2>/dev/null
|
||||
test "$varname" = __fish_user_dots_symlink
|
||||
and __fish_user_dots_link
|
||||
else
|
||||
__config_settings_set_value $varname $vtype "$v_defaults[$ridx]"
|
||||
end
|
||||
end
|
||||
case enter
|
||||
if test $cur_page -le 1
|
||||
if test $in_subcat -eq 0 -a $cur_row -le 5
|
||||
set in_subcat 1
|
||||
set subcat_row 0
|
||||
end
|
||||
else if test $cur_page -ge 2
|
||||
set -l v_vars $sponge_vars
|
||||
set -l v_types $sponge_types
|
||||
set -l v_defaults $sponge_defaults
|
||||
if test $cur_page -eq 3
|
||||
set v_vars $paths_vars
|
||||
set v_types $paths_types
|
||||
set v_defaults $paths_defaults
|
||||
end
|
||||
set -l ridx (math $cur_row + 1)
|
||||
set -l varname $v_vars[$ridx]
|
||||
set -l vtype $v_types[$ridx]
|
||||
# Only path/int/list rows are editable; bool rows toggle with ←/→.
|
||||
if test "$vtype" != toggle -a "$vtype" != bool
|
||||
# Inline editor: edit in-place in the row's value field
|
||||
# using the raw key reader — no fish `read` / `read>`
|
||||
# prompt, and the panel cleans itself up on exit. The
|
||||
# buffer is pre-filled with the current value; clearing it
|
||||
# and pressing Enter reverts to the row's default.
|
||||
set -l page sponge
|
||||
test $cur_page -eq 3; and set page paths
|
||||
set -l buf (__config_settings_get_raw $varname)
|
||||
test "$buf" = DEFAULT; and set buf ""
|
||||
set -l committed 0
|
||||
# Full erase once to enter edit mode; per-keystroke
|
||||
# redraws below diff against the previous edit frame.
|
||||
set -l edit_frame (__config_settings_draw_value $cur_row $page edit "$buf")
|
||||
set -l prev_edit_frame
|
||||
set -l pml (math --scale=0 "($last_cols + 78) / 2")
|
||||
set -l eh (math --scale=0 "$panel_h * max(1, ceil($pml / $COLUMNS))")
|
||||
printf '\e[%dA\e[J' $eh
|
||||
printf '%s\n' $edit_frame
|
||||
set last_cols $COLUMNS
|
||||
while true
|
||||
set -l ek (__config_settings_read_key)
|
||||
or break
|
||||
switch $ek
|
||||
case enter
|
||||
set committed 1
|
||||
break
|
||||
case escape
|
||||
break
|
||||
case backspace
|
||||
set buf (string sub -s 1 -e -1 -- "$buf")
|
||||
case space
|
||||
set buf "$buf "
|
||||
case up down left right tab backtab quit ''
|
||||
# ignored while editing
|
||||
case '*'
|
||||
set buf "$buf$ek"
|
||||
end
|
||||
|
||||
set prev_edit_frame $edit_frame
|
||||
set edit_frame (__config_settings_draw_value $cur_row $page edit "$buf")
|
||||
if test (count $edit_frame) -eq (count $prev_edit_frame) -a "$COLUMNS" = "$last_cols" -a $COLUMNS -ge 52
|
||||
# `| string collect` is required on each join --
|
||||
# see the identical note in the main loop's
|
||||
# diff-path call.
|
||||
printf '\e[%dA' (count $edit_frame)
|
||||
__config_settings_diff_redraw (string join \n -- $prev_edit_frame | string collect) (string join \n -- $edit_frame | string collect)
|
||||
else
|
||||
set -l ph (count $prev_edit_frame)
|
||||
set -l pml (math --scale=0 "($last_cols + 78) / 2")
|
||||
set -l eh (math --scale=0 "$ph * max(1, ceil($pml / $COLUMNS))")
|
||||
printf '\e[%dA\e[J' $eh
|
||||
printf '%s\n' $edit_frame
|
||||
end
|
||||
set last_cols $COLUMNS
|
||||
end
|
||||
if test $committed -eq 1
|
||||
# Empty buffer reverts to the row default (a value, or
|
||||
# erase when the var tolerates being unset).
|
||||
if test -n "$buf"
|
||||
__config_settings_set_value $varname $vtype "$buf"
|
||||
else
|
||||
__config_settings_set_value $varname $vtype "$v_defaults[$ridx]"
|
||||
end
|
||||
end
|
||||
# Redraw the normal panel in place of the editor.
|
||||
set -l pml (math --scale=0 "($last_cols + 78) / 2")
|
||||
set -l eh (math --scale=0 "$panel_h * max(1, ceil($pml / $COLUMNS))")
|
||||
printf '\e[%dA\e[J' $eh
|
||||
set last_cols $COLUMNS
|
||||
__cs_dispatch_draw
|
||||
printf '%s\n' $new_frame
|
||||
set did_redraw 1
|
||||
end
|
||||
end
|
||||
case q Q quit
|
||||
break
|
||||
case escape
|
||||
if test $in_subcat -eq 1
|
||||
set in_subcat 0
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# Skip redraw entirely when the key reader timed out with no resize
|
||||
if test -z "$key" -a "$COLUMNS" = "$last_cols"
|
||||
continue
|
||||
end
|
||||
|
||||
# Skip redraw if the Enter handler already redrew (e.g. after path edit)
|
||||
if test $did_redraw -eq 1
|
||||
continue
|
||||
end
|
||||
|
||||
set -l old_h $panel_h
|
||||
set -l prev_frame $new_frame
|
||||
__cs_dispatch_draw
|
||||
|
||||
if test $panel_h -eq $old_h -a "$COLUMNS" = "$last_cols" -a $COLUMNS -ge 52
|
||||
# Diff path: geometry and width unchanged since the last frame --
|
||||
# move up without erasing, rewrite only the lines that changed.
|
||||
# `| string collect` is required on each join: command
|
||||
# substitution always re-splits on newlines, so without it
|
||||
# __config_settings_diff_redraw would receive many positional
|
||||
# arguments instead of the two joined strings it expects.
|
||||
printf '\e[%dA' $panel_h
|
||||
__config_settings_diff_redraw (string join \n -- $prev_frame | string collect) (string join \n -- $new_frame | string collect)
|
||||
else
|
||||
# Full-redraw path: resize, page switch, or subcat enter/exit --
|
||||
# same wrap-aware erase math as before, unchanged. 78 = widest
|
||||
# box (IW=76+2); the formula gives the worst-case old line width
|
||||
# for any tier drawn at last_cols.
|
||||
set -l prev_max_lw (math --scale=0 "($last_cols + 78) / 2")
|
||||
set -l erase_h (math --scale=0 "$old_h * max(1, ceil($prev_max_lw / $COLUMNS))")
|
||||
printf '\e[%dA\e[J' $erase_h
|
||||
printf '%s\n' $new_frame
|
||||
end
|
||||
set last_cols $COLUMNS
|
||||
if not python3 -c 'import curses' 2>/dev/null
|
||||
echo "$c_err""config-settings requires python3 with the curses module.$c_reset" >&2
|
||||
echo " Debian/Ubuntu: install the full $c_cmd""python3$c_reset package." >&2
|
||||
return 1
|
||||
end
|
||||
if not isatty stdout
|
||||
echo "$c_err""config-settings needs a terminal.$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
# ── Cleanup ───────────────────────────────────────────
|
||||
trap - INT # remove the signal handler
|
||||
set -l prev_max_lw (math --scale=0 "($last_cols + 78) / 2")
|
||||
set -l erase_h (math --scale=0 "$panel_h * max(1, ceil($prev_max_lw / $COLUMNS))")
|
||||
printf '\e[%dA\e[J' $erase_h # erase the panel (wrap-aware)
|
||||
printf '\e[?25h' # restore cursor
|
||||
functions --erase __cs_dispatch_draw
|
||||
set -l tui (dirname (status filename))/../scripts/config-settings-tui.py
|
||||
if not test -f $tui
|
||||
echo "$c_err""config-settings: missing $tui$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
# ── Run the TUI ───────────────────────────────────────
|
||||
# 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; the edits come back
|
||||
# as a fish script this function 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. `command` throughout: this repo's own aliases shadow rm.
|
||||
set -l work (command mktemp -d)
|
||||
__config_settings_state >$work/state
|
||||
|
||||
python3 $tui --state $work/state --emit $work/edits
|
||||
set -l rc $status
|
||||
|
||||
if test $rc -eq 0 -a -s $work/edits
|
||||
source $work/edits
|
||||
end
|
||||
|
||||
command rm -rf $work
|
||||
return $rc
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user