Initial commit: Core Teletext Editor functionality

This commit is contained in:
2025-12-28 21:38:21 +01:00
commit 6000897578
4494 changed files with 537255 additions and 0 deletions

BIN
bbc.t42 Normal file

Binary file not shown.

3
run_editor.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
export PYTHONPATH=$PYTHONPATH:$(pwd)/src
./venv/bin/python src/main.py

BIN
specification.pdf Normal file

Binary file not shown.

13
src/main.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from PyQt6.QtWidgets import QApplication
from teletext.ui import MainWindow
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

60
src/teletext/charsets.py Normal file
View File

@@ -0,0 +1,60 @@
"""
Teletext Character Sets (G0).
Maps the specific code points (0x23, 0x24, 0x40, 0x5B-0x5E, 0x60, 0x7B-0x7E)
to Unicode characters based on the National Option (3 bits).
"""
# Default (English) - Option 000
ENGLISH = {
0x23: '#', 0x24: '$', 0x40: '@',
0x5B: '[', 0x5C: '\\', 0x5D: ']', 0x5E: '^',
0x5F: '_', 0x60: '`',
0x7B: '{', 0x7C: '|', 0x7D: '}', 0x7E: '~'
}
# Swedish/Finnish/Hungarian - Option 010 (2)
SWEDISH_FINNISH = {
0x23: '#', 0x24: '¤', 0x40: 'É',
0x5B: 'Ä', 0x5C: 'Ö', 0x5D: 'Å', 0x5E: 'Ü',
0x5F: '_', 0x60: 'é',
0x7B: 'ä', 0x7C: 'ö', 0x7D: 'å', 0x7E: 'ü'
}
# German - Option 001 (1)
GERMAN = {
0x23: '#', 0x24: '$', 0x40: '§',
0x5B: 'Ä', 0x5C: 'Ö', 0x5D: 'Ü', 0x5E: '^',
0x5F: '_', 0x60: '`',
0x7B: 'ä', 0x7C: 'ö', 0x7D: 'ü', 0x7E: 'ß'
}
# We can add more as needed.
SETS = [
ENGLISH, # 000
GERMAN, # 001
SWEDISH_FINNISH, # 010
ENGLISH, # Italian (011) - placeholder
ENGLISH, # French (100) - placeholder
ENGLISH, # Portuguese/Spanish (101) - placeholder
ENGLISH, # Turkish (110) - placeholder
ENGLISH, # Romania (111) - placeholder
]
def get_char(byte_val, subset_idx):
if subset_idx < 0 or subset_idx >= len(SETS):
subset_idx = 0
mapping = SETS[subset_idx]
# If byte is in mapping, return mapped char.
# Else return ASCII equivalent (for basic chars)
valid_byte = byte_val & 0x7F # Strip parity if present (though our packet data is 8-bit usually already stripping parity?)
# Packet data we store is raw bytes. We should probably strip parity bit 7 before lookup.
if valid_byte in mapping:
return mapping[valid_byte]
return chr(valid_byte)

119
src/teletext/io.py Normal file
View File

