Compare commits

..

37 Commits

Author SHA1 Message Date
Daniel Dybing
a15ba67b1a feat: Optimize .t42 loading and improve decoder fidelity
All checks were successful
Build Linux / Build Linux (push) Successful in 1m29s
Build Windows / Build Windows (push) Successful in 4m46s
2026-02-21 20:44:26 +01:00
Daniel Dybing
18fef7b049 fix: Align CRC-16 calculation with ETSI EN 300 706 and improve retrieval
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 4m52s
2026-02-08 19:51:28 +01:00
9b846970b8 fix: Align CRC calculation with ETSI EN 300 706 standard
All checks were successful
Build Linux / Build Linux (push) Successful in 1m29s
Build Windows / Build Windows (push) Successful in 4m45s
2026-02-07 14:27:37 +01:00
de296b4711 fix(ci): Add robust apt flags for Windows build container
All checks were successful
Build Linux / Build Linux (push) Successful in 1m30s
Build Windows / Build Windows (push) Successful in 5m20s
2026-02-07 10:57:44 +01:00
84d1094d16 fix: Update CRC calc to use 0 init and full 16-bit stored value
Some checks failed
Build Linux / Build Linux (push) Successful in 1m27s
Build Windows / Build Windows (push) Failing after 17s
2026-02-07 10:47:29 +01:00
6a6df63980 feat: Add CRC checksum calculation and display
All checks were successful
Build Linux / Build Linux (push) Successful in 1m34s
Build Windows / Build Windows (push) Successful in 4m42s
2026-02-07 10:12:25 +01:00
06107a3d78 feat: Implement dynamic cursor height for double-height characters
All checks were successful
Build Linux / Build Linux (push) Successful in 1m27s
Build Windows / Build Windows (push) Successful in 4m45s
2026-02-06 17:31:39 +01:00
33e3ed2615 Fix NameError: import missing hamming functions in UI
All checks were successful
Build Linux / Build Linux (push) Successful in 1m47s
Build Windows / Build Windows (push) Successful in 5m21s
2026-02-05 13:03:34 +01:00
6ed8a79660 Fix AttributeError: Initialize language_names before use
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 4m51s
2026-01-31 13:38:13 +01:00
56657efa7c Implement 'Set Language' UI to persist language changes to page header
All checks were successful
Build Linux / Build Linux (push) Successful in 1m29s
Build Windows / Build Windows (push) Successful in 4m49s
2026-01-31 13:20:11 +01:00
fa195f2695 Implement session-based language overrides for the viewer
All checks were successful
Build Linux / Build Linux (push) Successful in 1m39s
Build Windows / Build Windows (push) Successful in 4m53s
2026-01-31 13:11:12 +01:00
988178f1c6 Fix header page number display: use hex formatting (P175 instead of P1117)
All checks were successful
Build Linux / Build Linux (push) Successful in 1m33s
Build Windows / Build Windows (push) Successful in 5m18s
2026-01-31 12:19:46 +01:00
71019bf399 Fix Windows build: use --collect-all PyQt6 to ensure platform plugins are bundled
All checks were successful
Build Linux / Build Linux (push) Successful in 1m30s
Build Windows / Build Windows (push) Successful in 4m46s
2026-01-31 11:52:45 +01:00
6a5f223a88 Fix Windows build: add missing PyQt6.sip hidden import
All checks were successful
Build Linux / Build Linux (push) Successful in 1m40s
Build Windows / Build Windows (push) Successful in 2m52s
2026-01-31 11:45:37 +01:00
274a6778b3 Fix Windows build: add missing pkgutil hidden import
All checks were successful
Build Linux / Build Linux (push) Successful in 1m37s
Build Windows / Build Windows (push) Successful in 2m52s
2026-01-31 11:14:18 +01:00
772827082e fix: prevent crash on save without loaded file and add status message
All checks were successful
Build Linux / Build Linux (push) Successful in 1m31s
Build Windows / Build Windows (push) Successful in 2m52s
2026-01-26 13:28:23 +01:00
f8a9ad0065 fix: track Black BG changes in background segments for correct DH backfill
All checks were successful
Build Linux / Build Linux (push) Successful in 1m30s
Build Windows / Build Windows (push) Successful in 2m49s
2026-01-26 13:23:26 +01:00
9726a82851 feat: add TTI export functionality
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 2m53s
2026-01-26 12:42:12 +01:00
233eed1ca7 fix: set desktop file name before QApplication init to resolve QDBusError
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 2m58s
2026-01-23 21:33:05 +01:00
4c3d860dc4 ui: replace file closed dialog with status bar message 2026-01-23 21:31:30 +01:00
670a2d9f8c ui: move graphics and color controls to right sidebar 2026-01-23 21:29:16 +01:00
e304034596 feat: add graphics control buttons (Hold/Release) and refine UI styling
All checks were successful
Build Linux / Build Linux (push) Successful in 1m31s
Build Windows / Build Windows (push) Successful in 2m51s
2026-01-21 14:42:14 +01:00
80cca7cd79 feat: add black/new background buttons and standard styling
All checks were successful
Build Linux / Build Linux (push) Successful in 1m33s
Build Windows / Build Windows (push) Successful in 2m55s
2026-01-21 14:11:48 +01:00
8475b512b8 feat: add delete page functionality
All checks were successful
Build Linux / Build Linux (push) Successful in 1m32s
Build Windows / Build Windows (push) Successful in 2m51s
- Added 'Delete Page' option to Edit menu
- Implemented deletion logic in MainWindow, removing page from service and refreshing UI
- Ensures changes are marked as modified for saving
2026-01-21 13:55:42 +01:00
6c12e29e0a feat: implement remaining G0 character sets (IT, FR, ES/PT, TR)
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 2m51s
2026-01-21 13:35:47 +01:00
0ebf18ee6e feat: add Mosaic Graphics support and fix char rendering
All checks were successful
Build Linux / Build Linux (push) Successful in 1m29s
Build Windows / Build Windows (push) Successful in 2m53s
- Added Mosaic Picker dialog with visual previews
- Added 'Graphics' mode toggle to UI
- Implemented status bar mode indicator (Text/Graphics)
- Corrected English character mapping for 0x60 to Hyphen (-)
- Verified German and Swedish/Finnish character sets against ETSI spec
2026-01-21 13:30:19 +01:00
e06fd2c776 Fix Linux app icon: Set DesktopFileName and add image debug logs
All checks were successful
Build Linux / Build Linux (push) Successful in 1m30s
Build Windows / Build Windows (push) Successful in 2m52s
2026-01-13 19:15:03 +01:00
48b966f9a8 Fix app icon visibility on Windows and add debug logs
Some checks failed
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Has been cancelled
2026-01-13 19:11:31 +01:00
f4af5f6389 Fix Linux build dependencies: Replace obsolete libgl1-mesa-glx/libegl1-mesa
All checks were successful
Build Linux / Build Linux (push) Successful in 1m32s
Build Windows / Build Windows (push) Successful in 2m55s
2026-01-13 18:30:26 +01:00
98a641ffde Update workflows with dependencies and icons, update gitignore
Some checks failed
Build Linux / Build Linux (push) Failing after 11s
Build Windows / Build Windows (push) Has been cancelled
2026-01-13 18:28:10 +01:00
13b08ac6a4 Update README with screenshot
Some checks failed
Build Linux / Build Linux (push) Successful in 1m9s
Build Windows / Build Windows (push) Has been cancelled
2026-01-13 18:25:28 +01:00
9fc75b7e39 Add app icon and update build process
Some checks failed
Build Linux / Build Linux (push) Successful in 1m13s
Build Windows / Build Windows (push) Has been cancelled
2026-01-13 18:23:00 +01:00
334d25c3ba Install nodejs/npm in Windows workflow for action compatibility
All checks were successful
Build Linux / Build Linux (push) Successful in 1m14s
Build Windows / Build Windows (push) Successful in 2m46s
2026-01-13 18:06:50 +01:00
cfbd2403e4 Update README with app description and build instructions
Some checks failed
Build Linux / Build Linux (push) Successful in 1m13s
Build Windows / Build Windows (push) Failing after 3s
2026-01-13 18:04:43 +01:00
e51e86e53b Stop tracking specification and check_ttx6.py
Some checks failed
Build Linux / Build Linux (push) Successful in 1m11s
Build Windows / Build Windows (push) Failing after 3s
2026-01-13 17:55:59 +01:00
d544cc6d9d Stop tracking test_t42.py
Some checks failed
Build Windows / Build Windows (push) Has been cancelled
Build Linux / Build Linux (push) Has been cancelled
2026-01-13 17:55:24 +01:00
876e2206b6 Stop tracking venv and .venv directories
Some checks failed
Build Linux / Build Linux (push) Has been cancelled
Build Windows / Build Windows (push) Has been cancelled
2026-01-13 17:54:17 +01:00
4494 changed files with 964 additions and 536734 deletions

