Files
MeshTerm/scripts/calculinux-console-font.sh
T
Jean-Pierre MartineauandClaude Fable 5 2f8f77be49 Review round: the standard palette, by JP's call
The console keeps its stock kernel palette - black background, classic
PC hues - instead of the custom setvtrgb remap. MESH_THEME_16 is
re-tuned against the stock slots, which hands back the map's own
language for free: clients wear bright pink (13), repeaters purple (5),
rooms cyan (6), sensors brown (3). The cost is one dark grey - muted,
faint and track all collapse onto slot 8. Heat quantizes on plain human
units (15 min / 1 h / 1 day). The custom design is archived, not
deleted: theme._VT_SLOTS_CUSTOM + MESHTERM_CUSTOM_PALETTE=1 on the font
script reinstalls it, with the parity test keeping the two in sync.

Undoing the remap on the device exposed a trap: setvtrgb overwrites the
kernel's DEFAULT colormap, so the OSC palette-reset "resets" straight
back to the custom values - the script's default mode now programs the
stock palette explicitly (verified live; specimen screenshot in
.claude/plans/p3-specimen-stock.png).

The header also follows JP's spec: the handheld brands the app -
"MeshTerm vX" - instead of naming a soldered port that never changes,
and the activity pulse takes whatever room remains.

Also root-causes the intermittent suite flake: Rich memoizes a parsed
Style's ANSI codes on first render (Style.parse is globally cached), so
a raw hex style first rendered by a truecolor console replays 38;2
verbatim on a standard console later in the process. The specimen's
nodes row now wears the type.* theme styles - which is also just
correct: the marks now match the names row. Six consecutive clean
full-suite runs.

Deferred at JP's request: the glyph substitution map (he will redo it),
the F-key lane design (awaits his explicit spec), the CLI fold gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 14:53:31 -04:00

425 lines
22 KiB
Bash

