mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +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"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
shouldAutocreateTestPlan = "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>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
|
|
||||||
|
|
@ -8,51 +8,57 @@ import UIKit
|
||||||
enum Theme {
|
enum Theme {
|
||||||
// MARK: - Colors
|
// MARK: - Colors
|
||||||
|
|
||||||
/// Color palette for the app.
|
/// Color palette for the app with automatic light/dark mode support.
|
||||||
enum Colors {
|
enum Colors {
|
||||||
// Terminal-inspired colors
|
// Background colors
|
||||||
static let terminalBackground = Color(hex: "0A0E14")
|
static let terminalBackground = Color(light: Color(hex: "FFFFFF"), dark: Color(hex: "0A0E14"))
|
||||||
static let terminalForeground = Color(hex: "B3B1AD")
|
static let cardBackground = Color(light: Color(hex: "F8F9FA"), dark: Color(hex: "0D1117"))
|
||||||
static let terminalSelection = Color(hex: "273747")
|
static let headerBackground = Color(light: Color(hex: "FFFFFF"), dark: Color(hex: "010409"))
|
||||||
|
|
||||||
// Accent colors
|
// 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 primaryAccent = Color(hex: "00FF88") // Green accent matching web
|
||||||
static let secondaryAccent = Color(hex: "59C2FF")
|
static let secondaryAccent = Color(hex: "59C2FF")
|
||||||
static let successAccent = Color(hex: "AAD94C")
|
static let successAccent = Color(hex: "AAD94C")
|
||||||
static let warningAccent = Color(hex: "FFB454")
|
static let warningAccent = Color(hex: "FFB454")
|
||||||
static let errorAccent = Color(hex: "F07178")
|
static let errorAccent = Color(hex: "F07178")
|
||||||
|
|
||||||
// UI colors
|
// Selection colors
|
||||||
static let cardBackground = Color(hex: "0D1117")
|
static let terminalSelection = Color(light: Color(hex: "E1E4E8"), dark: Color(hex: "273747"))
|
||||||
static let cardBorder = Color(hex: "1C2128")
|
|
||||||
static let headerBackground = Color(hex: "010409")
|
// Overlay colors
|
||||||
static let overlayBackground = Color.black.opacity(0.7)
|
static let overlayBackground = Color(light: Color.black.opacity(0.5), dark: Color.black.opacity(0.7))
|
||||||
|
|
||||||
// Additional UI colors for FileBrowser
|
// Additional UI colors for FileBrowser
|
||||||
static let terminalAccent = primaryAccent
|
static let terminalAccent = primaryAccent
|
||||||
static let terminalGray = Color(hex: "8B949E")
|
static let terminalGray = Color(light: Color(hex: "586069"), dark: Color(hex: "8B949E"))
|
||||||
static let terminalDarkGray = Color(hex: "161B22")
|
static let terminalDarkGray = Color(light: Color(hex: "F6F8FA"), dark: Color(hex: "161B22"))
|
||||||
static let terminalWhite = Color.white
|
static let terminalWhite = Color(light: Color(hex: "000000"), dark: Color.white)
|
||||||
|
|
||||||
// Terminal ANSI colors
|
// Terminal ANSI colors - using slightly adjusted colors for light mode
|
||||||
static let ansiBlack = Color(hex: "01060E")
|
static let ansiBlack = Color(light: Color(hex: "24292E"), dark: Color(hex: "01060E"))
|
||||||
static let ansiRed = Color(hex: "EA6C73")
|
static let ansiRed = Color(light: Color(hex: "D73A49"), dark: Color(hex: "EA6C73"))
|
||||||
static let ansiGreen = Color(hex: "91B362")
|
static let ansiGreen = Color(light: Color(hex: "28A745"), dark: Color(hex: "91B362"))
|
||||||
static let ansiYellow = Color(hex: "F9AF4F")
|
static let ansiYellow = Color(light: Color(hex: "DBAB09"), dark: Color(hex: "F9AF4F"))
|
||||||
static let ansiBlue = Color(hex: "53BDFA")
|
static let ansiBlue = Color(light: Color(hex: "0366D6"), dark: Color(hex: "53BDFA"))
|
||||||
static let ansiMagenta = Color(hex: "FAE994")
|
static let ansiMagenta = Color(light: Color(hex: "6F42C1"), dark: Color(hex: "FAE994"))
|
||||||
static let ansiCyan = Color(hex: "90E1C6")
|
static let ansiCyan = Color(light: Color(hex: "0598BC"), dark: Color(hex: "90E1C6"))
|
||||||
static let ansiWhite = Color(hex: "C7C7C7")
|
static let ansiWhite = Color(light: Color(hex: "586069"), dark: Color(hex: "C7C7C7"))
|
||||||
|
|
||||||
// Bright ANSI colors
|
// Bright ANSI colors
|
||||||
static let ansiBrightBlack = Color(hex: "686868")
|
static let ansiBrightBlack = Color(light: Color(hex: "959DA5"), dark: Color(hex: "686868"))
|
||||||
static let ansiBrightRed = Color(hex: "F07178")
|
static let ansiBrightRed = Color(light: Color(hex: "CB2431"), dark: Color(hex: "F07178"))
|
||||||
static let ansiBrightGreen = Color(hex: "C2D94C")
|
static let ansiBrightGreen = Color(light: Color(hex: "22863A"), dark: Color(hex: "C2D94C"))
|
||||||
static let ansiBrightYellow = Color(hex: "FFB454")
|
static let ansiBrightYellow = Color(light: Color(hex: "B08800"), dark: Color(hex: "FFB454"))
|
||||||
static let ansiBrightBlue = Color(hex: "59C2FF")
|
static let ansiBrightBlue = Color(light: Color(hex: "005CC5"), dark: Color(hex: "59C2FF"))
|
||||||
static let ansiBrightMagenta = Color(hex: "FFEE99")
|
static let ansiBrightMagenta = Color(light: Color(hex: "5A32A3"), dark: Color(hex: "FFEE99"))
|
||||||
static let ansiBrightCyan = Color(hex: "95E6CB")
|
static let ansiBrightCyan = Color(light: Color(hex: "0598BC"), dark: Color(hex: "95E6CB"))
|
||||||
static let ansiBrightWhite = Color(hex: "FFFFFF")
|
static let ansiBrightWhite = Color(light: Color(hex: "24292E"), dark: Color(hex: "FFFFFF"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Typography
|
// MARK: - Typography
|
||||||
|
|
@ -107,21 +113,21 @@ enum Theme {
|
||||||
// MARK: - Shadows
|
// MARK: - Shadows
|
||||||
|
|
||||||
enum CardShadow {
|
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 radius: CGFloat = 8
|
||||||
static let xOffset: CGFloat = 0
|
static let xOffset: CGFloat = 0
|
||||||
static let yOffset: CGFloat = 2
|
static let yOffset: CGFloat = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ButtonShadow {
|
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 radius: CGFloat = 4
|
||||||
static let xOffset: CGFloat = 0
|
static let xOffset: CGFloat = 0
|
||||||
static let yOffset: CGFloat = 1
|
static let yOffset: CGFloat = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Color Extension
|
// MARK: - Color Extensions
|
||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
init(hex: String) {
|
init(hex: String) {
|
||||||
|
|
@ -148,6 +154,18 @@ extension Color {
|
||||||
opacity: Double(alpha) / 255
|
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
|
// MARK: - View Modifiers
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ struct SessionListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(item: $selectedSession) { session in
|
.fullScreenCover(item: $selectedSession) { session in
|
||||||
TerminalView(session: session)
|
TerminalView(session: session)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingFileBrowser) {
|
.sheet(isPresented: $showingFileBrowser) {
|
||||||
|
|
@ -187,7 +187,6 @@ struct SessionListView: View {
|
||||||
viewModel.stopAutoRefresh()
|
viewModel.stopAutoRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
.onChange(of: navigationManager.shouldNavigateToSession) { _, shouldNavigate in
|
.onChange(of: navigationManager.shouldNavigateToSession) { _, shouldNavigate in
|
||||||
if shouldNavigate,
|
if shouldNavigate,
|
||||||
let sessionId = navigationManager.selectedSessionId,
|
let sessionId = navigationManager.selectedSessionId,
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,8 @@ struct TerminalHostingView: UIViewRepresentable {
|
||||||
func updateBuffer(from snapshot: BufferSnapshot) {
|
func updateBuffer(from snapshot: BufferSnapshot) {
|
||||||
guard let terminal else { return }
|
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
|
// Update terminal dimensions if needed
|
||||||
let currentCols = terminal.getTerminal().cols
|
let currentCols = terminal.getTerminal().cols
|
||||||
let currentRows = terminal.getTerminal().rows
|
let currentRows = terminal.getTerminal().rows
|
||||||
|
|
@ -201,11 +203,13 @@ struct TerminalHostingView: UIViewRepresentable {
|
||||||
let ansiData: String
|
let ansiData: String
|
||||||
if isFirstUpdate || previousSnapshot == nil || viewportChanged {
|
if isFirstUpdate || previousSnapshot == nil || viewportChanged {
|
||||||
// Full redraw needed
|
// Full redraw needed
|
||||||
ansiData = convertBufferToOptimizedANSI(snapshot)
|
ansiData = convertBufferToOptimizedANSI(snapshot, clearScreen: isFirstUpdate)
|
||||||
isFirstUpdate = false
|
isFirstUpdate = false
|
||||||
|
print("[Terminal] Full redraw performed")
|
||||||
} else {
|
} else {
|
||||||
// Incremental update
|
// Incremental update
|
||||||
ansiData = generateIncrementalUpdate(from: previousSnapshot!, to: snapshot)
|
ansiData = generateIncrementalUpdate(from: previousSnapshot!, to: snapshot)
|
||||||
|
print("[Terminal] Incremental update performed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store current snapshot for next update
|
// 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 = ""
|
var output = ""
|
||||||
|
|
||||||
// Clear screen and reset cursor
|
if clearScreen {
|
||||||
|
// Clear screen and reset cursor for first update
|
||||||
output += "\u{001B}[2J\u{001B}[H"
|
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
|
// Track current attributes to minimize escape sequences
|
||||||
var currentFg: Int?
|
var currentFg: Int?
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,78 @@ import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
@testable import VibeTunnel
|
@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))
|
@Suite("BufferWebSocketClient Tests", .tags(.critical, .websocket))
|
||||||
@MainActor
|
@MainActor
|
||||||
struct BufferWebSocketClientTests {
|
struct BufferWebSocketClientTests {
|
||||||
|
|
@ -270,8 +342,7 @@ private func saveTestServerConfig() {
|
||||||
let config = ServerConfig(
|
let config = ServerConfig(
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
port: 8888,
|
port: 8888,
|
||||||
useSSL: false,
|
name: nil,
|
||||||
username: nil,
|
|
||||||
password: nil
|
password: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ enum TestFixtures {
|
||||||
|
|
||||||
static let validSession = Session(
|
static let validSession = Session(
|
||||||
id: "test-session-123",
|
id: "test-session-123",
|
||||||
command: "/bin/bash",
|
command: ["/bin/bash"],
|
||||||
workingDir: "/Users/test",
|
workingDir: "/Users/test",
|
||||||
name: "Test Session",
|
name: "Test Session",
|
||||||
status: .running,
|
status: .running,
|
||||||
|
|
@ -26,14 +26,15 @@ enum TestFixtures {
|
||||||
startedAt: "2024-01-01T10:00:00Z",
|
startedAt: "2024-01-01T10:00:00Z",
|
||||||
lastModified: "2024-01-01T10:05:00Z",
|
lastModified: "2024-01-01T10:05:00Z",
|
||||||
pid: 12_345,
|
pid: 12_345,
|
||||||
waiting: false,
|
source: nil,
|
||||||
width: 80,
|
remoteId: nil,
|
||||||
height: 24
|
remoteName: nil,
|
||||||
|
remoteUrl: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
static let exitedSession = Session(
|
static let exitedSession = Session(
|
||||||
id: "exited-session-456",
|
id: "exited-session-456",
|
||||||
command: "/usr/bin/echo",
|
command: ["/usr/bin/echo"],
|
||||||
workingDir: "/tmp",
|
workingDir: "/tmp",
|
||||||
name: "Exited Session",
|
name: "Exited Session",
|
||||||
status: .exited,
|
status: .exited,
|
||||||
|
|
@ -41,38 +42,33 @@ enum TestFixtures {
|
||||||
startedAt: "2024-01-01T09:00:00Z",
|
startedAt: "2024-01-01T09:00:00Z",
|
||||||
lastModified: "2024-01-01T09:00:05Z",
|
lastModified: "2024-01-01T09:00:05Z",
|
||||||
pid: nil,
|
pid: nil,
|
||||||
waiting: false,
|
source: nil,
|
||||||
width: 80,
|
remoteId: nil,
|
||||||
height: 24
|
remoteName: nil,
|
||||||
|
remoteUrl: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
static let sessionsJSON = """
|
static let sessionsJSON = """
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "test-session-123",
|
"id": "test-session-123",
|
||||||
"command": "/bin/bash",
|
"command": ["/bin/bash"],
|
||||||
"workingDir": "/Users/test",
|
"workingDir": "/Users/test",
|
||||||
"name": "Test Session",
|
"name": "Test Session",
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"startedAt": "2024-01-01T10:00:00Z",
|
"startedAt": "2024-01-01T10:00:00Z",
|
||||||
"lastModified": "2024-01-01T10:05:00Z",
|
"lastModified": "2024-01-01T10:05:00Z",
|
||||||
"pid": 12345,
|
"pid": 12345
|
||||||
"waiting": false,
|
|
||||||
"width": 80,
|
|
||||||
"height": 24
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "exited-session-456",
|
"id": "exited-session-456",
|
||||||
"command": "/usr/bin/echo",
|
"command": ["/usr/bin/echo"],
|
||||||
"workingDir": "/tmp",
|
"workingDir": "/tmp",
|
||||||
"name": "Exited Session",
|
"name": "Exited Session",
|
||||||
"status": "exited",
|
"status": "exited",
|
||||||
"exitCode": 0,
|
"exitCode": 0,
|
||||||
"startedAt": "2024-01-01T09:00:00Z",
|
"startedAt": "2024-01-01T09:00:00Z",
|
||||||
"lastModified": "2024-01-01T09:00:05Z",
|
"lastModified": "2024-01-01T09:00:05Z"
|
||||||
"waiting": false,
|
|
||||||
"width": 80,
|
|
||||||
"height": 24
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue