8b0eb42fec
First developer tool for the XDJ-100SX project. Connects Claude Code directly to the Pi over SSH — push skin files, take screenshots, restart Mixxx, flash Pico firmware, and more without leaving the editor. Available MCP tools: - run_command, read_file, write_file, list_files - push_skin, pull_skin, push_skin_file, pull_skin_file - push_midi, pull_midi - take_screenshot, navigate_panel - restart_mixxx - check (preflight: SSH, Mixxx, Pico, audio) - pico_bootloader, pico_flash - discover_units — scan network for all reachable XDJ Pi units - select_unit — switch active connection mid-session (multi-unit support) Also adds --about flag and TUI About modal with authors and credits, and fixes scrolling/close behavior on Help and About modals. By: Jeancarlo Cardoso de Faria Filho (jaianlab) <jaianlabworks@gmail.com>
128 lines
4.6 KiB
Python
128 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
"""
|
|
Image utilities: white-background removal and PIL-image → Rich-Text rendering.
|
|
|
|
Half-block trick: the ▄ character covers the BOTTOM half of a terminal cell.
|
|
• Both halves opaque → bgcolor=top, color=bottom, char=▄
|
|
• Top transparent → color=bottom, no bgcolor (panel bg shows through), char=▄
|
|
• Bottom transparent → color=top, no bgcolor, char=▀ (upper-half block)
|
|
• Both transparent → space (panel background shows through)
|
|
|
|
This avoids any fixed background-colour guessing, so the image blends naturally
|
|
against whatever Textual's panel colour happens to be.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from PIL import Image as _PILImage
|
|
|
|
_IMAGES_DIR = Path(__file__).parent / "images"
|
|
|
|
# Pixels with alpha below this are treated as fully transparent
|
|
_ALPHA_THRESH = 80
|
|
|
|
|
|
def _remove_white_bg(img: "_PILImage.Image", threshold: int = 230) -> "_PILImage.Image":
|
|
"""
|
|
BFS flood-fill from all four corners: connected white pixels become transparent.
|
|
Corner-flood avoids touching white silkscreen text on the board itself.
|
|
"""
|
|
img = img.convert("RGBA")
|
|
w, h = img.size
|
|
pixels = img.load()
|
|
|
|
def _is_white(px) -> bool:
|
|
return px[0] >= threshold and px[1] >= threshold and px[2] >= threshold
|
|
|
|
visited: set[tuple[int, int]] = set()
|
|
queue: list[tuple[int, int]] = []
|
|
for sx, sy in [(0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1)]:
|
|
if _is_white(pixels[sx, sy]) and (sx, sy) not in visited:
|
|
queue.append((sx, sy))
|
|
visited.add((sx, sy))
|
|
|
|
while queue:
|
|
x, y = queue.pop()
|
|
r, g, b, _ = pixels[x, y]
|
|
pixels[x, y] = (r, g, b, 0)
|
|
for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
|
|
if 0 <= nx < w and 0 <= ny < h and (nx, ny) not in visited:
|
|
if _is_white(pixels[nx, ny]):
|
|
visited.add((nx, ny))
|
|
queue.append((nx, ny))
|
|
|
|
return img
|
|
|
|
|
|
def img_to_rich(img: "_PILImage.Image", target_width: int = 32) -> object:
|
|
"""
|
|
Convert a PIL RGBA image to a Rich Text object using half-block characters.
|
|
Transparent areas render as plain spaces so the panel background shows through.
|
|
Returns a rich.text.Text ready to pass to Static() or RichLog.write().
|
|
"""
|
|
from PIL import Image
|
|
from rich.text import Text
|
|
from rich.style import Style
|
|
from rich.color import Color
|
|
|
|
aspect = img.height / img.width
|
|
target_height = max(2, int(target_width * aspect * 0.5) * 2) # must be even
|
|
img = img.resize((target_width, target_height), Image.LANCZOS).convert("RGBA")
|
|
pixels = img.load()
|
|
|
|
result = Text(no_wrap=True)
|
|
for row in range(0, target_height, 2):
|
|
if row > 0:
|
|
result.append("\n")
|
|
for col in range(target_width):
|
|
tr, tg, tb, ta = pixels[col, row]
|
|
br, bg, bb, ba = pixels[col, min(row + 1, target_height - 1)]
|
|
top_opaque = ta > _ALPHA_THRESH
|
|
bot_opaque = ba > _ALPHA_THRESH
|
|
|
|
if not top_opaque and not bot_opaque:
|
|
# Both transparent — let the panel background show through
|
|
result.append(" ")
|
|
elif top_opaque and bot_opaque:
|
|
# Both opaque — full half-block encoding
|
|
result.append("▄", style=Style(
|
|
bgcolor=Color.from_rgb(tr, tg, tb),
|
|
color=Color.from_rgb(br, bg, bb),
|
|
))
|
|
elif not top_opaque and bot_opaque:
|
|
# Bottom opaque, top transparent — ▄ with no background
|
|
result.append("▄", style=Style(color=Color.from_rgb(br, bg, bb)))
|
|
else:
|
|
# Top opaque, bottom transparent — upper-half block, no background
|
|
result.append("▀", style=Style(color=Color.from_rgb(tr, tg, tb)))
|
|
return result
|
|
|
|
|
|
def load_board_rich(board_key: str, target_width: int = 32) -> object | None:
|
|
"""
|
|
Load, background-remove, and render a board image as Rich Text.
|
|
Returns None if Pillow is not installed or the image is missing.
|
|
board_key: 'pico' | 'pico2' | 'teensy'
|
|
"""
|
|
_FILE_MAP = {
|
|
"pico": "raspberry pi pico.png",
|
|
"pico2": "raspberry pi pico 2.png",
|
|
"teensy": "teensy 4-0.png",
|
|
}
|
|
fname = _FILE_MAP.get(board_key)
|
|
if not fname:
|
|
return None
|
|
path = _IMAGES_DIR / fname
|
|
if not path.exists():
|
|
return None
|
|
try:
|
|
from PIL import Image
|
|
img = Image.open(path)
|
|
img = _remove_white_bg(img)
|
|
return img_to_rich(img, target_width)
|
|
except Exception:
|
|
return None
|