vibetunnel/ios/VibeTunnel/Views/Terminal/TerminalBufferPreview.swift
Peter Steinberger baaaa5a033 fix: CI and linting issues across all platforms
- Fix code signing in Mac and iOS test workflows
- Fix all SwiftFormat and SwiftLint issues
- Fix ESLint issues in web code
- Remove force casts and unwrapping in Swift code
- Update build scripts to use correct file paths
2025-06-23 19:40:53 +02:00

188 lines
7.1 KiB
Swift

import SwiftUI
/// A lightweight terminal preview component that renders buffer snapshots.
///
/// This view efficiently renders terminal content from BufferSnapshot data,
/// optimized for small preview sizes in session cards.
struct TerminalBufferPreview: View {
let snapshot: BufferSnapshot
let fontSize: CGFloat
init(snapshot: BufferSnapshot, fontSize: CGFloat = 10) {
self.snapshot = snapshot
self.fontSize = fontSize
}
var body: some View {
GeometryReader { _ in
ScrollViewReader { scrollProxy in
ScrollView([.horizontal, .vertical], showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<snapshot.rows, id: \.self) { row in
HStack(spacing: 0) {
ForEach(0..<min(snapshot.cols, 80), id: \.self) { col in
// Get cell at position, with bounds checking
if row < snapshot.cells.count && col < snapshot.cells[row].count {
let cell = snapshot.cells[row][col]
cellView(for: cell)
} else {
// Empty cell
Text(" ")
.font(Theme.Typography.terminalSystem(size: fontSize))
.frame(width: fontSize * 0.6)
}
}
Spacer(minLength: 0)
}
}
}
.padding(4)
.id("content")
}
.onAppear {
// Scroll to show cursor area if visible
if snapshot.cursorY >= 0 && snapshot.cursorY < snapshot.rows {
withAnimation(.none) {
scrollProxy.scrollTo("content", anchor: .bottom)
}
}
}
}
}
.background(Theme.Colors.terminalBackground)
.cornerRadius(Theme.CornerRadius.small)
}
@ViewBuilder
private func cellView(for cell: BufferCell) -> some View {
Text(cell.char.isEmpty ? " " : cell.char)
.font(Theme.Typography.terminalSystem(size: fontSize))
.foregroundColor(foregroundColor(for: cell))
.background(backgroundColor(for: cell))
.frame(width: fontSize * 0.6 * CGFloat(max(1, cell.width)))
}
private func foregroundColor(for cell: BufferCell) -> Color {
guard let fg = cell.fg else {
return Theme.Colors.terminalForeground
}
// Check if RGB color (has alpha channel flag)
if (fg & 0xFF00_0000) != 0 {
// RGB color
let red = Double((fg >> 16) & 0xFF) / 255.0
let green = Double((fg >> 8) & 0xFF) / 255.0
let blue = Double(fg & 0xFF) / 255.0
return Color(red: red, green: green, blue: blue)
} else {
// Palette color
return paletteColor(fg)
}
}
private func backgroundColor(for cell: BufferCell) -> Color {
guard let bg = cell.bg else {
return .clear
}
// Check if RGB color (has alpha channel flag)
if (bg & 0xFF00_0000) != 0 {
// RGB color
let red = Double((bg >> 16) & 0xFF) / 255.0
let green = Double((bg >> 8) & 0xFF) / 255.0
let blue = Double(bg & 0xFF) / 255.0
return Color(red: red, green: green, blue: blue)
} else {
// Palette color
return paletteColor(bg)
}
}
private func paletteColor(_ index: Int) -> Color {
// ANSI 256-color palette
switch index {
case 0: return Color(white: 0.0) // Black
case 1: return Color(red: 0.8, green: 0.0, blue: 0.0) // Red
case 2: return Color(red: 0.0, green: 0.8, blue: 0.0) // Green
case 3: return Color(red: 0.8, green: 0.8, blue: 0.0) // Yellow
case 4: return Color(red: 0.0, green: 0.0, blue: 0.8) // Blue
case 5: return Color(red: 0.8, green: 0.0, blue: 0.8) // Magenta
case 6: return Color(red: 0.0, green: 0.8, blue: 0.8) // Cyan
case 7: return Color(white: 0.8) // White
case 8: return Color(white: 0.4) // Bright Black
case 9: return Color(red: 1.0, green: 0.0, blue: 0.0) // Bright Red
case 10: return Color(red: 0.0, green: 1.0, blue: 0.0) // Bright Green
case 11: return Color(red: 1.0, green: 1.0, blue: 0.0) // Bright Yellow
case 12: return Color(red: 0.0, green: 0.0, blue: 1.0) // Bright Blue
case 13: return Color(red: 1.0, green: 0.0, blue: 1.0) // Bright Magenta
case 14: return Color(red: 0.0, green: 1.0, blue: 1.0) // Bright Cyan
case 15: return Color(white: 1.0) // Bright White
default:
// For extended colors, use a simplified mapping
if index < 256 {
let gray = Double(index - 232) / 23.0
return Color(white: gray)
}
return Theme.Colors.terminalForeground
}
}
}
/// A simplified terminal preview that shows only the last visible lines.
/// More efficient for small previews in session cards.
struct CompactTerminalPreview: View {
let snapshot: BufferSnapshot
let maxLines: Int
init(snapshot: BufferSnapshot, maxLines: Int = 6) {
self.snapshot = snapshot
self.maxLines = maxLines
}
var body: some View {
VStack(alignment: .leading, spacing: 1) {
// Get the last non-empty lines
let visibleLines = getVisibleLines()
ForEach(Array(visibleLines.enumerated()), id: \.offset) { _, line in
Text(line)
.font(Theme.Typography.terminalSystem(size: 10))
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.8))
.lineLimit(1)
.truncationMode(.tail)
}
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
}
private func getVisibleLines() -> [String] {
var lines: [String] = []
// Start from the bottom and work up to find non-empty lines
for row in (0..<snapshot.rows).reversed() {
guard row < snapshot.cells.count else { continue }
let line = snapshot.cells[row]
.map { $0.char.isEmpty ? " " : $0.char }
.joined()
.trimmingCharacters(in: .whitespaces)
if !line.isEmpty {
lines.insert(line, at: 0)
if lines.count >= maxLines {
break
}
}
}
// If we have fewer lines than maxLines, add empty lines at the top
while lines.count < min(maxLines, snapshot.rows) && lines.count < maxLines {
lines.insert("", at: 0)
}
return lines
}
}