Files
terminalX/vendor/libghostty-spm/Patches/ghostty/0007-disable-inspector.sh
kid aa92d0e676 初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- 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>
2026-07-24 10:20:46 +08:00

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"