- 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>
409 lines
17 KiB
Bash
Executable File
409 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
|
|
|
MARKER="LIBGHOSTTY_SPM_INSPECTOR_DISABLE"
|
|
|
|
if grep -q "$MARKER" "$SOURCE_DIR/src/build/Config.zig" 2>/dev/null; then
|
|
echo "[+] inspector disable patch already applied"
|
|
exit 0
|
|
fi
|
|
|
|
python3 - "$SOURCE_DIR" "$MARKER" <<'PYEOF'
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
source_dir = Path(sys.argv[1])
|
|
marker = sys.argv[2]
|
|
|
|
def patch_file(rel_path, replacements):
|
|
path = source_dir / rel_path
|
|
text = path.read_text()
|
|
for old, new in replacements:
|
|
if old not in text:
|
|
print(f"[-] pattern not found in {rel_path}:")
|
|
print(f" {old[:80]}...")
|
|
sys.exit(1)
|
|
count = text.count(old)
|
|
if count > 1:
|
|
print(f"[-] pattern matched {count} times in {rel_path} (expected 1):")
|
|
print(f" {old[:80]}...")
|
|
sys.exit(1)
|
|
text = text.replace(old, new, 1)
|
|
path.write_text(text)
|
|
print(f"[+] patched {rel_path}")
|
|
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
# 1. Config.zig — add inspector feature flag
|
|
# (runs AFTER 0006, so custom_shaders line already exists)
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
patch_file("src/build/Config.zig", [
|
|
# Add field after custom_shaders (added by 0006)
|
|
(
|
|
f"custom_shaders: bool = true, // LIBGHOSTTY_SPM_TRIM_PATCH",
|
|
f"custom_shaders: bool = true, // LIBGHOSTTY_SPM_TRIM_PATCH\n"
|
|
f"inspector: bool = true, // {marker}",
|
|
),
|
|
# Add option parsing after the custom_shaders option block (added by 0006)
|
|
(
|
|
' "custom-shaders",\n'
|
|
' "Build with custom shader (glslang/spirv-cross) support.",\n'
|
|
' ) orelse true;',
|
|
' "custom-shaders",\n'
|
|
' "Build with custom shader (glslang/spirv-cross) support.",\n'
|
|
' ) orelse true;\n'
|
|
'\n'
|
|
' config.inspector = b.option(\n'
|
|
' bool,\n'
|
|
' "inspector",\n'
|
|
' "Build with terminal inspector (dcimgui) support.",\n'
|
|
' ) orelse true;',
|
|
),
|
|
# Add to addOptions after custom_shaders (added by 0006)
|
|
(
|
|
' step.addOption(bool, "custom_shaders", self.custom_shaders);',
|
|
' step.addOption(bool, "custom_shaders", self.custom_shaders);\n'
|
|
' step.addOption(bool, "inspector", self.inspector);',
|
|
),
|
|
])
|
|
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
# 2. SharedDeps.zig — gate dcimgui linking on inspector flag
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
patch_file("src/build/SharedDeps.zig", [
|
|
(
|
|
' // cimgui\n'
|
|
' if (b.lazyDependency("dcimgui", .{',
|
|
' // cimgui — only needed for inspector\n'
|
|
' if (self.config.inspector) if (b.lazyDependency("dcimgui", .{',
|
|
),
|
|
# Close the extra if — find the end of the dcimgui block
|
|
(
|
|
' );\n'
|
|
' }\n'
|
|
'\n'
|
|
' // Fonts',
|
|
' );\n'
|
|
' };\n'
|
|
'\n'
|
|
' // Fonts',
|
|
),
|
|
])
|
|
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
# 3. build_config.zig — re-export inspector flag
|
|
# (runs AFTER 0006, so custom_shaders line already exists)
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
patch_file("src/build_config.zig", [
|
|
(
|
|
'pub const custom_shaders = options.custom_shaders;',
|
|
'pub const custom_shaders = options.custom_shaders;\n'
|
|
'pub const inspector = options.inspector;',
|
|
),
|
|
])
|
|
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
# 4. inspector/main.zig — COMPLETE FILE REPLACEMENT with stub module
|
|
#
|
|
# This is the key stability improvement. Instead of pattern-matching
|
|
# downstream files, we replace the single chokepoint module with
|
|
# stubs that satisfy all downstream type/method requirements.
|
|
# When inspector=true, everything re-exports as before.
|
|
# When inspector=false, stub types with no-op methods are provided
|
|
# so Surface.zig, renderer, termio all compile without modification.
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
inspector_main = source_dir / "src/inspector/main.zig"
|
|
inspector_main.write_text('''\
|
|
const build_config = @import("../build_config.zig");
|
|
const std = @import("std");
|
|
const terminal = @import("../terminal/main.zig");
|
|
const input = @import("../input.zig");
|
|
const renderer = @import("../renderer.zig");
|
|
|
|
pub const widgets = if (build_config.inspector) @import("widgets.zig") else struct {
|
|
pub const key = struct {
|
|
pub const Event = StubKeyEvent;
|
|
};
|
|
pub const renderer = struct {
|
|
pub const Info = StubRendererInfo;
|
|
};
|
|
pub const surface = struct {
|
|
pub const Mouse = StubMouse;
|
|
};
|
|
};
|
|
|
|
pub const Inspector = if (build_config.inspector) @import("Inspector.zig") else StubInspector;
|
|
pub const KeyEvent = widgets.key.Event;
|
|
|
|
/// Stub types — these satisfy all downstream method/field accesses
|
|
/// when inspector is disabled, so no other files need patching.
|
|
|
|
const StubMouse = struct {
|
|
last_xpos: f64 = 0,
|
|
last_ypos: f64 = 0,
|
|
last_point: ?terminal.Pin = null,
|
|
};
|
|
|
|
const StubRendererInfo = struct {
|
|
pub const empty: @This() = .{};
|
|
|
|
pub fn deinit(_: *@This(), _: std.mem.Allocator) void {}
|
|
|
|
pub fn overlayFeatures(
|
|
_: *const @This(),
|
|
_: std.mem.Allocator,
|
|
) std.mem.Allocator.Error![]renderer.Overlay.Feature {
|
|
return &.{};
|
|
}
|
|
};
|
|
|
|
const StubKeyEvent = struct {
|
|
event: input.KeyEvent = undefined,
|
|
binding: []const input.Binding.Action = &.{},
|
|
pty: []const u8 = "",
|
|
|
|
pub fn deinit(_: *const @This(), _: std.mem.Allocator) void {}
|
|
};
|
|
|
|
const StubInspector = struct {
|
|
mouse: StubMouse = .{},
|
|
|
|
pub fn setup() void {}
|
|
pub fn init(_: std.mem.Allocator) !@This() { return .{}; }
|
|
pub fn deinit(_: *@This(), _: std.mem.Allocator) void {}
|
|
pub fn render(_: *@This(), _: anytype) void {}
|
|
|
|
pub fn rendererInfo(_: *@This()) *StubRendererInfo {
|
|
return &stub_renderer_info;
|
|
}
|
|
|
|
pub fn recordKeyEvent(
|
|
_: *@This(),
|
|
_: std.mem.Allocator,
|
|
_: StubKeyEvent,
|
|
) std.mem.Allocator.Error!void {}
|
|
|
|
pub fn recordPtyRead(
|
|
_: *@This(),
|
|
_: std.mem.Allocator,
|
|
_: *terminal.Terminal,
|
|
_: []const u8,
|
|
) !void {}
|
|
|
|
var stub_renderer_info: StubRendererInfo = .{};
|
|
};
|
|
|
|
test {
|
|
if (build_config.inspector) {
|
|
@import("std").testing.refAllDecls(@This());
|
|
}
|
|
}
|
|
''')
|
|
print("[+] replaced src/inspector/main.zig with stub module")
|
|
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
# 5. input/key.zig — gate dcimgui import + imguiKey method
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
patch_file("src/input/key.zig", [
|
|
(
|
|
'const cimgui = @import("dcimgui");',
|
|
'const _build_config = @import("../build_config.zig");\n'
|
|
'const cimgui = if (_build_config.inspector) @import("dcimgui") else struct {};',
|
|
),
|
|
(
|
|
' pub fn imguiKey(self: Key) ?c_int {',
|
|
' pub fn imguiKey(self: Key) ?c_int {\n'
|
|
' if (comptime !_build_config.inspector) return null;',
|
|
),
|
|
])
|
|
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
# 6. apprt/embedded.zig — gate Inspector struct + CoreInspector import
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
embedded_path = source_dir / "src/apprt/embedded.zig"
|
|
text = embedded_path.read_text()
|
|
|
|
# 6a. Gate CoreInspector import
|
|
old = 'const CoreInspector = @import("../inspector/main.zig").Inspector;'
|
|
new = 'const CoreInspector = if (@import("../build_config.zig").inspector) @import("../inspector/main.zig").Inspector else struct {};'
|
|
assert old in text, f"pattern not found: {old}"
|
|
text = text.replace(old, new, 1)
|
|
|
|
# 6b. Gate the Inspector struct definition using brace-depth counting
|
|
# Find the struct opening
|
|
struct_marker = 'pub const Inspector = struct {\n const cimgui = @import("dcimgui");'
|
|
assert struct_marker in text, f"Inspector struct marker not found"
|
|
|
|
struct_start_idx = text.index('pub const Inspector = struct {\n const cimgui = @import("dcimgui");')
|
|
# Find the opening brace
|
|
brace_start = text.index('{', struct_start_idx)
|
|
# Count braces to find the matching close
|
|
depth = 0
|
|
i = brace_start
|
|
while i < len(text):
|
|
if text[i] == '{':
|
|
depth += 1
|
|
elif text[i] == '}':
|
|
depth -= 1
|
|
if depth == 0:
|
|
break
|
|
i += 1
|
|
# i now points to the closing }
|
|
# The struct ends with `};` — we want to capture up to and including `}`
|
|
# but NOT the `;` since the semicolon will come after the else branch
|
|
struct_end_brace = i + 1 # position after closing }
|
|
# Check for trailing semicolon to know where to resume after replacement
|
|
struct_end = struct_end_brace
|
|
if struct_end < len(text) and text[struct_end] == ';':
|
|
struct_end += 1 # skip the original ; in the replacement range
|
|
|
|
# Extract the original struct content (up to } but not ;)
|
|
original_struct = text[struct_start_idx:struct_end_brace]
|
|
|
|
# Wrap: replace opening with comptime conditional
|
|
new_struct = original_struct.replace(
|
|
'pub const Inspector = struct {',
|
|
'pub const Inspector = if (@import("../build_config.zig").inspector) struct {',
|
|
1,
|
|
)
|
|
|
|
# Add else branch after closing };
|
|
stub = """ else struct {
|
|
surface: *Surface = undefined,
|
|
pub fn init(_: *Surface) !@This() { return .{}; }
|
|
pub fn deinit(_: *@This()) void {}
|
|
};"""
|
|
new_struct = new_struct + stub
|
|
|
|
text = text[:struct_start_idx] + new_struct + text[struct_end:]
|
|
|
|
# 6c. Gate initInspector body
|
|
old_init = """ pub fn initInspector(self: *Surface) !*Inspector {
|
|
if (self.inspector) |v| return v;
|
|
|
|
const alloc = self.app.core_app.alloc;
|
|
const inspector = try alloc.create(Inspector);
|
|
errdefer alloc.destroy(inspector);
|
|
inspector.* = try .init(self);
|
|
self.inspector = inspector;
|
|
return inspector;
|
|
}"""
|
|
new_init = """ pub fn initInspector(self: *Surface) !*Inspector {
|
|
if (comptime !@import("../build_config.zig").inspector) return error.InspectorUnavailable;
|
|
if (self.inspector) |v| return v;
|
|
|
|
const alloc = self.app.core_app.alloc;
|
|
const inspector = try alloc.create(Inspector);
|
|
errdefer alloc.destroy(inspector);
|
|
inspector.* = try .init(self);
|
|
self.inspector = inspector;
|
|
return inspector;
|
|
}"""
|
|
assert old_init in text, "initInspector pattern not found"
|
|
text = text.replace(old_init, new_init, 1)
|
|
|
|
# 6d. Gate freeInspector body
|
|
old_free = """ pub fn freeInspector(self: *Surface) void {
|
|
if (self.inspector) |v| {
|
|
v.deinit();
|
|
self.app.core_app.alloc.destroy(v);
|
|
self.inspector = null;
|
|
}
|
|
}"""
|
|
new_free = """ pub fn freeInspector(self: *Surface) void {
|
|
if (comptime !@import("../build_config.zig").inspector) return;
|
|
if (self.inspector) |v| {
|
|
v.deinit();
|
|
self.app.core_app.alloc.destroy(v);
|
|
self.inspector = null;
|
|
}
|
|
}"""
|
|
assert old_free in text, "freeInspector pattern not found"
|
|
text = text.replace(old_free, new_free, 1)
|
|
|
|
# 6e. Gate CAPI inspector functions that call methods on Inspector
|
|
# These functions take *Inspector and call methods that don't exist on the stub struct.
|
|
# We add comptime early-returns so the method calls are never analyzed.
|
|
capi_guards = [
|
|
(' export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {\n'
|
|
' ptr.updateSize(w, h);',
|
|
' export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {\n'
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' ptr.updateSize(w, h);'),
|
|
(' export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {\n'
|
|
' ptr.updateContentScale(x, y);',
|
|
' export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {\n'
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' ptr.updateContentScale(x, y);'),
|
|
(' ptr.mouseButtonCallback(\n'
|
|
' action,\n'
|
|
' button,',
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' ptr.mouseButtonCallback(\n'
|
|
' action,\n'
|
|
' button,'),
|
|
(' export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void {\n'
|
|
' ptr.cursorPosCallback(x, y);',
|
|
' export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void {\n'
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' ptr.cursorPosCallback(x, y);'),
|
|
(' ptr.scrollCallback(\n'
|
|
' x,\n'
|
|
' y,',
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' ptr.scrollCallback(\n'
|
|
' x,\n'
|
|
' y,'),
|
|
(' ptr.keyCallback(\n'
|
|
' action,\n'
|
|
' key,',
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' ptr.keyCallback(\n'
|
|
' action,\n'
|
|
' key,'),
|
|
(' export fn ghostty_inspector_text(\n'
|
|
' ptr: *Inspector,\n'
|
|
' str: [*:0]const u8,\n'
|
|
' ) void {\n'
|
|
' ptr.textCallback(std.mem.sliceTo(str, 0));',
|
|
' export fn ghostty_inspector_text(\n'
|
|
' ptr: *Inspector,\n'
|
|
' str: [*:0]const u8,\n'
|
|
' ) void {\n'
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' ptr.textCallback(std.mem.sliceTo(str, 0));'),
|
|
(' export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void {\n'
|
|
' ptr.focusCallback(focused);',
|
|
' export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void {\n'
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' ptr.focusCallback(focused);'),
|
|
(' export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {\n'
|
|
' return ptr.initMetal(.fromId(device));',
|
|
' export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {\n'
|
|
' if (comptime !@import("../build_config.zig").inspector) return false;\n'
|
|
' return ptr.initMetal(.fromId(device));'),
|
|
(' return ptr.renderMetal(\n'
|
|
' .fromId(command_buffer),',
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' return ptr.renderMetal(\n'
|
|
' .fromId(command_buffer),'),
|
|
(' export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {\n'
|
|
' if (ptr.backend) |v| {',
|
|
' export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {\n'
|
|
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
|
' if (ptr.backend) |v| {'),
|
|
]
|
|
for old_capi, new_capi in capi_guards:
|
|
assert old_capi in text, f"CAPI pattern not found: {old_capi[:60]}..."
|
|
text = text.replace(old_capi, new_capi, 1)
|
|
|
|
embedded_path.write_text(text)
|
|
print("[+] patched apprt/embedded.zig")
|
|
|
|
print(f"[+] all inspector patches complete ({marker})")
|
|
PYEOF
|
|
|
|
echo "[+] inspector disable patch applied"
|