diff --git a/config/config.json b/config/config.json index edf17f5..0a2a76c 100644 --- a/config/config.json +++ b/config/config.json @@ -1,16 +1,30 @@ { "Mode": "Broadcaster", + "ScreenWidth": 800, "ScreenHeight": 600, + "ShowGrid": false, + "Font": { "Name": "Modeseven", "Path": "assets/fonts/Modeseven.ttf", "Size": 18 }, - "ShowGrid": false, + "Colours": { "Header": "blue", "Footer": "red", - "Background": "black" + "Background": "black", + "TextPrimary": "white", + "TextAccent": "yellow", + "Clock": "yellow" + }, + + "Header": { + "SelectedPage": "P100", + "CurrentPage": "100", + "ServiceName": "Telefact", + "ServiceFG": "yellow", + "ServiceBG": "red" } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..a14546f --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,61 @@ +# Telefact — Changelog + +Project: Telefact +Repository: Leak Technologies +Language: Python (Tkinter Implementation) +Version Line: v0.1.x (Foundational Builds) +Grid Layout: 40×24 Teletext-Compatible + +------------------------------------------------------------ + +## [v0.1.2] — Header Finalization & Renderer Upgrade (2025-11-03) + +### Added +- Full background rendering support in TelefactRenderer + - Each cell now supports independent background colour + - Renderer draws proper CRT-style safe-area margins +- TelefactHeader finalized + - Centered "Telefact" block with blue background + - White page numbers (P100 / 100) + - Yellow live-updating clock (%b%d %H:%M:%S) + - Accurate Ceefax-style alignment maintained across full grid width +- Config.json updated with Header section for colour and text configuration +- Updated TelefactFrame to support (char, fg, bg) tuples + +### Fixed +- Misaligned header text and spacing between elements +- Incorrect handling of colour blocks for service provider +- Missing current_page display now restored + +### Structure +telefact/ +├── src/ +│ ├── core/ +│ │ ├── telefact_frame.py +│ │ ├── telefact_header.py +│ │ └── telefact_formatter.py +│ ├── telefact_renderer.py +│ └── config_manager.py +├── docs/ +│ ├── README.md +│ └── CHANGELOG.md + +### Next Planned (v0.1.3) +- Introduce footer renderer with subpage counter and colored background band +- Add configuration-driven page rotation logic +- Implement "page tuning" animation for realism (Ceefax-style search effect) + +------------------------------------------------------------ + +## [v0.1.1] — Header Alignment Prototype (2025-11-02) +- Implemented basic header text alignment +- Added real-time clock display without background colour support +- Began refinement of grid-safe text spacing + +------------------------------------------------------------ + +## [v0.1.0] — Initial Python Framework (2025-11-01) +- Core grid model (TelefactFrame) defines 40×24 matrix +- Base TelefactRenderer with safe-area calibration +- Preliminary test layout with header, body, and footer +- Initial config.json support for font, size, and colour diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..8dbb7ac --- /dev/null +++ b/docs/README.md @@ -0,0 +1,119 @@ +# Telefact — Python Edition + +Telefact is a modern re-creation of a Ceefax-style Teletext broadcasting system, written in Python using the Tkinter library. +It emulates the visual and structural behaviour of broadcast Teletext systems, maintaining a strict 40×24 character grid layout. +This version focuses on the "Broadcaster" mode, replicating the look and timing of early digital information services such as BBC Ceefax and Channel 4 Oracle. + +------------------------------------------------------------ +VERSION INFORMATION +------------------------------------------------------------ +Version: v0.1.2 +Language: Python 3.13 +Interface: Tkinter (standard library) +Target: Desktop application (Linux / Windows) +Resolution: 40×24 Teletext grid +Font: Modeseven (Teletext recreation) + +------------------------------------------------------------ +PROJECT GOALS +------------------------------------------------------------ +- Faithfully reproduce the look and feel of classic Teletext pages. +- Maintain accurate grid alignment for all text and graphics. +- Provide a configurable header, footer, and body layout. +- Simulate real broadcast elements such as page numbers and timestamps. +- Establish the foundation for future expansion into dynamic page rotation and live content updates. + +------------------------------------------------------------ +CURRENT FEATURES +------------------------------------------------------------ +- 40×24 Teletext grid rendering via TelefactRenderer. +- Safe-area margins to simulate CRT overscan. +- Dynamic Ceefax-style header displaying: + - Selected page number (e.g., P100) + - Service provider name block (e.g., Telefact) + - Current page number (e.g., 100) + - Real-time clock (updates every second) +- Configurable colours and font settings via config.json. +- Keyboard shortcuts: + - Q or ESC to exit the application. + +------------------------------------------------------------ +SOURCE STRUCTURE +------------------------------------------------------------ +telefact/ +├── src/ +│ ├── core/ +│ │ ├── telefact_frame.py +│ │ ├── telefact_header.py +│ │ └── telefact_formatter.py +│ ├── telefact_renderer.py +│ └── config_manager.py +├── docs/ +│ ├── README.md +│ └── CHANGELOG.md +├── assets/ +│ └── fonts/ +│ ├── Modeseven.ttf +│ ├── EuropeanTeletext.ttf +│ └── EuropeanTeletextNuevo.ttf +└── main.py + +------------------------------------------------------------ +CONFIGURATION OVERVIEW +------------------------------------------------------------ +The configuration file is located at: + config/config.json + +Example: +{ + "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", + "TextPrimary": "white", + "TextAccent": "yellow", + "Clock": "yellow" + }, + "Header": { + "ServiceName": "Telefact", + "ServiceFG": "yellow", + "ServiceBG": "blue", + "SelectedPage": "P100", + "CurrentPage": "100" + } +} + +------------------------------------------------------------ +ROADMAP +------------------------------------------------------------ +v0.1.3 — Footer System and Subpage Counter + - Implement footer band with coloured background and subpage indicator. + - Integrate page tuning animation (Ceefax-style). + - Add configuration options for rotation speed and page timing. + +v0.2.0 — Page System Expansion + - Introduce multiple page definitions with rotation logic. + - Implement Teletext page index and feed loading. + - Begin adding dynamic content from local cache and static feeds. + +v0.3.0 — Broadcaster Automation + - Simulate channel loop broadcasting through scheduled pages. + - Add simple playback manager for testing page timing. + +------------------------------------------------------------ +AUTHOR AND COPYRIGHT +------------------------------------------------------------ +Developed by Stu Leak +Leak Technologies (2025) +All rights reserved. + +Fonts: Modeseven and European Teletext by their respective creators. diff --git a/main.py b/main.py index 060dd01..5c7ce73 100644 --- a/main.py +++ b/main.py @@ -1,33 +1,60 @@ +# ============================================================ +# File: /home/stu/Projects/Local REPO/telefact/main.py +# Description: +# Entry point for the Telefact Broadcaster mode. +# Initializes the Tkinter window, loads configuration, +# and renders the Teletext grid with dynamic header. +# ============================================================ + import tkinter as tk from src.config_manager import ConfigManager from src.telefact_renderer import TelefactRenderer from src.core.telefact_frame import TelefactFrame +from src.core.telefact_formatter import TelefactFormatter +from src.core.telefact_header import TelefactHeader + def main(): + # --- Load configuration --- config = ConfigManager().config + # --- Initialize Tkinter window --- root = tk.Tk() root.title(f"Telefact — Broadcaster ({config['Mode']})") + # --- Create renderer --- renderer = TelefactRenderer( root, width=config["ScreenWidth"], height=config["ScreenHeight"], show_grid=config.get("ShowGrid", False), - font_path=config["Font"]["Path"] + font_path=config["Font"]["Path"], ) + # --- Prepare frame --- frame = TelefactFrame() - for i, ch in enumerate("TELEFACT"): - frame.set_cell(2 + i, 2, ch, "yellow") - for i, ch in enumerate("BROADCASTER BASE"): - frame.set_cell(2 + i, 4, ch, "white") + # --- Initialize header --- + header = TelefactHeader(frame, config) + header.render() + header.update_time(root, renderer) + + # --- Body & footer test content --- + formatter = TelefactFormatter(frame) + formatter.add_body_line(1, "Welcome to the Telefact Broadcaster base.", align="center", color="white") + formatter.add_body_line(3, "Press Q or ESC to exit.", align="center", color="cyan") + formatter.set_footer("PAGE 100 TELEFACT", align="center", color="green") + + # --- Render frame --- renderer.render(frame) + # --- Bind exit keys --- root.bind("", lambda e: root.quit()) root.bind("q", lambda e: root.quit()) + + # --- Start loop --- root.mainloop() + if __name__ == "__main__": main() diff --git a/src/__pycache__/telefact_renderer.cpython-313.pyc b/src/__pycache__/telefact_renderer.cpython-313.pyc index 52a8cfb..4c62ac4 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/core/__pycache__/telefact_formatter.cpython-313.pyc b/src/core/__pycache__/telefact_formatter.cpython-313.pyc new file mode 100644 index 0000000..26ad071 Binary files /dev/null and b/src/core/__pycache__/telefact_formatter.cpython-313.pyc differ diff --git a/src/core/__pycache__/telefact_frame.cpython-313.pyc b/src/core/__pycache__/telefact_frame.cpython-313.pyc index 558292a..22ec12c 100644 Binary files a/src/core/__pycache__/telefact_frame.cpython-313.pyc and b/src/core/__pycache__/telefact_frame.cpython-313.pyc differ diff --git a/src/core/__pycache__/telefact_header.cpython-313.pyc b/src/core/__pycache__/telefact_header.cpython-313.pyc new file mode 100644 index 0000000..d2ed1c3 Binary files /dev/null and b/src/core/__pycache__/telefact_header.cpython-313.pyc differ diff --git a/src/core/telefact_formatter.py b/src/core/telefact_formatter.py index e69de29..14644cf 100644 --- a/src/core/telefact_formatter.py +++ b/src/core/telefact_formatter.py @@ -0,0 +1,76 @@ +""" +Telefact Formatter +------------------ +Handles layout and text placement for header, body, and footer regions +within a 40×24 Telefact frame. + +This keeps Teletext-style alignment and line boundaries. +""" + +from typing import Literal +from src.core.telefact_frame import TelefactFrame + + +class TelefactFormatter: + """Provides structured access to write header, body, and footer text.""" + + def __init__(self, frame: TelefactFrame): + self.frame = frame + # Define region boundaries (row numbers) + self.header_row = 0 + self.footer_row = frame.rows - 1 + self.body_start = 1 + self.body_end = self.footer_row - 1 + + # ------------------------------------------------------------------ + # text placement utilities + # ------------------------------------------------------------------ + def _place_text( + self, + text: str, + row: int, + align: Literal["left", "center", "right"] = "left", + color: str = "white", + pad_char: str = " ", + ) -> None: + """Place text on a given row with Teletext-style alignment.""" + max_width = self.frame.cols + text = text[:max_width] # clip overflow + + if align == "left": + col_start = 0 + elif align == "center": + col_start = max(0, (max_width - len(text)) // 2) + else: # right + col_start = max(0, max_width - len(text)) + + # pad to maintain clean grid line + padded = text.ljust(max_width, pad_char) + for i, ch in enumerate(padded): + self.frame.set_cell(col_start + i, row, ch, color) + + # ------------------------------------------------------------------ + # header/body/footer helpers + # ------------------------------------------------------------------ + def set_header(self, text: str, align="center", color="yellow") -> None: + """Write the header row (row 0).""" + self._place_text(text, self.header_row, align, color) + + def add_body_line( + self, line_num: int, text: str, align="left", color="white" + ) -> None: + """Write a line in the body area (rows 1–22).""" + target_row = self.body_start + line_num + if self.body_start <= target_row <= self.body_end: + self._place_text(text, target_row, align, color) + + def set_footer(self, text: str, align="center", color="cyan") -> None: + """Write the footer row (row 23).""" + self._place_text(text, self.footer_row, align, color) + + # ------------------------------------------------------------------ + # convenience + # ------------------------------------------------------------------ + def clear(self) -> None: + """Clear the frame (fills with spaces).""" + self.frame.clear() diff --git a/src/core/telefact_frame.py b/src/core/telefact_frame.py index 7821f15..9674e28 100644 --- a/src/core/telefact_frame.py +++ b/src/core/telefact_frame.py @@ -1,42 +1,67 @@ -""" -Telefact Frame (model) -- Defines a 40×24 grid and logical regions. -- No rendering here—pure data model. -""" +# ============================================================ +# File: /home/stu/Projects/Local REPO/telefact/src/core/telefact_frame.py +# Description: +# Telefact Frame (model) +# Defines a 40×24 grid and logical regions for Teletext layout. +# Each cell now supports both foreground and background colours. +# This file contains no rendering logic — pure data model. +# ============================================================ + class TelefactFrame: def __init__(self, cols: int = 40, rows: int = 24): self.cols = cols self.rows = rows + # Define Teletext-like regions self.header_rows = 1 self.footer_rows = 1 self.body_rows = self.rows - (self.header_rows + self.footer_rows) - # grid[row][col] -> (char, color_name) - self.grid = [[(" ", "white") for _ in range(self.cols)] for _ in range(self.rows)] + # grid[row][col] -> (char, fg_color, bg_color) + self.grid = [ + [(" ", "white", "black") for _ in range(self.cols)] + for _ in range(self.rows) + ] - # basic cell ops - def set_cell(self, col: int, row: int, char: str, color: str = "white"): + # -------------------------------------------------------- + # Basic cell operations + # -------------------------------------------------------- + def set_cell( + self, + col: int, + row: int, + char: str, + fg_color: str = "white", + bg_color: str = "black", + ): + """Sets a character and both colours in a given grid cell.""" if 0 <= col < self.cols and 0 <= row < self.rows and char: - self.grid[row][col] = (char[0], color) + self.grid[row][col] = (char[0], fg_color, bg_color) def get_cell(self, col: int, row: int): + """Returns (char, fg, bg) for the given grid position.""" if 0 <= col < self.cols and 0 <= row < self.rows: return self.grid[row][col] - return (" ", "white") + return (" ", "white", "black") - def clear(self, char: str = " ", color: str = "white"): + def clear(self, char: str = " ", fg_color: str = "white", bg_color: str = "black"): + """Clears the entire frame to a given colour and character.""" for r in range(self.rows): for c in range(self.cols): - self.grid[r][c] = (char, color) + self.grid[r][c] = (char, fg_color, bg_color) - # regions + # -------------------------------------------------------- + # Region helpers + # -------------------------------------------------------- def header_region(self): + """Row indices for header region (top row).""" return range(0, self.header_rows) def body_region(self): + """Row indices for main content area.""" return range(self.header_rows, self.header_rows + self.body_rows) def footer_region(self): + """Row indices for footer region (bottom row).""" return range(self.rows - self.footer_rows, self.rows) diff --git a/src/core/telefact_header.py b/src/core/telefact_header.py index e69de29..3b1fa22 100644 --- a/src/core/telefact_header.py +++ b/src/core/telefact_header.py @@ -0,0 +1,114 @@ +# ============================================================ +# File: /home/stu/Projects/Local REPO/telefact/src/core/telefact_header.py +# Description: +# Dynamic Telefact header renderer for broadcaster mode. +# Displays page numbers, centered service name block with +# colored background, and a live timestamp. +# ============================================================ + +import tkinter as tk +from datetime import datetime +from src.core.telefact_frame import TelefactFrame + + +class TelefactHeader: + """ + Final layout (Ceefax style): + ┌────────────────────────────────────────────────────────┐ + │ P100 █ Telefact █ 100 Nov03 12:53:10 │ + └────────────────────────────────────────────────────────┘ + """ + + def __init__(self, frame: TelefactFrame, config: dict): + self.frame = frame + self.config = config + + # --- Read config values --- + colours = config.get("Colours", {}) + header_cfg = config.get("Header", {}) + + # Service name & colours + self.service_name = header_cfg.get("ServiceName", "Telefact") + self.service_fg = header_cfg.get("ServiceFG", colours.get("TextAccent", "yellow")) + self.service_bg = header_cfg.get("ServiceBG", colours.get("Header", "blue")) + + # Page numbers + self.selected_page = header_cfg.get("SelectedPage", "P100") + self.current_page = header_cfg.get("CurrentPage", "100") + + # Colours + self.text_white = colours.get("TextPrimary", "white") + self.time_yellow = colours.get("Clock", "yellow") + + # Layout + self.padding = 1 + self.time_format = "%b%d %H:%M:%S" + + # -------------------------------------------------------- + def _clear_header_row(self): + """Clears only the top row.""" + for c in range(self.frame.cols): + self.frame.set_cell(c, 0, " ", self.text_white, "black") + + def _draw_text(self, col: int, text: str, color: str) -> int: + """Draws text from a given column; returns new cursor position.""" + for i, ch in enumerate(text): + pos = col + i + if pos < self.frame.cols: + self.frame.set_cell(pos, 0, ch, color, "black") + return col + len(text) + + def _draw_service_block(self, text: str, fg: str, bg: str, left_bound: int, right_bound: int): + """Draw centered service block with full blue background.""" + block = f"{' ' * self.padding}{text}{' ' * self.padding}" + total_space = right_bound - left_bound + start_col = left_bound + (total_space // 2) - (len(block) // 2) + start_col = max(start_col, 0) + end_col = start_col + len(block) + + # --- Fill background cells properly --- + for c in range(start_col, end_col): + if 0 <= c < self.frame.cols: + self.frame.set_cell(c, 0, " ", bg, bg) + + # --- Overlay text with proper foreground and background --- + for i, ch in enumerate(text): + pos = start_col + i + self.padding + if 0 <= pos < self.frame.cols: + self.frame.set_cell(pos, 0, ch, fg, bg) + + # -------------------------------------------------------- + def render(self): + """Renders header row with page numbers, service, and clock.""" + self._clear_header_row() + + # Left: selected page + col_left = 0 + col_left = self._draw_text(col_left, self.selected_page, self.text_white) + col_left += 2 # spacing after + + # Right: timestamp (always yellow) + time_text = datetime.now().strftime(self.time_format) + right_start = self.frame.cols - len(time_text) + self._draw_text(right_start, time_text, self.time_yellow) + + # Mid-right: current page number (white) + current_page_len = len(self.current_page) + current_page_start = right_start - (current_page_len + 3) + self._draw_text(current_page_start, self.current_page, self.text_white) + + # Center: service provider (blue block) + self._draw_service_block( + self.service_name, + self.service_fg, + self.service_bg, + left_bound=col_left, + right_bound=current_page_start - 1, + ) + + # -------------------------------------------------------- + def update_time(self, root: tk.Tk, renderer, interval_ms: int = 1000): + """Updates time every second without redrawing other rows.""" + self.render() + renderer.render(self.frame) + root.after(interval_ms, lambda: self.update_time(root, renderer, interval_ms)) diff --git a/src/telefact_renderer.py b/src/telefact_renderer.py index 812069b..5188667 100644 --- a/src/telefact_renderer.py +++ b/src/telefact_renderer.py @@ -1,9 +1,11 @@ -""" -Telefact Renderer (Tkinter) -- Renders a 40×24 Telefact grid within a safe-area margin. -- No header/footer bands; for layout calibration only. -- Pure Python (stdlib only). -""" +# ============================================================ +# File: /home/stu/Projects/Local REPO/telefact/src/telefact_renderer.py +# Description: +# Telefact Renderer (Tkinter) +# Renders a 40×24 Telefact grid with full foreground/background +# colour support, aligned inside a CRT-style safe area margin. +# No header/footer bars — layout calibration only. +# ============================================================ import os import tkinter as tk @@ -11,15 +13,18 @@ from tkinter import font as tkfont from typing import Dict from src.core.telefact_frame import TelefactFrame +# ------------------------------------------------------------ +# Standard Teletext colour palette (8 basic colours) +# ------------------------------------------------------------ PALETTE: Dict[str, str] = { - "black": "#000000", - "blue": "#0000FF", - "red": "#FF0000", + "black": "#000000", + "blue": "#0000FF", + "red": "#FF0000", "magenta": "#FF00FF", - "green": "#00FF00", - "cyan": "#00FFFF", - "yellow": "#FFFF00", - "white": "#FFFFFF", + "green": "#00FF00", + "cyan": "#00FFFF", + "yellow": "#FFFF00", + "white": "#FFFFFF", } @@ -39,27 +44,30 @@ class TelefactRenderer: self.height = height self.show_grid = show_grid - # Safe area for 40×24 grid (simulate CRT overscan) + # Teletext dimensions self.cols, self.rows = 40, 24 + + # Safe area margins (simulate CRT overscan) self.margin_x = 24 self.margin_y = 24 self.inner_w = width - self.margin_x * 2 self.inner_h = height - self.margin_y * 2 + # Per-cell dimensions 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 + # Base colours self.colors = { "Background": "black", - "Grid": "#303030" + "Grid": "#303030", } if colors: self.colors.update(colors) - # Canvas covers the whole window + # Canvas setup self.canvas = tk.Canvas( root, width=self.width, @@ -72,8 +80,9 @@ class TelefactRenderer: # Font setup self.font = self._load_font(font_path, font_size) - # ---------------------------------------------------------------------- + # ------------------------------------------------------------------ def _load_font(self, font_path: str | None, font_size: int) -> tuple: + """Loads Teletext font or falls back to Courier New.""" if font_path and os.path.exists(font_path): try: font_name = os.path.splitext(os.path.basename(font_path))[0] @@ -91,23 +100,45 @@ class TelefactRenderer: print(f"[warn] Font path not found: {font_path}. Using Courier New.") return ("Courier New", font_size, "bold") - # ---------------------------------------------------------------------- + # ------------------------------------------------------------------ def _gx(self, col: int) -> int: + """Grid X coordinate.""" return self.margin_x + col * self.cell_w def _gy(self, row: int) -> int: + """Grid Y coordinate.""" 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) + # ------------------------------------------------------------------ + def _draw_cell(self, col: int, row: int, char: str, fg: str, bg: str) -> None: + """Draw a full cell (background + glyph).""" + x = self._gx(col) + y = self._gy(row) + + # Draw background block + self.canvas.create_rectangle( + x, + y, + x + self.cell_w, + y + self.cell_h, + fill=PALETTE.get(bg, bg), + outline=PALETTE.get(bg, bg), ) + # Draw text glyph + if char.strip(): + self.canvas.create_text( + x + 2, + y + self.cell_h // 2, + anchor="w", + text=char, + font=self.font, + fill=PALETTE.get(fg, fg), + ) + + # ------------------------------------------------------------------ def _draw_grid(self) -> None: - """Debug overlay for exact cell spacing.""" + """Optional debug overlay for cell alignment.""" for c in range(self.cols + 1): x = self._gx(c) self.canvas.create_line( @@ -119,24 +150,26 @@ class TelefactRenderer: self.margin_x, y, self.margin_x + self.w, y, fill=self.colors["Grid"] ) - # ---------------------------------------------------------------------- + # ------------------------------------------------------------------ def render(self, frame: TelefactFrame) -> None: - """Render just the text grid; no header/footer bars.""" + """Renders the full Telefact grid (foreground + background).""" self.canvas.delete("all") - # Fill total background + # Fill background self.canvas.create_rectangle( - 0, 0, self.width, self.height, + 0, + 0, + self.width, + self.height, fill=PALETTE.get(self.colors["Background"], self.colors["Background"]), width=0, ) - # Draw Teletext cells + # Draw all cells (bg first, then text) for row in range(frame.rows): for col in range(frame.cols): - ch, fg = frame.grid[row][col] - if ch.strip(): - self._draw_char(col, row, ch, fg) + ch, fg, bg = frame.get_cell(col, row) + self._draw_cell(col, row, ch, fg, bg) if self.show_grid: self._draw_grid()