View File

@@ -9,6 +9,25 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-x11-0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-xinerama0 \
libxcb-xinput0 \
libxcb-xfixes0 \
libxcb-shape0 \
libgl1 \
libegl1 \
libdbus-1-3 \
libx11-xcb1
- name: Setup Python
uses: actions/setup-python@v4
with:
@@ -21,7 +40,7 @@ jobs:
- name: Build Executable
run: |
pyinstaller --onefile --windowed --name TeletextEditor_Linux --paths src src/main.py
pyinstaller --onefile --windowed --name TeletextEditor_Linux --paths src --add-data "app_icon.png:." src/main.py
- name: Upload Artifact
uses: actions/upload-artifact@v3

View File

@@ -8,6 +8,12 @@ jobs:
container:
image: tobix/pywine:3.10
steps:
- name: Install Node.js
run: |
apt-get clean
apt-get update
apt-get install -y --fix-missing nodejs npm
- name: Checkout
uses: actions/checkout@v3
@@ -18,10 +24,10 @@ jobs:
- name: Build Executable
run: |
wine pyinstaller --onefile --windowed --name TeletextEditor_Windows.exe --paths src src/main.py
wine pyinstaller --onefile --windowed --hidden-import=pkgutil --hidden-import=PyQt6.sip --collect-all PyQt6 --name TeletextEditor_Windows.exe --paths src --add-data "app_icon.png;." --icon=app_icon.ico src/main.py
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: TeletextEditor-Windows
path: dist/TeletextEditor_Windows.exe
path: dist/TeletextEditor_Windows.exe

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
venv/
.venv/
__pycache__/
*.pyc
dist/

View File

@@ -1 +1,48 @@
# Teletext Editor
A cross-platform (Linux/Windows) desktop application for viewing, creating, and editing Teletext pages. This tool is designed to work with the `.t42` file format, providing a robust environment for Teletext recovery and design.
![Application Screenshot](screenshot.png)
## Features
- **File Support:** Load and save standard `.t42` Teletext data files.
- **Navigation:**
- Browse pages using a list with Hexadecimal ID support (e.g., 100, 1FF).
- Full support for navigating subpages.
- **Visual Editor:**
- Direct "canvas" grid editing.
- **Hex Inspector:** View and manually edit the raw byte value of the selected cell.
- **Control Characters:** Quick access buttons for inserting standard Teletext colors (Red, Green, Yellow, Blue, Magenta, Cyan, White).
- **National Option Sets:** Correctly renders characters for various languages (English, German, Swedish/Finnish, etc.) based on page metadata.
- **Productivity Tools:**
- Undo/Redo support.
- Copy/Paste row content between pages.
- Unsaved changes warning.
## How to Build Manually
The application is built using Python and PyQt6, and packaged into a standalone executable using PyInstaller. A helper script `build_app.py` is provided to streamline the process.
### Prerequisites
- Python 3.10 or higher
- Install dependencies:
```bash
pip install -r requirements.txt
```
### Build Steps
1. Open a terminal in the project's root directory.
2. Run the build script:
```bash
python build_app.py
```
3. The build script will clean previous builds, detect your OS, and run PyInstaller with the correct configuration.
4. Once complete, the standalone executable will be available in the `dist/` folder:
- **Linux:** `dist/TeletextEditor_Linux`
- **Windows:** `dist/TeletextEditor_Windows.exe`
> **Note for Cross-Compilation:**
> The build script produces an executable for the **current OS** running the script. To build the Windows executable on Linux, you can use Wine (as configured in the Gitea workflow) or run the script natively on a Windows machine.

BIN
app_icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 9.0 MiB

View File

@@ -19,11 +19,19 @@ def build():
system = platform.system()
print(f"Detected OS: {system}")
# Determine separator for --add-data
sep = ";" if system == "Windows" else ":"
# Base command
base_cmd = [
sys.executable, "-m", "PyInstaller",
"--onefile",
"--windowed",
"--hidden-import=pkgutil",
"--hidden-import=PyQt6.sip",
"--collect-all", "PyQt6",
"--paths", "src",
f"--add-data=app_icon.png{sep}.",
"src/main.py"
]
@@ -31,6 +39,8 @@ def build():
name = "TeletextEditor_Linux"
elif system == "Windows":
name = "TeletextEditor_Windows.exe"
# Add icon for Windows executable
base_cmd.append("--icon=app_icon.ico")
else:
print(f"Unsupported platform: {system}")
return
@@ -57,4 +67,4 @@ def build():
if __name__ == "__main__":
clean_build_dirs()
build()
build()

View File

@@ -1,28 +0,0 @@
import sys
import os
# Add src to path
sys.path.append(os.path.join(os.getcwd(), 'src'))
from teletext.io import load_t42
def check_file(filename):
if not os.path.exists(filename):
print(f"File {filename} not found")
return
service = load_t42(filename)
print(f"Analysis of {filename}:")
print(f"Total packets: {len(service.all_packets)}")
print(f"Total pages: {len(service.pages)}")
language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
for i, page in enumerate(service.pages):
lang_idx = page.language
lang_name = language_names[lang_idx] if 0 <= lang_idx < len(language_names) else f"Unknown ({lang_idx})"
print(f"Page {i+1}: Mag {page.magazine} Num {page.page_number:02d}, Lang: {lang_idx} ({lang_name})")
if __name__ == "__main__":
check_file("TTX-6_RAW.t42")

17
convert_icon.py Normal file
View File

