Files
byre/mac/Sources/Byre/Terminal.swift
T

118 lines
3.6 KiB
Swift
Raw Normal View History

import AppKit
import SwiftTerm
import SwiftUI
let exitedCode = 4000
let internalErrorCode = 1011
struct TerminalHost: NSViewRepresentable {
2026-10-16 22:46:54 -07:00
let api: API
let taskId: String
@Binding var status: String
func makeCoordinator() -> TerminalSession {
2026-10-16 22:46:54 -07:00
TerminalSession(api: api, taskId: taskId) { s in
status = s
}
}
func makeNSView(context: Context) -> TerminalView {
let t = TerminalView(frame: NSRect(x: 0, y: 0, width: 800, height: 400))
t.font = Fonts.ct(13) as NSFont
t.terminalDelegate = context.coordinator
context.coordinator.view = t
DispatchQueue.main.async {
t.window?.makeFirstResponder(t)
context.coordinator.connect()
}
return t
}
func updateNSView(_ nsView: TerminalView, context: Context) {}
static func dismantleNSView(_ nsView: TerminalView, coordinator: TerminalSession) {
coordinator.close()
}
}
@MainActor
final class TerminalSession: NSObject, TerminalViewDelegate {
2026-10-16 22:46:54 -07:00
let api: API
let taskId: String
let show: (String) -> Void
weak var view: TerminalView?
private var ws: URLSessionWebSocketTask?
2026-10-16 22:46:54 -07:00
init(api: API, taskId: String, show: @escaping (String) -> Void) {
self.api = api
self.taskId = taskId
self.show = show
}
func connect() {
guard let view else {
return
}
let t = view.terminalDimensions
2026-10-16 22:46:54 -07:00
let wss = api.url("/api/tasks/\(taskId)/terminal", [URLQueryItem(name: "cols", value: String(t.cols)), URLQueryItem(name: "rows", value: String(t.rows))], scheme: "wss")
let task = api.session.webSocketTask(with: wss)
ws = task
show("connecting…")
task.resume()
receive(task)
}
func close() {
ws?.cancel(with: .normalClosure, reason: nil)
ws = nil
}
private func receive(_ task: URLSessionWebSocketTask) {
task.receive { result in
Task { @MainActor in
guard self.ws === task else {
return
}
switch result {
case .success(.data(let d)):
self.show("")
self.view?.feed(byteArray: [UInt8](d)[...])
self.receive(task)
case .success(.string(let s)):
self.view?.feed(text: s)
self.receive(task)
case .success:
self.receive(task)
case .failure(let error):
let code = task.closeCode.rawValue
let reason = task.closeReason.map { String(decoding: $0, as: UTF8.self) } ?? ""
if code == exitedCode {
self.show("shell ended: \(reason)")
} else if code == internalErrorCode {
self.show(reason)
} else {
self.show("disconnected (\(code) \(error.localizedDescription))")
}
self.ws = nil
}
}
}
}
func send(source: TerminalView, data: ArraySlice<UInt8>) {
ws?.send(.data(Data(data))) { _ in }
}
func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) {
ws?.send(.string("{\"cols\":\(newCols),\"rows\":\(newRows)}")) { _ in }
}
func setTerminalTitle(source: TerminalView, title: String) {}
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {}
func scrolled(source: TerminalView, position: Double) {}
func rangeChanged(source: TerminalView, startY: Int, endY: Int) {}
}