#!/bin/sh
# calculinux-console-font.sh -- build, install, and persist the "meshterm" console fonts
# and the 16-slot palette.
#
# MeshTerm runs on a Luckfox Lyra inside a ClockworkPi PicoCalc under Calculinux, drawing
# to the bare ILI9488 framebuffer console (fbcon). That console loads a single PSF font,
# and every font Calculinux ships is a classic 256-glyph VGA font -- so out of the box the
# panel cannot draw what MeshTerm's UI leans on hardest: braille charts (U+2800-U+28FF),
# the node/status marks, the P3 compact-icon marks, rounded panel corners, or the list
# cursor. This script synthesizes TWO 512-glyph PSF2 fonts:
#
# * meshterm.psf.gz 6x12 (Terminus base) -> 53x26 -- the installed default
# * meshterm8.psf.gz 6x8 (kernel font_6x8) -> 53x40 -- the A/B candidate
#
# Flip live with `setfont /usr/share/consolefonts/meshterm8.psf.gz` (and back with
# meshterm.psf.gz); persist the winner via FONT= in /etc/vconsole.conf.
#
# Why 512 and not more: fbcon caps a font at 512 glyphs. Braille alone is 256 and the base
# set is another 256 -- the budget is already full, so every extra mark is drawn into a
# DONOR slot (a pictograph MeshTerm never emits) whose codepoint is repointed. Choosing a
# donor means knowing its slot's FULL codepoint list: bases alias lookalikes onto one glyph
# (donating pi once erased Cyrillic pe), so a donor miss aborts the build and a keeper list
# is verified after it. The rounded corners and the midline ellipsis cost no glyph: they
# are aliased onto existing bitmaps.
#
# The 6x8 base is the Linux kernel's own font_6x8 (lib/fonts/font_6x8.c, GPL-2.0),
# embedded below -- CP437 coverage, so unlike the Terminus base it has NO Cyrillic;
# its keeper list drops the Cyrillic canary accordingly.
#
# Everything else is pure geometry, generated on-device; only the stock Terminus font and
# python3 are required. Run as root on the Lyra (serial console is fine):
#
# sh calculinux-console-font.sh
#
# Idempotent -- safe to re-run after a MeshTerm update or a font tweak. Also installs
# /etc/vtrgb + the meshterm-vtrgb boot oneshot (the 16-slot palette; see
# meshterm/ui/theme._VT_SLOTS -- a repo test pins the two to each other).
set -eu
FONT_NAME=meshterm
CONSOLEFONTS=/usr/share/consolefonts
BASE="$CONSOLEFONTS/ter-u12n.psf.gz" # stock Terminus 6x12, 256 glyphs
OUT="$CONSOLEFONTS/$FONT_NAME.psf.gz" # the 6x12 default
OUT8="$CONSOLEFONTS/${FONT_NAME}8.psf.gz" # the 6x8 A/B candidate
VCONSOLE=/etc/vconsole.conf
# --- preflight: fail early with a plain reason, never half-apply -----------------------
[ "$(id -u)" = 0 ] || { echo "error: run as root (writes $CONSOLEFONTS and $VCONSOLE)" >&2; exit 1; }
command -v python3 >/dev/null 2>&1 || { echo "error: python3 not found" >&2; exit 1; }
command -v setfont >/dev/null 2>&1 || { echo "error: setfont not found (install kbd tools)" >&2; exit 1; }
[ -f "$BASE" ] || { echo "error: base font not found: $BASE" >&2; exit 1; }
# --- build both fonts ------------------------------------------------------------------
# The generator is inlined (quoted heredoc, so the shell expands nothing) and reads its
# paths from the environment. It is the single source of truth for the synthesized glyphs.
echo "building $OUT (6x12) and $OUT8 (6x8) ..."
BASE="$BASE" OUT="$OUT" OUT8="$OUT8" python3 - <<'PYEOF'
import base64, os, struct, gzip
BASE = os.environ["BASE"]
OUT = os.environ["OUT"]
OUT8 = os.environ["OUT8"]
PSF2_MAGIC = 0x864AB572
# --- braille: 2-wide dot grid; the codepoint's low byte says which dots lit ------------
COLS = [[1, 2], [4, 5]]
BIT = {0: (0, 0), 1: (0, 1), 2: (0, 2), 6: (0, 3),
3: (1, 0), 4: (1, 1), 5: (1, 2), 7: (1, 3)}
#: Dot-row bands per cell height: 3px pitch at 6x12, 2px at 6x8.
BANDS12 = [[0, 1], [3, 4], [6, 7], [9, 10]]
BANDS8 = [[0, 1], [2, 3], [4, 5], [6, 7]]
def braille_glyph(value, bands, height):
rows = [0] * height
for bit in range(8):
if value >> bit & 1:
col, row = BIT[bit]
for x in COLS[col]:
for y in bands[row]:
rows[y] |= 0x80 >> x
return bytes(rows)
# --- marks + cursors, drawn as pixel art ('#' lit) -------------------------------------
def art(rows):
return bytes(sum(0x80 >> x for x, ch in enumerate(r) if ch == "#") for r in rows)
MARKS = {
0x25CF: art([ # BLACK CIRCLE -- node / unread
"......", "......", "..##..", ".####.", "######", "######",
"######", "######", ".####.", "..##..", "......", "......"]),
0x25C9: art([ # FISHEYE -- sensor
"......", "......", "..##..", ".####.", "##..##", "#.##.#",
"#.##.#", "##..##", ".####.", "..##..", "......", "......"]),
0x2605: art([ # BLACK STAR -- you / best
"......", "..##..", "..##..", "######", ".####.", "..##..",
".####.", "##..##", "#....#", "......", "......", "......"]),
0x2014: art([ # EM DASH -- title separator, full width
"......", "......", "......", "......", "......", "######",
"######", "......", "......", "......", "......", "......"]),
0x2713: art([ # CHECK MARK -- ok
"......", "......", "......", ".....#", "....#.", "...#..",
"#.#...", ".#....", "......", "......", "......", "......"]),
0x2717: art([ # BALLOT X -- error
"......", "......", "......", "#....#", ".#..#.", "..##..",
"..##..", ".#..#.", "#....#", "......", "......", "......"]),
0x25B6: art([ # RIGHT-POINTING TRIANGLE -- run / play
"......", "#.....", "##....", "###...", "####..", "#####.",
"#####.", "####..", "###...", "##....", "#.....", "......"]),
0x276F: art([ # HEAVY RIGHT ANGLE QUOTE -- THE list selection cursor ("> ")
"......", "##....", ".##...", "..##..", "...##.", "....##",
"....##", "...##.", "..##..", ".##...", "##....", "......"]),
0x25B8: art([ # SMALL RIGHT-POINTING TRIANGLE -- grabbed/reorder cursor
"......", "......", "......", ".#....", ".##...", ".###..",
".###..", ".##...", ".#....", "......", "......", "......"]),
# --- P3 marks: the compact icon language (see meshterm/ui/theme._GLYPH_MAP and
# --- meshterm/ui/fontset.py, which must mirror every codepoint drawn here) --------
0x2026: art([ # HORIZONTAL ELLIPSIS -- "opens further prompts", truncation
"......", "......", "......", "......", "......", "......",
"......", "......", "#.#.#.", "#.#.#.", "......", "......"]),
0x26A0: art([ # WARNING SIGN -- the warn status mark
"......", "..##..", ".#..#.", ".#..#.", "#....#", "#.##.#",
"#.##.#", "#....#", "#.##.#", "######", "......", "......"]),
0x232B: art([ # ERASE TO THE LEFT -- the backspace key in footer hints
"......", "......", "......", "..####", ".#...#", ".##.##",
"#..#.#", ".##.##", ".#...#", "..####", "......", "......"]),
0x21E7: art([ # UPWARDS WHITE ARROW -- the shift key in footer hints
"......", "......", "..##..", ".#..#.", "#....#", "##..##",
".#..#.", ".#..#.", ".#..#.", ".####.", "......", "......"]),
0x2699: art([ # GEAR -- parameter/config
"......", "......", "..##..", ".####.", "######", "##..##",
"##..##", "######", ".####.", "..##..", "......", "......"]),
0x21BB: art([ # CLOCKWISE OPEN CIRCLE ARROW -- re-read/refresh
"......", "....#.", ".#####", "#...#.", "#.....", "#.....",
"#.....", "#....#", ".####.", "......", "......", "......"]),
0x25F7: art([ # WHITE CIRCLE UPPER RIGHT QUADRANT -- the clock face (sync/time)
"......", "......", ".####.", "#..#.#", "#..#.#", "#..###",
"#....#", "#....#", ".####.", "......", "......", "......"]),
0x2316: art([ # POSITION INDICATOR -- the crosshair (trace/map position)
"......", "..##..", "..##..", "......", "......", "#.##.#",
"#.##.#", "......", "......", "..##..", "..##..", "......"]),
0x26BF: art([ # SQUARED KEY (drawn as a padlock) -- private channel
"......", "......", ".####.", ".#..#.", "######", "######",
"##..##", "##..##", "######", "######", "......", "......"]),
}
# The same 18 marks redrawn for the 6x8 cell (first-draft art; the P5 tweak round and
# JP's eyeball pass refine whichever font wins the A/B).
MARKS8 = {
0x25CF: art([ # BLACK CIRCLE
"......", "..##..", ".####.", "######", "######", ".####.", "..##..", "......"]),
0x25C9: art([ # FISHEYE
"......", "..##..", ".#..#.", "#.##.#", "#.##.#", ".#..#.", "..##..", "......"]),
0x2605: art([ # BLACK STAR
"......", "..#...", "..#...", "######", ".####.", "..##..", ".#..#.", "......"]),
0x2014: art([ # EM DASH
"......", "......", "......", "######", "######", "......", "......", "......"]),
0x2713: art([ # CHECK MARK
"......", "......", ".....#", "....#.", "#..#..", ".##...", ".#....", "......"]),
0x2717: art([ # BALLOT X
"......", "#...#.", ".#.#..", "..#...", ".#.#..", "#...#.", "......", "......"]),
0x25B6: art([ # RIGHT-POINTING TRIANGLE
"#.....", "##....", "###...", "####..", "####..", "###...", "##....", "#....."]),
0x276F: art([ # HEAVY RIGHT ANGLE QUOTE -- the list cursor
"##....", ".##...", "..##..", "...##.", "..##..", ".##...", "##....", "......"]),
0x25B8: art([ # SMALL RIGHT-POINTING TRIANGLE -- reorder cursor
"......", "......", ".#....", ".##...", ".###..", ".##...", ".#....", "......"]),
0x2026: art([ # HORIZONTAL ELLIPSIS
"......", "......", "......", "......", "......", "#.#.#.", "#.#.#.", "......"]),
0x26A0: art([ # WARNING SIGN
"..##..", ".#..#.", "#.##.#", "#.##.#", "#....#", "#.##.#", "######", "......"]),
0x232B: art([ # ERASE TO THE LEFT
"......", "..####", ".##.##", "#..#.#", ".##.##", "..####", "......", "......"]),
0x21E7: art([ # UPWARDS WHITE ARROW -- shift
"..##..", ".#..#.", "#....#", "##..##", ".#..#.", ".#..#.", ".####.", "......"]),
0x2699: art([ # GEAR
"......", "..##..", "######", "##..##", "##..##", "######", "..##..", "......"]),
0x21BB: art([ # CLOCKWISE OPEN CIRCLE ARROW -- refresh
"....#.", ".#####", "#...#.", "#.....", "#.....", "#....#", ".####.", "......"]),
0x25F7: art([ # CLOCK FACE
"......", ".####.", "#..#.#", "#..###", "#....#", "#....#", ".####.", "......"]),
0x2316: art([ # POSITION INDICATOR -- crosshair
"..##..", "......", "#.##.#", "#.##.#", "......", "..##..", "......", "......"]),
0x26BF: art([ # SQUARED KEY -- padlock
".####.", ".#..#.", "######", "##..##", "##..##", "######", "######", "......"]),
}
# Donor codepoints whose glyph slots we may repurpose (glyphs MeshTerm never draws).
# Order matters: MARKS consume donors front to back, one each. See the header comment
# for the shared-slot trap; the tail donors double as MAP TARGETS in meshterm/ui/theme
# (advert / message / data / packet / control marks) and are last-resort spares only.
# NOTE 266C: in the Terminus base it shares 266B's slot (a freebie, never a donor of its
# own -- listing it would abort); it is NOT in this list for that reason.
DONORS = [0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x25D8, 0x25D9, 0x266A,
0x266B, 0x203C, 0x2640, 0x2642, 0x2320, 0x2321, 0x00F7, 0x2552, 0x2558,
0x2559, 0x255B, 0x255E, 0x255F, 0x2561, 0x2567, 0x2568, 0x256A,
0x263C, 0x00B6, 0x00A7, 0x25AC, 0x21A8]
# Rounded panel corners aliased onto the existing square corners (no new bitmap), and the
# midline ellipsis onto the baseline mark.
ALIASES = {
0x256D: 0x250C, # rounded top-left -> square top-left
0x256E: 0x2510, # rounded top-right -> square top-right
0x256F: 0x2518, # rounded bottom-right -> square bottom-right
0x2570: 0x2514, # rounded bottom-left -> square bottom-left
0x22EF: 0x2026, # midline ellipsis -> ellipsis mark
}
# Codepoints that must survive both builds: the theme's base-font map targets.
KEEP_COMMON = [0x263C, 0x00B6, 0x00A7, 0x25AC, 0x21A8]
def build(glyphs, entries, charsize, height, marks, bands, keep_extra, out_path):
"""Append braille, draw marks into donors, alias, verify, and write one PSF2."""
glyphs = [bytearray(g) for g in glyphs]
entries = list(entries)
glyphs += [bytearray(braille_glyph(v, bands, height)) for v in range(256)]
entries += [chr(0x2800 + i).encode("utf-8") for i in range(256)]
def slot_of(cp):
needle = chr(cp).encode("utf-8")
for i, e in enumerate(entries):
if needle in e:
return i
return None
donors = list(DONORS)
for cp, bitmap in marks.items():
if not donors:
raise SystemExit("out of donor slots for U+%04X" % cp)
donor = donors.pop(0)
slot = slot_of(donor)
if slot is None:
raise SystemExit(
"donor U+%04X (for mark U+%04X) not found in the base font -- "
"fix DONORS instead of skipping" % (donor, cp))
padded = bytearray(bitmap)
padded += bytes(charsize - len(padded))
glyphs[slot] = padded
entries[slot] = chr(cp).encode("utf-8")
for new_cp, existing_cp in ALIASES.items():
slot = slot_of(existing_cp)
if slot is None:
raise SystemExit("no glyph for U+%04X to alias U+%04X onto" % (existing_cp, new_cp))
entries[slot] += chr(new_cp).encode("utf-8")
for cp in list(marks) + list(ALIASES) + KEEP_COMMON + keep_extra:
if slot_of(cp) is None:
raise SystemExit("build ate U+%04X -- a donor slot carried it; fix DONORS" % cp)
header = struct.pack("<IIIIIIII", PSF2_MAGIC, 0, 32, 1, 512, charsize, height, 6)
out = header + b"".join(bytes(g) for g in glyphs) + b"".join(e + b"\xff" for e in entries)
gzip.open(out_path, "wb").write(out)
print(" wrote %s: 512 glyphs (6x%d), +%d marks, +%d aliases"
% (out_path, height, len(marks), len(ALIASES)))
# --- 6x12: the Terminus base -----------------------------------------------------------
base = gzip.open(BASE, "rb").read()
magic, ver, hsize, flags, length, charsize, h, w = struct.unpack("<IIIIIIII", base[:32])
assert magic == PSF2_MAGIC and (length, charsize, h, w) == (256, 12, 12, 6), \
"base is not the expected Terminus 6x12 PSF2 (%r)" % ((length, charsize, h, w),)
glyphs12 = [base[32 + i * charsize:32 + (i + 1) * charsize] for i in range(256)]
entries12 = base[32 + 256 * charsize:].split(b"\xff")[:256]
# Cyrillic pe + pi: the shared-slot canaries (donating pi once erased pe).
build(glyphs12, entries12, 12, 12, MARKS, BANDS12, [0x043F, 0x03C0], OUT)
# --- 6x8: the kernel's font_6x8 (CP437 coverage -- no Cyrillic) ------------------------
FONT8 = base64.b64decode("""
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
""")
assert len(FONT8) == 2048
glyphs8 = [FONT8[i * 8:(i + 1) * 8] for i in range(256)]
# CP437's graphics mapping: the control range 0x00-0x1F holds pictographs, 0x7F a house;
# the rest decodes through Python's cp437 codec.
CP437_LOW = [
0x0000, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022,
0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C,
0x25BA, 0x25C4, 0x2195, 0x203C, 0x00B6, 0x00A7, 0x25AC, 0x21A8,
0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC,
]
entries8 = []
for i in range(256):
if i < 0x20:
cp = CP437_LOW[i]
elif i == 0x7F:
cp = 0x2302
else:
cp = ord(bytes([i]).decode("cp437"))
entries8.append(chr(cp).encode("utf-8") if cp else b"")
build(glyphs8, entries8, 8, 8, MARKS8, BANDS8, [0x03C0], OUT8)
PYEOF
# --- apply live: setfont re-renders the whole console immediately -----------------------
TTY=/dev/tty1
[ -c "$TTY" ] || TTY=/dev/tty0
echo "applying $OUT (6x12, the default) to $TTY ..."
setfont -C "$TTY" "$OUT"
# --- persist: systemd-vconsole-setup reads FONT= from vconsole.conf at every boot -------
if [ -f "$VCONSOLE" ] && grep -q '^FONT=' "$VCONSOLE"; then
sed -i "s/^FONT=.*/FONT=$FONT_NAME/" "$VCONSOLE"
else
echo "FONT=$FONT_NAME" >> "$VCONSOLE"
fi
echo "persisted FONT=$FONT_NAME in $VCONSOLE (loads on every boot)"
echo "A/B: 'setfont $OUT8' for 53x40, 'setfont $OUT' for 53x26; persist the winner in $VCONSOLE"
# --- palette -----------------------------------------------------------------------------
# JP's decision (2026-08-01): the console keeps its STANDARD kernel palette -- black
# background, stock hues -- and meshterm/ui/theme.MESH_THEME_16 is designed against those
# (theme._VT_SLOTS). The custom tailwind-family remap P3 originally shipped is ARCHIVED,
# one environment variable away in case he changes his mind:
#
# MESHTERM_CUSTOM_PALETTE=1 sh calculinux-console-font.sh
#
# The opt-in block's values match theme.vtrgb_lines() / theme._VT_SLOTS_CUSTOM exactly (a
# test keeps them in sync). A DEFAULT run removes any previously-installed remap and
# resets the live palette to stock.
VTRGB=/etc/vtrgb
APPLIER=/usr/local/sbin/meshterm-vtrgb
UNIT=/etc/systemd/system/meshterm-vtrgb.service
if [ "${MESHTERM_CUSTOM_PALETTE:-0}" = 1 ]; then
echo "writing $VTRGB (custom palette opt-in) ..."
cat > "$VTRGB" <<'EOF'
15,239,34,245,99,51,100,203,148,248,74,251,129,165,94,255
23,68,197,158,102,65,116,213,163,113,222,191,140,180,234,255
42,68,94,11,241,85,139,225,184,113,128,36,248,252,212,255
EOF
mkdir -p "$(dirname "$APPLIER")"
cat > "$APPLIER" <<'EOF'
#!/bin/sh
# Apply the MeshTerm custom console palette (meshterm/ui/theme._VT_SLOTS_CUSTOM).
if command -v setvtrgb >/dev/null 2>&1; then
exec setvtrgb /etc/vtrgb
fi
# No kbd setvtrgb: the kernel VT accepts its own OSC palette sequences (ESC ] P n rrggbb).
TTY=${1:-/dev/tty1}
printf '\033]P00f172a\033]P1ef4444\033]P222c55e\033]P3f59e0b\033]P46366f1\033]P5334155\033]P664748b\033]P7cbd5e1\033]P894a3b8\033]P9f87171\033]Pa4ade80\033]Pbfbbf24\033]Pc818cf8\033]Pda5b4fc\033]Pe5eead4\033]Pfffffff' > "$TTY"
EOF
chmod +x "$APPLIER"
cat > "$UNIT" <<EOF
[Unit]
Description=MeshTerm console palette (custom 16-slot vtrgb)
After=systemd-vconsole-setup.service
[Service]
Type=oneshot
ExecStart=$APPLIER
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable meshterm-vtrgb.service >/dev/null 2>&1 || true
"$APPLIER" || echo "warning: could not apply the palette live (boot service will)"
echo "custom palette installed ($VTRGB + boot oneshot)"
else
systemctl disable meshterm-vtrgb.service >/dev/null 2>&1 || true
rm -f "$UNIT" "$VTRGB" "$APPLIER"
systemctl daemon-reload 2>/dev/null || true
# Restore the STANDARD palette explicitly: a previous setvtrgb overwrote the
# kernel's *default* colormap, so the OSC reset (ESC ] R) alone would "reset"
# straight back to the custom values. Program the stock PC palette, then reset.
TTY=/dev/tty1
[ -c "$TTY" ] || TTY=/dev/tty0
if command -v setvtrgb >/dev/null 2>&1; then
STOCK=$(mktemp)
cat > "$STOCK" <<'EOF'
0,170,0,170,0,170,0,170,85,255,85,255,85,255,85,255
0,0,170,85,0,0,170,170,85,85,255,255,85,85,255,255
0,0,0,0,170,170,170,170,85,85,85,85,255,255,255,255
EOF
setvtrgb "$STOCK" || true
rm -f "$STOCK"
fi
printf '\033]P0000000\033]P1aa0000\033]P200aa00\033]P3aa5500\033]P40000aa\033]P5aa00aa\033]P600aaaa\033]P7aaaaaa\033]P8555555\033]P9ff5555\033]Pa55ff55\033]Pbffff55\033]Pc5555ff\033]Pdff55ff\033]Pe55ffff\033]Pfffffff\033]R' > "$TTY" 2>/dev/null || true
echo "palette: standard (custom remap removed; opt back in with MESHTERM_CUSTOM_PALETTE=1)"
fi
echo "done -- launch 'meshterm' to see braille charts, node glyphs, framed panels, and the > cursor."