Compare commits
31 Commits
42e189635b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,11 @@ jobs:
|
||||
container:
|
||||
image: tobix/pywine:3.10
|
||||
steps:
|
||||
- name: Install Node.js
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y nodejs npm
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -18,10 +23,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):
|
||||
|
||||
@@ -228,3 +228,65 @@ def parse_header(data: bytearray):
|
||||
language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4)
|
||||
|
||||
return page_num, sub_code, 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
|
||||
|
||||
@@ -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))
|
||||
@@ -251,6 +252,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,7 +266,7 @@ 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)
|
||||
|
||||
@@ -310,12 +314,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 +350,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 +439,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,104 @@ 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.addStretch()
|
||||
|
||||
self.layout.addLayout(center_layout, 1)
|
||||
|
||||
@@ -127,12 +335,14 @@ 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()
|
||||
|
||||
@@ -140,6 +350,7 @@ class MainWindow(QMainWindow):
|
||||
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 +409,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 +434,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 +464,83 @@ 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 8 (Control Bits 2): C12, C13, C14
|
||||
# We need to preserve C11 (bit 3 of encoded 4-bit val) which is "Inhibit Display" usually 0
|
||||
|
||||
# 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:
|
||||
old_val = decode_hamming_8_4(header_packet.data[8])
|
||||
# Encoded nibble structure: D1(b0), D2(b1), D3(b2), D4(b3)
|
||||
# D1 maps to C12
|
||||
# D2 maps to C13
|
||||
# D3 maps to C14
|
||||
# D4 maps to C11
|
||||
|
||||
# io.py logic for reading:
|
||||
# language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4)
|
||||
# i.e. Lang Bit 0 comes from D2, Lang Bit 1 comes from D1, Lang Bit 2 comes from D3
|
||||
|
||||
# So for writing:
|
||||
# D1 = Lang Bit 1
|
||||
# D2 = Lang Bit 0
|
||||
# D3 = Lang Bit 2
|
||||
|
||||
l0 = (idx >> 0) & 1
|
||||
l1 = (idx >> 1) & 1
|
||||
l2 = (idx >> 2) & 1
|
||||
|
||||
d1 = l1
|
||||
d2 = l0
|
||||
d3 = l2
|
||||
d4 = (old_val >> 3) & 1 # Preserve C11
|
||||
|
||||
new_val = d1 | (d2 << 1) | (d3 << 2) | (d4 << 3)
|
||||
|
||||
header_packet.data[8] = 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 +575,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 +617,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 +645,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
|
||||
@@ -413,6 +720,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
|
||||
@@ -529,6 +866,14 @@ 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.canvas.setFocus()
|
||||
|
||||
@@ -538,9 +883,22 @@ 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}")
|
||||
|
||||
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.
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