@@ -0,0 +1,17 @@
from PIL import Image
import os
def convert_to_ico(png_path, ico_path):
if not os.path.exists(png_path):
print(f"Error: {png_path} not found.")
return
try:
img = Image.open(png_path)
img.save(ico_path, format='ICO', sizes=[(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)])
print(f"Successfully converted {png_path} to {ico_path}")
except Exception as e:
print(f"Error converting image: {e}")
if __name__ == "__main__":
convert_to_ico("app_icon.png", "app_icon.ico")

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

View File

@@ -1,11 +1,60 @@
import sys
import os
import platform
from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QIcon, QImageReader
from teletext.ui import MainWindow
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
def main():
# Fix for Windows Taskbar Icon
if platform.system() == 'Windows':
import ctypes
myappid = 'ddybing.teletexteditor.1.0' # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
QApplication.setDesktopFileName("no.ddybing.TeletextEditor") # Helps Linux DEs group windows
app = QApplication(sys.argv)
app.setApplicationName("TeletextEditor")
app.setOrganizationName("DanielDybing")
# Debug Image Formats
supported_formats = [str(fmt, 'utf-8') for fmt in QImageReader.supportedImageFormats()]
print(f"DEBUG: Supported Image Formats: {supported_formats}")
# Set App Icon
icon_path = resource_path("app_icon.png")
print(f"DEBUG: Looking for icon at: {icon_path}")
if os.path.exists(icon_path):
print("DEBUG: Icon file found.")
app_icon = QIcon(icon_path)
app.setWindowIcon(app_icon)
# Verify icon loaded
if app_icon.isNull():
print("DEBUG: QIcon is null (failed to load image data)")
else:
print(f"DEBUG: QIcon loaded. Available sizes: {app_icon.availableSizes()}")
else:
print("DEBUG: Icon file NOT found.")
window = MainWindow()
# Ensure window inherits the icon
if os.path.exists(icon_path):
window.setWindowIcon(QIcon(icon_path))
window.show()
sys.exit(app.exec())

View File

@@ -9,7 +9,7 @@ to Unicode characters based on the National Option (3 bits).
ENGLISH = {
0x23: '#', 0x24: '$', 0x40: '@',
0x5B: '[', 0x5C: '\\', 0x5D: ']', 0x5E: '^',
0x5F: '_', 0x60: '`',
0x5F: '_', 0x60: '-',
0x7B: '{', 0x7C: '|', 0x7D: '}', 0x7E: '~'
}
@@ -29,17 +29,49 @@ GERMAN = {
0x7B: 'ä', 0x7C: 'ö', 0x7D: 'ü', 0x7E: 'ß'
}
# Italian - Option 011 (3)
ITALIAN = {
0x23: '£', 0x24: '$', 0x40: 'é',
0x5B: '°', 0x5C: 'ç', 0x5D: '', 0x5E: '',
0x5F: '#', 0x60: 'ù',
0x7B: 'à', 0x7C: 'ò', 0x7D: 'è', 0x7E: 'ì'
}
# French - Option 100 (4)
FRENCH = {
0x23: 'é', 0x24: 'ï', 0x40: 'à',
0x5B: 'ë', 0x5C: 'ê', 0x5D: 'ù', 0x5E: 'î',
0x5F: '#', 0x60: 'è',
0x7B: 'â', 0x7C: 'ô', 0x7D: 'û', 0x7E: 'ç'
}
# Portuguese/Spanish - Option 101 (5)
PORTUGUESE_SPANISH = {
0x23: 'Ç', 0x24: '$', 0x40: '¡',
0x5B: 'á', 0x5C: 'é', 0x5D: 'í', 0x5E: 'ó',
0x5F: 'ú', 0x60: '¿',
0x7B: 'ü', 0x7C: 'ñ', 0x7D: 'è', 0x7E: 'à'
}
# Turkish - Option 110 (6)
TURKISH = {
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
ENGLISH, # 000
GERMAN, # 001
SWEDISH_FINNISH, # 010
ITALIAN, # 011
FRENCH, # 100
PORTUGUESE_SPANISH, # 101
TURKISH, # 110
ENGLISH, # 111 (Romania placeholder)
]
def get_char(byte_val, subset_idx):

View File