@@ -0,0 +1,119 @@
import os
from typing import List
from .models import Packet, Page, TeletextService
def load_t42(file_path: str) -> TeletextService:
service = TeletextService()
with open(file_path, 'rb') as f:
while True:
chunk = f.read(42)
if not chunk:
break
if len(chunk) < 42:
# Should not happen in a valid T42 stream, or we just ignore incomplete tail
break
packet = Packet(chunk)
service.all_packets.append(packet)
# Logic to group into pages.
# This is non-trivial because packets for a page might be interleaved or sequential.
# Standard implementation: Packets arrive in order. Row 0 starts a new page/subpage.
if packet.row == 0:
# Start of a new page header.
# Byte 2-9 of header contain Page Number, Subcode, Control bits etc.
# We need to parse the header to identify the page.
# Header format (after Mag/Row):
# Bytes: P1 P2 S1 S2 S3 S4 C1 C2 ...
# All Hamming 8/4 encoded.
# For now, let's just create a new page entry for every Header we see,
# or find the existing one if we want to support updates (but T42 usually is a stream capture).
# If it's an editor file, it's likely sequential.
p_num, sub_code = parse_header(packet.data)
# Create new page
new_page = Page(magazine=packet.magazine, page_number=p_num, sub_code=sub_code)
new_page.packets.append(packet)
service.pages.append(new_page)
else:
# Add to the "current" page of this magazine.
# We need to track the current active page for each magazine.
# A simplistic approach: add to the last page added that matches the magazine ??
# Robust approach: Maintain a dict of current_pages_by_magazine.
# Let's find the last page in service that matches the packet's magazine
# This is O(N) but N (pages) is small.
target_page = None
for p in reversed(service.pages):
if p.magazine == packet.magazine:
target_page = p
break
if target_page:
target_page.packets.append(packet)
else:
# Packet without a header? Orphaned. Just keep in all_packets
pass
return service
def save_t42(file_path: str, service: TeletextService):
with open(file_path, 'wb') as f:
# User requirement: "without rearranging the order of the packets"
# Implies we should iterate the original list.
# However, if we edit data, we modify the Packet objects in place.
# If we Add/Delete packets, we need to handle that.
for packet in service.all_packets:
# Reconstruct the 42 bytes from the packet fields
# The packet.data (bytearray) should be mutable and edited by the UI.
# packet.original_data (first 2 bytes) + packet.data
# Note: If we changed Magazine or Row, we'd need to re-encode the first 2 bytes.
# For now, assume we primarily edit content (bytes 2-41).
header = packet.original_data[:2] # Keep original address for now
# TODO: regenerating header if Mag/Row changed
f.write(header + packet.data)
def decode_hamming_8_4(byte_val):
return ((byte_val >> 1) & 1) | \
(((byte_val >> 3) & 1) << 1) | \
(((byte_val >> 5) & 1) << 2) | \
(((byte_val >> 7) & 1) << 3)
def parse_header(data: bytearray):
# Data is 40 bytes.
# Bytes 0-7 are Page Num (2), Subcode (4), Control (2) - ALL Hamming encoded.
# 0: Page Units (PU)
# 1: Page Tens (PT)
pu = decode_hamming_8_4(data[0])
pt = decode_hamming_8_4(data[1])
page_num = (pt & 0xF) * 10 + (pu & 0xF)
# Subcode: S1, S2, S3, S4
# S1 (low), S2, S3, S4 (high)
s1 = decode_hamming_8_4(data[2])
s2 = decode_hamming_8_4(data[3])
s3 = decode_hamming_8_4(data[4])
s4 = decode_hamming_8_4(data[5])
# Subcode logic is a bit complex with specific bit mapping for "Time" vs "Subcode"
# But usually just combining them gives the raw subcode value.
# S1: bits 0-3
# S2: bits 4-6 (bit 4 is C4) -> actually S2 has 3 bits of subcode + 1 control bit usually?
# Let's simplify and just concat them for a unique identifier.
sub_code = s1 | (s2 << 4) | (s3 << 8) | (s4 << 12)
return page_num, sub_code

93
src/teletext/models.py Normal file
View File

