iOS tweaks

This commit is contained in:
Peter Steinberger 2025-06-23 05:51:09 +02:00
parent e29c25d623
commit 74d7e9fda5
6 changed files with 169 additions and 63 deletions

View file

@ -29,6 +29,19 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9B67BEFAD916B893D5F56E87"
BuildableName = "VibeTunnelTests.xctest"
BlueprintName = "VibeTunnelTests"
ReferencedContainer = "container:VibeTunnel-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"

View file

@ -8,51 +8,57 @@ import UIKit
enum Theme {
// MARK: - Colors
/// Color palette for the app.
/// Color palette for the app with automatic light/dark mode support.
enum Colors {
// Terminal-inspired colors
static let terminalBackground = Color(hex: "0A0E14")
static let terminalForeground = Color(hex: "B3B1AD")
static let terminalSelection = Color(hex: "273747")
// Accent colors
// Background colors
static let terminalBackground = Color(light: Color(hex: "FFFFFF"), dark: Color(hex: "0A0E14"))
static let cardBackground = Color(light: Color(hex: "F8F9FA"), dark: Color(hex: "0D1117"))
static let headerBackground = Color(light: Color(hex: "FFFFFF"), dark: Color(hex: "010409"))
// Border colors
static let cardBorder = Color(light: Color(hex: "E1E4E8"), dark: Color(hex: "1C2128"))
// Text colors
static let terminalForeground = Color(light: Color(hex: "24292E"), dark: Color(hex: "B3B1AD"))
// Accent colors (same for both modes)
static let primaryAccent = Color(hex: "00FF88") // Green accent matching web
static let secondaryAccent = Color(hex: "59C2FF")
static let successAccent = Color(hex: "AAD94C")
static let warningAccent = Color(hex: "FFB454")
static let errorAccent = Color(hex: "F07178")
// UI colors
static let cardBackground = Color(hex: "0D1117")
static let cardBorder = Color(hex: "1C2128")
static let headerBackground = Color(hex: "010409")
static let overlayBackground = Color.black.opacity(0.7)
// Selection colors
static let terminalSelection = Color(light: Color(hex: "E1E4E8"), dark: Color(hex: "273747"))
// Overlay colors
static let overlayBackground = Color(light: Color.black.opacity(0.5), dark: Color.black.opacity(0.7))
// Additional UI colors for FileBrowser
static let terminalAccent = primaryAccent
static let terminalGray = Color(hex: "8B949E")
static let terminalDarkGray = Color(hex: "161B22")
static let terminalWhite = Color.white
static let terminalGray = Color(light: Color(hex: "586069"), dark: Color(hex: "8B949E"))
static let terminalDarkGray = Color(light: Color(hex: "F6F8FA"), dark: Color(hex: "161B22"))
static let terminalWhite = Color(light: Color(hex: "000000"), dark: Color.white)
// Terminal ANSI colors
static let ansiBlack = Color(hex: "01060E")
static let ansiRed = Color(hex: "EA6C73")
static let ansiGreen = Color(hex: "91B362")
static let ansiYellow = Color(hex: "F9AF4F")
static let ansiBlue = Color(hex: "53BDFA")
static let ansiMagenta = Color(hex: "FAE994")
static let ansiCyan = Color(hex: "90E1C6")
static let ansiWhite = Color(hex: "C7C7C7")
// Terminal ANSI colors - using slightly adjusted colors for light mode
static let ansiBlack = Color(light: Color(hex: "24292E"), dark: Color(hex: "01060E"))
static let ansiRed = Color(light: Color(hex: "D73A49"), dark: Color(hex: "EA6C73"))
static let ansiGreen = Color(light: Color(hex: "28A745"), dark: Color(hex: "91B362"))
static let ansiYellow = Color(light: Color(hex: "DBAB09"), dark: Color(hex: "F9AF4F"))
static let ansiBlue = Color(light: Color(hex: "0366D6"), dark: Color(hex: "53BDFA"))
static let ansiMagenta = Color(light: Color(hex: "6F42C1"), dark: Color(hex: "FAE994"))
static let ansiCyan = Color(light: Color(hex: "0598BC"), dark: Color(hex: "90E1C6"))
static let ansiWhite = Color(light: Color(hex: "586069"), dark: Color(hex: "C7C7C7"))
// Bright ANSI colors
static let ansiBrightBlack = Color(hex: "686868")
static let ansiBrightRed = Color(hex: "F07178")
static let ansiBrightGreen = Color(hex: "C2D94C")
static let ansiBrightYellow = Color(hex: "FFB454")
static let ansiBrightBlue = Color(hex: "59C2FF")
static let ansiBrightMagenta = Color(hex: "FFEE99")
static let ansiBrightCyan = Color(hex: "95E6CB")
static let ansiBrightWhite = Color(hex: "FFFFFF")
static let ansiBrightBlack = Color(light: Color(hex: "959DA5"), dark: Color(hex: "686868"))
static let ansiBrightRed = Color(light: Color(hex: "CB2431"), dark: Color(hex: "F07178"))
static let ansiBrightGreen = Color(light: Color(hex: "22863A"), dark: Color(hex: "C2D94C"))
static let ansiBrightYellow = Color(light: Color(hex: "B08800"), dark: Color(hex: "FFB454"))
static let ansiBrightBlue = Color(light: Color(hex: "005CC5"), dark: Color(hex: "59C2FF"))
static let ansiBrightMagenta = Color(light: Color(hex: "5A32A3"), dark: Color(hex: "FFEE99"))
static let ansiBrightCyan = Color(light: Color(hex: "0598BC"), dark: Color(hex: "95E6CB"))
static let ansiBrightWhite = Color(light: Color(hex: "24292E"), dark: Color(hex: "FFFFFF"))
}
// MARK: - Typography
@ -107,21 +113,21 @@ enum Theme {
// MARK: - Shadows
enum CardShadow {
static let color = Color.black.opacity(0.3)
static let color = Color(light: Color.black.opacity(0.1), dark: Color.black.opacity(0.3))
static let radius: CGFloat = 8
static let xOffset: CGFloat = 0
static let yOffset: CGFloat = 2
}
enum ButtonShadow {
static let color = Color.black.opacity(0.2)
static let color = Color(light: Color.black.opacity(0.08), dark: Color.black.opacity(0.2))
static let radius: CGFloat = 4
static let xOffset: CGFloat = 0
static let yOffset: CGFloat = 1
}
}
// MARK: - Color Extension
// MARK: - Color Extensions
extension Color {
init(hex: String) {
@ -148,6 +154,18 @@ extension Color {
opacity: Double(alpha) / 255
)
}
/// Creates a color that automatically adapts to light/dark mode
init(light: Color, dark: Color) {
self.init(UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark:
return UIColor(dark)
default:
return UIColor(light)
}
})
}
}
// MARK: - View Modifiers
@ -282,4 +300,4 @@ extension View {
}
}
}
}
}

View file

@ -148,7 +148,7 @@ struct SessionListView: View {
}
}
}
.sheet(item: $selectedSession) { session in
.fullScreenCover(item: $selectedSession) { session in
TerminalView(session: session)
}
.sheet(isPresented: $showingFileBrowser) {
@ -187,7 +187,6 @@ struct SessionListView: View {
viewModel.stopAutoRefresh()
}
}
.preferredColorScheme(.dark)
.onChange(of: navigationManager.shouldNavigateToSession) { _, shouldNavigate in
if shouldNavigate,
let sessionId = navigationManager.selectedSessionId,

View file

@ -178,6 +178,8 @@ struct TerminalHostingView: UIViewRepresentable {
/// Update terminal buffer from binary buffer data using optimized ANSI sequences
func updateBuffer(from snapshot: BufferSnapshot) {
guard let terminal else { return }
print("[Terminal] updateBuffer called with snapshot: \(snapshot.cols)x\(snapshot.rows), cursor: (\(snapshot.cursorX),\(snapshot.cursorY))")
// Update terminal dimensions if needed
let currentCols = terminal.getTerminal().cols
@ -201,11 +203,13 @@ struct TerminalHostingView: UIViewRepresentable {
let ansiData: String
if isFirstUpdate || previousSnapshot == nil || viewportChanged {
// Full redraw needed
ansiData = convertBufferToOptimizedANSI(snapshot)
ansiData = convertBufferToOptimizedANSI(snapshot, clearScreen: isFirstUpdate)
isFirstUpdate = false
print("[Terminal] Full redraw performed")
} else {
// Incremental update
ansiData = generateIncrementalUpdate(from: previousSnapshot!, to: snapshot)
print("[Terminal] Incremental update performed")
}
// Store current snapshot for next update
@ -244,11 +248,16 @@ struct TerminalHostingView: UIViewRepresentable {
}
}
private func convertBufferToOptimizedANSI(_ snapshot: BufferSnapshot) -> String {
private func convertBufferToOptimizedANSI(_ snapshot: BufferSnapshot, clearScreen: Bool = false) -> String {
var output = ""
// Clear screen and reset cursor
output += "\u{001B}[2J\u{001B}[H"
if clearScreen {
// Clear screen and reset cursor for first update
output += "\u{001B}[2J\u{001B}[H"
} else {
// Just reset cursor to home position
output += "\u{001B}[H"
}
// Track current attributes to minimize escape sequences
var currentFg: Int?

View file

@ -2,6 +2,78 @@ import Foundation
import Testing
@testable import VibeTunnel
// Temporarily include MockWebSocketFactory here until it's properly added to the project
@MainActor
class MockWebSocket: WebSocketProtocol {
weak var delegate: WebSocketDelegate?
// State tracking
private(set) var isConnected = false
private(set) var lastConnectURL: URL?
private(set) var lastConnectHeaders: [String: String]?
// Control test behavior
var shouldFailConnection = false
var connectionError: Error?
func connect(to url: URL, with headers: [String: String]) async throws {
lastConnectURL = url
lastConnectHeaders = headers
if shouldFailConnection {
let error = connectionError ?? WebSocketError.connectionFailed
throw error
}
isConnected = true
delegate?.webSocketDidConnect(self)
}
func send(_ message: WebSocketMessage) async throws {
guard isConnected else {
throw WebSocketError.connectionFailed
}
}
func sendPing() async throws {
guard isConnected else {
throw WebSocketError.connectionFailed
}
}
func disconnect(with code: URLSessionWebSocketTask.CloseCode, reason: Data?) {
if isConnected {
isConnected = false
delegate?.webSocketDidDisconnect(self, closeCode: code, reason: reason)
}
}
func simulateMessage(_ message: WebSocketMessage) {
guard isConnected else { return }
delegate?.webSocket(self, didReceiveMessage: message)
}
func simulateError(_ error: Error) {
guard isConnected else { return }
delegate?.webSocket(self, didFailWithError: error)
}
}
@MainActor
class MockWebSocketFactory: WebSocketFactory {
private(set) var createdWebSockets: [MockWebSocket] = []
func createWebSocket() -> WebSocketProtocol {
let webSocket = MockWebSocket()
createdWebSockets.append(webSocket)
return webSocket
}
var lastCreatedWebSocket: MockWebSocket? {
createdWebSockets.last
}
}
@Suite("BufferWebSocketClient Tests", .tags(.critical, .websocket))
@MainActor
struct BufferWebSocketClientTests {
@ -270,8 +342,7 @@ private func saveTestServerConfig() {
let config = ServerConfig(
host: "localhost",
port: 8888,
useSSL: false,
username: nil,
name: nil,
password: nil
)

View file

@ -18,7 +18,7 @@ enum TestFixtures {
static let validSession = Session(
id: "test-session-123",
command: "/bin/bash",
command: ["/bin/bash"],
workingDir: "/Users/test",
name: "Test Session",
status: .running,
@ -26,14 +26,15 @@ enum TestFixtures {
startedAt: "2024-01-01T10:00:00Z",
lastModified: "2024-01-01T10:05:00Z",
pid: 12_345,
waiting: false,
width: 80,
height: 24
source: nil,
remoteId: nil,
remoteName: nil,
remoteUrl: nil
)
static let exitedSession = Session(
id: "exited-session-456",
command: "/usr/bin/echo",
command: ["/usr/bin/echo"],
workingDir: "/tmp",
name: "Exited Session",
status: .exited,
@ -41,38 +42,33 @@ enum TestFixtures {
startedAt: "2024-01-01T09:00:00Z",
lastModified: "2024-01-01T09:00:05Z",
pid: nil,
waiting: false,
width: 80,
height: 24
source: nil,
remoteId: nil,
remoteName: nil,
remoteUrl: nil
)
static let sessionsJSON = """
[
{
"id": "test-session-123",
"command": "/bin/bash",
"command": ["/bin/bash"],
"workingDir": "/Users/test",
"name": "Test Session",
"status": "running",
"startedAt": "2024-01-01T10:00:00Z",
"lastModified": "2024-01-01T10:05:00Z",
"pid": 12345,
"waiting": false,
"width": 80,
"height": 24
"pid": 12345
},
{
"id": "exited-session-456",
"command": "/usr/bin/echo",
"command": ["/usr/bin/echo"],
"workingDir": "/tmp",
"name": "Exited Session",
"status": "exited",
"exitCode": 0,
"startedAt": "2024-01-01T09:00:00Z",
"lastModified": "2024-01-01T09:00:05Z",
"waiting": false,
"width": 80,
"height": 24
"lastModified": "2024-01-01T09:00:05Z"
}
]
"""