terminal work

This commit is contained in:
Peter Steinberger 2025-06-21 10:53:58 +02:00
parent 4c954308d6
commit dfc3d48dfa
3 changed files with 195 additions and 108 deletions

View file

@ -6,6 +6,26 @@ enum TerminalWebSocketEvent {
case output(timestamp: Double, data: String)
case resize(timestamp: Double, dimensions: String)
case exit(code: Int)
case bufferUpdate(snapshot: BufferSnapshot)
}
/// Binary buffer snapshot data
struct BufferSnapshot {
let cols: Int
let rows: Int
let viewportY: Int
let cursorX: Int
let cursorY: Int
let cells: [[BufferCell]]
}
/// Individual cell data
struct BufferCell {
let char: String
let width: Int
let fg: Int?
let bg: Int?
let attributes: Int?
}
/// Errors that can occur during WebSocket operations.
@ -234,30 +254,12 @@ class BufferWebSocketClient: NSObject {
return nil
}
// Convert buffer snapshot to terminal output
let outputData = convertBufferToANSI(bufferSnapshot)
print("[BufferWebSocket] Decoded buffer: \(bufferSnapshot.cols)x\(bufferSnapshot.rows), \(outputData.count) bytes output")
print("[BufferWebSocket] Decoded buffer: \(bufferSnapshot.cols)x\(bufferSnapshot.rows)")
// Return as output event with current timestamp
return .output(timestamp: Date().timeIntervalSince1970, data: outputData)
// Return buffer update event
return .bufferUpdate(snapshot: bufferSnapshot)
}
private struct BufferSnapshot {
let cols: Int
let rows: Int
let viewportY: Int
let cursorX: Int
let cursorY: Int
let cells: [[BufferCell]]
}
private struct BufferCell {
let char: String
let width: Int
let fg: Int?
let bg: Int?
let attributes: Int?
}
private func decodeBinaryBuffer(_ data: Data) -> BufferSnapshot? {
var offset = 0
@ -475,93 +477,6 @@ class BufferWebSocketClient: NSObject {
return (BufferCell(char: char, width: width, fg: fg, bg: bg, attributes: attributes), currentOffset)
}
private func convertBufferToANSI(_ snapshot: BufferSnapshot) -> String {
var output = ""
// Clear screen and move cursor to top
output += "\u{001B}[2J\u{001B}[H"
// Render each row
for (rowIndex, row) in snapshot.cells.enumerated() {
if rowIndex > 0 {
output += "\n"
}
var currentFg: Int?
var currentBg: Int?
var currentAttrs: Int = 0
for cell in row {
// Handle attributes
if let attrs = cell.attributes, attrs != currentAttrs {
// Reset all attributes
output += "\u{001B}[0m"
currentAttrs = attrs
currentFg = nil
currentBg = nil
// Apply new attributes
if (attrs & 0x01) != 0 { output += "\u{001B}[1m" } // Bold
if (attrs & 0x02) != 0 { output += "\u{001B}[3m" } // Italic
if (attrs & 0x04) != 0 { output += "\u{001B}[4m" } // Underline
if (attrs & 0x08) != 0 { output += "\u{001B}[2m" } // Dim
if (attrs & 0x10) != 0 { output += "\u{001B}[7m" } // Inverse
if (attrs & 0x40) != 0 { output += "\u{001B}[9m" } // Strikethrough
}
// Handle foreground color
if cell.fg != currentFg {
currentFg = cell.fg
if let fg = cell.fg {
if fg & 0xFF000000 != 0 {
// RGB color
let r = (fg >> 16) & 0xFF
let g = (fg >> 8) & 0xFF
let b = fg & 0xFF
output += "\u{001B}[38;2;\(r);\(g);\(b)m"
} else if fg <= 255 {
// Palette color
output += "\u{001B}[38;5;\(fg)m"
}
} else {
// Default foreground
output += "\u{001B}[39m"
}
}
// Handle background color
if cell.bg != currentBg {
currentBg = cell.bg
if let bg = cell.bg {
if bg & 0xFF000000 != 0 {
// RGB color
let r = (bg >> 16) & 0xFF
let g = (bg >> 8) & 0xFF
let b = bg & 0xFF
output += "\u{001B}[48;2;\(r);\(g);\(b)m"
} else if bg <= 255 {
// Palette color
output += "\u{001B}[48;5;\(bg)m"
}
} else {
// Default background
output += "\u{001B}[49m"
}
}
// Add the character
output += cell.char
}
}
// Reset attributes at the end
output += "\u{001B}[0m"
// Position cursor
output += "\u{001B}[\(snapshot.cursorY + 1);\(snapshot.cursorX + 1)H"
return output
}
func subscribe(to sessionId: String, handler: @escaping (TerminalWebSocketEvent) -> Void) {
subscriptions[sessionId] = handler

View file

@ -129,6 +129,25 @@ struct TerminalHostingView: UIViewRepresentable {
terminal.font = font
}
// MARK: - Buffer Types
struct BufferSnapshot {
let cols: Int
let rows: Int
let viewportY: Int
let cursorX: Int
let cursorY: Int
let cells: [[BufferCell]]
}
struct BufferCell {
let char: String
let width: Int
let fg: Int?
let bg: Int?
let attributes: Int?
}
@MainActor
class Coordinator: NSObject {
let onInput: (String) -> Void
@ -151,6 +170,133 @@ struct TerminalHostingView: UIViewRepresentable {
viewModel.terminalCoordinator = self
}
}
/// Update terminal buffer from binary buffer data using optimized ANSI sequences
func updateBuffer(from snapshot: BufferSnapshot) {
guard let terminal = terminal else { return }
// Update terminal dimensions if needed
let currentCols = terminal.getTerminal().cols
let currentRows = terminal.getTerminal().rows
if currentCols != snapshot.cols || currentRows != snapshot.rows {
terminal.resize(cols: snapshot.cols, rows: snapshot.rows)
}
// Convert buffer to optimized ANSI sequences
let ansiData = convertBufferToOptimizedANSI(snapshot)
// Feed the ANSI data to the terminal
feedData(ansiData)
}
private func convertBufferToOptimizedANSI(_ snapshot: BufferSnapshot) -> String {
var output = ""
// Clear screen and reset cursor
output += "\u{001B}[2J\u{001B}[H"
// Track current attributes to minimize escape sequences
var currentFg: Int?
var currentBg: Int?
var currentAttrs: Int = 0
// Render each row
for (rowIndex, row) in snapshot.cells.enumerated() {
if rowIndex > 0 {
output += "\r\n"
}
var lastNonSpaceIndex = -1
for (index, cell) in row.enumerated() {
if cell.char != " " || cell.bg != nil {
lastNonSpaceIndex = index
}
}
// Only render up to the last non-space character
for (colIndex, cell) in row.enumerated() {
if colIndex > lastNonSpaceIndex {
break
}
// Handle attributes efficiently
var needsReset = false
if let attrs = cell.attributes, attrs != currentAttrs {
needsReset = true
currentAttrs = attrs
}
// Handle colors efficiently
if cell.fg != currentFg || cell.bg != currentBg || needsReset {
if needsReset {
output += "\u{001B}[0m"
currentFg = nil
currentBg = nil
// Apply attributes
if let attrs = cell.attributes {
if (attrs & 0x01) != 0 { output += "\u{001B}[1m" } // Bold
if (attrs & 0x02) != 0 { output += "\u{001B}[3m" } // Italic
if (attrs & 0x04) != 0 { output += "\u{001B}[4m" } // Underline
if (attrs & 0x08) != 0 { output += "\u{001B}[2m" } // Dim
if (attrs & 0x10) != 0 { output += "\u{001B}[7m" } // Inverse
if (attrs & 0x40) != 0 { output += "\u{001B}[9m" } // Strikethrough
}
}
// Apply foreground color
if cell.fg != currentFg {
currentFg = cell.fg
if let fg = cell.fg {
if fg & 0xFF000000 != 0 {
// RGB color
let r = (fg >> 16) & 0xFF
let g = (fg >> 8) & 0xFF
let b = fg & 0xFF
output += "\u{001B}[38;2;\(r);\(g);\(b)m"
} else if fg <= 255 {
// Palette color
output += "\u{001B}[38;5;\(fg)m"
}
} else {
output += "\u{001B}[39m"
}
}
// Apply background color
if cell.bg != currentBg {
currentBg = cell.bg
if let bg = cell.bg {
if bg & 0xFF000000 != 0 {
// RGB color
let r = (bg >> 16) & 0xFF
let g = (bg >> 8) & 0xFF
let b = bg & 0xFF
output += "\u{001B}[48;2;\(r);\(g);\(b)m"
} else if bg <= 255 {
// Palette color
output += "\u{001B}[48;5;\(bg)m"
}
} else {
output += "\u{001B}[49m"
}
}
}
// Add the character
output += cell.char
}
}
// Reset attributes
output += "\u{001B}[0m"
// Position cursor
output += "\u{001B}[\(snapshot.cursorY + 1);\(snapshot.cursorX + 1)H"
return output
}
func feedData(_ data: String) {
Task { @MainActor in

View file

@ -506,6 +506,32 @@ class TerminalViewModel {
if castRecorder.isRecording {
stopRecording()
}
case .bufferUpdate(let snapshot):
// Update terminal buffer directly
if let coordinator = terminalCoordinator {
coordinator.updateBuffer(from: TerminalHostingView.BufferSnapshot(
cols: snapshot.cols,
rows: snapshot.rows,
viewportY: snapshot.viewportY,
cursorX: snapshot.cursorX,
cursorY: snapshot.cursorY,
cells: snapshot.cells.map { row in
row.map { cell in
TerminalHostingView.BufferCell(
char: cell.char,
width: cell.width,
fg: cell.fg,
bg: cell.bg,
attributes: cell.attributes
)
}
}
))
} else {
// Fallback: buffer updates not available yet
print("Warning: Direct buffer update not available")
}
}
}