@@ -0,0 +1,93 @@
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class Packet:
"""
Represents a single Teletext packet (Row).
In T42, we have 42 bytes:
Byte 0-1: Magazine and Row Address (Hamming 8/4 encoding)
Byte 2-41: Data bytes (40 bytes)
"""
original_data: bytes
magazine: int = field(init=False)
row: int = field(init=False)
data: bytearray = field(init=False)
def __post_init__(self):
if len(self.original_data) != 42:
raise ValueError(f"Packet must be 42 bytes, got {len(self.original_data)}")
# Parse Magazine and Row from the first 2 bytes (Hamming 8/4)
# MRAG (Magazine + Row Address Group)
# Byte 0: P1 D1 P2 D2 P3 D3 P4 D4
# Byte 1: P1 D1 P2 D2 P3 D3 P4 D4
b1 = self.original_data[0]
b2 = self.original_data[1]
# De-interleave Hamming bits to get M (3 bits) and R (5 bits)
# This is the "basic" interpretation.
# For a robust editor we assume the input T42 is valid or we just store bytes.
# But we need Mag/Row to organize pages.
# Decode Hamming 8/4 logic is complex to implementation from scratch correctly
# without a reference, but usually D1, D2, D3, D4 are at bit positions 1, 3, 5, 7
# (0-indexed, where 0 is LSB).
# Let's perform a simple extraction assuming no bit errors for now.
def decode_hamming_8_4(byte_val):
# Extract data bits: bits 1, 3, 5, 7
return ((byte_val >> 1) & 1) | \
(((byte_val >> 3) & 1) << 1) | \
(((byte_val >> 5) & 1) << 2) | \
(((byte_val >> 7) & 1) << 3)
d1 = decode_hamming_8_4(b1)
d2 = decode_hamming_8_4(b2)
# Magazine is 3 bits (Logic is specific: Mag 8 is encoded as 0)
# Row is 5 bits.
# According to Spec (ETSI EN 300 706):
# b1 encoded: M1 M2 M3 R1
# b2 encoded: R2 R3 R4 R5
self.magazine = (d1 & 0b0111)
if self.magazine == 0:
self.magazine = 8
row_low_bit = (d1 >> 3) & 1
row_high_bits = d2
self.row = (row_high_bits << 1) | row_low_bit
self.data = bytearray(self.original_data[2:])
@property
def is_header(self):
return self.row == 0
@dataclass
class Page:
"""
Represents a Teletext Page (e.g., 100).
Can have multiple subpages.
"""
magazine: int
page_number: int # 00-99
sub_code: int = 0 # Subpage code (0000 to 3F7F hex usually, simplest is 0-99 equivalent)
packets: List[Packet] = field(default_factory=list)
@property
def full_page_number(self):
return f"{self.magazine}{self.page_number:02d}"
@dataclass
class TeletextService:
"""
Container for all pages.
"""
pages: List[Page] = field(default_factory=list)
# We also keep a flat list of all packets to preserve order on save
all_packets: List[Packet] = field(default_factory=list)

274
src/teletext/renderer.py Normal file
View File

