240 lines
9.0 KiB
Python
240 lines
9.0 KiB
Python
"""
|
||
Configuration constants, CLI flags, and glyph tables.
|
||
Supports both global constants (backward compatible) and injected config for testing.
|
||
"""
|
||
|
||
import sys
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
|
||
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
||
_FONT_EXTENSIONS = {".otf", ".ttf", ".ttc"}
|
||
|
||
|
||
def _arg_value(flag, argv: list[str] | None = None):
|
||
"""Get value following a CLI flag, if present."""
|
||
argv = argv or sys.argv
|
||
if flag not in argv:
|
||
return None
|
||
i = argv.index(flag)
|
||
return argv[i + 1] if i + 1 < len(argv) else None
|
||
|
||
|
||
def _arg_int(flag, default, argv: list[str] | None = None):
|
||
"""Get int CLI argument with safe fallback."""
|
||
raw = _arg_value(flag, argv)
|
||
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)
|
||
|
||
|
||
def _get_platform_font_paths() -> dict[str, str]:
|
||
"""Get platform-appropriate font paths for non-Latin scripts."""
|
||
import platform
|
||
|
||
system = platform.system()
|
||
|
||
if system == "Darwin":
|
||
return {
|
||
"zh-cn": "/System/Library/Fonts/STHeiti Medium.ttc",
|
||
"ja": "/System/Library/Fonts/ヒラギノ角ゴシック W9.ttc",
|
||
"ko": "/System/Library/Fonts/AppleSDGothicNeo.ttc",
|
||
"ru": "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||
"uk": "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||
"el": "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||
"he": "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||
"ar": "/System/Library/Fonts/GeezaPro.ttc",
|
||
"fa": "/System/Library/Fonts/GeezaPro.ttc",
|
||
"hi": "/System/Library/Fonts/Kohinoor.ttc",
|
||
"th": "/System/Library/Fonts/ThonburiUI.ttc",
|
||
}
|
||
elif system == "Linux":
|
||
return {
|
||
"zh-cn": "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||
"ja": "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||
"ko": "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||
"ru": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
"uk": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
"el": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
"he": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
"ar": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
"fa": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
"hi": "/usr/share/fonts/truetype/noto/NotoSansDevanagari-Regular.ttf",
|
||
"th": "/usr/share/fonts/truetype/noto/NotoSansThai-Regular.ttf",
|
||
}
|
||
else:
|
||
return {}
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Config:
|
||
"""Immutable configuration container for injected config."""
|
||
|
||
headline_limit: int = 1000
|
||
feed_timeout: int = 10
|
||
mic_threshold_db: int = 50
|
||
mode: str = "news"
|
||
firehose: bool = False
|
||
|
||
ntfy_topic: str = "https://ntfy.sh/klubhaus_terminal_mainline/json"
|
||
ntfy_cc_cmd_topic: str = "https://ntfy.sh/klubhaus_terminal_mainline_cc_cmd/json"
|
||
ntfy_cc_resp_topic: str = "https://ntfy.sh/klubhaus_terminal_mainline_cc_resp/json"
|
||
ntfy_reconnect_delay: int = 5
|
||
message_display_secs: int = 30
|
||
|
||
font_dir: str = "fonts"
|
||
font_path: str = ""
|
||
font_index: int = 0
|
||
font_picker: bool = True
|
||
font_sz: int = 60
|
||
render_h: int = 8
|
||
|
||
ssaa: int = 4
|
||
|
||
scroll_dur: float = 5.625
|
||
frame_dt: float = 0.05
|
||
firehose_h: int = 12
|
||
grad_speed: float = 0.08
|
||
|
||
glitch_glyphs: str = "░▒▓█▌▐╌╍╎╏┃┆┇┊┋"
|
||
kata_glyphs: str = "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ"
|
||
|
||
script_fonts: dict[str, str] = field(default_factory=_get_platform_font_paths)
|
||
|
||
@classmethod
|
||
def from_args(cls, argv: list[str] | None = None) -> "Config":
|
||
"""Create Config from CLI arguments (or custom argv for testing)."""
|
||
argv = argv or sys.argv
|
||
|
||
font_dir = _resolve_font_path(_arg_value("--font-dir", argv) or "fonts")
|
||
font_file_arg = _arg_value("--font-file", argv)
|
||
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 "")
|
||
)
|
||
|
||
return cls(
|
||
headline_limit=1000,
|
||
feed_timeout=10,
|
||
mic_threshold_db=50,
|
||
mode="poetry" if "--poetry" in argv or "-p" in argv else "news",
|
||
firehose="--firehose" in argv,
|
||
ntfy_topic="https://ntfy.sh/klubhaus_terminal_mainline/json",
|
||
ntfy_cc_cmd_topic="https://ntfy.sh/klubhaus_terminal_mainline_cc_cmd/json",
|
||
ntfy_cc_resp_topic="https://ntfy.sh/klubhaus_terminal_mainline_cc_resp/json",
|
||
ntfy_reconnect_delay=5,
|
||
message_display_secs=30,
|
||
font_dir=font_dir,
|
||
font_path=font_path,
|
||
font_index=max(0, _arg_int("--font-index", 0, argv)),
|
||
font_picker="--no-font-picker" not in argv,
|
||
font_sz=60,
|
||
render_h=8,
|
||
ssaa=4,
|
||
scroll_dur=5.625,
|
||
frame_dt=0.05,
|
||
firehose_h=12,
|
||
grad_speed=0.08,
|
||
glitch_glyphs="░▒▓█▌▐╌╍╎╏┃┆┇┊┋",
|
||
kata_glyphs="ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ",
|
||
script_fonts=_get_platform_font_paths(),
|
||
)
|
||
|
||
|
||
_config: Config | None = None
|
||
|
||
|
||
def get_config() -> Config:
|
||
"""Get the global config instance (lazy-loaded)."""
|
||
global _config
|
||
if _config is None:
|
||
_config = Config.from_args()
|
||
return _config
|
||
|
||
|
||
def set_config(config: Config) -> None:
|
||
"""Set the global config instance (for testing)."""
|
||
global _config
|
||
_config = config
|
||
|
||
|
||
# ─── RUNTIME ──────────────────────────────────────────────
|
||
HEADLINE_LIMIT = 1000
|
||
FEED_TIMEOUT = 10
|
||
MIC_THRESHOLD_DB = 50 # dB above which glitches intensify
|
||
MODE = "poetry" if "--poetry" in sys.argv or "-p" in sys.argv else "news"
|
||
FIREHOSE = "--firehose" in sys.argv
|
||
|
||
# ─── NTFY MESSAGE QUEUE ──────────────────────────────────
|
||
NTFY_TOPIC = "https://ntfy.sh/klubhaus_terminal_mainline/json"
|
||
NTFY_CC_CMD_TOPIC = "https://ntfy.sh/klubhaus_terminal_mainline_cc_cmd/json"
|
||
NTFY_CC_RESP_TOPIC = "https://ntfy.sh/klubhaus_terminal_mainline_cc_resp/json"
|
||
NTFY_RECONNECT_DELAY = 5 # seconds before reconnecting after a dropped stream
|
||
MESSAGE_DISPLAY_SECS = 30 # how long a message holds the screen
|
||
|
||
# ─── FONT RENDERING ──────────────────────────────────────
|
||
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
|
||
RENDER_H = 8 # terminal rows per rendered text line
|
||
|
||
# ─── FONT RENDERING (ADVANCED) ────────────────────────────
|
||
SSAA = 4 # super-sampling factor: render at SSAA× then downsample
|
||
|
||
# ─── SCROLL / FRAME ──────────────────────────────────────
|
||
SCROLL_DUR = 5.625 # seconds per headline (2/3 original speed)
|
||
FRAME_DT = 0.05 # 50ms base frame rate (20 FPS)
|
||
FIREHOSE_H = 12 # firehose zone height (terminal rows)
|
||
GRAD_SPEED = 0.08 # gradient traversal speed (cycles/sec, ~12s full sweep)
|
||
|
||
# ─── GLYPHS ───────────────────────────────────────────────
|
||
GLITCH = "░▒▓█▌▐╌╍╎╏┃┆┇┊┋"
|
||
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))
|