diff --git a/assets/fonts/EuropeanTeletext.ttf b/assets/fonts/EuropeanTeletext.ttf new file mode 100644 index 0000000..6439184 Binary files /dev/null and b/assets/fonts/EuropeanTeletext.ttf differ diff --git a/assets/fonts/EuropeanTeletextNuevo.ttf b/assets/fonts/EuropeanTeletextNuevo.ttf new file mode 100644 index 0000000..99ea385 Binary files /dev/null and b/assets/fonts/EuropeanTeletextNuevo.ttf differ diff --git a/assets/fonts/Modeseven.ttf b/assets/fonts/Modeseven.ttf new file mode 100755 index 0000000..87c9280 Binary files /dev/null and b/assets/fonts/Modeseven.ttf differ diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..edf17f5 --- /dev/null +++ b/config/config.json @@ -0,0 +1,16 @@ +{ + "Mode": "Broadcaster", + "ScreenWidth": 800, + "ScreenHeight": 600, + "Font": { + "Name": "Modeseven", + "Path": "assets/fonts/Modeseven.ttf", + "Size": 18 + }, + "ShowGrid": false, + "Colours": { + "Header": "blue", + "Footer": "red", + "Background": "black" + } +} diff --git a/main.py b/main.py index 0d179e0..060dd01 100644 --- a/main.py +++ b/main.py @@ -1,22 +1,23 @@ -""" -Telefact — minimal runner -- Opens a 4:3 800×600 window -- Renders header (blue band) and footer (red band) -- No formatter yet; that’s next. -""" - import tkinter as tk +from src.config_manager import ConfigManager from src.telefact_renderer import TelefactRenderer from src.core.telefact_frame import TelefactFrame def main(): + config = ConfigManager().config + root = tk.Tk() - root.title("Telefact — Broadcaster Prototype") + root.title(f"Telefact — Broadcaster ({config['Mode']})") + + renderer = TelefactRenderer( + root, + width=config["ScreenWidth"], + height=config["ScreenHeight"], + show_grid=config.get("ShowGrid", False), + font_path=config["Font"]["Path"] + ) - renderer = TelefactRenderer(root, width=800, height=600, show_grid=False) frame = TelefactFrame() - - # put a couple test glyphs so you can confirm alignment quickly for i, ch in enumerate("TELEFACT"): frame.set_cell(2 + i, 2, ch, "yellow") for i, ch in enumerate("BROADCASTER BASE"): diff --git a/src/__pycache__/config_manager.cpython-313.pyc b/src/__pycache__/config_manager.cpython-313.pyc new file mode 100644 index 0000000..d9a63ad Binary files /dev/null and b/src/__pycache__/config_manager.cpython-313.pyc differ diff --git a/src/__pycache__/telefact_renderer.cpython-313.pyc b/src/__pycache__/telefact_renderer.cpython-313.pyc index f839bc3..52a8cfb 100644 Binary files a/src/__pycache__/telefact_renderer.cpython-313.pyc and b/src/__pycache__/telefact_renderer.cpython-313.pyc differ diff --git a/src/config_manager.py b/src/config_manager.py new file mode 100644 index 0000000..b4b56a1 --- /dev/null +++ b/src/config_manager.py @@ -0,0 +1,22 @@ +import json +import os + +class ConfigManager: + def __init__(self, config_path: str = "config/config.json"): + self.config_path = config_path + self.config = self._load() + + def _load(self): + if not os.path.exists(self.config_path): + raise FileNotFoundError(f"Config file not found: {self.config_path}") + with open(self.config_path, "r", encoding="utf-8") as f: + return json.load(f) + + def get(self, key, default=None): + """Return a top-level config key, e.g. 'Font' or 'Mode'.""" + return self.config.get(key, default) + + def save(self): + """Write any changes back to disk.""" + with open(self.config_path, "w", encoding="utf-8") as f: + json.dump(self.config, f, indent=2) diff --git a/src/telefact_renderer.py b/src/telefact_renderer.py index 54ff0f5..812069b 100644 --- a/src/telefact_renderer.py +++ b/src/telefact_renderer.py @@ -1,10 +1,13 @@ """ Telefact Renderer (Tkinter) -- Renders a TelefactFrame to a Canvas. +- Renders a 40×24 Telefact grid within a safe-area margin. +- No header/footer bands; for layout calibration only. - Pure Python (stdlib only). """ +import os import tkinter as tk +from tkinter import font as tkfont from typing import Dict from src.core.telefact_frame import TelefactFrame @@ -12,81 +15,123 @@ PALETTE: Dict[str, str] = { "black": "#000000", "blue": "#0000FF", "red": "#FF0000", - "magenta":"#FF00FF", + "magenta": "#FF00FF", "green": "#00FF00", "cyan": "#00FFFF", "yellow": "#FFFF00", "white": "#FFFFFF", } + class TelefactRenderer: - def __init__(self, root: tk.Tk, width: int = 800, height: int = 600, show_grid: bool = False): + def __init__( + self, + root: tk.Tk, + width: int = 880, + height: int = 660, + show_grid: bool = True, + colors: dict | None = None, + font_path: str | None = None, + font_size: int = 18, + ): self.root = root self.width = width self.height = height self.show_grid = show_grid - # 40×24 grid for Telefact pages + # Safe area for 40×24 grid (simulate CRT overscan) self.cols, self.rows = 40, 24 - self.cell_w = max(1, width // self.cols) - self.cell_h = max(1, height // self.rows) - self.w = self.cell_w * self.cols - self.h = self.cell_h * self.rows + self.margin_x = 24 + self.margin_y = 24 + self.inner_w = width - self.margin_x * 2 + self.inner_h = height - self.margin_y * 2 + self.cell_w = self.inner_w // self.cols + self.cell_h = self.inner_h // self.rows + self.w = self.inner_w + self.h = self.inner_h + + # Colours + self.colors = { + "Background": "black", + "Grid": "#303030" + } + if colors: + self.colors.update(colors) + + # Canvas covers the whole window self.canvas = tk.Canvas( - root, width=self.w, height=self.h, - highlightthickness=0, bg=PALETTE["black"] + root, + width=self.width, + height=self.height, + highlightthickness=0, + bg=PALETTE.get(self.colors["Background"], self.colors["Background"]), ) self.canvas.pack() - # simple monospace font; will swap for Teletext font later - self.font = ("Courier New", max(12, self.cell_h - 9), "bold") + # Font setup + self.font = self._load_font(font_path, font_size) - # ----- low-level drawing helpers ----- + # ---------------------------------------------------------------------- + def _load_font(self, font_path: str | None, font_size: int) -> tuple: + if font_path and os.path.exists(font_path): + try: + font_name = os.path.splitext(os.path.basename(font_path))[0] + try: + self.root.tk.call( + "font", "create", font_name, "-family", font_name, "-size", font_size + ) + except tk.TclError: + pass + print(f"[info] Loaded Telefact font: {font_name}") + return (font_name, font_size) + except Exception as e: + print(f"[warn] Could not load custom font ({e}). Falling back to Courier New.") + elif font_path: + print(f"[warn] Font path not found: {font_path}. Using Courier New.") + return ("Courier New", font_size, "bold") + + # ---------------------------------------------------------------------- def _gx(self, col: int) -> int: - return col * self.cell_w + return self.margin_x + col * self.cell_w def _gy(self, row: int) -> int: - return row * self.cell_h - - def _fill_row(self, row: int, color: str) -> None: - self.canvas.create_rectangle( - self._gx(0), self._gy(row), self._gx(self.cols), self._gy(row + 1), - fill=PALETTE.get(color, color), width=0 - ) + return self.margin_y + row * self.cell_h def _draw_char(self, col: int, row: int, char: str, color: str) -> None: + """Draw a single glyph.""" x = self._gx(col) + 2 y = self._gy(row) + self.cell_h // 2 self.canvas.create_text( - x, y, anchor="w", text=char, - font=self.font, fill=PALETTE.get(color, color) + x, y, anchor="w", text=char, font=self.font, fill=PALETTE.get(color, color) ) def _draw_grid(self) -> None: + """Debug overlay for exact cell spacing.""" for c in range(self.cols + 1): x = self._gx(c) - self.canvas.create_line(x, 0, x, self.h, fill="#202020") + self.canvas.create_line( + x, self.margin_y, x, self.margin_y + self.h, fill=self.colors["Grid"] + ) for r in range(self.rows + 1): y = self._gy(r) - self.canvas.create_line(0, y, self.w, y, fill="#202020") + self.canvas.create_line( + self.margin_x, y, self.margin_x + self.w, y, fill=self.colors["Grid"] + ) - # ----- public API ----- + # ---------------------------------------------------------------------- def render(self, frame: TelefactFrame) -> None: - """Paint the whole frame to the canvas.""" + """Render just the text grid; no header/footer bars.""" self.canvas.delete("all") - # background - self.canvas.create_rectangle(0, 0, self.w, self.h, - fill=PALETTE["black"], width=0) + # Fill total background + self.canvas.create_rectangle( + 0, 0, self.width, self.height, + fill=PALETTE.get(self.colors["Background"], self.colors["Background"]), + width=0, + ) - # header/footer colour bands - for r in frame.header_region(): - self._fill_row(r, "blue") - for r in frame.footer_region(): - self._fill_row(r, "red") - - # draw text cells + # Draw Teletext cells for row in range(frame.rows): for col in range(frame.cols): ch, fg = frame.grid[row][col]