mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-26 15:07:39 +00:00
iOS tweaks
This commit is contained in:
parent
e29c25d623
commit
74d7e9fda5
6 changed files with 169 additions and 63 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue