feat: Implement interactive font selection by scanning the fonts/ directory for .otf, .ttf, and .ttc files, adding new fonts and updating documentation.

This commit is contained in:
2026-03-15 03:52:10 -07:00
parent e6826c884c
commit 11226872a1
8 changed files with 112 additions and 43 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -29,7 +29,8 @@ All constants live in `engine/config.py`:
| `HEADLINE_LIMIT` | `1000` | Total headlines per session | | `HEADLINE_LIMIT` | `1000` | Total headlines per session |
| `FEED_TIMEOUT` | `10` | Per-feed HTTP timeout (seconds) | | `FEED_TIMEOUT` | `10` | Per-feed HTTP timeout (seconds) |
| `MIC_THRESHOLD_DB` | `50` | dB floor above which glitches spike | | `MIC_THRESHOLD_DB` | `50` | dB floor above which glitches spike |
| `FONT_PATH` | hardcoded path | Path to your OTF/TTF display font | | `FONT_DIR` | `fonts/` | Folder scanned for `.otf`, `.ttf`, `.ttc` files used by the font picker |
| `FONT_PATH` | first supported font in `fonts/` | Active display font file selected at startup |
| `FONT_SZ` | `60` | Font render size (affects block density) | | `FONT_SZ` | `60` | Font render size (affects block density) |
| `RENDER_H` | `8` | Terminal rows per headline line | | `RENDER_H` | `8` | Terminal rows per headline line |
| `SSAA` | `4` | Super-sampling factor (render at 4× then downsample) | | `SSAA` | `4` | Super-sampling factor (render at 4× then downsample) |
@@ -41,7 +42,7 @@ All constants live in `engine/config.py`:
| `NTFY_POLL_INTERVAL` | `15` | Seconds between ntfy polls | | `NTFY_POLL_INTERVAL` | `15` | Seconds between ntfy polls |
| `MESSAGE_DISPLAY_SECS` | `30` | How long an ntfy message holds the screen | | `MESSAGE_DISPLAY_SECS` | `30` | How long an ntfy message holds the screen |
**Font:** `FONT_PATH` is hardcoded to a local path. Update it to point to whatever display font you want — anything with strong contrast and wide letterforms works well. **Font:** Put your `.otf`, `.ttf`, or `.ttc` files in `fonts/`. Startup opens the font picker from that folder and applies your selected font before streaming.
--- ---

View File

