Compare commits
37 Commits
42e189635b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a15ba67b1a | ||
|
|
18fef7b049 | ||
| 9b846970b8 | |||
| de296b4711 | |||
| 84d1094d16 | |||
| 6a6df63980 | |||
| 06107a3d78 | |||
| 33e3ed2615 | |||
| 6ed8a79660 | |||
| 56657efa7c | |||
| fa195f2695 | |||
| 988178f1c6 | |||
| 71019bf399 | |||
| 6a5f223a88 | |||
| 274a6778b3 | |||
| 772827082e | |||
| f8a9ad0065 | |||
| 9726a82851 | |||
| 233eed1ca7 | |||
| 4c3d860dc4 | |||
| 670a2d9f8c | |||
| e304034596 | |||
| 80cca7cd79 | |||
| 8475b512b8 | |||
| 6c12e29e0a | |||
| 0ebf18ee6e | |||
| e06fd2c776 | |||
| 48b966f9a8 | |||
| f4af5f6389 | |||
| 98a641ffde | |||
| 13b08ac6a4 | |||
| 9fc75b7e39 | |||
| 334d25c3ba | |||
| cfbd2403e4 | |||
| e51e86e53b | |||
| d544cc6d9d | |||
| 876e2206b6 |
@@ -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
|
||||
|
||||
@@ -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
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
dist/
|
||||
|
||||
47
README.md
47
README.md
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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
BIN
app_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
BIN
venv/lib/python3.12/site-packages/PyQt6/Qt6/lib/libQt6Gui.so.6 → app_icon.png
Executable file → Normal file
BIN
venv/lib/python3.12/site-packages/PyQt6/Qt6/lib/libQt6Gui.so.6 → app_icon.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 11 MiB After Width: | Height: | Size: 9.0 MiB |
12
build_app.py
12
build_app.py
@@ -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()
|
||||
@@ -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
17
convert_icon.py
Normal 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
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
Binary file not shown.
49
src/main.py
49
src/main.py
@@ -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())
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
94
test_t42.py
94
test_t42.py
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -1 +0,0 @@
|
||||
python3
|
||||
@@ -1 +0,0 @@
|
||||
/usr/bin/python3
|
||||
@@ -1 +0,0 @@
|
||||
python3
|
||||
@@ -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())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user