@@ -5,69 +5,82 @@ from .models import Packet, Page, TeletextService
def load_t42(file_path: str, progress_callback: Optional[Callable[[int, int], None]] = None) -> TeletextService:
service = TeletextService()
if not os.path.exists(file_path):
return service
total_bytes = os.path.getsize(file_path)
# Each packet is 42 bytes
total_packets = total_bytes // 42
processed_packets = 0
# Magazine buffers: magazine -> {row_num: Packet}
magazine_buffers = {m: {} for m in range(1, 9)}
# Active page lookup: magazine -> Page object (for O(1) access)
active_pages = {m: None for m in range(1, 9)}
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
if not chunk: break
if len(chunk) < 42: break
processed_packets += 1
if progress_callback and processed_packets % 100 == 0:
if progress_callback and processed_packets % 500 == 0:
progress_callback(processed_packets, total_packets)
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.
mag = packet.magazine
buffer = magazine_buffers[mag]
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.
p_num, sub_code, control_bits, language = parse_header(packet.data)
# Header format (after Mag/Row):
# Bytes: P1 P2 S1 S2 S3 S4 C1 C2 ...
# All Hamming 8/4 encoded.
# Check Erase Page bit (C4 is bit 0 of control_bits)
erase_page = bool(control_bits & 1)
# 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, language = parse_header(packet.data)
# Create new page
new_page = Page(magazine=packet.magazine, page_number=p_num, sub_code=sub_code, language=language)
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)
if erase_page:
magazine_buffers[mag] = {0: packet}
buffer = magazine_buffers[mag]
else:
# Packet without a header? Orphaned. Just keep in all_packets
pass
buffer[0] = packet
# Create snapshot
new_page = Page(
magazine=mag,
page_number=p_num,
sub_code=sub_code,
control_bits=control_bits,
language=language
)
# Efficient cloning: use the existing Packet objects where possible,
# but we MUST clone the data bytearray if we plan to edit it later.
for r_num, pkt in sorted(buffer.items()):
# Create a new packet shell sharing the original_data but with its own data bytearray
cloned_pkt = Packet(pkt.original_data)
cloned_pkt.data = bytearray(pkt.data)
new_page.packets.append(cloned_pkt)
service.pages.append(new_page)
active_pages[mag] = new_page # Update active page lookup
elif 1 <= packet.row <= 31:
# Update the running buffer
buffer[packet.row] = packet
# Update the active snapshot immediately
target_page = active_pages[mag]
if target_page:
# Update row in the current active page
found_row = False
for i, p in enumerate(target_page.packets):
if p.row == packet.row:
target_page.packets[i] = packet
found_row = True
break
if not found_row:
target_page.packets.append(packet)
return service
@@ -182,49 +195,110 @@ def decode_hamming_8_4(byte_val):
(((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)
# Data is 40 bytes (after MRAG).
# Byte 0: Page Units (PU)
# Byte 1: Page Tens (PT)
# Byte 2: Subcode S1 (bits 0-3)
# Byte 3: Subcode S2 (bits 4-6), C4 (bit 7)
# Byte 4: Subcode S3 (bits 8-11)
# Byte 5: Subcode S4 (bits 12-13), C5 (bit 14), C6 (bit 15)
# Byte 6: C7-C10
# Byte 7: C11-C14 (C12-C14 are Language)
pu = decode_hamming_8_4(data[0])
pt = decode_hamming_8_4(data[1])
# Use BCD/Hex-like storage: High nibble is Tens, Low nibble is Units.
# This preserves Hex pages (A-F) without colliding with decimal pages.
# E.g. Page 1FF -> Tens=F(15), Units=F(15) -> 0xFF (255)
# Page 12E -> Tens=2, Units=E(14) -> 0x2E (46)
# Page 134 -> Tens=3, Units=4 -> 0x34 (52)
# 0x2E != 0x34. No collision.
# Page number: pt (tens), pu (units). 0x00 to 0xFF.
page_num = ((pt & 0xF) << 4) | (pu & 0xF)
# Subcode: S1, S2, S3, S4
# S1 (low), S2, S3, S4 (high)
# Subcode (13 bits)
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 & 0xF) | \
((s2 & 0x7) << 4) | \
((s3 & 0xF) << 7) | \
((s4 & 0x3) << 11)
# Control bits C4-C14
c4 = (s2 >> 3) & 1
c5 = (s4 >> 2) & 1
c6 = (s4 >> 3) & 1
sub_code = s1 | (s2 << 4) | (s3 << 8) | (s4 << 12)
c_7_10 = decode_hamming_8_4(data[6])
c_11_14 = decode_hamming_8_4(data[7])
# Control bits C12, C13, C14 are in Byte 8 (index 8)
# They determine the National Option (Language)
c_bits_2 = decode_hamming_8_4(data[8])
# bitmask starting at index 0 for C4
control_bits = c4 | (c5 << 1) | (c6 << 2) | \
((c_7_10 & 0xF) << 3) | \
((c_11_14 & 0xF) << 7)
# Language (C12, C13, C14)
# c_11_14: bit 0:C11, bit 1:C12, bit 2:C13, bit 3:C14
language = (c_11_14 >> 1) & 0x7
# Fix for Language Detection:
# It seems C12 and C13 are swapped in the Hamming decoding or file format relative to expected values.
# C12 is bit 0, C13 is bit 1.
# We swap them so D1 maps to C13 (Swedish bit) and D2 maps to C12 (German bit).
# Original: language = c_bits_2 & 0b111
language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4)
return page_num, sub_code, language
return page_num, sub_code, control_bits, language
def save_tti(file_path: str, page: Page):
"""
Saves a single Page object to a TTI file.
"""
with open(file_path, 'w', encoding='latin-1') as f:
# Header Info
# DS - Source? Description?
f.write(f"DS,Teletext Editor Export\n")
f.write(f"SP,{file_path}\n")
# PN - Page Number mpp00
# Typically TTI uses decimal integer for mppss?
# Or mppss as hex digits?
# Standard convention: mppss where m is 1-8, pp is 00-FF, ss is 00-99
# Example: Page 100 -> PN,10000
# Example: Page 1F0 -> PN,1F000
f.write(f"PN,{page.magazine}{page.page_number:02X}00\n")
# SC - Subcode ssss
f.write(f"SC,{page.sub_code:04X}\n")
# PS - Page Status
# 8000 is typical for "Transmission"
f.write(f"PS,8000\n")
# RE - Region (Language)
f.write(f"RE,{page.language}\n")
# Lines
# We need to construct the 40-char string for each row
# Row 0 is special (Header)
# Get all packets for this page
# Map row -> packet
rows = {}
for p in page.packets:
rows[p.row] = p
for r in range(26): # 0 to 25
if r in rows:
packet = rows[r]
# Packet data is 40 bytes (after the 2-byte header we stripped in Packet class? No wait)
# Packet.data in our model IS the 40 bytes of character data (we strip MRAG in __post_init__)
# So we just decode it as latin-1 to get the chars
# However, we must ensure we don't have newlines or nulls breaking the text file structure
# TTI format usually accepts raw bytes 0x00-0xFF if strictly handled, but often expects
# mapped control codes.
# Standard VBIT2 TTI handling treats it as a binary-safe string if mapped to char.
# Row 0 special handling: The first 8 bytes of Row 0 are usually header flags in the packet,
# but visually they are "P100 ".
# TTI usually expects the visual representation for the line content.
# But for transmission, we want the raw bytes.
# OL,r,String
data_str = packet.data.decode('latin-1')
f.write(f"OL,{r},{data_str}\n")
else:
# Empty line? Usually omitted or written as empty
pass

View File

@@ -1,6 +1,13 @@
from dataclasses import dataclass, field
from typing import List, Optional
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)
@dataclass
class Packet:
"""
@@ -27,22 +34,6 @@ class Packet:
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)
@@ -74,9 +65,13 @@ class Page:
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)
language: int = 0 # National Option (0-7)
page_number: int # 00-99 (Hex storage: 0x00-0xFF)
sub_code: int = 0 # 13-bit subcode (0000 to 3F7F hex)
# Control bits C4-C14
control_bits: int = 0
language: int = 0 # National Option (0-7, from C12-C14)
packets: List[Packet] = field(default_factory=list)
@property
@@ -84,6 +79,112 @@ class Page:
# Format as Hex to support A-F pages
return f"{self.magazine}{self.page_number:02X}"
def get_control_bit(self, n: int) -> bool:
""" Returns value of control bit Cn (4-14) """
if 4 <= n <= 14:
return bool((self.control_bits >> (n - 4)) & 1)
return False
def set_control_bit(self, n: int, value: bool):
""" Sets value of control bit Cn (4-14) """
if 4 <= n <= 14:
if value:
self.control_bits |= (1 << (n - 4))
else:
self.control_bits &= ~(1 << (n - 4))
def calculate_crc(self) -> int:
"""
Calculates the CRC-16 checksum for the page.
According to ETSI EN 300 706 (Section 9.6.1 & Figure 13):
- G(x) = x^16 + x^12 + x^9 + x^7 + 1 (Poly 0x1281)
- Initial value: 0.
- Processed bits b8 to b1 (MSB first for stored bytes).
- Total 1024 bytes (32 packets * 32 bytes).
- Packet X/0: Bytes 14 to 37 (24 bytes) + 8 spaces.
- Packets X/1 to X/25: Bytes 14 to 45 (32 bytes).
- Packets X/26 to X/31: 32 spaces each.
"""
crc = 0
poly = 0x1281
# Helper to update CRC with a byte (MSB first)
def update_crc(c, val):
v = (val << 8) & 0xFFFF
for _ in range(8):
if (c ^ v) & 0x8000:
c = (c << 1) ^ poly
else:
c = c << 1
v <<= 1
c &= 0xFFFF
return c
# Organize packets by row
rows = {p.row: p for p in self.packets}
for r in range(32): # Process 32 slots (0-31)
start, end = 0, 0
padding = 0
if r == 0:
# Row 0: Bytes 14-37 (24 bytes)
start, end = 8, 32 # data[8..31]
padding = 8
elif 1 <= r <= 25:
# Rows 1-25: Bytes 14-45 (32 bytes)
start, end = 8, 40 # data[8..39]
padding = 0
else:
# Rows 26-31: 32 spaces each
padding = 32
# Process packet data if available
if r in rows and start < end:
p_data = rows[r].data
for i in range(start, end):
byte_val = (p_data[i] & 0x7F) if i < len(p_data) else 0x20
crc = update_crc(crc, byte_val)
elif start < end:
# Missing packet but slot exists
for _ in range(start, end):
crc = update_crc(crc, 0x20)
# Add padding for this slot
for _ in range(padding):
crc = update_crc(crc, 0x20)
return crc
def get_stored_crc(self) -> Optional[int]:
"""
Attempts to retrieve the stored CRC from Packet 27/0 if present.
Returns None if not found.
"""
# Look for Packet 27
for p in self.packets:
if p.row == 27:
# Check Designation Code (Byte 0)
try:
if len(p.data) >= 40:
b0 = p.data[0]
# Decode Hamming 8/4
designation = decode_hamming_8_4(b0)
# Packets X/27/0 to X/27/3 exist, but only X/27/0 has the CRC.
# We also check if b0 is raw 0 as a fallback for some captures.
if designation == 0 or b0 == 0:
# Packet 27/0
# Checksum is in bytes 38 and 39 (TBytes 44 and 45).
hi = p.data[38]
lo = p.data[39]
crc = (hi << 8) | lo
return crc
except:
pass
return None
@dataclass
class TeletextService:
"""

