import SwiftUI import GhosttyTerminal struct ContentView: View { @StateObject private var model = SSHTerminalModel() @Environment(\.scenePhase) private var scenePhase var body: some View { // M1 纯自检分支:仅 -txTsnetKey(无 -txHost)时跑 tsnet 加入+对端枚举自检。 // 若同时有 -txHost,则走正常终端流程(model 经 tsnet fd 桥连接)。 if let tsnetKey = UserDefaults.standard.string(forKey: "txTsnetKey"), !tsnetKey.isEmpty, (UserDefaults.standard.string(forKey: "txHost") ?? "").isEmpty { TsnetProbeView(authKey: tsnetKey) } else { Group { if model.showsTerminal { TerminalScreen(model: model) } else { ConnectionForm(model: model) } } .onAppear { model.autoConnectIfConfigured() } .onChange(of: scenePhase) { _, phase in switch phase { case .background: model.enterBackground() case .active: model.enterForeground() default: break } } } } } /// M0 连接表单:SSH 直连(不过 tsnet)。真实运维即输入自己的服务器。 struct ConnectionForm: View { @ObservedObject var model: SSHTerminalModel @State private var host = "" @State private var port = "22" @State private var username = "" @State private var password = "" var body: some View { NavigationStack { Form { Section("SSH 直连 (M0)") { TextField("主机 / IP", text: $host) .autocorrectionDisabled() .textInputAutocapitalization(.never) TextField("端口", text: $port) .keyboardType(.numberPad) TextField("用户名", text: $username) .autocorrectionDisabled() .textInputAutocapitalization(.never) SecureField("密码", text: $password) } Section { Button { model.connect(host: host, port: Int(port) ?? 22, username: username, password: password) } label: { Text("连接").frame(maxWidth: .infinity) } .disabled(host.isEmpty || username.isEmpty) } Section("状态") { Text(model.phaseText) .foregroundStyle(model.phaseText.hasPrefix("失败") ? .red : .secondary) if !model.banner.isEmpty { Text(model.banner).font(.caption).foregroundStyle(.secondary) } } } .navigationTitle("terminalX") } } } struct TerminalScreen: View { @ObservedObject var model: SSHTerminalModel var body: some View { if let tmux = model.tmuxController { TmuxTabbedView(controller: tmux) // tmux -CC → 原生 tab } else { RawTerminalView(model: model) // 普通单终端 } } } /// 普通(非 tmux)单终端视图。 struct RawTerminalView: View { @ObservedObject var model: SSHTerminalModel @FocusState private var focused: Bool var body: some View { TerminalSurfaceView(context: model.state) .terminalFocusOnAppear($focused) .overlay(alignment: .top) { if model.phaseText != "已连接", !model.banner.isEmpty { Text(model.banner) .font(.caption) .padding(.horizontal, 10).padding(.vertical, 6) .background(.thinMaterial, in: Capsule()) .padding(.top, 6) } } .task { for _ in 0 ..< 300 where model.state.surfaceSize == nil { try? await Task.sleep(nanoseconds: 30_000_000) } model.markSurfaceReady() } } } /// tmux control-mode:window 映射为原生 tab 条 + 活动 window 终端。 struct TmuxTabbedView: View { @ObservedObject var controller: TmuxController var body: some View { VStack(spacing: 0) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 6) { ForEach(controller.windows) { win in TmuxTabChip(window: win, isActive: win.id == controller.activeWindowID) { controller.selectWindow(win.id) } } Button { controller.newWindow() } label: { Image(systemName: "plus").padding(.horizontal, 6) } } .padding(.horizontal, 8).padding(.vertical, 6) } .background(.thinMaterial) Divider() if let win = controller.activeWindow { TmuxWindowView(window: win, controller: controller) .id(win.id.raw) } else { Spacer(); Text("无 tmux 窗口").foregroundStyle(.secondary); Spacer() } } } } struct TmuxTabChip: View { @ObservedObject var window: TmuxWindow let isActive: Bool let onTap: () -> Void var body: some View { Button(action: onTap) { Text(window.title) .font(.caption).lineLimit(1) .padding(.horizontal, 12).padding(.vertical, 6) .background(isActive ? Color.accentColor.opacity(0.25) : Color.gray.opacity(0.12), in: RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.plain) } } struct TmuxWindowView: View { @ObservedObject var window: TmuxWindow let controller: TmuxController @FocusState private var focused: Bool var body: some View { TerminalSurfaceView(context: window.state) .terminalFocusOnAppear($focused) .task { for _ in 0 ..< 300 where window.state.surfaceSize == nil { try? await Task.sleep(nanoseconds: 30_000_000) } controller.markWindowReady(window.id) } } }