@@ -90,7 +90,7 @@ def _draw_font_picker(faces, selected):
print() print()
print(f" {G_HI}FONT PICKER{RST}") print(f" {G_HI}FONT PICKER{RST}")
print(f" {W_GHOST}{'' * (w - 4)}{RST}") print(f" {W_GHOST}{'' * (w - 4)}{RST}")
print(f" {W_DIM}{config.FONT_PATH[:max_preview_w]}{RST}") print(f" {W_DIM}{config.FONT_DIR[:max_preview_w]}{RST}")
print(f" {W_GHOST}↑/↓ move · Enter select · q accept current{RST}") print(f" {W_GHOST}↑/↓ move · Enter select · q accept current{RST}")
print() print()
@@ -99,7 +99,7 @@ def _draw_font_picker(faces, selected):
active = pos == selected active = pos == selected
pointer = "" if active else " " pointer = "" if active else " "
color = G_HI if active else W_DIM color = G_HI if active else W_DIM
print(f" {color}{pointer} [{face['index']}] {face['name']}{RST}") print(f" {color}{pointer} {face['name']}{RST}{W_GHOST} · {face['file_name']}{RST}")
if top > 0: if top > 0:
print(f" {W_GHOST}{top} above{RST}") print(f" {W_GHOST}{top} above{RST}")
@@ -108,53 +108,93 @@ def _draw_font_picker(faces, selected):
print() print()
print(f" {W_GHOST}{'' * (w - 4)}{RST}") print(f" {W_GHOST}{'' * (w - 4)}{RST}")
print(f" {W_DIM}Preview: {faces[selected]['name']}{RST}") print(
f" {W_DIM}Preview: {faces[selected]['name']} · {faces[selected]['file_name']}{RST}"
)
preview_rows = faces[selected]["preview_rows"][:preview_h] preview_rows = faces[selected]["preview_rows"][:preview_h]
for row in preview_rows: for row in preview_rows:
shown = row[:max_preview_w] shown = row[:max_preview_w]
print(f" {shown}") print(f" {shown}")
def pick_font_face(): def pick_font_face():
"""Interactive startup picker for selecting a face from a font file.""" """Interactive startup picker for selecting a face from repo OTF files."""
if not config.FONT_PICKER: if not config.FONT_PICKER:
return return
font_files = config.list_repo_font_files()
if not font_files:
print(CLR, end="") print(CLR, end="")
print(CURSOR_OFF, end="") print(CURSOR_OFF, end="")
print() print()
print(f" {G_HI}FONT PICKER{RST}") print(f" {G_HI}FONT PICKER{RST}")
print(f" {W_GHOST}{'' * (tw() - 4)}{RST}") print(f" {W_GHOST}{'' * (tw() - 4)}{RST}")
print(f" {W_DIM}{config.FONT_PATH}{RST}") print(f" {G_DIM}> no .otf/.ttf/.ttc files found in: {config.FONT_DIR}{RST}")
print() print(f" {W_GHOST}> add font files to the fonts folder, then rerun{RST}")
time.sleep(1.8)
try:
faces = render.list_font_faces(config.FONT_PATH, max_faces=64)
except Exception as exc:
print(f" {G_DIM}> unable to load font file: {exc}{RST}")
print(f" {W_GHOST}> startup aborted (font selection is required){RST}")
time.sleep(1.4)
sys.exit(1) sys.exit(1)
face_ids = [face["index"] for face in faces]
default = config.FONT_INDEX if config.FONT_INDEX in face_ids else faces[0]["index"]
prepared = [] prepared = []
for font_path in font_files:
try:
faces = render.list_font_faces(font_path, max_faces=64)
except Exception:
fallback = os.path.splitext(os.path.basename(font_path))[0]
faces = [{"index": 0, "name": fallback}]
for face in faces: for face in faces:
idx = face["index"] idx = face["index"]
name = face["name"] name = face["name"]
file_name = os.path.basename(font_path)
try: try:
fnt = render.load_font_face(config.FONT_PATH, idx) fnt = render.load_font_face(font_path, idx)
rows = _normalize_preview_rows(render.render_line(name, fnt)) rows = _normalize_preview_rows(render.render_line(name, fnt))
except Exception: except Exception:
rows = ["(preview unavailable)"] rows = ["(preview unavailable)"]
prepared.append({"index": idx, "name": name, "preview_rows": rows}) prepared.append(
{
"font_path": font_path,
"font_index": idx,
"name": name,
"file_name": file_name,
"preview_rows": rows,
}
)
if not prepared:
print(CLR, end="")
print(CURSOR_OFF, end="")
print()
print(f" {G_HI}FONT PICKER{RST}")
print(f" {W_GHOST}{'' * (tw() - 4)}{RST}")
print(f" {G_DIM}> no readable font faces found in: {config.FONT_DIR}{RST}")
time.sleep(1.8)
sys.exit(1)
def _same_path(a, b):
try:
return os.path.samefile(a, b)
except Exception:
return os.path.abspath(a) == os.path.abspath(b)
selected = next(
(
i
for i, f in enumerate(prepared)
if _same_path(f["font_path"], config.FONT_PATH)
and f["font_index"] == config.FONT_INDEX
),
0,
)
selected = next((i for i, f in enumerate(prepared) if f["index"] == default), 0)
if not sys.stdin.isatty(): if not sys.stdin.isatty():
selected_index = prepared[selected]["index"] selected_font = prepared[selected]
config.set_font_selection(font_index=selected_index) config.set_font_selection(
font_path=selected_font["font_path"],
font_index=selected_font["font_index"],
)
render.clear_font_cache() render.clear_font_cache()
print(f" {G_DIM}> using face {selected_index}{RST}") print(
f" {G_DIM}> using {selected_font['name']} ({selected_font['file_name']}){RST}"
)
time.sleep(0.8) time.sleep(0.8)
print(CLR, end="") print(CLR, end="")
print(CURSOR_OFF, end="") print(CURSOR_OFF, end="")
@@ -179,11 +219,13 @@ def pick_font_face():
finally: finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
selected = prepared[selected]["index"] selected_font = prepared[selected]
config.set_font_selection(
config.set_font_selection(font_index=selected) font_path=selected_font["font_path"],
font_index=selected_font["font_index"],
)
render.clear_font_cache() render.clear_font_cache()
print(f" {G_DIM}> using face {selected}{RST}") print(f" {G_DIM}> using {selected_font['name']} ({selected_font['file_name']}){RST}")
time.sleep(0.8) time.sleep(0.8)
print(CLR, end="") print(CLR, end="")
print(CURSOR_OFF, end="") print(CURSOR_OFF, end="")

