From b11027ec3d54bb3418be6f4c550bf9458593a58c Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Sun, 26 Jul 2026 00:10:50 -0400 Subject: [PATCH] Add Calculinux console-font setup script Builds, applies, and persists the 512-glyph "meshterm" PSF: Terminus base + 256 braille + node/status marks and the list cursor in donor slots + rounded panel corners aliased onto the square corners. Idempotent; runs on-device as root against the stock ter-u12n font. Co-Authored-By: Claude Opus 4.8 --- scripts/calculinux-console-font.sh | 186 +++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 scripts/calculinux-console-font.sh diff --git a/scripts/calculinux-console-font.sh b/scripts/calculinux-console-font.sh new file mode 100644 index 0000000..62a6b90 --- /dev/null +++ b/scripts/calculinux-console-font.sh @@ -0,0 +1,186 @@ +#!/bin/sh +# calculinux-console-font.sh -- build, install, and persist the "meshterm" console font. +# +# 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 the four things MeshTerm's UI leans on hardest: +# +# * braille charts U+2800-U+28FF (256 codepoints -- a whole block on their own) +# * node / status marks * filled circle, fisheye, star, em-dash, check, cross, play +# * rounded panel corners Rich's ROUNDED box emits U+256D/E/F U+2570, absent from CP437 +# * the list cursor U+276F "heavy angle" + U+25B8 small triangle (reorder state) +# +# Missing glyphs render blank, so charts vanish, node rows lose their icons, every window +# frame loses its corners, and the highlighted menu row shows a two-space gap where its +# pointer should be. This script synthesizes all of them and installs one 512-glyph PSF2. +# +# Why 512 and not more: fbcon caps a font at 512 glyphs. Braille alone is 256, and the base +# Terminus set is another 256 -- that already fills the budget exactly. So the marks and +# cursors do NOT grow the table: each is drawn into a DONOR slot (a useless CP437 pictograph +# -- smiley, card suit, music note -- that MeshTerm never emits), whose old codepoint is +# dropped from the Unicode map and repointed at the new glyph. The rounded corners cost no +# glyph at all: a 1-2px curve is meaningless at 6x12, so each rounded corner is aliased onto +# the existing square-corner glyph (an extra codepoint on one entry -- the frame just closes). +# +# Everything is pure geometry, generated on-device, so nothing but the stock Terminus font +# and python3 is required. Run it as root on the Lyra (over the serial console is fine): +# +# sh calculinux-console-font.sh +# +# It is idempotent -- safe to re-run after a MeshTerm update or a font tweak. +# +# Scope: this covers ONLY the console-font configuration. The rest of the Calculinux bring-up +# (opkg python3-modules/pip, the venv + `pip install -e .`, the Wi-Fi boot-scan kick) is a +# one-time deploy done separately and is not repeated here. +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" # what we build +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 meshterm.psf.gz from the stock Terminus font -------------------------------- +# 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 from $(basename "$BASE") ..." +BASE="$BASE" OUT="$OUT" python3 - <<'PYEOF' +import os, struct, gzip + +BASE = os.environ["BASE"] +OUT = os.environ["OUT"] +PSF2_MAGIC = 0x864AB572 + +# --- braille: 2-wide x 4-tall dot grid; the codepoint's low byte says which dots lit --- +COLS = [[1, 2], [4, 5]] +ROWS = [[0, 1], [3, 4], [6, 7], [9, 10]] +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)} + + +def braille_glyph(value): + rows = [0] * 12 + for bit in range(8): + if value >> bit & 1: + col, row = BIT[bit] + for x in COLS[col]: + for y in ROWS[row]: + rows[y] |= 0x80 >> x + return bytes(rows) + + +# --- MeshTerm marks + cursors, drawn as 6x12 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 + "......", "......", "......", ".#....", ".##...", ".###..", + ".###..", ".##...", ".#....", "......", "......", "......"]), +} + +# Donor codepoints whose glyph slots we may repurpose (CP437 junk MeshTerm never uses). +DONORS = [0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x25D8, 0x25D9, + 0x266A, 0x266B, 0x263C, 0x203C, 0x00B6, 0x00A7, 0x25AC, 0x21A8] + +# Rounded panel corners aliased onto the existing square corners (no new bitmap). +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 +} + +base = gzip.open(BASE, "rb").read() +magic, ver, hsize, flags, length, charsize, h, w = struct.unpack(" cursor."