View File

@@ -53,7 +53,7 @@ COLORS = [
]
class TeletextCanvas(QWidget):
cursorChanged = pyqtSignal(int, int, int) # x, y, byte_val
cursorChanged = pyqtSignal(int, int, int, bool) # x, y, byte_val, is_graphics
def __init__(self, parent=None):
super().__init__(parent)
@@ -83,6 +83,7 @@ class TeletextCanvas(QWidget):
self.cursor_x = 0
self.cursor_y = 0
self.cursor_visible = True
self.cursor_is_graphics = False # Tracked during draw
# Blinking cursor timer could be added, for now static inverted is fine or toggle on timer elsewhere
def get_byte_at(self, x, y):
@@ -103,7 +104,7 @@ class TeletextCanvas(QWidget):
def emit_cursor_change(self):
val = self.get_byte_at(self.cursor_x, self.cursor_y)
self.cursorChanged.emit(self.cursor_x, self.cursor_y, val)
self.cursorChanged.emit(self.cursor_x, self.cursor_y, val, self.cursor_is_graphics)
def set_cursor(self, x, y):
self.cursor_x = max(0, min(self.cols - 1, x))
@@ -213,10 +214,18 @@ class TeletextCanvas(QWidget):
painter.end()
return
# Draw each packet
# Initialize a grid of empty chars
# Check Control Bits for "Inhibit Display" (C10)
# In our bitmask (from parse_header):
# C4:0, C5:1, C6:2, C7:3, C8:4, C9:5, C10:6, C11:7, C12:8, C13:9, C14:10
inhibit_display = bool((self.page.control_bits >> 6) & 1)
if inhibit_display:
painter.setPen(Qt.GlobalColor.gray)
painter.drawText(10, 20, f"Page {self.page.full_page_number} - INHIBIT DISPLAY (C10 set)")
painter.end()
return
# Organize each packet by row
grid = [None] * 26 # 0-25
for p in self.page.packets:
if 0 <= p.row <= 25:
grid[p.row] = p
@@ -242,6 +251,10 @@ class TeletextCanvas(QWidget):
# Output mask for the next row
next_occlusion_mask = [False] * 40
# Check for Suppress Header (C7)
# C7:3, so bit 3 of control_bits
suppress_header = bool((self.page.control_bits >> 3) & 1)
# Default State at start of row
fg = COLORS[7] # White
bg = COLORS[0] # Black
@@ -251,6 +264,9 @@ class TeletextCanvas(QWidget):
held_char = 0x20 # Space
double_height = False
last_visible_idx = -1
bg_segments = [(0, bg)] # Track BG changes: (index, color)
y = row * self.cell_h
data = b''
@@ -262,35 +278,24 @@ class TeletextCanvas(QWidget):
# 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}"
header_prefix = f"P{self.page.magazine}{self.page.page_number:02X}"
# Pad to 8 chars
header_prefix = header_prefix.ljust(8)
for c in range(40):
x = c * self.cell_w
# If this cell is occluded by the row above, skip drawing and attribute processing?
# Spec says "The characters in the row below are ignored."
# Ideally we shouldn't even process attributes, but for simple renderer we just skip draw.
# However, if we skip attribute processing, state (fg/bg) won't update.
# Teletext attributes are serial.
# BUT, if the row above covers it, the viewer sees the row above.
# Does the hidden content affect the *rest* of the row?
# Likely yes, attributes usually propagate.
# But the spec says "ignored". Let's assume we skip *everything* for this cell visually,
# but maybe we should technically maintain state?
# For "Double Height" visual correctness, skipping drawing is the key.
# We will Process attributes (to keep state consistent) but Skip Drawing if occluded.
# Wait, if we process attributes, we might set double_height=True for the NEXT row?
# If this cell is occluded, it shouldn't trigger DH for the next row.
is_occluded = occlusion_mask[c]
# Decide byte value
if row == 0 and c < 8:
# Use generated header prefix
byte_val = ord(header_prefix[c])
if row == 0:
if c < 8:
# Column 0-7: Header prefix
byte_val = ord(header_prefix[c])
elif suppress_header and c < 32:
# Column 8-31: Hide header if C7 set
byte_val = 0x20
else:
byte_val = data[c] if c < len(data) else 0x20
else:
byte_val = data[c] if c < len(data) else 0x20
@@ -310,12 +315,33 @@ class TeletextCanvas(QWidget):
graphics_mode = True
elif byte_val == 0x1C: # Black BG
bg = COLORS[0]
bg_segments.append((c, bg))
elif byte_val == 0x1D: # New BG
bg = fg
bg_segments.append((c, bg))
elif byte_val == 0x0C: # Normal Height
double_height = False
elif byte_val == 0x0D: # Double Height
double_height = True
# Backfill Height if we are in leading controls
if last_visible_idx == -1:
# Update occlusion mask for 0..c-1
for k in range(c):
next_occlusion_mask[k] = True
# Repaint 0..c-1 with DH
if draw_bg:
# Repaint each cell with its historical BG
for k in range(c):
# Resolve BG for k
cell_bg = bg_segments[0][1]
for idx, color in reversed(bg_segments):
if idx <= k:
cell_bg = color
break
painter.fillRect(k * self.cell_w, y, self.cell_w, self.cell_h * 2, cell_bg)
elif byte_val == 0x19: # Contiguous Graphics
contiguous = True
elif byte_val == 0x1A: # Separated Graphics
@@ -325,10 +351,31 @@ class TeletextCanvas(QWidget):
elif byte_val == 0x1F: # Release Graphics
hold_graphics = False
# Update visibility tracker
# If it's a control code, it's "invisible" (space) UNLESS Held Graphics draws something
visible_content = False
if is_control:
if hold_graphics and graphics_mode:
visible_content = True
else:
# Treat Space (0x20) as "invisible" for backfill purposes
# This allows backfilling height over leading spaces in banners
if byte_val > 0x20:
visible_content = True
if visible_content:
# If this is the first visible char, mark it
if last_visible_idx == -1:
last_visible_idx = c
# Record Double Height for next row
if double_height and not is_occluded:
next_occlusion_mask[c] = True
# Capture cursor state if this is the cursor position
if c == self.cursor_x and row == self.cursor_y:
self.cursor_is_graphics = graphics_mode
# If occluded, do not draw anything for this cell
if is_occluded:
continue
@@ -393,8 +440,8 @@ class TeletextCanvas(QWidget):
if draw_fg and self.cursor_visible and c == self.cursor_x and row == self.cursor_y:
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Difference)
# Difference with white creates inversion
# Note: Cursor follows double height? Probably just the active cell.
painter.fillRect(x, y, self.cell_w, self.cell_h, QColor(255, 255, 255))
h_cursor = self.cell_h * 2 if double_height else self.cell_h
painter.fillRect(x, y, self.cell_w, h_cursor, QColor(255, 255, 255))
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
return next_occlusion_mask

