初始提交: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>
This commit is contained in:
70
vendor/libghostty-spm/Sources/ShellCraftKit/Definition/SandboxShell.swift
vendored
Normal file
70
vendor/libghostty-spm/Sources/ShellCraftKit/Definition/SandboxShell.swift
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
import GhosttyTerminal
|
||||
|
||||
public let defaultSandboxShell = ShellDefinition(
|
||||
prompt: SandboxShellStyle.prompt,
|
||||
welcomeMessage: SandboxShellStyle.welcomeMessage,
|
||||
fallback: { command in
|
||||
"\(SandboxShellStyle.error)\(command)\(SandboxShellStyle.reset): command not found"
|
||||
}
|
||||
) {
|
||||
ShellCommand("echo", summary: "Echo text back") { context in
|
||||
.output(context.arguments + "\r\n")
|
||||
}
|
||||
ShellCommand("date", summary: "Show current date/time") { _ in
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEE MMM dd HH:mm:ss zzz yyyy"
|
||||
return .output(formatter.string(from: Date()) + "\r\n")
|
||||
}
|
||||
ShellCommand("uname", summary: "Show system information") { _ in
|
||||
.output("Darwin ghostty-sandbox host-managed\r\n")
|
||||
}
|
||||
ShellCommand("whoami", summary: "Show current user") { context in
|
||||
.output(context.username + "\r\n")
|
||||
}
|
||||
ShellCommand("env", summary: "Show environment variables") { context in
|
||||
.output("""
|
||||
TERM=xterm-ghostty\r
|
||||
SHELL=/bin/zsh\r
|
||||
USER=\(context.username)\r
|
||||
HOME=/Users/\(context.username)\r
|
||||
LANG=en_US.UTF-8\r
|
||||
TERM_PROGRAM=GhosttyKit\r\n
|
||||
""")
|
||||
}
|
||||
ShellCommand("size", summary: "Show terminal size") { context in
|
||||
let size = context.terminalSize
|
||||
return .output(
|
||||
"columns: \(size.columns), rows: \(size.rows), " +
|
||||
"pixels: \(size.widthPixels)x\(size.heightPixels)\r\n"
|
||||
)
|
||||
}
|
||||
ShellCommand("clear", summary: "Clear the screen") { _ in
|
||||
.clear
|
||||
}
|
||||
ShellCommand("exit", summary: "Exit the terminal") { _ in
|
||||
.exit
|
||||
}
|
||||
ShellCommand("logout", summary: "Exit the terminal") { _ in
|
||||
.exit
|
||||
}
|
||||
}
|
||||
|
||||
public let sandboxShell = defaultSandboxShell
|
||||
|
||||
private enum SandboxShellStyle {
|
||||
static let reset = "\u{1B}[0m"
|
||||
static let accent = "\u{1B}[38;5;110m"
|
||||
static let highlight = "\u{1B}[38;5;221m"
|
||||
static let error = "\u{1B}[38;5;203m"
|
||||
static let emphasis = "\u{1B}[1m"
|
||||
|
||||
static let prompt = "\(accent)sandbox\(reset)@\(highlight)ghostty\(reset) % "
|
||||
|
||||
static let welcomeMessage = """
|
||||
\r\n \(emphasis)\(accent)GhosttyKit Sandbox Demo\(reset)\r
|
||||
\r\n This terminal runs inside App Sandbox.\r
|
||||
\(accent)No subprocesses are spawned.\(reset)\r
|
||||
Type '\(highlight)help\(reset)' for available commands.\r\n\r\n
|
||||
"""
|
||||
}
|
||||
35
vendor/libghostty-spm/Sources/ShellCraftKit/Definition/ShellCommand.swift
vendored
Normal file
35
vendor/libghostty-spm/Sources/ShellCraftKit/Definition/ShellCommand.swift
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
import GhosttyTerminal
|
||||
|
||||
public enum CommandResult: Sendable {
|
||||
case output(String)
|
||||
case clear
|
||||
case exit
|
||||
}
|
||||
|
||||
public struct CommandContext: Sendable {
|
||||
public let command: String
|
||||
public let arguments: String
|
||||
public let username: String
|
||||
public let terminalSize: InMemoryTerminalViewport
|
||||
}
|
||||
|
||||
public struct ShellCommand: Sendable {
|
||||
public let name: String
|
||||
public let summary: String
|
||||
let handler: @Sendable (CommandContext) -> CommandResult
|
||||
|
||||
public init(
|
||||
_ name: String,
|
||||
summary: String = "",
|
||||
handler: @escaping @Sendable (CommandContext) -> CommandResult
|
||||
) {
|
||||
self.name = name
|
||||
self.summary = summary
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
func execute(_ context: CommandContext) -> CommandResult {
|
||||
handler(context)
|
||||
}
|
||||
}
|
||||
236
vendor/libghostty-spm/Sources/ShellCraftKit/Definition/ShellDefinition.swift
vendored
Normal file
236
vendor/libghostty-spm/Sources/ShellCraftKit/Definition/ShellDefinition.swift
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
import Foundation
|
||||
import GhosttyTerminal
|
||||
|
||||
@resultBuilder
|
||||
public enum ShellCommandBuilder {
|
||||
public static func buildBlock(_ components: [ShellCommand]...) -> [ShellCommand] {
|
||||
components.flatMap(\.self)
|
||||
}
|
||||
|
||||
public static func buildExpression(_ expression: ShellCommand) -> [ShellCommand] {
|
||||
[expression]
|
||||
}
|
||||
|
||||
public static func buildOptional(_ component: [ShellCommand]?) -> [ShellCommand] {
|
||||
component ?? []
|
||||
}
|
||||
|
||||
public static func buildEither(first component: [ShellCommand]) -> [ShellCommand] {
|
||||
component
|
||||
}
|
||||
|
||||
public static func buildEither(second component: [ShellCommand]) -> [ShellCommand] {
|
||||
component
|
||||
}
|
||||
|
||||
public static func buildArray(_ components: [[ShellCommand]]) -> [ShellCommand] {
|
||||
components.flatMap(\.self)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ShellDefinition: Sendable {
|
||||
public let prompt: String
|
||||
public let welcomeMessage: String
|
||||
public let fallbackMessage: @Sendable (String) -> String
|
||||
let promptDisplayWidth: Int
|
||||
private let commands: [String: ShellCommand]
|
||||
private let commandOrder: [String]
|
||||
|
||||
public init(
|
||||
prompt: String = "$ ",
|
||||
welcomeMessage: String? = nil,
|
||||
fallback: (@Sendable (String) -> String)? = nil,
|
||||
@ShellCommandBuilder _ build: () -> [ShellCommand]
|
||||
) {
|
||||
self.prompt = prompt
|
||||
self.welcomeMessage = welcomeMessage ?? Self.defaultWelcomeMessage
|
||||
fallbackMessage = fallback ?? { cmd in "\(cmd): command not found" }
|
||||
promptDisplayWidth = prompt.terminalDisplayWidth
|
||||
|
||||
let userCommands = build()
|
||||
var ordered: [String] = []
|
||||
var map: [String: ShellCommand] = [:]
|
||||
for cmd in userCommands {
|
||||
let key = cmd.name.lowercased()
|
||||
if map[key] == nil {
|
||||
ordered.append(key)
|
||||
}
|
||||
map[key] = cmd
|
||||
}
|
||||
commandOrder = ordered
|
||||
commands = map
|
||||
}
|
||||
|
||||
func processCommand(
|
||||
_ input: String,
|
||||
username: String,
|
||||
terminalSize: InMemoryTerminalViewport
|
||||
) -> CommandResult {
|
||||
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else {
|
||||
return .output("")
|
||||
}
|
||||
|
||||
let parts = trimmed.split(separator: " ", maxSplits: 1)
|
||||
let cmd = String(parts[0]).lowercased()
|
||||
let args = parts.count > 1 ? String(parts[1]) : ""
|
||||
|
||||
if cmd == "help" {
|
||||
return .output(generateHelp())
|
||||
}
|
||||
|
||||
let context = CommandContext(
|
||||
command: cmd,
|
||||
arguments: args,
|
||||
username: username,
|
||||
terminalSize: terminalSize
|
||||
)
|
||||
|
||||
guard let command = commands[cmd] else {
|
||||
return .output(fallbackMessage(trimmed) + "\r\n")
|
||||
}
|
||||
|
||||
return command.execute(context)
|
||||
}
|
||||
|
||||
private func generateHelp() -> String {
|
||||
var lines = ["Available commands:\r"]
|
||||
|
||||
let maxNameLen = max(
|
||||
commandOrder.map(\.count).max() ?? 0,
|
||||
4 // "help"
|
||||
)
|
||||
|
||||
lines.append(" \("help".padding(toLength: maxNameLen + 2, withPad: " ", startingAt: 0))- Show this help message\r")
|
||||
|
||||
for name in commandOrder {
|
||||
guard let cmd = commands[name] else { continue }
|
||||
let summary = cmd.summary.isEmpty ? cmd.name : cmd.summary
|
||||
lines.append(" \(name.padding(toLength: maxNameLen + 2, withPad: " ", startingAt: 0))- \(summary)\r")
|
||||
}
|
||||
|
||||
lines.append("")
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static let defaultWelcomeMessage = """
|
||||
\r\n ShellCraftKit Sandbox Demo\r
|
||||
\r\n This terminal runs inside App Sandbox.\r
|
||||
No subprocesses are spawned.\r
|
||||
Type 'help' for available commands.\r\n\r\n
|
||||
"""
|
||||
}
|
||||
|
||||
extension String {
|
||||
var terminalDisplayWidth: Int {
|
||||
var width = 0
|
||||
var scalars = unicodeScalars.makeIterator()
|
||||
|
||||
while let scalar = scalars.next() {
|
||||
guard scalar == "\u{1B}" else {
|
||||
width += scalar.terminalCellWidth
|
||||
continue
|
||||
}
|
||||
|
||||
guard let next = scalars.next() else {
|
||||
break
|
||||
}
|
||||
|
||||
guard next == "[" else {
|
||||
continue
|
||||
}
|
||||
|
||||
while let parameter = scalars.next() {
|
||||
if (0x40 ... 0x7E).contains(parameter.value) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return width
|
||||
}
|
||||
}
|
||||
|
||||
private extension UnicodeScalar {
|
||||
var terminalCellWidth: Int {
|
||||
// Terminal width needs a tailored policy instead of raw wcwidth():
|
||||
// - UAX #11 says East_Asian_Width is useful, but "not intended for use by
|
||||
// modern terminal emulators without appropriate tailoring on a
|
||||
// case-by-case basis", and ambiguous characters should default to narrow
|
||||
// when context is unknown.
|
||||
// - EastAsianWidth.txt provides the normative W/F defaults and wide
|
||||
// defaults for the ideographic planes we mirror below.
|
||||
// - POSIX / Apple wcwidth() is locale-dependent, which made test and
|
||||
// runtime behavior diverge in this project.
|
||||
//
|
||||
// References:
|
||||
// https://www.unicode.org/reports/tr11/
|
||||
// https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt
|
||||
// https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/wcwidth.3.html
|
||||
if properties.generalCategory == .control ||
|
||||
properties.generalCategory == .format ||
|
||||
properties.generalCategory == .nonspacingMark ||
|
||||
properties.generalCategory == .enclosingMark
|
||||
{
|
||||
return 0
|
||||
}
|
||||
|
||||
switch value {
|
||||
case 0x1100 ... 0x115F,
|
||||
0x231A ... 0x231B,
|
||||
0x2329 ... 0x232A,
|
||||
0x23E9 ... 0x23EC,
|
||||
0x23F0,
|
||||
0x23F3,
|
||||
0x25FD ... 0x25FE,
|
||||
0x2614 ... 0x2615,
|
||||
0x2648 ... 0x2653,
|
||||
0x267F,
|
||||
0x2693,
|
||||
0x26A1,
|
||||
0x26AA ... 0x26AB,
|
||||
0x26BD ... 0x26BE,
|
||||
0x26C4 ... 0x26C5,
|
||||
0x26CE,
|
||||
0x26D4,
|
||||
0x26EA,
|
||||
0x26F2 ... 0x26F3,
|
||||
0x26F5,
|
||||
0x26FA,
|
||||
0x26FD,
|
||||
0x2705,
|
||||
0x270A ... 0x270B,
|
||||
0x2728,
|
||||
0x274C,
|
||||
0x274E,
|
||||
0x2753 ... 0x2755,
|
||||
0x2757,
|
||||
0x2795 ... 0x2797,
|
||||
0x27B0,
|
||||
0x27BF,
|
||||
0x2B1B ... 0x2B1C,
|
||||
0x2B50,
|
||||
0x2B55,
|
||||
0x2E80 ... 0x2FFB,
|
||||
0x3000 ... 0x303E,
|
||||
0x3041 ... 0x33FF,
|
||||
0x3400 ... 0x4DBF,
|
||||
0x4E00 ... 0xA4C6,
|
||||
0xA960 ... 0xA97C,
|
||||
0xAC00 ... 0xD7A3,
|
||||
0xF900 ... 0xFAFF,
|
||||
0xFE10 ... 0xFE19,
|
||||
0xFE30 ... 0xFE6B,
|
||||
0xFF01 ... 0xFF60,
|
||||
0xFFE0 ... 0xFFE6,
|
||||
0x1F300 ... 0x1F64F,
|
||||
0x1F900 ... 0x1F9FF,
|
||||
0x20000 ... 0x2FFFD,
|
||||
0x30000 ... 0x3FFFD:
|
||||
return 2
|
||||
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user