- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连 - M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire - M1.5: tmux -CC 原生 tab(MVP) - 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/ - 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口) - 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名 - 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
6.5 KiB
Bash
Executable File
196 lines
6.5 KiB
Bash
Executable File
#!/bin/zsh
|
|
set -euo pipefail
|
|
|
|
# Downloads all Ghostty theme files from iTerm2-Color-Schemes and generates
|
|
# Swift source files for the GhosttyTheme library.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
OUTPUT_DIR="$ROOT_DIR/Sources/GhosttyTheme/Themes"
|
|
TEMP_DIR="$(mktemp -d)"
|
|
|
|
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
|
|
mkdir -p "$OUTPUT_DIR"
|
|
|
|
# Fetch the list of theme files from GitHub API
|
|
echo "[+] fetching theme list from github api"
|
|
curl -sL "https://api.github.com/repos/mbadolato/iTerm2-Color-Schemes/contents/ghostty" \
|
|
| python3 -c "
|
|
import json, sys
|
|
data = json.load(sys.stdin)
|
|
for item in data:
|
|
if item['type'] == 'file':
|
|
print(item['name'])
|
|
" > "$TEMP_DIR/theme_list.txt"
|
|
|
|
THEME_COUNT=$(wc -l < "$TEMP_DIR/theme_list.txt" | tr -d ' ')
|
|
echo "[+] found $THEME_COUNT themes"
|
|
|
|
# Download all themes
|
|
echo "[+] downloading themes"
|
|
mkdir -p "$TEMP_DIR/themes"
|
|
while IFS= read -r name; do
|
|
encoded_name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$name'))")
|
|
curl -sL "https://raw.githubusercontent.com/mbadolato/iTerm2-Color-Schemes/master/ghostty/$encoded_name" \
|
|
-o "$TEMP_DIR/themes/$name" &
|
|
# Limit concurrent downloads
|
|
if (( $(jobs -r | wc -l) >= 20 )); then
|
|
wait -n
|
|
fi
|
|
done < "$TEMP_DIR/theme_list.txt"
|
|
wait
|
|
echo "[+] downloaded all themes"
|
|
|
|
# Generate Swift files using Python
|
|
echo "[+] generating swift source files"
|
|
python3 - "$TEMP_DIR/themes" "$OUTPUT_DIR" "$TEMP_DIR/theme_list.txt" << 'PYEOF'
|
|
import os, sys, re, string
|
|
|
|
themes_dir = sys.argv[1]
|
|
output_dir = sys.argv[2]
|
|
list_file = sys.argv[3]
|
|
|
|
with open(list_file) as f:
|
|
theme_names = [line.strip() for line in f if line.strip()]
|
|
|
|
def parse_theme(path):
|
|
result = {}
|
|
palette = {}
|
|
with open(path) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
if '=' not in line:
|
|
continue
|
|
key, _, value = line.partition('=')
|
|
key = key.strip()
|
|
value = value.strip()
|
|
if key == 'palette':
|
|
# format: N=#RRGGBB or N=RRGGBB
|
|
idx_str, _, color = value.partition('=')
|
|
idx = int(idx_str.strip())
|
|
color = color.strip().lstrip('#')
|
|
palette[idx] = color
|
|
else:
|
|
# strip # from color values
|
|
clean_val = value.lstrip('#')
|
|
result[key] = clean_val
|
|
result['_palette'] = palette
|
|
return result
|
|
|
|
def swift_identifier(name):
|
|
"""Convert theme name to a valid Swift identifier."""
|
|
# Replace + with Plus before stripping
|
|
ident = name.replace('+', 'Plus')
|
|
# Replace non-alphanumeric with underscore
|
|
ident = re.sub(r'[^a-zA-Z0-9]', '_', ident)
|
|
# Collapse multiple underscores
|
|
ident = re.sub(r'_+', '_', ident)
|
|
# Strip leading/trailing underscores
|
|
ident = ident.strip('_')
|
|
# Prefix with underscore if starts with digit
|
|
if ident and ident[0].isdigit():
|
|
ident = '_' + ident
|
|
# camelCase: lowercase first char
|
|
if ident:
|
|
ident = ident[0].lower() + ident[1:]
|
|
return ident
|
|
|
|
def swift_string(s):
|
|
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
|
|
|
def optional_field(data, ghostty_key, swift_type="String"):
|
|
val = data.get(ghostty_key)
|
|
if val:
|
|
return swift_string(val)
|
|
return "nil"
|
|
|
|
# Group themes by first letter
|
|
groups = {}
|
|
all_themes = []
|
|
|
|
for name in sorted(theme_names, key=str.lower):
|
|
path = os.path.join(themes_dir, name)
|
|
if not os.path.isfile(path):
|
|
continue
|
|
try:
|
|
data = parse_theme(path)
|
|
except Exception as e:
|
|
print(f" [-] skipping {name}: {e}", file=sys.stderr)
|
|
continue
|
|
|
|
bg = data.get('background')
|
|
fg = data.get('foreground')
|
|
if not bg or not fg:
|
|
print(f" [-] skipping {name}: missing background/foreground", file=sys.stderr)
|
|
continue
|
|
|
|
ident = swift_identifier(name)
|
|
first_char = name[0].upper()
|
|
if first_char not in string.ascii_uppercase:
|
|
first_char = 'Symbols'
|
|
|
|
palette = data.get('_palette', {})
|
|
palette_str = '['
|
|
for i in sorted(palette.keys()):
|
|
palette_str += f'{i}: {swift_string(palette[i])}, '
|
|
if palette_str.endswith(', '):
|
|
palette_str = palette_str[:-2]
|
|
palette_str += ']'
|
|
|
|
entry = {
|
|
'name': name,
|
|
'ident': ident,
|
|
'background': swift_string(bg),
|
|
'foreground': swift_string(fg),
|
|
'cursorColor': optional_field(data, 'cursor-color'),
|
|
'cursorText': optional_field(data, 'cursor-text'),
|
|
'selectionBackground': optional_field(data, 'selection-background'),
|
|
'selectionForeground': optional_field(data, 'selection-foreground'),
|
|
'palette': palette_str,
|
|
'group': first_char,
|
|
}
|
|
|
|
groups.setdefault(first_char, []).append(entry)
|
|
all_themes.append(entry)
|
|
|
|
# Write per-group Swift files
|
|
for group, entries in sorted(groups.items()):
|
|
filename = f"Themes_{group}.swift"
|
|
filepath = os.path.join(output_dir, filename)
|
|
with open(filepath, 'w') as f:
|
|
f.write("// Auto-generated by Script/generate-themes.sh — do not edit\n\n")
|
|
f.write("public extension GhosttyThemeDefinition {\n")
|
|
for e in entries:
|
|
f.write(f" static let {e['ident']} = GhosttyThemeDefinition(\n")
|
|
f.write(f" name: {swift_string(e['name'])},\n")
|
|
f.write(f" background: {e['background']},\n")
|
|
f.write(f" foreground: {e['foreground']},\n")
|
|
f.write(f" cursorColor: {e['cursorColor']},\n")
|
|
f.write(f" cursorText: {e['cursorText']},\n")
|
|
f.write(f" selectionBackground: {e['selectionBackground']},\n")
|
|
f.write(f" selectionForeground: {e['selectionForeground']},\n")
|
|
f.write(f" palette: {e['palette']}\n")
|
|
f.write(f" )\n\n")
|
|
f.write("}\n")
|
|
print(f" [+] wrote {filename} ({len(entries)} themes)")
|
|
|
|
# Write catalog file
|
|
catalog_path = os.path.join(output_dir, "ThemeCatalog_Generated.swift")
|
|
with open(catalog_path, 'w') as f:
|
|
f.write("// Auto-generated by Script/generate-themes.sh — do not edit\n\n")
|
|
f.write("public extension GhosttyThemeCatalog {\n")
|
|
f.write(" static let allThemes: [GhosttyThemeDefinition] = [\n")
|
|
for e in all_themes:
|
|
f.write(f" .{e['ident']},\n")
|
|
f.write(" ]\n")
|
|
f.write("}\n")
|
|
print(f" [+] wrote ThemeCatalog_Generated.swift ({len(all_themes)} themes)")
|
|
|
|
print(f"[+] generated {len(all_themes)} themes total")
|
|
PYEOF
|
|
|
|
echo "[+] done"
|