Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eeb817dca | |||
| ac80ab23cc | |||
| 516123345e | |||
| 11226872a1 | |||
| e6826c884c | |||
| 0740e34293 |
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+208
-1
@@ -3,11 +3,14 @@ Application orchestrator — boot sequence, signal handling, main loop wiring.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import signal
|
import signal
|
||||||
import atexit
|
import atexit
|
||||||
|
import termios
|
||||||
|
import tty
|
||||||
|
|
||||||
from engine import config
|
from engine import config, render
|
||||||
from engine.terminal import (
|
from engine.terminal import (
|
||||||
RST, G_HI, G_MID, G_DIM, W_DIM, W_GHOST, CLR, CURSOR_OFF, CURSOR_ON, tw,
|
RST, G_HI, G_MID, G_DIM, W_DIM, W_GHOST, CLR, CURSOR_OFF, CURSOR_ON, tw,
|
||||||
slow_print, boot_ln,
|
slow_print, boot_ln,
|
||||||
@@ -26,6 +29,208 @@ TITLE = [
|
|||||||
" ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝",
|
" ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _read_picker_key():
|
||||||
|
ch = sys.stdin.read(1)
|
||||||
|
if ch == "\x03":
|
||||||
|
return "interrupt"
|
||||||
|
if ch in ("\r", "\n"):
|
||||||
|
return "enter"
|
||||||
|
if ch == "\x1b":
|
||||||
|
c1 = sys.stdin.read(1)
|
||||||
|
if c1 != "[":
|
||||||
|
return None
|
||||||
|
c2 = sys.stdin.read(1)
|
||||||
|
if c2 == "A":
|
||||||
|
return "up"
|
||||||
|
if c2 == "B":
|
||||||
|
return "down"
|
||||||
|
return None
|
||||||
|
if ch in ("k", "K"):
|
||||||
|
return "up"
|
||||||
|
if ch in ("j", "J"):
|
||||||
|
return "down"
|
||||||
|
if ch in ("q", "Q"):
|
||||||
|
return "enter"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _normalize_preview_rows(rows):
|
||||||
|
"""Trim shared left padding and trailing spaces for stable on-screen previews."""
|
||||||
|
non_empty = [r for r in rows if r.strip()]
|
||||||
|
if not non_empty:
|
||||||
|
return [""]
|
||||||
|
left_pad = min(len(r) - len(r.lstrip(" ")) for r in non_empty)
|
||||||
|
out = []
|
||||||
|
for row in rows:
|
||||||
|
if left_pad < len(row):
|
||||||
|
out.append(row[left_pad:].rstrip())
|
||||||
|
else:
|
||||||
|
out.append(row.rstrip())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_font_picker(faces, selected):
|
||||||
|
w = tw()
|
||||||
|
h = 24
|
||||||
|
try:
|
||||||
|
h = os.get_terminal_size().lines
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
max_preview_w = max(24, w - 8)
|
||||||
|
header_h = 6
|
||||||
|
footer_h = 3
|
||||||
|
preview_h = max(4, min(config.RENDER_H + 2, max(4, h // 2)))
|
||||||
|
visible = max(1, h - header_h - preview_h - footer_h)
|
||||||
|
top = max(0, selected - (visible // 2))
|
||||||
|
bottom = min(len(faces), top + visible)
|
||||||
|
top = max(0, bottom - visible)
|
||||||
|
|
||||||
|
print(CLR, end="")
|
||||||
|
print(CURSOR_OFF, end="")
|
||||||
|
print()
|
||||||
|
print(f" {G_HI}FONT PICKER{RST}")
|
||||||
|
print(f" {W_GHOST}{'─' * (w - 4)}{RST}")
|
||||||
|
print(f" {W_DIM}{config.FONT_DIR[:max_preview_w]}{RST}")
|
||||||
|
print(f" {W_GHOST}↑/↓ move · Enter select · q accept current{RST}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for pos in range(top, bottom):
|
||||||
|
face = faces[pos]
|
||||||
|
active = pos == selected
|
||||||
|
pointer = "▶" if active else " "
|
||||||
|
color = G_HI if active else W_DIM
|
||||||
|
print(f" {color}{pointer} {face['name']}{RST}{W_GHOST} · {face['file_name']}{RST}")
|
||||||
|
|
||||||
|
if top > 0:
|
||||||
|
print(f" {W_GHOST}… {top} above{RST}")
|
||||||
|
if bottom < len(faces):
|
||||||
|
print(f" {W_GHOST}… {len(faces) - bottom} below{RST}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f" {W_GHOST}{'─' * (w - 4)}{RST}")
|
||||||
|
print(
|
||||||
|
f" {W_DIM}Preview: {faces[selected]['name']} · {faces[selected]['file_name']}{RST}"
|
||||||
|
)
|
||||||
|
preview_rows = faces[selected]["preview_rows"][:preview_h]
|
||||||
|
for row in preview_rows:
|
||||||
|
shown = row[:max_preview_w]
|
||||||
|
print(f" {shown}")
|
||||||
|
|
||||||
|
def pick_font_face():
|
||||||
|
"""Interactive startup picker for selecting a face from repo OTF files."""
|
||||||
|
if not config.FONT_PICKER:
|
||||||
|
return
|
||||||
|
|
||||||
|
font_files = config.list_repo_font_files()
|
||||||
|
if not font_files:
|
||||||
|
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 .otf/.ttf/.ttc files found in: {config.FONT_DIR}{RST}")
|
||||||
|
print(f" {W_GHOST}> add font files to the fonts folder, then rerun{RST}")
|
||||||
|
time.sleep(1.8)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
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:
|
||||||
|
idx = face["index"]
|
||||||
|
name = face["name"]
|
||||||
|
file_name = os.path.basename(font_path)
|
||||||
|
try:
|
||||||
|
fnt = render.load_font_face(font_path, idx)
|
||||||
|
rows = _normalize_preview_rows(render.render_line(name, fnt))
|
||||||
|
except Exception:
|
||||||
|
rows = ["(preview unavailable)"]
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
selected_font = prepared[selected]
|
||||||
|
config.set_font_selection(
|
||||||
|
font_path=selected_font["font_path"],
|
||||||
|
font_index=selected_font["font_index"],
|
||||||
|
)
|
||||||
|
render.clear_font_cache()
|
||||||
|
print(
|
||||||
|
f" {G_DIM}> using {selected_font['name']} ({selected_font['file_name']}){RST}"
|
||||||
|
)
|
||||||
|
time.sleep(0.8)
|
||||||
|
print(CLR, end="")
|
||||||
|
print(CURSOR_OFF, end="")
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setcbreak(fd)
|
||||||
|
while True:
|
||||||
|
_draw_font_picker(prepared, selected)
|
||||||
|
key = _read_picker_key()
|
||||||
|
if key == "up":
|
||||||
|
selected = max(0, selected - 1)
|
||||||
|
elif key == "down":
|
||||||
|
selected = min(len(prepared) - 1, selected + 1)
|
||||||
|
elif key == "enter":
|
||||||
|
break
|
||||||
|
elif key == "interrupt":
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
|
||||||
|
selected_font = prepared[selected]
|
||||||
|
config.set_font_selection(
|
||||||
|
font_path=selected_font["font_path"],
|
||||||
|
font_index=selected_font["font_index"],
|
||||||
|
)
|
||||||
|
render.clear_font_cache()
|
||||||
|
print(f" {G_DIM}> using {selected_font['name']} ({selected_font['file_name']}){RST}")
|
||||||
|
time.sleep(0.8)
|
||||||
|
print(CLR, end="")
|
||||||
|
print(CURSOR_OFF, end="")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
atexit.register(lambda: print(CURSOR_ON, end="", flush=True))
|
atexit.register(lambda: print(CURSOR_ON, end="", flush=True))
|
||||||
@@ -40,6 +245,8 @@ def main():
|
|||||||
w = tw()
|
w = tw()
|
||||||
print(CLR, end="")
|
print(CLR, end="")
|
||||||
print(CURSOR_OFF, end="")
|
print(CURSOR_OFF, end="")
|
||||||
|
pick_font_face()
|
||||||
|
w = tw()
|
||||||
print()
|
print()
|
||||||
time.sleep(0.4)
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
|||||||
+66
-1
@@ -4,6 +4,53 @@ Configuration constants, CLI flags, and glyph tables.
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
_FONT_EXTENSIONS = {".otf", ".ttf", ".ttc"}
|
||||||
|
|
||||||
|
|
||||||
|
def _arg_value(flag):
|
||||||
|
"""Get value following a CLI flag, if present."""
|
||||||
|
if flag not in sys.argv:
|
||||||
|
return None
|
||||||
|
i = sys.argv.index(flag)
|
||||||
|
return sys.argv[i + 1] if i + 1 < len(sys.argv) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _arg_int(flag, default):
|
||||||
|
"""Get int CLI argument with safe fallback."""
|
||||||
|
raw = _arg_value(flag)
|
||||||
|
if raw is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_font_path(raw_path):
|
||||||
|
"""Resolve font path; relative paths are anchored to repo root."""
|
||||||
|
p = Path(raw_path).expanduser()
|
||||||
|
if p.is_absolute():
|
||||||
|
return str(p)
|
||||||
|
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
|
||||||
@@ -17,7 +64,16 @@ 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 = "/Users/genejohnson/Documents/CS Bishop Drawn/CSBishopDrawn-Italic.otf"
|
FONT_DIR = _resolve_font_path(_arg_value('--font-dir') or "fonts")
|
||||||
|
_FONT_FILE_ARG = _arg_value('--font-file')
|
||||||
|
_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_PICKER = '--no-font-picker' not in sys.argv
|
||||||
FONT_SZ = 60
|
FONT_SZ = 60
|
||||||
RENDER_H = 8 # terminal rows per rendered text line
|
RENDER_H = 8 # terminal rows per rendered text line
|
||||||
|
|
||||||
@@ -33,3 +89,12 @@ GRAD_SPEED = 0.08 # gradient traversal speed (cycles/sec, ~12s full swee
|
|||||||
# ─── GLYPHS ───────────────────────────────────────────────
|
# ─── GLYPHS ───────────────────────────────────────────────
|
||||||
GLITCH = "░▒▓█▌▐╌╍╎╏┃┆┇┊┋"
|
GLITCH = "░▒▓█▌▐╌╍╎╏┃┆┇┊┋"
|
||||||
KATA = "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ"
|
KATA = "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ"
|
||||||
|
|
||||||
|
|
||||||
|
def set_font_selection(font_path=None, font_index=None):
|
||||||
|
"""Set runtime primary font selection."""
|
||||||
|
global FONT_PATH, FONT_INDEX
|
||||||
|
if font_path is not None:
|
||||||
|
FONT_PATH = _resolve_font_path(font_path)
|
||||||
|
if font_index is not None:
|
||||||
|
FONT_INDEX = max(0, int(font_index))
|
||||||
|
|||||||
+44
-4
@@ -6,6 +6,7 @@ Depends on: config, terminal, sources, translate.
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
@@ -49,16 +50,55 @@ MSG_GRAD_COLS = [
|
|||||||
|
|
||||||
# ─── FONT LOADING ─────────────────────────────────────────
|
# ─── FONT LOADING ─────────────────────────────────────────
|
||||||
_FONT_OBJ = None
|
_FONT_OBJ = None
|
||||||
|
_FONT_OBJ_KEY = None
|
||||||
_FONT_CACHE = {}
|
_FONT_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
def font():
|
def font():
|
||||||
"""Lazy-load the primary OTF font."""
|
"""Lazy-load the primary OTF font (path + face index aware)."""
|
||||||
global _FONT_OBJ
|
global _FONT_OBJ, _FONT_OBJ_KEY
|
||||||
if _FONT_OBJ is None:
|
if not config.FONT_PATH:
|
||||||
_FONT_OBJ = ImageFont.truetype(config.FONT_PATH, config.FONT_SZ)
|
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)
|
||||||
|
if _FONT_OBJ is None or _FONT_OBJ_KEY != key:
|
||||||
|
_FONT_OBJ = ImageFont.truetype(
|
||||||
|
config.FONT_PATH, config.FONT_SZ, index=config.FONT_INDEX
|
||||||
|
)
|
||||||
|
_FONT_OBJ_KEY = key
|
||||||
return _FONT_OBJ
|
return _FONT_OBJ
|
||||||
|
|
||||||
|
def clear_font_cache():
|
||||||
|
"""Reset cached font objects after changing primary font selection."""
|
||||||
|
global _FONT_OBJ, _FONT_OBJ_KEY
|
||||||
|
_FONT_OBJ = None
|
||||||
|
_FONT_OBJ_KEY = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_font_face(font_path, font_index=0, size=None):
|
||||||
|
"""Load a specific face from a font file or collection."""
|
||||||
|
font_size = size or config.FONT_SZ
|
||||||
|
return ImageFont.truetype(font_path, font_size, index=font_index)
|
||||||
|
|
||||||
|
|
||||||
|
def list_font_faces(font_path, max_faces=64):
|
||||||
|
"""Return discoverable face indexes + display names from a font file."""
|
||||||
|
faces = []
|
||||||
|
for idx in range(max_faces):
|
||||||
|
try:
|
||||||
|
fnt = load_font_face(font_path, idx)
|
||||||
|
except Exception:
|
||||||
|
if idx == 0:
|
||||||
|
raise
|
||||||
|
break
|
||||||
|
family, style = fnt.getname()
|
||||||
|
display = f"{family} {style}".strip()
|
||||||
|
if not display:
|
||||||
|
display = f"{Path(font_path).stem} [{idx}]"
|
||||||
|
faces.append({"index": idx, "name": display})
|
||||||
|
return faces
|
||||||
|
|
||||||
|
|
||||||
def font_for_lang(lang=None):
|
def font_for_lang(lang=None):
|
||||||
"""Get appropriate font for a language."""
|
"""Get appropriate font for a language."""
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user