View File

@@ -5,6 +5,8 @@ Configuration constants, CLI flags, and glyph tables.
import sys import sys
from pathlib import Path from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parent.parent
_FONT_EXTENSIONS = {".otf", ".ttf", ".ttc"}
def _arg_value(flag): def _arg_value(flag):
@@ -31,8 +33,24 @@ def _resolve_font_path(raw_path):
p = Path(raw_path).expanduser() p = Path(raw_path).expanduser()
if p.is_absolute(): if p.is_absolute():
return str(p) return str(p)
repo_root = Path(__file__).resolve().parent.parent return str((_REPO_ROOT / p).resolve())
return str((repo_root / p).resolve())
def _list_font_files(font_dir):
"""List supported font files within a font directory."""
font_root = Path(font_dir)
if not font_root.exists() or not font_root.is_dir():
return []
return [
str(path.resolve())
for path in sorted(font_root.iterdir())
if path.is_file() and path.suffix.lower() in _FONT_EXTENSIONS
]
def list_repo_font_files():
"""Public helper for discovering repository font files."""
return _list_font_files(FONT_DIR)
# ─── RUNTIME ────────────────────────────────────────────── # ─── RUNTIME ──────────────────────────────────────────────
HEADLINE_LIMIT = 1000 HEADLINE_LIMIT = 1000
FEED_TIMEOUT = 10 FEED_TIMEOUT = 10
@@ -46,9 +64,13 @@ NTFY_POLL_INTERVAL = 15 # seconds between polls
MESSAGE_DISPLAY_SECS = 30 # how long a message holds the screen MESSAGE_DISPLAY_SECS = 30 # how long a message holds the screen
# ─── FONT RENDERING ────────────────────────────────────── # ─── FONT RENDERING ──────────────────────────────────────
FONT_PATH = _resolve_font_path( FONT_DIR = _resolve_font_path(_arg_value('--font-dir') or "fonts")
_arg_value('--font-file') _FONT_FILE_ARG = _arg_value('--font-file')
or "/Users/genejohnson/Documents/CS Bishop Drawn/CSBishopDrawn-Italic.otf" _FONT_FILES = _list_font_files(FONT_DIR)
FONT_PATH = (
_resolve_font_path(_FONT_FILE_ARG)
if _FONT_FILE_ARG
else (_FONT_FILES[0] if _FONT_FILES else "")
) )
FONT_INDEX = max(0, _arg_int('--font-index', 0)) FONT_INDEX = max(0, _arg_int('--font-index', 0))
FONT_PICKER = '--no-font-picker' not in sys.argv FONT_PICKER = '--no-font-picker' not in sys.argv

View File

@@ -57,6 +57,10 @@ _FONT_CACHE = {}
def font(): def font():
"""Lazy-load the primary OTF font (path + face index aware).""" """Lazy-load the primary OTF font (path + face index aware)."""
global _FONT_OBJ, _FONT_OBJ_KEY global _FONT_OBJ, _FONT_OBJ_KEY
if not config.FONT_PATH:
raise FileNotFoundError(
f"No primary font selected. Add .otf/.ttf/.ttc files to {config.FONT_DIR}."
)
key = (config.FONT_PATH, config.FONT_INDEX, config.FONT_SZ) key = (config.FONT_PATH, config.FONT_INDEX, config.FONT_SZ)
if _FONT_OBJ is None or _FONT_OBJ_KEY != key: if _FONT_OBJ is None or _FONT_OBJ_KEY != key:
_FONT_OBJ = ImageFont.truetype( _FONT_OBJ = ImageFont.truetype(

BIN
fonts/Eyekons.otf Normal file

Binary file not shown.

BIN
fonts/Pixel Sparta.otf Normal file

Binary file not shown.

BIN
fonts/Xeonic.ttf Normal file

Binary file not shown.