- 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>
177 lines
5.7 KiB
Swift
177 lines
5.7 KiB
Swift
//
|
|
// AppTerminalView.swift
|
|
// libghostty-spm
|
|
//
|
|
// Created by Lakr233 on 2026/3/16.
|
|
//
|
|
|
|
#if canImport(AppKit) && !canImport(UIKit)
|
|
import AppKit
|
|
import GhosttyKit
|
|
|
|
@MainActor
|
|
open class AppTerminalView: NSView {
|
|
let core = TerminalSurfaceCoordinator()
|
|
var metalLayer: CAMetalLayer?
|
|
var inputHandler: TerminalKeyEventHandler?
|
|
var lastPerformKeyEvent: TimeInterval?
|
|
var pointerSelectionStartPoint: CGPoint?
|
|
var lastPointerSelectionRect: CGRect?
|
|
var pendingSelectionMenuPoint: CGPoint?
|
|
var onFocusChange: ((Bool) -> Void)?
|
|
|
|
open weak var delegate: (any TerminalSurfaceViewDelegate)? {
|
|
get { core.delegate }
|
|
set { core.delegate = newValue }
|
|
}
|
|
|
|
open var controller: TerminalController? {
|
|
get { core.controller }
|
|
set { core.controller = newValue }
|
|
}
|
|
|
|
open var configuration: TerminalSurfaceOptions {
|
|
get { core.configuration }
|
|
set { core.configuration = newValue }
|
|
}
|
|
|
|
open func setSurfaceVisible(_ visible: Bool) {
|
|
core.setDisplayVisible(visible)
|
|
}
|
|
|
|
var surface: TerminalSurface? {
|
|
core.surface
|
|
}
|
|
|
|
override public init(frame: NSRect) {
|
|
super.init(frame: frame)
|
|
commonInit()
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
public required init?(coder _: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func commonInit() {
|
|
wantsLayer = true
|
|
|
|
let metal = CAMetalLayer()
|
|
metal.device = MTLCreateSystemDefaultDevice()
|
|
metal.pixelFormat = .bgra8Unorm
|
|
metal.framebufferOnly = true
|
|
metal.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0
|
|
metal.isOpaque = false
|
|
metal.backgroundColor = NSColor.clear.cgColor
|
|
layer = metal
|
|
metalLayer = metal
|
|
layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
|
inputHandler = TerminalKeyEventHandler(view: self)
|
|
setupTrackingArea()
|
|
|
|
core.isAttached = { [weak self] in self?.window != nil }
|
|
core.scaleFactor = { [weak self] in
|
|
Double(
|
|
self?.window?.backingScaleFactor
|
|
?? NSScreen.main?.backingScaleFactor ?? 2.0
|
|
)
|
|
}
|
|
core.viewSize = { [weak self] in
|
|
guard let self else { return (0, 0) }
|
|
return (bounds.width, bounds.height)
|
|
}
|
|
core.platformSetup = { [weak self] config in
|
|
guard let self else { return }
|
|
config.platform_tag = GHOSTTY_PLATFORM_MACOS
|
|
config.platform = ghostty_platform_u(
|
|
macos: ghostty_platform_macos_s(
|
|
nsview: Unmanaged.passUnretained(self).toOpaque()
|
|
)
|
|
)
|
|
}
|
|
core.onMetricsUpdate = { [weak self] in
|
|
self?.updateMetalLayerMetrics()
|
|
}
|
|
core.onPostRender = { [weak self] in
|
|
self?.enforceMetalLayerScale()
|
|
}
|
|
}
|
|
|
|
open func selectionMenuPoint(at point: CGPoint) -> CGPoint? {
|
|
guard surface?.hasSelection() == true else {
|
|
TerminalDebugLog.log(
|
|
.input,
|
|
"selection menu miss point=\(selectionPointDescription(point))"
|
|
)
|
|
return nil
|
|
}
|
|
|
|
if let rect = lastPointerSelectionRect {
|
|
guard rect.insetBy(dx: -4, dy: -4).contains(point) else {
|
|
TerminalDebugLog.log(
|
|
.input,
|
|
"selection menu miss point=\(selectionPointDescription(point)) outside pointer selection"
|
|
)
|
|
return nil
|
|
}
|
|
|
|
TerminalDebugLog.log(
|
|
.input,
|
|
"selection menu hit point=\(selectionPointDescription(point)) inside pointer selection"
|
|
)
|
|
return point
|
|
}
|
|
|
|
guard surface?.selectionContainsQuicklookWord() == true else {
|
|
TerminalDebugLog.log(
|
|
.input,
|
|
"selection menu miss point=\(selectionPointDescription(point)) outside quicklook word"
|
|
)
|
|
return nil
|
|
}
|
|
|
|
TerminalDebugLog.log(
|
|
.input,
|
|
"selection menu hit point=\(selectionPointDescription(point))"
|
|
)
|
|
return point
|
|
}
|
|
|
|
open func selectionContextMenu() -> NSMenu {
|
|
let menu = NSMenu()
|
|
let copyItem = NSMenuItem(
|
|
title: "Copy",
|
|
action: #selector(copy(_:)),
|
|
keyEquivalent: ""
|
|
)
|
|
copyItem.target = self
|
|
menu.addItem(copyItem)
|
|
return menu
|
|
}
|
|
|
|
@discardableResult
|
|
open func copySelectedTextToPasteboard() -> Bool {
|
|
guard surface?.hasSelection() == true else {
|
|
return false
|
|
}
|
|
guard surface?.performBindingAction("copy_to_clipboard") == true else {
|
|
return false
|
|
}
|
|
TerminalDebugLog.log(
|
|
.input,
|
|
"selection copied to clipboard"
|
|
)
|
|
return true
|
|
}
|
|
|
|
private func selectionPointDescription(_ point: CGPoint) -> String {
|
|
"\(String(format: "%.2f", point.x))x\(String(format: "%.2f", point.y))"
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
}
|
|
#endif
|