按 Claude Design 定稿(归档在 docs/design/)重做 UI,并把 tmux 从 「从不自动进」修成「连接前探测 → 让用户选会话」。 设计令牌与导航地基 - Theme 拆三层:TXAccent(恒定 blurple) / TXChrome(中性阶×4 表) / TXFlavor(Catppuccin 终端) - vendor JetBrains Mono 四权重(附 OFL 许可) - AppRouter + SessionManager:多会话并存,路由与会话生命周期解耦 - SessionCanvas 常驻挂载会话 surface(摘除即丢内容,也是缩回动画的前提) 按设计稿落地的屏 - 沉浸轨道页:56pt 轨道 + 浮起标题/状态胶囊 + 侧边栏三态(遮罩不 resize、Pin 各一次) - 首页:活动会话卡(readViewportText 文本镜像)+ 主机网格 + 筛选 chips - 关闭二次确认(tmux 仅断开 / 原生窗口两套文案)、分屏菜单、pane 拖拽条 - 空状态:首次运行 / 无会话 / 搜索无命中 / 连接中·失败·已关闭 tmux 真实链路(真机查出并修掉 4 个 bug) - 全代码库从来没人发起 attach → 连接前探测 + 会话选择器(接回 / 新建 / 原生终端) - format 分隔符 tab 经 PTY 变成下划线 → 改用 |:| - controller 变化不冒泡到 session → Combine 转发(否则数据解析对了 UI 不刷新) - 当前会话名不能靠 session_attached 反推 → 改用 display-message 传输层:SSH connect 加超时(原来阻塞到系统 TCP 超时 75s+) 无头验证设施:假会话 fixture · terminalx://ui/* 驱动 · 横屏截图脚本 · 对拍走查法 TXCore 45 tests 绿(新增 5 个会话探测单测) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
81 lines
3.1 KiB
Python
81 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
||
"""把设计稿画板拆成每屏一个独立 HTML,便于 headless chrome 逐屏截图。"""
|
||
import re, os, sys
|
||
from html.parser import HTMLParser
|
||
|
||
# python3 docs/design/render-screens.py && \
|
||
# for f in /tmp/tx-screens/*.html; do "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
||
# --headless=new --window-size=1366,1024 --screenshot="${f%.html}.png" "file://$f"; done
|
||
SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "terminalX-iPadOS.dc.html")
|
||
OUT = "/tmp/tx-screens"
|
||
os.makedirs(OUT, exist_ok=True)
|
||
html = open(SRC, encoding="utf-8").read()
|
||
|
||
# 提取 <style>…</style>(helmet 内的全局样式)
|
||
style = "\n".join(re.findall(r"<style>(.*?)</style>", html, re.S))
|
||
|
||
VOID = {"br", "img", "hr", "input", "meta", "link", "source", "area", "base", "col", "embed", "param", "track", "wbr"}
|
||
|
||
class Finder(HTMLParser):
|
||
def __init__(self):
|
||
super().__init__(convert_charrefs=False)
|
||
self.stack = [] # 当前打开的 tag 名
|
||
self.capture = None # (label, depth, start_offset)
|
||
self.results = [] # (label, start, end)
|
||
|
||
def cur_off(self):
|
||
line, col = self.getpos()
|
||
return self.line_starts[line - 1] + col
|
||
|
||
def handle_starttag(self, tag, attrs):
|
||
if tag in VOID:
|
||
return
|
||
d = dict(attrs)
|
||
if self.capture is None and "data-screen-label" in d:
|
||
self.capture = (d["data-screen-label"], len(self.stack), self.cur_off())
|
||
self.stack.append(tag)
|
||
|
||
def handle_startendtag(self, tag, attrs):
|
||
pass
|
||
|
||
def handle_endtag(self, tag):
|
||
if tag in VOID:
|
||
return
|
||
while self.stack and self.stack[-1] != tag:
|
||
self.stack.pop()
|
||
if self.stack:
|
||
self.stack.pop()
|
||
if self.capture and len(self.stack) == self.capture[1]:
|
||
label, _, start = self.capture
|
||
end = self.cur_off() + len(f"</{tag}>")
|
||
self.results.append((label, start, end))
|
||
self.capture = None
|
||
|
||
f = Finder()
|
||
f.line_starts = [0]
|
||
for line in html.splitlines(keepends=True):
|
||
f.line_starts.append(f.line_starts[-1] + len(line))
|
||
f.feed(html)
|
||
|
||
HEAD = """<!DOCTYPE html><html><head><meta charset="utf-8">
|
||
<link rel="stylesheet" href="https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css">
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap">
|
||
<style>%s
|
||
html,body{margin:0;padding:0;background:#11121c}</style></head><body>"""
|
||
|
||
manifest = []
|
||
for i, (label, s, e) in enumerate(f.results):
|
||
frag = html[s:e]
|
||
m = re.search(r"width:(\d+)px;height:(\d+)px", frag)
|
||
w, h = (m.group(1), m.group(2)) if m else ("1366", "1024")
|
||
slug = re.sub(r"[^\w一-鿿]+", "_", label).strip("_")
|
||
path = f"{OUT}/{i:02d}_{slug}.html"
|
||
open(path, "w", encoding="utf-8").write(HEAD % style + frag + "</body></html>")
|
||
manifest.append((path, w, h, label))
|
||
print(f"{i:02d}\t{label}\t{w}x{h}\t{path}")
|
||
|
||
with open(f"{OUT}/manifest.tsv", "w") as fh:
|
||
for p, w, h, l in manifest:
|
||
fh.write(f"{p}\t{w}\t{h}\t{l}\n")
|
||
print(f"total {len(manifest)}")
|