@@ -0,0 +1,274 @@
from PyQt6.QtWidgets import QWidget
from PyQt6.QtGui import QPainter, QColor, QFont, QImage, QBrush, QPen
from PyQt6.QtCore import Qt, QRect, QSize, pyqtSignal
from .models import Page, Packet
from .charsets import get_char
# Teletext Palette
COLORS = [
QColor(0, 0, 0), # Black
QColor(255, 0, 0), # Red
QColor(0, 255, 0), # Green
QColor(255, 255, 0), # Yellow
QColor(0, 0, 255), # Blue
QColor(255, 0, 255), # Magenta
QColor(0, 255, 255), # Cyan
QColor(255, 255, 255) # White
]
class TeletextCanvas(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setMinimumSize(480, 500) # 40x12 * 25x20 approx
self.page: Page = None
self.subset_idx = 0 # Default English
# Teletext is 40 columns x 25 rows
# We will render to a fixed size QImage and scale it
self.cols = 40
self.rows = 25
self.cell_w = 12
self.cell_h = 20
self.img_w = self.cols * self.cell_w
self.img_h = self.rows * self.cell_h
self.buffer = QImage(self.img_w, self.img_h, QImage.Format.Format_RGB32)
self.buffer.fill(Qt.GlobalColor.black)
# Font for text
self.font = QFont("Courier New", 14)
self.font.setStyleHint(QFont.StyleHint.Monospace)
self.font.setBold(True)
# Cursor state
self.cursor_x = 0
self.cursor_y = 0
self.cursor_visible = True
# Blinking cursor timer could be added, for now static inverted is fine or toggle on timer elsewhere
def set_cursor(self, x, y):
self.cursor_x = max(0, min(self.cols - 1, x))
self.cursor_y = max(0, min(self.rows - 1, y))
self.update()
def move_cursor(self, dx, dy):
self.cursor_x = max(0, min(self.cols - 1, self.cursor_x + dx))
self.cursor_y = max(0, min(self.rows - 1, self.cursor_y + dy))
self.update()
def set_page(self, page: Page):
self.page = page
self.cursor_x = 0
self.cursor_y = 0
self.redraw()
self.update()
def handle_input(self, text):
if not self.page:
return
# Find packet for current row
packet = None
for p in self.page.packets:
if p.row == self.cursor_y:
packet = p
break
if packet:
# Edit the data
# Data bytes start at index 0 of packet.data corresponding to Col 0?
# Standard Teletext row is 40 chars. Packet data is 40 bytes.
if 0 <= self.cursor_x < 40:
# Get ASCII value
# We need to Reverse Map chars to Teletext Bytes if we want full suppport
# For now, just taking ord() for basic ASCII
# TODO: reverse lookup for National Chars
# Check if text is a single char
if len(text) == 1:
byte_val = ord(text)
# Simple filter
if byte_val > 255: byte_val = 0x3F # ?
packet.data[self.cursor_x] = byte_val
self.redraw()
self.move_cursor(1, 0)
else:
# Create a packet if it doesn't exist for this row?
# Creating new packets in a T42 stream is tricky (insertion).
# For now, only edit existing rows.
pass
def redraw(self):
self.buffer.fill(Qt.GlobalColor.black)
painter = QPainter(self.buffer)
painter.setFont(self.font)
if not self.page:
painter.setPen(Qt.GlobalColor.white)
painter.drawText(10, 20, "No Page Loaded")
painter.end()
return
# Draw each packet
# Initialize a grid of empty chars
grid = [None] * 26 # 0-25
for p in self.page.packets:
if 0 <= p.row <= 25:
grid[p.row] = p
for r in range(25):
packet = grid[r]
self.draw_row(painter, r, packet)
painter.end()
def draw_row(self, painter, row, packet):
# Default State at start of row
fg = COLORS[7] # White
bg = COLORS[0] # Black
graphics_mode = False
contiguous = True # Mosaic
hold_graphics = False
held_char = 0x20 # Space
y = row * self.cell_h
data = b''
if packet:
data = packet.data # 40 bytes
else:
data = b'\x20' * 40
# Header string for Row 0 columns 0-7
header_prefix = ""
if row == 0 and self.page:
header_prefix = f"P{self.page.magazine}{self.page.page_number:02d}"
# Pad to 8 chars
header_prefix = header_prefix.ljust(8)
for c in range(40):
x = c * self.cell_w
# Decide byte value
if row == 0 and c < 8:
# Use generated header prefix
byte_val = ord(header_prefix[c])
else:
byte_val = data[c] if c < len(data) else 0x20
byte_val &= 0x7F # Strip parity
# Control Codes
is_control = False
if byte_val < 0x20:
is_control = True
# Handle control codes
if 0x00 <= byte_val <= 0x07: # Alpha Color
fg = COLORS[byte_val]
graphics_mode = False
elif 0x10 <= byte_val <= 0x17: # Mosaic Color
fg = COLORS[byte_val - 0x10]
graphics_mode = True
elif byte_val == 0x1C: # Black BG
bg = COLORS[0]
elif byte_val == 0x1D: # New BG
bg = fg
elif byte_val == 0x0C: # Normal Height
pass
elif byte_val == 0x0D: # Double Height
pass # Not implemented yet
elif byte_val == 0x19: # Contiguous Graphics
contiguous = True
elif byte_val == 0x1A: # Separated Graphics
contiguous = False
elif byte_val == 0x1E: # Hold Graphics
hold_graphics = True
elif byte_val == 0x1F: # Release Graphics
hold_graphics = False
# Draw Background
painter.fillRect(x, y, self.cell_w, self.cell_h, bg)
# Draw Foreground
if is_control:
# "Set-at" spacing attribute? Teletext control codes occupy a space
# unless "Hold Graphics" replaces it with previous graphic char.
if hold_graphics and graphics_mode:
self.draw_mosaic(painter, x, y, held_char, fg, contiguous)
else:
# Draw space (nothing, since we filled BG)
pass
else:
if graphics_mode:
# Mosaic Graphics
if (0x20 <= byte_val <= 0x3F) or (0x60 <= byte_val <= 0x7F):
self.draw_mosaic(painter, x, y, byte_val, fg, contiguous)
held_char = byte_val
else:
# Capital letter in graphics mode? Usually shows char?
char = get_char(byte_val, self.subset_idx)
painter.setPen(fg)
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
held_char = 0x20
else:
# Alphanumeric
char = get_char(byte_val, self.subset_idx)
painter.setPen(fg)
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
# Draw Cursor
# Invert the cell at cursor position
if self.cursor_visible and c == self.cursor_x and row == self.cursor_y:
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceXorDestination)
# XOR with white (creates inversion)
painter.fillRect(x, y, self.cell_w, self.cell_h, QColor(255, 255, 255))
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
def draw_mosaic(self, painter, x, y, char_code, color, contiguous):
val = char_code & 0x7F
bits = 0
if val >= 0x20:
bits = val - 0x20
blocks = [
(0, 0), (1, 0), # Top
(0, 1), (1, 1), # Mid
(0, 2), (1, 2) # Bot
]
bit_mask = [1, 2, 4, 8, 16, 64] # 64 is bit 6
bw = self.cell_w / 2
bh = self.cell_h / 3
if not contiguous:
bw -= 1
bh -= 1
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QBrush(color))
for i in range(6):
if bits & bit_mask[i]:
bx = x + blocks[i][0] * (self.cell_w / 2)
by = y + blocks[i][1] * (self.cell_h / 3)
if not contiguous:
bx += 1
by += 1
painter.drawRect(QRect(int(bx), int(by), int(bw), int(bh)))
def paintEvent(self, event):
painter = QPainter(self)
target_rect = self.rect()
scaled = self.buffer.scaled(target_rect.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation)
ox = (target_rect.width() - scaled.width()) // 2
oy = (target_rect.height() - scaled.height()) // 2
painter.drawImage(ox, oy, scaled)