View File

@@ -2,22 +2,117 @@
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton,
QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication
QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication,
QCheckBox, QDialog, QGridLayout
)
from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor
from PyQt6.QtCore import Qt, QRect, QTimer
# ... (imports remain)
# ... (imports remain)
from PyQt6.QtGui import QAction, QKeyEvent
from PyQt6.QtCore import Qt
from .io import load_t42, save_t42
from .io import load_t42, save_t42, save_tti, decode_hamming_8_4, encode_hamming_8_4
from .renderer import TeletextCanvas, create_blank_packet
import copy
import sys
import os
from .models import TeletextService, Page, Packet
class MosaicButton(QPushButton):
def __init__(self, code, main_window):
super().__init__()
self.code = code
self.main_window = main_window
self.setFixedSize(32, 32)
self.setToolTip(f"Hex: {code:02X}")
self.clicked.connect(self.on_click)
def on_click(self):
self.main_window.insert_char(self.code)
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
# Draw content area (centered, smaller than button)
w = self.width()
h = self.height()
m = 4
rect = QRect(m, m, w - 2*m, h - 2*m)
# Background (Black)
painter.fillRect(rect, Qt.GlobalColor.black)
# Foreground (White)
painter.setBrush(QBrush(Qt.GlobalColor.white))
painter.setPen(Qt.PenStyle.NoPen)
# Mosaic Logic
val = self.code & 0x7F
bits = 0
if val >= 0x20:
bits = val - 0x20
# 2x3 grid
cw = rect.width()
ch = rect.height()
x_splits = [0, cw // 2, cw]
y_splits = [0, ch // 3, (2 * ch) // 3, ch]
# bit 0: TL, 1: TR, 2: ML, 3: MR, 4: BL, 6: BR
block_indices = [
(0, 0), (1, 0), # Top
(0, 1), (1, 1), # Mid
(0, 2), (1, 2) # Bot
]
bit_mask = [1, 2, 4, 8, 16, 64]
for i in range(6):
if bits & bit_mask[i]:
c, r = block_indices[i]
bx = rect.x() + x_splits[c]
by = rect.y() + y_splits[r]
bw = x_splits[c+1] - x_splits[c]
bh = y_splits[r+1] - y_splits[r]
painter.drawRect(bx, by, bw, bh)
class MosaicDialog(QDialog):
def __init__(self, main_window):
super().__init__(main_window)
self.setWindowTitle("Insert Mosaic")
self.main_window = main_window
self.setLayout(QVBoxLayout())
lbl = QLabel("Click to insert mosaic character:")
self.layout().addWidget(lbl)
hint = QLabel("Note: Mosaics only appear if the line segment is in Graphics Mode.\n"
"Insert a Graphics Color code (e.g. Red Graphics 0x11) first.")
hint.setStyleSheet("color: gray; font-style: italic;")
self.layout().addWidget(hint)
grid = QGridLayout()
self.layout().addLayout(grid)
# Ranges: 0x20-0x3F, 0x60-0x7F
codes = []
codes.extend(range(0x20, 0x40))
codes.extend(range(0x60, 0x80))
row = 0
col = 0
max_cols = 8
for code in codes:
btn = MosaicButton(code, main_window)
grid.addWidget(btn, row, col)
col += 1
if col >= max_cols:
col = 0
row += 1
close_btn = QPushButton("Close")
close_btn.clicked.connect(self.accept)
self.layout().addWidget(close_btn)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
@@ -26,11 +121,15 @@ class MainWindow(QMainWindow):
self.service = TeletextService()
self.current_page: Page = None
self.current_file_path = None
self.clipboard = [] # List of (row, data_bytes)
self.undo_stack = []
self.redo_stack = []
self.is_modified = False
self.language_overrides = {} # Session-based viewer overrides: (mag, pnum) -> lang_idx
self.language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
# UI Components
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
@@ -88,9 +187,33 @@ class MainWindow(QMainWindow):
center_layout.addLayout(top_bar)
# Middle Layout (Canvas + Right Sidebar)
middle_layout = QHBoxLayout()
center_layout.addLayout(middle_layout, 1)
# Canvas
self.canvas = TeletextCanvas()
self.canvas.cursorChanged.connect(self.on_cursor_changed)
middle_layout.addWidget(self.canvas, 1) # Expand
# Right Sidebar
right_sidebar = QWidget()
right_sidebar.setFixedWidth(160)
right_layout = QVBoxLayout(right_sidebar)
right_layout.setContentsMargins(5, 0, 0, 0)
middle_layout.addWidget(right_sidebar)
# Color Shortcuts
color_layout = QHBoxLayout()
# Graphics Mode Toggle
self.chk_graphics = QCheckBox("Graphics")
self.chk_graphics.setToolTip("If checked, inserts Graphics Color codes (e.g. Red Graphics 0x11) instead of Alpha (0x01)")
right_layout.addWidget(self.chk_graphics)
colors_grid = QGridLayout()
colors_grid.setSpacing(5)
colors = [
("Black", 0x00, "#000000"),
("Red", 0x01, "#FF0000"),
("Green", 0x02, "#00FF00"),
("Yellow", 0x03, "#FFFF00"),
@@ -100,19 +223,114 @@ class MainWindow(QMainWindow):
("White", 0x07, "#FFFFFF"),
]
row, col = 0, 0
for name, code, hex_color in colors:
btn = QPushButton(name)
btn.setStyleSheet(f"background-color: {hex_color}; font-weight: bold; color: black;")
btn.clicked.connect(lambda checked, c=code: self.insert_char(c))
color_layout.addWidget(btn)
btn.setFixedSize(60, 30)
color_layout.addStretch()
center_layout.addLayout(color_layout)
# Common style
style = f"background-color: {hex_color}; font-weight: bold; border: 1px solid #555; border-radius: 3px;"
if name == "Black":
style += " color: white;"
elif name in ["Blue", "Red", "Magenta"]: # Darker backgrounds
style += " color: white;"
else:
style += " color: black;"
btn.setStyleSheet(style)
# Use separate method to handle graphics check
btn.clicked.connect(lambda checked, c=code: self.insert_color(c))
colors_grid.addWidget(btn, row, col)
col += 1
if col > 1:
col = 0
row += 1
right_layout.addLayout(colors_grid)
# Mosaics Button
btn_mosaic = QPushButton("Mosaics...")
btn_mosaic.clicked.connect(self.open_mosaic_dialog)
right_layout.addWidget(btn_mosaic)
# Canvas
self.canvas = TeletextCanvas()
self.canvas.cursorChanged.connect(self.on_cursor_changed)
center_layout.addWidget(self.canvas, 1) # Expand
right_layout.addSpacing(10)
# Background Controls
bg_label = QLabel("Background:")
right_layout.addWidget(bg_label)
# New Background (0x1D)
btn_new_bg = QPushButton("New BG")
btn_new_bg.setFixedSize(80, 30)
btn_new_bg.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
btn_new_bg.setToolTip("Sets the current foreground color as the new background color (0x1D)")
btn_new_bg.clicked.connect(lambda: self.insert_char(0x1D))
right_layout.addWidget(btn_new_bg)
# Black Background (0x1C)
btn_black_bg = QPushButton("Black BG")
btn_black_bg.setFixedSize(80, 30)
btn_black_bg.setStyleSheet("background-color: black; color: white; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
btn_black_bg.setToolTip("Resets the background color to Black (0x1C)")
btn_black_bg.clicked.connect(lambda: self.insert_char(0x1C))
right_layout.addWidget(btn_black_bg)
right_layout.addSpacing(10)
# Graphics Control
gfx_ctrl_label = QLabel("Graphics Control:")
right_layout.addWidget(gfx_ctrl_label)
# Hold Graphics (0x1E)
btn_hold = QPushButton("Hold Gfx")
btn_hold.setFixedSize(80, 30)
btn_hold.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
btn_hold.setToolTip("Hold Graphics (0x1E): Displays the last graphic char in place of subsequent control codes.")
btn_hold.clicked.connect(lambda: self.insert_char(0x1E))
right_layout.addWidget(btn_hold)
# Release Graphics (0x1F)
btn_release = QPushButton("Release Gfx")
btn_release.setFixedSize(90, 30)
btn_release.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
btn_release.setToolTip("Release Graphics (0x1F): Ends the 'Hold Graphics' effect.")
btn_release.clicked.connect(lambda: self.insert_char(0x1F))
right_layout.addWidget(btn_release)
right_layout.addSpacing(10)
# Page Language Setting
lang_group_label = QLabel("Page Language:")
right_layout.addWidget(lang_group_label)
lang_layout = QHBoxLayout()
self.lang_combo = QComboBox()
self.lang_combo.addItems(self.language_names)
self.lang_combo.setToolTip("Select the National Option Character Set for this page.")
btn_set_lang = QPushButton("Set")
btn_set_lang.setFixedWidth(40)
btn_set_lang.clicked.connect(self.apply_language_change)
btn_set_lang.setToolTip("Apply this language setting to the page header.")
lang_layout.addWidget(self.lang_combo)
lang_layout.addWidget(btn_set_lang)
right_layout.addLayout(lang_layout)
right_layout.addSpacing(10)
# CRC Checksum
crc_label = QLabel("CRC Checksum:")
right_layout.addWidget(crc_label)
self.lbl_crc_info = QLabel("Page: ----\nCalc: ----")
self.lbl_crc_info.setStyleSheet("font-family: monospace; font-weight: bold;")
right_layout.addWidget(self.lbl_crc_info)
right_layout.addStretch()
self.layout.addLayout(center_layout, 1)
@@ -127,19 +345,46 @@ class MainWindow(QMainWindow):
self.status_label = QLabel("Ready")
self.status_bar.addWidget(self.status_label)
self.mode_label = QLabel("Mode: Text")
self.mode_label.setFixedWidth(120)
self.status_bar.addPermanentWidget(self.mode_label)
self.language_label = QLabel("Lang: English")
self.status_bar.addPermanentWidget(self.language_label)
self.language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
# Menus
self.create_menus()
def update_crc_display(self):
if not self.current_page:
self.lbl_crc_info.setText("Page: ----\nCalc: ----")
self.lbl_crc_info.setStyleSheet("font-family: monospace; font-weight: bold;")
return
calc_crc = self.current_page.calculate_crc()
stored_crc = self.current_page.get_stored_crc()
stored_str = f"{stored_crc:04X}" if stored_crc is not None else "----"
calc_str = f"{calc_crc:04X}"
# Highlight if match
if stored_crc is not None:
if stored_crc == calc_crc:
style = "font-family: monospace; font-weight: bold; color: green;"
else:
style = "font-family: monospace; font-weight: bold; color: red;"
else:
style = "font-family: monospace; font-weight: bold;"
self.lbl_crc_info.setStyleSheet(style)
self.lbl_crc_info.setText(f"Page: {stored_str}\nCalc: {calc_str}")
def update_language_label(self):
idx = self.canvas.subset_idx
if 0 <= idx < len(self.language_names):
self.language_label.setText(f"Lang: {self.language_names[idx]}")
self.lang_combo.setCurrentIndex(idx)
else:
self.language_label.setText(f"Lang: Unknown ({idx})")
@@ -198,6 +443,10 @@ class MainWindow(QMainWindow):
save_as_action.triggered.connect(self.save_as_file)
file_menu.addAction(save_as_action)
export_tti_action = QAction("Export to TTI...", self)
export_tti_action.triggered.connect(self.export_tti)
file_menu.addAction(export_tti_action)
close_action = QAction("Close File", self)
close_action.triggered.connect(self.close_file)
file_menu.addAction(close_action)
@@ -219,6 +468,10 @@ class MainWindow(QMainWindow):
paste_action.triggered.connect(self.paste_page_content)
edit_menu.addAction(paste_action)
delete_page_action = QAction("Delete Page", self)
delete_page_action.triggered.connect(self.delete_page)
edit_menu.addAction(delete_page_action)
edit_menu.addSeparator()
undo_action = QAction("Undo", self)
@@ -245,10 +498,71 @@ class MainWindow(QMainWindow):
if action:
idx = action.data()
self.canvas.subset_idx = idx
# Store session override for the current page (magazine + page number)
if self.current_page:
key = (self.current_page.magazine, self.current_page.page_number)
self.language_overrides[key] = idx
self.canvas.redraw()
self.canvas.update()
self.update_language_label()
def apply_language_change(self):
if not self.current_page:
return
idx = self.lang_combo.currentIndex()
if idx < 0: return
# Update the model
self.current_page.language = idx
# Also update the session override/viewer to match
self.canvas.subset_idx = idx
key = (self.current_page.magazine, self.current_page.page_number)
self.language_overrides[key] = idx
# Patch Row 0 packet data to persist language selection to file
# Language bits are in Byte 7 (Control Bits C11-C14)
# Byte 7 encoded structure: bit 0:C11, bit 1:C12, bit 2:C13, bit 3:C14
# National Option index corresponds to (C14 C13 C12)
# Find Row 0 packet
header_packet = None
for p in self.current_page.packets:
if p.row == 0:
header_packet = p
break
if header_packet and len(header_packet.data) >= 8:
try:
# Byte 7 contains C11, C12, C13, C14
old_val = decode_hamming_8_4(header_packet.data[7])
l0 = (idx >> 0) & 1 # C12
l1 = (idx >> 1) & 1 # C13
l2 = (idx >> 2) & 1 # C14
d1 = (old_val >> 0) & 1 # Preserve C11
d2 = l0
d3 = l1
d4 = l2
new_val = d1 | (d2 << 1) | (d3 << 2) | (d4 << 3)
header_packet.data[7] = encode_hamming_8_4(new_val)
self.set_modified(True)
self.status_label.setText(f"Language set to {self.language_names[idx]} (saved to header).")
except Exception as e:
self.status_label.setText(f"Error setting language: {e}")
else:
self.status_label.setText("No header packet found to update.")
self.canvas.redraw()
self.canvas.update()
self.update_language_label()
def prev_subpage(self):
count = self.subpage_combo.count()
if count <= 1: return
@@ -283,9 +597,10 @@ class MainWindow(QMainWindow):
self.undo_stack.clear()
self.redo_stack.clear()
self.language_overrides.clear()
QMessageBox.information(self, "Closed", "File closed.")
self.status_label.setText("Ready")
self.status_label.setText("File closed.")
QTimer.singleShot(3000, lambda: self.status_label.setText("Ready"))
self.set_modified(False)
@@ -324,9 +639,9 @@ class MainWindow(QMainWindow):
def save_file(self) -> bool:
if not self.current_file_path:
fname, _ = QFileDialog.getSaveFileName(self, "Save T42", "", "Teletext Files (*.t42)")
if not fname: return False
self.current_file_path = fname
# User requested status message instead of Save As behavior for empty state
self.status_label.setText("No file loaded to save. Please use 'Save As...' or 'Open' first.")
return False
try:
self.progress_bar.setVisible(True)
@@ -352,6 +667,20 @@ class MainWindow(QMainWindow):
self.status_label.setText("Error saving file")
return False
def export_tti(self):
if not self.current_page:
QMessageBox.warning(self, "Export TTI", "No page selected to export.")
return
fname, _ = QFileDialog.getSaveFileName(self, "Export to TTI", "", "Teletext Text Image (*.tti)")
if not fname: return
try:
save_tti(fname, self.current_page)
QMessageBox.information(self, "Export Successful", f"Page exported to {os.path.basename(fname)}")
except Exception as e:
QMessageBox.critical(self, "Export Failed", f"Failed to export TTI: {e}")
def copy_page_content(self):
if not self.current_page:
return
@@ -405,6 +734,7 @@ class MainWindow(QMainWindow):
# Force redraw
self.canvas.redraw()
self.canvas.update()
self.update_crc_display()
self.status_label.setText(f"Pasted {modified_count} rows.")
self.push_undo_state() # Push state after paste? NO, before!
# Wait, usually we push before modifying.
@@ -413,6 +743,36 @@ class MainWindow(QMainWindow):
# So I need to refactor paste_page_content to call push_undo_state() first.
# For now, I'll add the methods here.
def delete_page(self):
if not self.current_page:
return
ret = QMessageBox.question(self, "Delete Page",
f"Are you sure you want to delete Page {self.current_page.full_page_number} (Sub {self.current_page.sub_code:04X})?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if ret == QMessageBox.StandardButton.Yes:
# Remove from service.pages
if self.current_page in self.service.pages:
self.service.pages.remove(self.current_page)
# Remove packets from all_packets
# This is important for saving cleanly
# Filter out packets that belong to this page instance
# Note: We rely on object identity or need robust tracking.
# Since we reconstruct all_packets on save from pages, we don't strictly need to prune all_packets NOW,
# but it's good practice or we can just rely on save_file's reconstruction logic.
# save_file logic:
# new_all_packets = []
# for page in self.service.pages: ...
# So removing from service.pages is sufficient for the next Save.
self.set_modified(True)
self.current_page = None
self.canvas.set_page(None)
self.populate_list()
self.status_label.setText("Page deleted.")
def push_undo_state(self):
if not self.current_page: return
# Push deep copy of current page
@@ -478,6 +838,7 @@ class MainWindow(QMainWindow):
self.canvas.set_page(self.current_page)
self.canvas.redraw()
self.canvas.update()
self.update_crc_display()
def populate_list(self):
self.page_list.clear()
@@ -511,9 +872,21 @@ class MainWindow(QMainWindow):
self.subpage_combo.clear()
for i, p in enumerate(pages):
# Display format: Index or Subcode?
# Subcode is often 0000. Index 1/N is clearer for editing.
label = f"{i+1}/{len(pages)} (Sub {p.sub_code:04X})"
# Try to find the clock in Row 0 (last 8 characters)
clock_str = ""
for pkt in p.packets:
if pkt.row == 0:
# Bytes 32-39 of the 40-byte data are the clock
raw_clock = pkt.data[32:40].decode('latin-1', errors='replace')
# Strip parity from each char and filter non-printables
clock_str = "".join([chr(ord(c) & 0x7F) if 32 <= (ord(c) & 0x7F) <= 126 else " " for c in raw_clock])
break
label = f"{i+1}/{len(pages)} "
if clock_str.strip():
label += f"[{clock_str.strip()}] "
label += f"(Sub {p.sub_code:04X})"
self.subpage_combo.addItem(label, p)
self.subpage_combo.blockSignals(False)
@@ -529,7 +902,16 @@ class MainWindow(QMainWindow):
if isinstance(page, Page):
self.current_page = page
self.canvas.set_page(page)
# Apply session language override if it exists for this page
key = (page.magazine, page.page_number)
if key in self.language_overrides:
self.canvas.subset_idx = self.language_overrides[key]
self.canvas.redraw()
self.canvas.update()
self.update_language_label()
self.update_crc_display()
self.canvas.setFocus()
def insert_char(self, char_code):
@@ -538,9 +920,23 @@ class MainWindow(QMainWindow):
# Advance cursor
self.canvas.move_cursor(1, 0)
self.canvas.setFocus()
def insert_color(self, base_code):
code = base_code
if self.chk_graphics.isChecked():
# Convert 0x01..0x07 to 0x11..0x17
code += 0x10
self.insert_char(code)
def open_mosaic_dialog(self):
dlg = MosaicDialog(self)
dlg.exec()
def on_cursor_changed(self, x, y, val):
def on_cursor_changed(self, x, y, val, is_graphics):
self.hex_input.setText(f"{val:02X}")
mode_str = "Graphics" if is_graphics else "Text"
self.mode_label.setText(f"Mode: {mode_str}")
self.update_crc_display()
def on_hex_entered(self):
text = self.hex_input.text()

View File

@@ -1,94 +0,0 @@
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()

View File

@@ -1,247 +0,0 @@
<#
.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"

View File

@@ -1,70 +0,0 @@
# 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

View File

@@ -1,27 +0,0 @@
# 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

View File

@@ -1,69 +0,0 @@
# 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

View File

@@ -1,8 +0,0 @@
#!/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())

View File

@@ -1,8 +0,0 @@
#!/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())

View File

@@ -1,8 +0,0 @@
#!/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())

View File

@@ -1,8 +0,0 @@
#!/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())

View File

@@ -1 +0,0 @@
python3

View File

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

View File

@@ -1 +0,0 @@
python3

View File

@@ -1,8 +0,0 @@
#!/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