- 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>
87 lines
3.0 KiB
Swift
87 lines
3.0 KiB
Swift
import UIKit
|
|
|
|
final class TerminalSelectionViewController: UIViewController {
|
|
private let pendingText: String
|
|
private let pendingAnchorRange: NSRange?
|
|
|
|
var onDone: (() -> Void)?
|
|
|
|
private lazy var textView: UITextView = {
|
|
let view = UITextView()
|
|
view.isEditable = false
|
|
view.isSelectable = true
|
|
view.accessibilityIdentifier = "terminal.selectionTextView"
|
|
view.alwaysBounceVertical = true
|
|
view.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
|
|
view.backgroundColor = .clear
|
|
view.textColor = .label
|
|
view.textContainerInset = .init(top: 12, left: 12, bottom: 12, right: 12)
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
return view
|
|
}()
|
|
|
|
init(text: String, anchorRange: NSRange?) {
|
|
pendingText = text
|
|
pendingAnchorRange = anchorRange
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder _: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
view.backgroundColor = .clear
|
|
|
|
textView.text = pendingText
|
|
view.addSubview(textView)
|
|
|
|
// Pin to view edges (not safe area) so scrolled content extends
|
|
// under the transparent navigation bar / home indicator the way
|
|
// iOS sheets expect. UIKit's contentInsetAdjustmentBehavior keeps
|
|
// the initial scroll position below the nav bar via
|
|
// adjustedContentInset.
|
|
NSLayoutConstraint.activate([
|
|
textView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
])
|
|
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
|
barButtonSystemItem: .done,
|
|
target: self,
|
|
action: #selector(handleDone)
|
|
)
|
|
navigationItem.rightBarButtonItem?.accessibilityIdentifier = "terminal.selectionDoneButton"
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
navigationController?.view.backgroundColor = .clear
|
|
navigationController?.navigationBar.standardAppearance.configureWithTransparentBackground()
|
|
navigationController?.navigationBar.scrollEdgeAppearance = navigationController?.navigationBar.standardAppearance
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
textView.becomeFirstResponder()
|
|
|
|
let nsText = textView.text as NSString
|
|
if let range = pendingAnchorRange, NSMaxRange(range) <= nsText.length {
|
|
textView.selectedRange = range
|
|
textView.scrollRangeToVisible(range)
|
|
} else {
|
|
textView.selectAll(nil)
|
|
}
|
|
}
|
|
|
|
@objc private func handleDone() {
|
|
dismiss(animated: true) { [weak self] in
|
|
self?.onDone?()
|
|
}
|
|
}
|
|
}
|