152
src/teletext/ui.py Normal file
View File

@@ -0,0 +1,152 @@
import os
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTreeWidget, QTreeWidgetItem, QFileDialog, QMenuBar, QMenu, QMessageBox)
from PyQt6.QtGui import QAction, QKeyEvent
from PyQt6.QtCore import Qt
from .io import load_t42, save_t42
from .renderer import TeletextCanvas
from .models import TeletextService, Page, Packet
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Teletext Editor")
self.resize(1024, 768)
self.service = TeletextService()
self.current_page: Page = None
# UI Components
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QHBoxLayout(self.central_widget)
# Left Panel: Page Tree
self.tree = QTreeWidget()
self.tree.setHeaderLabel("Pages")
self.tree.setFixedWidth(200)
self.tree.itemClicked.connect(self.on_page_selected)
self.layout.addWidget(self.tree)
# Center: Teletext Canvas
self.canvas = TeletextCanvas()
self.layout.addWidget(self.canvas, 1) # Expand
# Menus
self.create_menus()
def create_menus(self):
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu("File")
open_action = QAction("Open T42...", self)
open_action.triggered.connect(self.open_file)
file_menu.addAction(open_action)
save_action = QAction("Save T42...", self)
save_action.triggered.connect(self.save_file)
file_menu.addAction(save_action)
exit_action = QAction("Exit", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
view_menu = menu_bar.addMenu("View")
lang_menu = view_menu.addMenu("Language")
langs = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
for i, lang in enumerate(langs):
action = QAction(lang, self)
action.setData(i)
action.triggered.connect(self.set_language)
lang_menu.addAction(action)
def set_language(self):
action = self.sender()
if action:
idx = action.data()
self.canvas.subset_idx = idx
self.canvas.redraw()
self.canvas.update()
def open_file(self):
fname, _ = QFileDialog.getOpenFileName(self, "Open T42", "", "Teletext Files (*.t42);;All Files (*)")
if fname:
try:
self.service = load_t42(fname)
self.populate_tree()
QMessageBox.information(self, "Loaded", f"Loaded {len(self.service.pages)} pages.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to load file: {e}")
def save_file(self):
fname, _ = QFileDialog.getSaveFileName(self, "Save T42", "", "Teletext Files (*.t42);;All Files (*)")
if fname:
try:
save_t42(fname, self.service)
QMessageBox.information(self, "Saved", "File saved successfully.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to save file: {e}")
def populate_tree(self):
self.tree.clear()
# Group by Magazine
mags = {}
for p in self.service.pages:
if p.magazine not in mags:
mags[p.magazine] = QTreeWidgetItem([f"Magazine {p.magazine}"])
self.tree.addTopLevelItem(mags[p.magazine])
# Format: PPP-SS (Page-Subcode)
# Create Item
# Subcode is complicated to display "nicely" without decoding,
# let's just show hex or raw for now if not standard 0000.
label = f"{p.page_number:02d} (Sub: {p.sub_code:04X})"
item = QTreeWidgetItem([label])
item.setData(0, Qt.ItemDataRole.UserRole, p)
mags[p.magazine].addChild(item)
self.tree.expandAll()
def on_page_selected(self, item, column):
page = item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(page, Page):
self.current_page = page
self.canvas.set_page(page)
# Also set window focus to canvas or handle key events?
self.canvas.setFocus()
# Input Handling (Editor Logic)
def keyPressEvent(self, event: QKeyEvent):
if not self.current_page:
return
key = event.key()
text = event.text()
# Navigation
if key == Qt.Key.Key_Up:
self.canvas.move_cursor(0, -1)
elif key == Qt.Key.Key_Down:
self.canvas.move_cursor(0, 1)
elif key == Qt.Key.Key_Left:
self.canvas.move_cursor(-1, 0)
elif key == Qt.Key.Key_Right:
self.canvas.move_cursor(1, 0)
else:
# Typing
# Filter non-printable
if text and len(text) == 1 and 32 <= ord(text) <= 126:
self.canvas.handle_input(text)
elif key == Qt.Key.Key_Backspace:
# Move back and delete
self.canvas.move_cursor(-1, 0)
self.canvas.handle_input(' ')
self.canvas.move_cursor(-1, 0) # Compensate for the auto-advance logic if any

BIN
test.t42 Normal file

Binary file not shown.

BIN
test_out.t42 Normal file

Binary file not shown.

94
test_t42.py Normal file
View File

@@ -0,0 +1,94 @@
import os
import sys
# Add src to path
sys.path.append(os.path.join(os.getcwd(), 'src'))
from teletext.models import Packet, Page
from teletext.io import load_t42, save_t42
def create_dummy_t42(filename):
# Create a 42-byte packet
# Byte 0: Mag 1, Row 0.
# M=1 (001), R=0 (00000)
# Encoded:
# B1: M1 M2 M3 R1 -> 1 0 0 0. With Hamming: P1, D1(1), P2, D2(0), P3, D3(0), P4, D4(0)
# D1=1 -> P1=1 (1,3,5,7 parity).
# Actually let's use a simpler way or pre-calculated bytes for testing.
# Magazine 1, Row 0 is often: 0x15 0x15 (example guess, need real hamming)
# Let's simple write 42 zero bytes, then set some manually to test "parsing" robustness
# or just trust the load/save loop for raw data conservation.
# We'll create a "Header" packet (Row 0) and a "Content" packet (Row 1).
# Packet 1: Row 0.
# We need to construct bytes that pass our minimal decoder.
# decode_common: returns D1..D4 for bits 1,3,5,7.
# Mag=1 => 001. R=0 => 00000.
# B1 (Low row bits + Mag): M1, M2, M3, R1 -> 1, 0, 0, 0
# D1=1, D2=0, D3=0, D4=0.
# Byte value: x1x0x0x0.
# B2 (High row bits): R2, R3, R4, R5 -> 0, 0, 0, 0
# Byte value: x0x0x0x0.
# Let's arbitrarily set parity bits to 0 for this test as my decoder ignores them (it only reads D bits).
# B1: 0 1 0 0 0 0 0 0 -> 0x02
# B2: 0 0 0 0 0 0 0 0 -> 0x00
p1_data = bytearray(42)
p1_data[0] = 0x02
p1_data[1] = 0x00
# Add some text in the rest
p1_data[2:] = b'Header Packet' + b'\x00' * (40 - 13)
# Packet 2: Row 1.
# M=1, R=1.
# B1: M1 M2 M3 R1 -> 1 0 0 1
# D1=1, D2=0, D3=0, D4=1.
# Byte: x1x0x0x1 -> 0x82 (if bit 7 is D4).
# Position: 0(P1) 1(D1-b0) 2(P2) 3(D2-b1) 4(P3) 5(D3-b2) 6(P4) 7(D4-b3)
# My decoder keys off D1(bit1), D2(bit3), D3(bit5), D4(bit7).
# So we want bits 1 and 7 set. 0x82 = 1000 0010. Correct.
p2_data = bytearray(42)
p2_data[0] = 0x82
p2_data[1] = 0x00 # Row high bits 0
p2_data[2:] = b'Content Row 1' + b'\x00' * (40 - 13)
with open(filename, 'wb') as f:
f.write(p1_data)
f.write(p2_data)
print(f"Created {filename}")
def test_load_save():
fname = "test.t42"
out_fname = "test_out.t42"
create_dummy_t42(fname)
service = load_t42(fname)
print(f"Loaded {len(service.all_packets)} packets")
print(f"Loaded {len(service.pages)} pages")
if len(service.pages) > 0:
p = service.pages[0]
print(f"Page 0: Mag {p.magazine} Num {p.page_number}")
print(f"Packets in page: {len(p.packets)}")
save_t42(out_fname, service)
# Verify binary identity
with open(fname, 'rb') as f1, open(out_fname, 'rb') as f2:
b1 = f1.read()
b2 = f2.read()
if b1 == b2:
print("SUCCESS: Output matches input")
else:
print("FAILURE: Output differs")
print(f"In: {len(b1)}, Out: {len(b2)}")
if __name__ == "__main__":
test_load_save()

247
venv/bin/Activate.ps1 Normal file
View File

@@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

70
venv/bin/activate Normal file
View File

@@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /home/daniel/Documents/Projects/teletext_editor/venv)
else
# use the path as-is
export VIRTUAL_ENV=/home/daniel/Documents/Projects/teletext_editor/venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

27
venv/bin/activate.csh Normal file
View File

@@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/daniel/Documents/Projects/teletext_editor/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash

69
venv/bin/activate.fish Normal file
View File

@@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/daniel/Documents/Projects/teletext_editor/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end

8
venv/bin/pip Executable file
View File

@@ -0,0 +1,8 @@
#!/home/daniel/Documents/Projects/teletext_editor/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3 Executable file
View File

@@ -0,0 +1,8 @@
#!/home/daniel/Documents/Projects/teletext_editor/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3.12 Executable file
View File

@@ -0,0 +1,8 @@
#!/home/daniel/Documents/Projects/teletext_editor/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pylupdate6 Executable file
View File

@@ -0,0 +1,8 @@
#!/home/daniel/Documents/Projects/teletext_editor/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from PyQt6.lupdate.pylupdate import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

1
venv/bin/python Symbolic link
View File

@@ -0,0 +1 @@
python3

1
venv/bin/python3 Symbolic link
View File

@@ -0,0 +1 @@
/usr/bin/python3

1
venv/bin/python3.12 Symbolic link
View File

@@ -0,0 +1 @@
python3

8
venv/bin/pyuic6 Executable file
View File

@@ -0,0 +1,8 @@
#!/home/daniel/Documents/Projects/teletext_editor/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from PyQt6.uic.pyuic import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

Some files were not shown because too many files have changed in this diff Show More