mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
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
This commit is contained in:
parent
d72b009696
commit
baaaa5a033
61 changed files with 685 additions and 609 deletions
3
.github/workflows/mac.yml
vendored
3
.github/workflows/mac.yml
vendored
|
|
@ -206,6 +206,9 @@ jobs:
|
||||||
-scheme VibeTunnel-Mac \
|
-scheme VibeTunnel-Mac \
|
||||||
-configuration Debug \
|
-configuration Debug \
|
||||||
-destination "platform=macOS" \
|
-destination "platform=macOS" \
|
||||||
|
CODE_SIGN_IDENTITY="" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
| xcbeautify || {
|
| xcbeautify || {
|
||||||
echo "::error::Tests failed"
|
echo "::error::Tests failed"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ struct VibeTunnelApp: App {
|
||||||
}
|
}
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
.macCatalystWindowStyle(getStoredWindowStyle())
|
.macCatalystWindowStyle(getStoredWindowStyle())
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +45,8 @@ struct VibeTunnelApp: App {
|
||||||
|
|
||||||
if url.host == "session",
|
if url.host == "session",
|
||||||
let sessionId = url.pathComponents.last,
|
let sessionId = url.pathComponents.last,
|
||||||
!sessionId.isEmpty {
|
!sessionId.isEmpty
|
||||||
|
{
|
||||||
navigationManager.navigateToSession(sessionId)
|
navigationManager.navigateToSession(sessionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +76,8 @@ class ConnectionManager {
|
||||||
|
|
||||||
private func loadSavedConnection() {
|
private func loadSavedConnection() {
|
||||||
if let data = UserDefaults.standard.data(forKey: "savedServerConfig"),
|
if let data = UserDefaults.standard.data(forKey: "savedServerConfig"),
|
||||||
let config = try? JSONDecoder().decode(ServerConfig.self, from: data) {
|
let config = try? JSONDecoder().decode(ServerConfig.self, from: data)
|
||||||
|
{
|
||||||
self.serverConfig = config
|
self.serverConfig = config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ enum AppConfig {
|
||||||
/// Change this to control verbosity of logs
|
/// Change this to control verbosity of logs
|
||||||
static func configureLogging() {
|
static func configureLogging() {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// In debug builds, default to info level to reduce noise
|
// In debug builds, default to info level to reduce noise
|
||||||
// Change to .verbose only when debugging binary protocol issues
|
// Change to .verbose only when debugging binary protocol issues
|
||||||
Logger.globalLevel = .info
|
Logger.globalLevel = .info
|
||||||
#else
|
#else
|
||||||
// In release builds, only show warnings and errors
|
// In release builds, only show warnings and errors
|
||||||
Logger.globalLevel = .warning
|
Logger.globalLevel = .warning
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,8 @@ class CastRecorder {
|
||||||
let eventArray: [Any] = [event.time, event.type, event.data]
|
let eventArray: [Any] = [event.time, event.type, event.data]
|
||||||
|
|
||||||
if let jsonData = try? JSONSerialization.data(withJSONObject: eventArray),
|
if let jsonData = try? JSONSerialization.data(withJSONObject: eventArray),
|
||||||
let jsonString = String(data: jsonData, encoding: .utf8) {
|
let jsonString = String(data: jsonData, encoding: .utf8)
|
||||||
|
{
|
||||||
castContent += jsonString + "\n"
|
castContent += jsonString + "\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,16 @@ struct ServerProfile: Identifiable, Codable, Equatable {
|
||||||
/// Create a ServerConfig from this profile
|
/// Create a ServerConfig from this profile
|
||||||
func toServerConfig(password: String? = nil) -> ServerConfig? {
|
func toServerConfig(password: String? = nil) -> ServerConfig? {
|
||||||
guard let urlComponents = URLComponents(string: url),
|
guard let urlComponents = URLComponents(string: url),
|
||||||
let host = urlComponents.host else {
|
let host = urlComponents.host
|
||||||
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine default port based on scheme
|
// Determine default port based on scheme
|
||||||
let defaultPort: Int
|
let defaultPort: Int = if let scheme = urlComponents.scheme?.lowercased() {
|
||||||
if let scheme = urlComponents.scheme?.lowercased() {
|
scheme == "https" ? 443 : 80
|
||||||
defaultPort = scheme == "https" ? 443 : 80
|
|
||||||
} else {
|
} else {
|
||||||
defaultPort = 80
|
80
|
||||||
}
|
}
|
||||||
|
|
||||||
let port = urlComponents.port ?? defaultPort
|
let port = urlComponents.port ?? defaultPort
|
||||||
|
|
@ -68,7 +68,8 @@ extension ServerProfile {
|
||||||
/// Load all saved profiles from UserDefaults
|
/// Load all saved profiles from UserDefaults
|
||||||
static func loadAll() -> [ServerProfile] {
|
static func loadAll() -> [ServerProfile] {
|
||||||
guard let data = UserDefaults.standard.data(forKey: storageKey),
|
guard let data = UserDefaults.standard.data(forKey: storageKey),
|
||||||
let profiles = try? JSONDecoder().decode([ServerProfile].self, from: data) else {
|
let profiles = try? JSONDecoder().decode([ServerProfile].self, from: data)
|
||||||
|
else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return profiles
|
return profiles
|
||||||
|
|
@ -117,7 +118,8 @@ extension ServerProfile {
|
||||||
|
|
||||||
static func suggestedName(for url: String) -> String {
|
static func suggestedName(for url: String) -> String {
|
||||||
if let urlComponents = URLComponents(string: url),
|
if let urlComponents = URLComponents(string: url),
|
||||||
let host = urlComponents.host {
|
let host = urlComponents.host
|
||||||
|
{
|
||||||
// Remove common suffixes
|
// Remove common suffixes
|
||||||
let cleanHost = host
|
let cleanHost = host
|
||||||
.replacingOccurrences(of: ".local", with: "")
|
.replacingOccurrences(of: ".local", with: "")
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ enum TerminalEvent {
|
||||||
let exitString = array[0] as? String,
|
let exitString = array[0] as? String,
|
||||||
exitString == "exit",
|
exitString == "exit",
|
||||||
let exitCode = array[1] as? Int,
|
let exitCode = array[1] as? Int,
|
||||||
let sessionId = array[2] as? String {
|
let sessionId = array[2] as? String
|
||||||
|
{
|
||||||
self = .exit(code: exitCode, sessionId: sessionId)
|
self = .exit(code: exitCode, sessionId: sessionId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ enum TerminalRenderer: String, CaseIterable, Codable {
|
||||||
static var selected: Self {
|
static var selected: Self {
|
||||||
get {
|
get {
|
||||||
if let rawValue = UserDefaults.standard.string(forKey: "selectedTerminalRenderer"),
|
if let rawValue = UserDefaults.standard.string(forKey: "selectedTerminalRenderer"),
|
||||||
let renderer = Self(rawValue: rawValue) {
|
let renderer = Self(rawValue: rawValue)
|
||||||
|
{
|
||||||
return renderer
|
return renderer
|
||||||
}
|
}
|
||||||
return .swiftTerm // Default
|
return .swiftTerm // Default
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,8 @@ class APIClient: APIClientProtocol {
|
||||||
// This is the header
|
// This is the header
|
||||||
if let version = json["version"] as? Int,
|
if let version = json["version"] as? Int,
|
||||||
let width = json["width"] as? Int,
|
let width = json["width"] as? Int,
|
||||||
let height = json["height"] as? Int {
|
let height = json["height"] as? Int
|
||||||
|
{
|
||||||
header = AsciinemaHeader(
|
header = AsciinemaHeader(
|
||||||
version: version,
|
version: version,
|
||||||
width: width,
|
width: width,
|
||||||
|
|
@ -401,7 +402,8 @@ class APIClient: APIClientProtocol {
|
||||||
if json.count >= 3,
|
if json.count >= 3,
|
||||||
let timestamp = json[0] as? Double,
|
let timestamp = json[0] as? Double,
|
||||||
let typeStr = json[1] as? String,
|
let typeStr = json[1] as? String,
|
||||||
let eventData = json[2] as? String {
|
let eventData = json[2] as? String
|
||||||
|
{
|
||||||
let eventType: AsciinemaEvent.EventType
|
let eventType: AsciinemaEvent.EventType
|
||||||
switch typeStr {
|
switch typeStr {
|
||||||
case "o": eventType = .output
|
case "o": eventType = .output
|
||||||
|
|
@ -479,7 +481,8 @@ class APIClient: APIClientProtocol {
|
||||||
showHidden: Bool = false,
|
showHidden: Bool = false,
|
||||||
gitFilter: String = "all"
|
gitFilter: String = "all"
|
||||||
)
|
)
|
||||||
async throws -> DirectoryListing {
|
async throws -> DirectoryListing
|
||||||
|
{
|
||||||
guard let baseURL else {
|
guard let baseURL else {
|
||||||
throw APIError.noServerConfigured
|
throw APIError.noServerConfigured
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,8 @@ class BufferWebSocketClient: NSObject {
|
||||||
// Add authentication header if needed
|
// Add authentication header if needed
|
||||||
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"),
|
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"),
|
||||||
let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config),
|
let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config),
|
||||||
let authHeader = serverConfig.authorizationHeader {
|
let authHeader = serverConfig.authorizationHeader
|
||||||
|
{
|
||||||
headers["Authorization"] = authHeader
|
headers["Authorization"] = authHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +212,8 @@ class BufferWebSocketClient: NSObject {
|
||||||
|
|
||||||
// Decode terminal event
|
// Decode terminal event
|
||||||
if let event = decodeTerminalEvent(from: messageData),
|
if let event = decodeTerminalEvent(from: messageData),
|
||||||
let handler = subscriptions[sessionId] {
|
let handler = subscriptions[sessionId]
|
||||||
|
{
|
||||||
logger.verbose("Dispatching event to handler")
|
logger.verbose("Dispatching event to handler")
|
||||||
handler(event)
|
handler(event)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -598,14 +600,16 @@ class BufferWebSocketClient: NSObject {
|
||||||
(0xFF00...0xFF60).contains(value) || // Fullwidth Forms
|
(0xFF00...0xFF60).contains(value) || // Fullwidth Forms
|
||||||
(0xFFE0...0xFFE6).contains(value) || // Fullwidth Forms
|
(0xFFE0...0xFFE6).contains(value) || // Fullwidth Forms
|
||||||
(0x20000...0x2FFFD).contains(value) || // CJK Extension B-F
|
(0x20000...0x2FFFD).contains(value) || // CJK Extension B-F
|
||||||
(0x30000...0x3FFFD).contains(value) { // CJK Extension G
|
(0x30000...0x3FFFD).contains(value)
|
||||||
|
{ // CJK Extension G
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zero-width characters
|
// Zero-width characters
|
||||||
if (0x200B...0x200F).contains(value) || // Zero-width spaces
|
if (0x200B...0x200F).contains(value) || // Zero-width spaces
|
||||||
(0xFE00...0xFE0F).contains(value) || // Variation selectors
|
(0xFE00...0xFE0F).contains(value) || // Variation selectors
|
||||||
scalar.properties.isJoinControl {
|
scalar.properties.isJoinControl
|
||||||
|
{
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ enum KeychainService {
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let data = result as? Data,
|
guard let data = result as? Data,
|
||||||
let password = String(data: data, encoding: .utf8) else {
|
let password = String(data: data, encoding: .utf8)
|
||||||
|
else {
|
||||||
throw KeychainError.unexpectedData
|
throw KeychainError.unexpectedData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,16 +75,17 @@ final class LivePreviewManager {
|
||||||
|
|
||||||
// Schedule delayed update if not already scheduled
|
// Schedule delayed update if not already scheduled
|
||||||
if self.updateTimers[sessionId] == nil {
|
if self.updateTimers[sessionId] == nil {
|
||||||
let timer = Timer.scheduledTimer(withTimeInterval: self.updateInterval, repeats: false) { _ in
|
let timer = Timer
|
||||||
Task { @MainActor in
|
.scheduledTimer(withTimeInterval: self.updateInterval, repeats: false) { _ in
|
||||||
if let pending = pendingSnapshot {
|
Task { @MainActor in
|
||||||
subscription.latestSnapshot = pending
|
if let pending = pendingSnapshot {
|
||||||
subscription.lastUpdate = Date()
|
subscription.latestSnapshot = pending
|
||||||
pendingSnapshot = nil
|
subscription.lastUpdate = Date()
|
||||||
|
pendingSnapshot = nil
|
||||||
|
}
|
||||||
|
self.updateTimers.removeValue(forKey: sessionId)
|
||||||
}
|
}
|
||||||
self.updateTimers.removeValue(forKey: sessionId)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.updateTimers[sessionId] = timer
|
self.updateTimers[sessionId] = timer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +174,7 @@ struct LivePreviewModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Environment key for passing subscription down the view hierarchy
|
/// Environment key for passing subscription down the view hierarchy
|
||||||
private struct LivePreviewSubscriptionKey: EnvironmentKey {
|
private struct LivePreviewSubscriptionKey: EnvironmentKey {
|
||||||
static let defaultValue: LivePreviewSubscription? = nil
|
static let defaultValue: LivePreviewSubscription? = nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,8 @@ class QuickLookManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
for file in files {
|
for file in files {
|
||||||
if let creationDate = try? file.resourceValues(forKeys: [.creationDateKey]).creationDate,
|
if let creationDate = try? file.resourceValues(forKeys: [.creationDateKey]).creationDate,
|
||||||
creationDate < oneHourAgo {
|
creationDate < oneHourAgo
|
||||||
|
{
|
||||||
try? FileManager.default.removeItem(at: file)
|
try? FileManager.default.removeItem(at: file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,13 @@ class ReconnectionManager {
|
||||||
|
|
||||||
extension ReconnectionManager {
|
extension ReconnectionManager {
|
||||||
/// Calculate the next retry delay using exponential backoff
|
/// Calculate the next retry delay using exponential backoff
|
||||||
static func calculateBackoff(attempt: Int, baseDelay: TimeInterval = 1.0, maxDelay: TimeInterval = 60.0) -> TimeInterval {
|
static func calculateBackoff(
|
||||||
|
attempt: Int,
|
||||||
|
baseDelay: TimeInterval = 1.0,
|
||||||
|
maxDelay: TimeInterval = 60.0
|
||||||
|
)
|
||||||
|
-> TimeInterval
|
||||||
|
{
|
||||||
let exponentialDelay = baseDelay * pow(2.0, Double(attempt - 1))
|
let exponentialDelay = baseDelay * pow(2.0, Double(attempt - 1))
|
||||||
return min(exponentialDelay, maxDelay)
|
return min(exponentialDelay, maxDelay)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,13 +117,15 @@ final class SSEClient: NSObject, @unchecked Sendable {
|
||||||
// Check for exit event
|
// Check for exit event
|
||||||
if let firstElement = array[0] as? String, firstElement == "exit",
|
if let firstElement = array[0] as? String, firstElement == "exit",
|
||||||
let exitCode = array[1] as? Int,
|
let exitCode = array[1] as? Int,
|
||||||
let sessionId = array[2] as? String {
|
let sessionId = array[2] as? String
|
||||||
|
{
|
||||||
delegate?.sseClient(self, didReceiveEvent: .exit(exitCode: exitCode, sessionId: sessionId))
|
delegate?.sseClient(self, didReceiveEvent: .exit(exitCode: exitCode, sessionId: sessionId))
|
||||||
}
|
}
|
||||||
// Regular terminal output
|
// Regular terminal output
|
||||||
else if let timestamp = array[0] as? Double,
|
else if let timestamp = array[0] as? Double,
|
||||||
let type = array[1] as? String,
|
let type = array[1] as? String,
|
||||||
let outputData = array[2] as? String {
|
let outputData = array[2] as? String
|
||||||
|
{
|
||||||
delegate?.sseClient(
|
delegate?.sseClient(
|
||||||
self,
|
self,
|
||||||
didReceiveEvent: .terminalOutput(timestamp: timestamp, type: type, data: outputData)
|
didReceiveEvent: .terminalOutput(timestamp: timestamp, type: type, data: outputData)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ extension View {
|
||||||
func errorAlert(
|
func errorAlert(
|
||||||
error: Binding<Error?>,
|
error: Binding<Error?>,
|
||||||
onDismiss: (() -> Void)? = nil
|
onDismiss: (() -> Void)? = nil
|
||||||
) -> some View {
|
)
|
||||||
|
-> some View
|
||||||
|
{
|
||||||
modifier(ErrorAlertModifier(error: error, onDismiss: onDismiss))
|
modifier(ErrorAlertModifier(error: error, onDismiss: onDismiss))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,22 +72,22 @@ extension APIError: RecoverableError {
|
||||||
var recoverySuggestion: String? {
|
var recoverySuggestion: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .noServerConfigured:
|
case .noServerConfigured:
|
||||||
return "Please configure a server connection in Settings."
|
"Please configure a server connection in Settings."
|
||||||
case .networkError:
|
case .networkError:
|
||||||
return "Check your internet connection and try again."
|
"Check your internet connection and try again."
|
||||||
case .serverError(let code, _):
|
case .serverError(let code, _):
|
||||||
switch code {
|
switch code {
|
||||||
case 401:
|
case 401:
|
||||||
return "Check your authentication credentials in Settings."
|
"Check your authentication credentials in Settings."
|
||||||
case 500...599:
|
case 500...599:
|
||||||
return "The server is experiencing issues. Please try again later."
|
"The server is experiencing issues. Please try again later."
|
||||||
default:
|
default:
|
||||||
return nil
|
nil
|
||||||
}
|
}
|
||||||
case .resizeDisabledByServer:
|
case .resizeDisabledByServer:
|
||||||
return "Terminal resizing is not supported by this server."
|
"Terminal resizing is not supported by this server."
|
||||||
default:
|
default:
|
||||||
return nil
|
nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +153,9 @@ extension Task where Failure == Error {
|
||||||
priority: TaskPriority? = nil,
|
priority: TaskPriority? = nil,
|
||||||
errorHandler: @escaping @Sendable (Error) -> Void,
|
errorHandler: @escaping @Sendable (Error) -> Void,
|
||||||
operation: @escaping @Sendable () async throws -> T
|
operation: @escaping @Sendable () async throws -> T
|
||||||
) -> Task<T, Error> {
|
)
|
||||||
|
-> Task<T, Error>
|
||||||
|
{
|
||||||
Task<T, Error>(priority: priority) {
|
Task<T, Error>(priority: priority) {
|
||||||
do {
|
do {
|
||||||
return try await operation()
|
return try await operation()
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ struct Logger {
|
||||||
|
|
||||||
// Global log level - only messages at this level or higher will be printed
|
// Global log level - only messages at this level or higher will be printed
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
nonisolated(unsafe) static var globalLevel: LogLevel = .info // Default to info level in debug builds
|
nonisolated(unsafe) static var globalLevel: LogLevel = .info // Default to info level in debug builds
|
||||||
#else
|
#else
|
||||||
nonisolated(unsafe) static var globalLevel: LogLevel = .warning // Only warnings and errors in release
|
nonisolated(unsafe) static var globalLevel: LogLevel = .warning // Only warnings and errors in release
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
init(category: String) {
|
init(category: String) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
import UIKit
|
|
||||||
import Dynamic
|
import Dynamic
|
||||||
|
import UIKit
|
||||||
|
|
||||||
// MARK: - Window Style
|
// MARK: - Window Style
|
||||||
|
|
||||||
enum MacWindowStyle {
|
enum MacWindowStyle {
|
||||||
case standard // Normal title bar with traffic lights
|
case standard // Normal title bar with traffic lights
|
||||||
case inline // Hidden title bar with repositioned traffic lights
|
case inline // Hidden title bar with repositioned traffic lights
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UIWindow Extension
|
// MARK: - UIWindow Extension
|
||||||
|
|
@ -61,8 +61,9 @@ class MacCatalystWindowManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyWindowStyle(_ style: MacWindowStyle) {
|
private func applyWindowStyle(_ style: MacWindowStyle) {
|
||||||
guard let window = window,
|
guard let window,
|
||||||
let nsWindow = window.nsWindow else {
|
let nsWindow = window.nsWindow
|
||||||
|
else {
|
||||||
logger.warning("Unable to access NSWindow")
|
logger.warning("Unable to access NSWindow")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +84,12 @@ class MacCatalystWindowManager: ObservableObject {
|
||||||
// Show title bar
|
// Show title bar
|
||||||
nsWindow.titlebarAppearsTransparent = false
|
nsWindow.titlebarAppearsTransparent = false
|
||||||
nsWindow.titleVisibility = Dynamic.NSWindowTitleVisibility.visible
|
nsWindow.titleVisibility = Dynamic.NSWindowTitleVisibility.visible
|
||||||
nsWindow.styleMask = nsWindow.styleMask.asObject! as! UInt | Dynamic.NSWindowStyleMask.titled.asObject! as! UInt
|
guard let currentMask = nsWindow.styleMask.asObject as? UInt,
|
||||||
|
let titledMask = Dynamic.NSWindowStyleMask.titled.asObject as? UInt else {
|
||||||
|
logger.error("Failed to get window style masks")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nsWindow.styleMask = currentMask | titledMask
|
||||||
|
|
||||||
// Reset traffic light positions
|
// Reset traffic light positions
|
||||||
resetTrafficLightPositions(nsWindow)
|
resetTrafficLightPositions(nsWindow)
|
||||||
|
|
@ -104,8 +110,12 @@ class MacCatalystWindowManager: ObservableObject {
|
||||||
nsWindow.backgroundColor = Dynamic.NSColor.clearColor
|
nsWindow.backgroundColor = Dynamic.NSColor.clearColor
|
||||||
|
|
||||||
// Keep the titled style mask to preserve traffic lights
|
// Keep the titled style mask to preserve traffic lights
|
||||||
let currentMask = nsWindow.styleMask.asObject! as! UInt
|
guard let currentMask = nsWindow.styleMask.asObject as? UInt,
|
||||||
nsWindow.styleMask = currentMask | Dynamic.NSWindowStyleMask.titled.asObject! as! UInt
|
let titledMask = Dynamic.NSWindowStyleMask.titled.asObject as? UInt else {
|
||||||
|
logger.error("Failed to get window style masks")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nsWindow.styleMask = currentMask | titledMask
|
||||||
|
|
||||||
// Reposition traffic lights
|
// Reposition traffic lights
|
||||||
repositionTrafficLights(nsWindow, window: window)
|
repositionTrafficLights(nsWindow, window: window)
|
||||||
|
|
@ -164,8 +174,13 @@ class MacCatalystWindowManager: ObservableObject {
|
||||||
|
|
||||||
// Add new tracking area at the button's current position
|
// Add new tracking area at the button's current position
|
||||||
let trackingRect = button.bounds
|
let trackingRect = button.bounds
|
||||||
let options = Dynamic.NSTrackingAreaOptions.mouseEnteredAndExited.asObject! as! UInt |
|
guard let mouseEnteredAndExited = Dynamic.NSTrackingAreaOptions.mouseEnteredAndExited.asObject as? UInt,
|
||||||
Dynamic.NSTrackingAreaOptions.activeAlways.asObject! as! UInt
|
let activeAlways = Dynamic.NSTrackingAreaOptions.activeAlways.asObject as? UInt
|
||||||
|
else {
|
||||||
|
logger.error("Failed to get tracking area options")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let options = mouseEnteredAndExited | activeAlways
|
||||||
|
|
||||||
let trackingArea = Dynamic.NSTrackingArea.alloc()
|
let trackingArea = Dynamic.NSTrackingArea.alloc()
|
||||||
.initWithRect(trackingRect, options: options, owner: button, userInfo: nil)
|
.initWithRect(trackingRect, options: options, owner: button, userInfo: nil)
|
||||||
|
|
@ -189,7 +204,7 @@ class MacCatalystWindowManager: ObservableObject {
|
||||||
object: nil,
|
object: nil,
|
||||||
queue: .main
|
queue: .main
|
||||||
) { [weak self] notification in
|
) { [weak self] notification in
|
||||||
guard let self = self,
|
guard let self,
|
||||||
self.windowStyle == .inline,
|
self.windowStyle == .inline,
|
||||||
let window = self.window,
|
let window = self.window,
|
||||||
let notificationWindow = notification.object as? NSObject,
|
let notificationWindow = notification.object as? NSObject,
|
||||||
|
|
@ -208,7 +223,7 @@ class MacCatalystWindowManager: ObservableObject {
|
||||||
object: window,
|
object: window,
|
||||||
queue: .main
|
queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
guard let self = self,
|
guard let self,
|
||||||
self.windowStyle == .inline else { return }
|
self.windowStyle == .inline else { return }
|
||||||
|
|
||||||
// Reapply inline style when window becomes key
|
// Reapply inline style when window becomes key
|
||||||
|
|
@ -223,12 +238,13 @@ class MacCatalystWindowManager: ObservableObject {
|
||||||
object: nil,
|
object: nil,
|
||||||
queue: .main
|
queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
guard let self = self,
|
guard let self,
|
||||||
self.windowStyle == .inline else { return }
|
self.windowStyle == .inline else { return }
|
||||||
|
|
||||||
// Reposition if needed
|
// Reposition if needed
|
||||||
if let window = self.window,
|
if let window = self.window,
|
||||||
let nsWindow = window.nsWindow {
|
let nsWindow = window.nsWindow
|
||||||
|
{
|
||||||
self.repositionTrafficLights(Dynamic(nsWindow), window: window)
|
self.repositionTrafficLights(Dynamic(nsWindow), window: window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +275,8 @@ struct MacCatalystWindowStyle: ViewModifier {
|
||||||
|
|
||||||
private func setupWindow() {
|
private func setupWindow() {
|
||||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
let window = windowScene.windows.first else {
|
let window = windowScene.windows.first
|
||||||
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,13 @@ class ServerProfilesViewModel {
|
||||||
profiles = ServerProfile.loadAll().sorted { profile1, profile2 in
|
profiles = ServerProfile.loadAll().sorted { profile1, profile2 in
|
||||||
// Sort by last connected (most recent first), then by name
|
// Sort by last connected (most recent first), then by name
|
||||||
if let date1 = profile1.lastConnected, let date2 = profile2.lastConnected {
|
if let date1 = profile1.lastConnected, let date2 = profile2.lastConnected {
|
||||||
return date1 > date2
|
date1 > date2
|
||||||
} else if profile1.lastConnected != nil {
|
} else if profile1.lastConnected != nil {
|
||||||
return true
|
true
|
||||||
} else if profile2.lastConnected != nil {
|
} else if profile2.lastConnected != nil {
|
||||||
return false
|
false
|
||||||
} else {
|
} else {
|
||||||
return profile1.name < profile2.name
|
profile1.name < profile2.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ class ServerProfilesViewModel {
|
||||||
ServerProfile.save(profile)
|
ServerProfile.save(profile)
|
||||||
|
|
||||||
// Save password to keychain if provided
|
// Save password to keychain if provided
|
||||||
if let password = password, !password.isEmpty {
|
if let password, !password.isEmpty {
|
||||||
try KeychainService.savePassword(password, for: profile.id)
|
try KeychainService.savePassword(password, for: profile.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ class ServerProfilesViewModel {
|
||||||
ServerProfile.save(updatedProfile)
|
ServerProfile.save(updatedProfile)
|
||||||
|
|
||||||
// Update password if provided
|
// Update password if provided
|
||||||
if let password = password {
|
if let password {
|
||||||
if password.isEmpty {
|
if password.isEmpty {
|
||||||
// Delete password if empty
|
// Delete password if empty
|
||||||
try KeychainService.deletePassword(for: profile.id)
|
try KeychainService.deletePassword(for: profile.id)
|
||||||
|
|
@ -140,7 +140,8 @@ extension ServerProfilesViewModel {
|
||||||
|
|
||||||
// Validate URL
|
// Validate URL
|
||||||
guard let url = URL(string: cleanURL),
|
guard let url = URL(string: cleanURL),
|
||||||
let _ = url.host else {
|
let _ = url.host
|
||||||
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,8 @@ class ConnectionViewModel {
|
||||||
|
|
||||||
func loadLastConnection() {
|
func loadLastConnection() {
|
||||||
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"),
|
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"),
|
||||||
let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config) {
|
let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config)
|
||||||
|
{
|
||||||
self.host = serverConfig.host
|
self.host = serverConfig.host
|
||||||
self.port = String(serverConfig.port)
|
self.port = String(serverConfig.port)
|
||||||
self.name = serverConfig.name ?? ""
|
self.name = serverConfig.name ?? ""
|
||||||
|
|
@ -162,7 +163,8 @@ class ConnectionViewModel {
|
||||||
let (_, response) = try await URLSession.shared.data(for: request)
|
let (_, response) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse,
|
if let httpResponse = response as? HTTPURLResponse,
|
||||||
httpResponse.statusCode == 200 {
|
httpResponse.statusCode == 200
|
||||||
|
{
|
||||||
onSuccess(config)
|
onSuccess(config)
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "Failed to connect to server"
|
errorMessage = "Failed to connect to server"
|
||||||
|
|
|
||||||
|
|
@ -21,44 +21,44 @@ struct EnhancedConnectionView: View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ZStack {
|
ZStack {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: Theme.Spacing.extraLarge) {
|
VStack(spacing: Theme.Spacing.extraLarge) {
|
||||||
// Logo and Title
|
// Logo and Title
|
||||||
headerView
|
headerView
|
||||||
.padding(.top, {
|
.padding(.top, {
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
return windowManager.windowStyle == .inline ? 60 : 40
|
return windowManager.windowStyle == .inline ? 60 : 40
|
||||||
#else
|
#else
|
||||||
return 40
|
return 40
|
||||||
#endif
|
#endif
|
||||||
}())
|
}())
|
||||||
|
|
||||||
// Quick Connect Section
|
// Quick Connect Section
|
||||||
if !profilesViewModel.profiles.isEmpty && !showingNewServerForm {
|
if !profilesViewModel.profiles.isEmpty && !showingNewServerForm {
|
||||||
quickConnectSection
|
quickConnectSection
|
||||||
.opacity(contentOpacity)
|
.opacity(contentOpacity)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
withAnimation(Theme.Animation.smooth.delay(0.3)) {
|
withAnimation(Theme.Animation.smooth.delay(0.3)) {
|
||||||
contentOpacity = 1.0
|
contentOpacity = 1.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// New Connection Form
|
// New Connection Form
|
||||||
if showingNewServerForm || profilesViewModel.profiles.isEmpty {
|
if showingNewServerForm || profilesViewModel.profiles.isEmpty {
|
||||||
newConnectionSection
|
newConnectionSection
|
||||||
.opacity(contentOpacity)
|
.opacity(contentOpacity)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
withAnimation(Theme.Animation.smooth.delay(0.3)) {
|
withAnimation(Theme.Animation.smooth.delay(0.3)) {
|
||||||
contentOpacity = 1.0
|
contentOpacity = 1.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(minLength: 50)
|
Spacer(minLength: 50)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
.padding()
|
.scrollBounceBehavior(.basedOnSize)
|
||||||
}
|
|
||||||
.scrollBounceBehavior(.basedOnSize)
|
|
||||||
}
|
}
|
||||||
.toolbar(.hidden, for: .navigationBar)
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
.background(Theme.Colors.terminalBackground.ignoresSafeArea())
|
.background(Theme.Colors.terminalBackground.ignoresSafeArea())
|
||||||
|
|
@ -418,7 +418,7 @@ struct ServerProfileEditView: View {
|
||||||
onDelete()
|
onDelete()
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
Button("Cancel", role: .cancel) { }
|
Button("Cancel", role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
Text("Are you sure you want to delete \"\(profile.name)\"? This action cannot be undone.")
|
Text("Are you sure you want to delete \"\(profile.name)\"? This action cannot be undone.")
|
||||||
}
|
}
|
||||||
|
|
@ -426,7 +426,8 @@ struct ServerProfileEditView: View {
|
||||||
.task {
|
.task {
|
||||||
// Load existing password from keychain
|
// Load existing password from keychain
|
||||||
if profile.requiresAuth,
|
if profile.requiresAuth,
|
||||||
let existingPassword = try? KeychainService.getPassword(for: profile.id) {
|
let existingPassword = try? KeychainService.getPassword(for: profile.id)
|
||||||
|
{
|
||||||
password = existingPassword
|
password = existingPassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,13 +143,13 @@ struct ServerConfigForm: View {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.foregroundColor(isConnecting || !networkMonitor.isConnected ? Theme.Colors.terminalForeground : Theme
|
.foregroundColor(isConnecting || !networkMonitor.isConnected ? Theme.Colors.terminalForeground : Theme
|
||||||
.Colors.primaryAccent
|
.Colors.primaryAccent
|
||||||
)
|
)
|
||||||
.padding(.vertical, Theme.Spacing.medium)
|
.padding(.vertical, Theme.Spacing.medium)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||||
.fill(isConnecting || !networkMonitor.isConnected ? Theme.Colors.cardBackground : Theme.Colors
|
.fill(isConnecting || !networkMonitor.isConnected ? Theme.Colors.cardBackground : Theme.Colors
|
||||||
.terminalBackground
|
.terminalBackground
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
@ -218,7 +218,8 @@ struct ServerConfigForm: View {
|
||||||
private func loadRecentServers() {
|
private func loadRecentServers() {
|
||||||
// Load recent servers from UserDefaults
|
// Load recent servers from UserDefaults
|
||||||
if let data = UserDefaults.standard.data(forKey: "recentServers"),
|
if let data = UserDefaults.standard.data(forKey: "recentServers"),
|
||||||
let servers = try? JSONDecoder().decode([ServerConfig].self, from: data) {
|
let servers = try? JSONDecoder().decode([ServerConfig].self, from: data)
|
||||||
|
{
|
||||||
recentServers = servers
|
recentServers = servers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,8 @@ struct FilePreviewView: View {
|
||||||
case .image:
|
case .image:
|
||||||
if let content = preview.content,
|
if let content = preview.content,
|
||||||
let data = Data(base64Encoded: content),
|
let data = Data(base64Encoded: content),
|
||||||
let uiImage = UIImage(data: data) {
|
let uiImage = UIImage(data: data)
|
||||||
|
{
|
||||||
Image(uiImage: uiImage)
|
Image(uiImage: uiImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
|
|
|
||||||
|
|
@ -109,14 +109,14 @@ struct FileBrowserView: View {
|
||||||
.font(.custom("SF Mono", size: 12))
|
.font(.custom("SF Mono", size: 12))
|
||||||
}
|
}
|
||||||
.foregroundColor(viewModel.gitFilter == .changed ? Theme.Colors.successAccent : Theme.Colors
|
.foregroundColor(viewModel.gitFilter == .changed ? Theme.Colors.successAccent : Theme.Colors
|
||||||
.terminalGray
|
.terminalGray
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 6)
|
RoundedRectangle(cornerRadius: 6)
|
||||||
.fill(viewModel.gitFilter == .changed ? Theme.Colors.successAccent.opacity(0.2) : Theme.Colors
|
.fill(viewModel.gitFilter == .changed ? Theme.Colors.successAccent.opacity(0.2) : Theme.Colors
|
||||||
.terminalGray.opacity(0.1)
|
.terminalGray.opacity(0.1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +140,7 @@ struct FileBrowserView: View {
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 6)
|
RoundedRectangle(cornerRadius: 6)
|
||||||
.fill(viewModel.showHidden ? Theme.Colors.terminalAccent.opacity(0.2) : Theme.Colors
|
.fill(viewModel.showHidden ? Theme.Colors.terminalAccent.opacity(0.2) : Theme.Colors
|
||||||
.terminalGray.opacity(0.1)
|
.terminalGray.opacity(0.1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -566,7 +566,7 @@ struct FileBrowserRow: View {
|
||||||
Text(name)
|
Text(name)
|
||||||
.font(.custom("SF Mono", size: 14))
|
.font(.custom("SF Mono", size: 14))
|
||||||
.foregroundColor(isParent ? Theme.Colors
|
.foregroundColor(isParent ? Theme.Colors
|
||||||
.terminalAccent : (isDirectory ? Theme.Colors.terminalWhite : Theme.Colors.terminalGray)
|
.terminalAccent : (isDirectory ? Theme.Colors.terminalWhite : Theme.Colors.terminalGray)
|
||||||
)
|
)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.middle)
|
.truncationMode(.middle)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ struct SessionCardView: View {
|
||||||
// Convert absolute paths back to ~ notation for display
|
// Convert absolute paths back to ~ notation for display
|
||||||
let homePrefix = "/Users/"
|
let homePrefix = "/Users/"
|
||||||
if session.workingDir.hasPrefix(homePrefix),
|
if session.workingDir.hasPrefix(homePrefix),
|
||||||
let userEndIndex = session.workingDir[homePrefix.endIndex...].firstIndex(of: "/") {
|
let userEndIndex = session.workingDir[homePrefix.endIndex...].firstIndex(of: "/")
|
||||||
|
{
|
||||||
let restOfPath = String(session.workingDir[userEndIndex...])
|
let restOfPath = String(session.workingDir[userEndIndex...])
|
||||||
return "~\(restOfPath)"
|
return "~\(restOfPath)"
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +62,7 @@ struct SessionCardView: View {
|
||||||
Image(systemName: session.isRunning ? "xmark.circle" : "trash.circle")
|
Image(systemName: session.isRunning ? "xmark.circle" : "trash.circle")
|
||||||
.font(.system(size: 18))
|
.font(.system(size: 18))
|
||||||
.foregroundColor(session.isRunning ? Theme.Colors.errorAccent : Theme.Colors
|
.foregroundColor(session.isRunning ? Theme.Colors.errorAccent : Theme.Colors
|
||||||
.terminalForeground.opacity(0.6)
|
.terminalForeground.opacity(0.6)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -106,13 +107,13 @@ struct SessionCardView: View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(session.isRunning ? Theme.Colors.successAccent : Theme.Colors.terminalForeground
|
.fill(session.isRunning ? Theme.Colors.successAccent : Theme.Colors.terminalForeground
|
||||||
.opacity(0.3)
|
.opacity(0.3)
|
||||||
)
|
)
|
||||||
.frame(width: 6, height: 6)
|
.frame(width: 6, height: 6)
|
||||||
Text(session.isRunning ? "running" : "exited")
|
Text(session.isRunning ? "running" : "exited")
|
||||||
.font(Theme.Typography.terminalSystem(size: 10))
|
.font(Theme.Typography.terminalSystem(size: 10))
|
||||||
.foregroundColor(session.isRunning ? Theme.Colors.successAccent : Theme.Colors
|
.foregroundColor(session.isRunning ? Theme.Colors.successAccent : Theme.Colors
|
||||||
.terminalForeground.opacity(0.5)
|
.terminalForeground.opacity(0.5)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Live preview indicator
|
// Live preview indicator
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,9 @@ struct SessionCreateView: View {
|
||||||
if presentedError != nil {
|
if presentedError != nil {
|
||||||
ErrorBanner(
|
ErrorBanner(
|
||||||
message: presentedError?.error.localizedDescription ?? "An error occurred"
|
message: presentedError?.error.localizedDescription ?? "An error occurred"
|
||||||
) {
|
) {
|
||||||
presentedError = nil
|
presentedError = nil
|
||||||
}
|
}
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
|
||||||
.stroke(Theme.Colors.errorAccent.opacity(0.3), lineWidth: 1)
|
.stroke(Theme.Colors.errorAccent.opacity(0.3), lineWidth: 1)
|
||||||
|
|
@ -156,14 +156,14 @@ struct SessionCreateView: View {
|
||||||
.font(Theme.Typography.terminalSystem(size: 13))
|
.font(Theme.Typography.terminalSystem(size: 13))
|
||||||
}
|
}
|
||||||
.foregroundColor(workingDirectory == dir ? Theme.Colors
|
.foregroundColor(workingDirectory == dir ? Theme.Colors
|
||||||
.terminalBackground : Theme.Colors.terminalForeground
|
.terminalBackground : Theme.Colors.terminalForeground
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
|
||||||
.fill(workingDirectory == dir ? Theme.Colors
|
.fill(workingDirectory == dir ? Theme.Colors
|
||||||
.primaryAccent : Theme.Colors.cardBorder.opacity(0.1)
|
.primaryAccent : Theme.Colors.cardBorder.opacity(0.1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
@ -209,16 +209,16 @@ struct SessionCreateView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.foregroundColor(command == item.command ? Theme.Colors
|
.foregroundColor(command == item.command ? Theme.Colors
|
||||||
.terminalBackground : Theme.Colors
|
.terminalBackground : Theme.Colors
|
||||||
.terminalForeground
|
.terminalForeground
|
||||||
)
|
)
|
||||||
.padding(.horizontal, Theme.Spacing.medium)
|
.padding(.horizontal, Theme.Spacing.medium)
|
||||||
.padding(.vertical, 14)
|
.padding(.vertical, 14)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||||
.fill(command == item.command ? Theme.Colors.primaryAccent : Theme
|
.fill(command == item.command ? Theme.Colors.primaryAccent : Theme
|
||||||
.Colors
|
.Colors
|
||||||
.cardBackground
|
.cardBackground
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
@ -283,7 +283,7 @@ struct SessionCreateView: View {
|
||||||
Text("Create")
|
Text("Create")
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.system(size: 17, weight: .semibold))
|
||||||
.foregroundColor(command.isEmpty ? Theme.Colors.primaryAccent.opacity(0.5) : Theme
|
.foregroundColor(command.isEmpty ? Theme.Colors.primaryAccent.opacity(0.5) : Theme
|
||||||
.Colors.primaryAccent
|
.Colors.primaryAccent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,8 @@ struct SessionListView: View {
|
||||||
.onChange(of: navigationManager.shouldNavigateToSession) { _, shouldNavigate in
|
.onChange(of: navigationManager.shouldNavigateToSession) { _, shouldNavigate in
|
||||||
if shouldNavigate,
|
if shouldNavigate,
|
||||||
let sessionId = navigationManager.selectedSessionId,
|
let sessionId = navigationManager.selectedSessionId,
|
||||||
let session = viewModel.sessions.first(where: { $0.id == sessionId }) {
|
let session = viewModel.sessions.first(where: { $0.id == sessionId })
|
||||||
|
{
|
||||||
selectedSession = session
|
selectedSession = session
|
||||||
navigationManager.clearNavigation()
|
navigationManager.clearNavigation()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ struct SettingsView: View {
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, Theme.Spacing.medium)
|
.padding(.vertical, Theme.Spacing.medium)
|
||||||
.foregroundColor(selectedTab == tab ? Theme.Colors.primaryAccent : Theme.Colors
|
.foregroundColor(selectedTab == tab ? Theme.Colors.primaryAccent : Theme.Colors
|
||||||
.terminalForeground.opacity(0.5)
|
.terminalForeground.opacity(0.5)
|
||||||
)
|
)
|
||||||
.background(
|
.background(
|
||||||
selectedTab == tab ? Theme.Colors.primaryAccent.opacity(0.1) : Color.clear
|
selectedTab == tab ? Theme.Colors.primaryAccent.opacity(0.1) : Color.clear
|
||||||
|
|
@ -294,10 +294,11 @@ struct AdvancedSettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(macWindowStyle == .inline ?
|
Text(macWindowStyle == .inline ?
|
||||||
"Traffic light buttons appear inline with content" :
|
"Traffic light buttons appear inline with content" :
|
||||||
"Standard macOS title bar with traffic lights")
|
"Standard macOS title bar with traffic lights"
|
||||||
.font(Theme.Typography.terminalSystem(size: 12))
|
)
|
||||||
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.6))
|
.font(Theme.Typography.terminalSystem(size: 12))
|
||||||
|
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.6))
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Theme.Colors.cardBackground)
|
.background(Theme.Colors.cardBackground)
|
||||||
|
|
@ -434,7 +435,7 @@ struct LinkRow: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if let url = url {
|
if let url {
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,8 @@ struct SystemLogsView: View {
|
||||||
// Present it
|
// Present it
|
||||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
let window = windowScene.windows.first,
|
let window = windowScene.windows.first,
|
||||||
let rootVC = window.rootViewController {
|
let rootVC = window.rootViewController
|
||||||
|
{
|
||||||
rootVC.present(activityVC, animated: true)
|
rootVC.present(activityVC, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,8 @@ struct CtrlKeyButton: View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
// Calculate control character (Ctrl+A = 1, Ctrl+B = 2, etc.)
|
// Calculate control character (Ctrl+A = 1, Ctrl+B = 2, etc.)
|
||||||
if let scalar = char.unicodeScalars.first,
|
if let scalar = char.unicodeScalars.first,
|
||||||
let ctrlScalar = UnicodeScalar(scalar.value - 64) {
|
let ctrlScalar = UnicodeScalar(scalar.value - 64)
|
||||||
|
{
|
||||||
let ctrlChar = Character(ctrlScalar)
|
let ctrlChar = Character(ctrlScalar)
|
||||||
onPress(String(ctrlChar))
|
onPress(String(ctrlChar))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ struct CtrlKeyGrid: View {
|
||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
let onKeyPress: (String) -> Void
|
let onKeyPress: (String) -> Void
|
||||||
|
|
||||||
// Common Ctrl combinations organized by category
|
/// Common Ctrl combinations organized by category
|
||||||
let navigationKeys = [
|
let navigationKeys = [
|
||||||
("A", "Beginning of line"),
|
("A", "Beginning of line"),
|
||||||
("E", "End of line"),
|
("E", "End of line"),
|
||||||
|
|
@ -70,7 +70,7 @@ struct CtrlKeyGrid: View {
|
||||||
CtrlGridKeyButton(
|
CtrlGridKeyButton(
|
||||||
key: key,
|
key: key,
|
||||||
description: description
|
description: description
|
||||||
) { sendCtrlKey(key) }
|
) { sendCtrlKey(key) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
@ -106,11 +106,11 @@ struct CtrlKeyGrid: View {
|
||||||
|
|
||||||
private var currentKeys: [(String, String)] {
|
private var currentKeys: [(String, String)] {
|
||||||
switch selectedCategory {
|
switch selectedCategory {
|
||||||
case 0: return navigationKeys
|
case 0: navigationKeys
|
||||||
case 1: return editingKeys
|
case 1: editingKeys
|
||||||
case 2: return processKeys
|
case 2: processKeys
|
||||||
case 3: return searchKeys
|
case 3: searchKeys
|
||||||
default: return navigationKeys
|
default: navigationKeys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,14 +81,14 @@ struct FontSizeSheet: View {
|
||||||
Text("\(Int(size))")
|
Text("\(Int(size))")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.system(size: 14, weight: .medium))
|
||||||
.foregroundColor(fontSize == size ? Theme.Colors.terminalBackground : Theme.Colors
|
.foregroundColor(fontSize == size ? Theme.Colors.terminalBackground : Theme.Colors
|
||||||
.terminalForeground
|
.terminalForeground
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, Theme.Spacing.small)
|
.padding(.vertical, Theme.Spacing.small)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
|
||||||
.fill(fontSize == size ? Theme.Colors.primaryAccent : Theme.Colors
|
.fill(fontSize == size ? Theme.Colors.primaryAccent : Theme.Colors
|
||||||
.cardBorder.opacity(0.3)
|
.cardBorder.opacity(0.3)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ struct QuickFontSizeButtons: View {
|
||||||
Button(action: decreaseFontSize) {
|
Button(action: decreaseFontSize) {
|
||||||
Image(systemName: "minus")
|
Image(systemName: "minus")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.system(size: 14, weight: .medium))
|
||||||
.foregroundColor(fontSize > minSize ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText.opacity(0.5))
|
.foregroundColor(fontSize > minSize ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText
|
||||||
|
.opacity(0.5)
|
||||||
|
)
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
.background(Theme.Colors.cardBackground)
|
.background(Theme.Colors.cardBackground)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
@ -41,7 +43,9 @@ struct QuickFontSizeButtons: View {
|
||||||
Button(action: increaseFontSize) {
|
Button(action: increaseFontSize) {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.system(size: 14, weight: .medium))
|
||||||
.foregroundColor(fontSize < maxSize ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText.opacity(0.5))
|
.foregroundColor(fontSize < maxSize ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText
|
||||||
|
.opacity(0.5)
|
||||||
|
)
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
.background(Theme.Colors.cardBackground)
|
.background(Theme.Colors.cardBackground)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ struct TerminalBufferPreview: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if RGB color (has alpha channel flag)
|
// Check if RGB color (has alpha channel flag)
|
||||||
if (fg & 0xFF000000) != 0 {
|
if (fg & 0xFF00_0000) != 0 {
|
||||||
// RGB color
|
// RGB color
|
||||||
let red = Double((fg >> 16) & 0xFF) / 255.0
|
let red = Double((fg >> 16) & 0xFF) / 255.0
|
||||||
let green = Double((fg >> 8) & 0xFF) / 255.0
|
let green = Double((fg >> 8) & 0xFF) / 255.0
|
||||||
|
|
@ -86,7 +86,7 @@ struct TerminalBufferPreview: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if RGB color (has alpha channel flag)
|
// Check if RGB color (has alpha channel flag)
|
||||||
if (bg & 0xFF000000) != 0 {
|
if (bg & 0xFF00_0000) != 0 {
|
||||||
// RGB color
|
// RGB color
|
||||||
let red = Double((bg >> 16) & 0xFF) / 255.0
|
let red = Double((bg >> 16) & 0xFF) / 255.0
|
||||||
let green = Double((bg >> 8) & 0xFF) / 255.0
|
let green = Double((bg >> 8) & 0xFF) / 255.0
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,8 @@ struct TerminalHostingView: UIViewRepresentable {
|
||||||
from oldSnapshot: BufferSnapshot,
|
from oldSnapshot: BufferSnapshot,
|
||||||
to newSnapshot: BufferSnapshot
|
to newSnapshot: BufferSnapshot
|
||||||
)
|
)
|
||||||
-> String {
|
-> String
|
||||||
|
{
|
||||||
var output = ""
|
var output = ""
|
||||||
var currentFg: Int?
|
var currentFg: Int?
|
||||||
var currentBg: Int?
|
var currentBg: Int?
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,8 @@ struct TerminalThemeSheet: View {
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||||
.fill(selectedTheme.id == theme.id
|
.fill(selectedTheme.id == theme.id
|
||||||
? Theme.Colors.primaryAccent.opacity(0.1)
|
? Theme.Colors.primaryAccent.opacity(0.1)
|
||||||
: Theme.Colors.cardBorder.opacity(0.1)
|
: Theme.Colors.cardBorder.opacity(0.1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,9 @@ struct TerminalView: View {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.task {
|
.task {
|
||||||
for await notification in NotificationCenter.default.notifications(named: UIResponder.keyboardWillShowNotification) {
|
for await notification in NotificationCenter.default
|
||||||
|
.notifications(named: UIResponder.keyboardWillShowNotification)
|
||||||
|
{
|
||||||
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
||||||
withAnimation(Theme.Animation.standard) {
|
withAnimation(Theme.Animation.standard) {
|
||||||
keyboardHeight = keyboardFrame.height
|
keyboardHeight = keyboardFrame.height
|
||||||
|
|
@ -524,10 +526,10 @@ struct TerminalView: View {
|
||||||
.overlay(
|
.overlay(
|
||||||
ScrollToBottomButton(
|
ScrollToBottomButton(
|
||||||
isVisible: showScrollToBottom
|
isVisible: showScrollToBottom
|
||||||
) {
|
) {
|
||||||
viewModel.scrollToBottom()
|
viewModel.scrollToBottom()
|
||||||
showScrollToBottom = false
|
showScrollToBottom = false
|
||||||
}
|
}
|
||||||
.padding(.bottom, Theme.Spacing.large)
|
.padding(.bottom, Theme.Spacing.large)
|
||||||
.padding(.leading, Theme.Spacing.large),
|
.padding(.leading, Theme.Spacing.large),
|
||||||
alignment: .bottomLeading
|
alignment: .bottomLeading
|
||||||
|
|
@ -698,7 +700,7 @@ class TerminalViewModel {
|
||||||
logger.info("Terminal initialized: \(width)x\(height)")
|
logger.info("Terminal initialized: \(width)x\(height)")
|
||||||
terminalCols = width
|
terminalCols = width
|
||||||
terminalRows = height
|
terminalRows = height
|
||||||
// The terminal will be resized when created
|
// The terminal will be resized when created
|
||||||
|
|
||||||
case .output(_, let data):
|
case .output(_, let data):
|
||||||
// Feed output data directly to the terminal
|
// Feed output data directly to the terminal
|
||||||
|
|
@ -723,7 +725,8 @@ class TerminalViewModel {
|
||||||
let parts = dimensions.split(separator: "x")
|
let parts = dimensions.split(separator: "x")
|
||||||
if parts.count == 2,
|
if parts.count == 2,
|
||||||
let cols = Int(parts[0]),
|
let cols = Int(parts[0]),
|
||||||
let rows = Int(parts[1]) {
|
let rows = Int(parts[1])
|
||||||
|
{
|
||||||
// Update terminal dimensions
|
// Update terminal dimensions
|
||||||
terminalCols = cols
|
terminalCols = cols
|
||||||
terminalRows = rows
|
terminalRows = rows
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,8 @@ struct TerminalWidthSheet: View {
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||||
.fill(selectedWidth == preset.columns
|
.fill(selectedWidth == preset.columns
|
||||||
? Theme.Colors.primaryAccent.opacity(0.1)
|
? Theme.Colors.primaryAccent.opacity(0.1)
|
||||||
: Theme.Colors.cardBorder.opacity(0.1)
|
: Theme.Colors.cardBorder.opacity(0.1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,8 @@ struct WidthSelectorPopover: View {
|
||||||
let customWidths = TerminalWidthManager.shared.customWidths
|
let customWidths = TerminalWidthManager.shared.customWidths
|
||||||
if !customWidths.isEmpty {
|
if !customWidths.isEmpty {
|
||||||
Section(header: Text("Recent Custom Widths")
|
Section(header: Text("Recent Custom Widths")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.7))
|
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.7))
|
||||||
) {
|
) {
|
||||||
ForEach(customWidths, id: \.self) { width in
|
ForEach(customWidths, id: \.self) { width in
|
||||||
WidthPresetRow(
|
WidthPresetRow(
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,8 @@ struct XtermWebView: UIViewRepresentable {
|
||||||
case "terminalResize":
|
case "terminalResize":
|
||||||
if let dict = message.body as? [String: Any],
|
if let dict = message.body as? [String: Any],
|
||||||
let cols = dict["cols"] as? Int,
|
let cols = dict["cols"] as? Int,
|
||||||
let rows = dict["rows"] as? Int {
|
let rows = dict["rows"] as? Int
|
||||||
|
{
|
||||||
parent.onResize(cols, rows)
|
parent.onResize(cols, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ if command -v xcpretty &> /dev/null; then
|
||||||
-scheme VibeTunnel-iOS \
|
-scheme VibeTunnel-iOS \
|
||||||
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
|
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
|
||||||
-resultBundlePath TestResults.xcresult \
|
-resultBundlePath TestResults.xcresult \
|
||||||
|
CODE_SIGN_IDENTITY="" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
2>&1 | xcpretty || {
|
2>&1 | xcpretty || {
|
||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
echo "Tests failed with exit code: $EXIT_CODE"
|
echo "Tests failed with exit code: $EXIT_CODE"
|
||||||
|
|
@ -68,6 +71,9 @@ else
|
||||||
-scheme VibeTunnel-iOS \
|
-scheme VibeTunnel-iOS \
|
||||||
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
|
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
|
||||||
-resultBundlePath TestResults.xcresult \
|
-resultBundlePath TestResults.xcresult \
|
||||||
|
CODE_SIGN_IDENTITY="" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|| {
|
|| {
|
||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
echo "Tests failed with exit code: $EXIT_CODE"
|
echo "Tests failed with exit code: $EXIT_CODE"
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ extension View {
|
||||||
systemPermissionManager: SystemPermissionManager? = nil,
|
systemPermissionManager: SystemPermissionManager? = nil,
|
||||||
terminalLauncher: TerminalLauncher? = nil
|
terminalLauncher: TerminalLauncher? = nil
|
||||||
)
|
)
|
||||||
-> some View
|
-> some View
|
||||||
{
|
{
|
||||||
self
|
self
|
||||||
.environment(\.serverManager, serverManager ?? ServerManager.shared)
|
.environment(\.serverManager, serverManager ?? ServerManager.shared)
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,8 @@ final class DockIconManager: NSObject, @unchecked Sendable {
|
||||||
|
|
||||||
// Log window details for debugging
|
// Log window details for debugging
|
||||||
// for window in visibleWindows {
|
// for window in visibleWindows {
|
||||||
// logger.debug(" Visible window: \(window.title.isEmpty ? "(untitled)" : window.title, privacy: .public)")
|
// logger.debug(" Visible window: \(window.title.isEmpty ? "(untitled)" : window.title, privacy:
|
||||||
|
// .public)")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Show dock if user wants it shown OR if any windows are open
|
// Show dock if user wants it shown OR if any windows are open
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable {
|
||||||
/// Static URLs to ensure they're validated at compile time
|
/// Static URLs to ensure they're validated at compile time
|
||||||
private static let stableAppcastURL: URL = {
|
private static let stableAppcastURL: URL = {
|
||||||
guard let url =
|
guard let url =
|
||||||
URL(string: "https://stats.store/api/v1/appcast/appcast.xml")
|
URL(string: "https://stats.store/api/v1/appcast/appcast.xml")
|
||||||
else {
|
else {
|
||||||
fatalError("Invalid stable appcast URL - this should never happen with a hardcoded URL")
|
fatalError("Invalid stable appcast URL - this should never happen with a hardcoded URL")
|
||||||
}
|
}
|
||||||
|
|
@ -50,9 +50,9 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable {
|
||||||
|
|
||||||
private static let prereleaseAppcastURL: URL = {
|
private static let prereleaseAppcastURL: URL = {
|
||||||
guard let url =
|
guard let url =
|
||||||
URL(
|
URL(
|
||||||
string: "https://stats.store/api/v1/appcast/appcast-prerelease.xml"
|
string: "https://stats.store/api/v1/appcast/appcast-prerelease.xml"
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
fatalError("Invalid prerelease appcast URL - this should never happen with a hardcoded URL")
|
fatalError("Invalid prerelease appcast URL - this should never happen with a hardcoded URL")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -480,7 +480,7 @@ final class BunServer {
|
||||||
seconds: TimeInterval,
|
seconds: TimeInterval,
|
||||||
operation: @escaping @Sendable () async -> T
|
operation: @escaping @Sendable () async -> T
|
||||||
)
|
)
|
||||||
async -> T?
|
async -> T?
|
||||||
{
|
{
|
||||||
await withTaskGroup(of: T?.self) { group in
|
await withTaskGroup(of: T?.self) { group in
|
||||||
group.addTask {
|
group.addTask {
|
||||||
|
|
|
||||||
|
|
@ -20,33 +20,33 @@ final class DashboardKeychain {
|
||||||
/// Get the dashboard password from keychain
|
/// Get the dashboard password from keychain
|
||||||
func getPassword() -> String? {
|
func getPassword() -> String? {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// In debug builds, skip keychain access to avoid authorization dialogs
|
// In debug builds, skip keychain access to avoid authorization dialogs
|
||||||
logger
|
logger
|
||||||
.info(
|
.info(
|
||||||
"Debug mode: Skipping keychain password retrieval. Password will only persist during current app session."
|
"Debug mode: Skipping keychain password retrieval. Password will only persist during current app session."
|
||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
#else
|
#else
|
||||||
let query: [String: Any] = [
|
let query: [String: Any] = [
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
kSecAttrService as String: service,
|
kSecAttrService as String: service,
|
||||||
kSecAttrAccount as String: account,
|
kSecAttrAccount as String: account,
|
||||||
kSecReturnData as String: true
|
kSecReturnData as String: true
|
||||||
]
|
]
|
||||||
|
|
||||||
var result: AnyObject?
|
var result: AnyObject?
|
||||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
guard status == errSecSuccess,
|
guard status == errSecSuccess,
|
||||||
let data = result as? Data,
|
let data = result as? Data,
|
||||||
let password = String(data: data, encoding: .utf8)
|
let password = String(data: data, encoding: .utf8)
|
||||||
else {
|
else {
|
||||||
logger.debug("No password found in keychain")
|
logger.debug("No password found in keychain")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Password retrieved from keychain")
|
logger.debug("Password retrieved from keychain")
|
||||||
return password
|
return password
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,12 +103,12 @@ final class DashboardKeychain {
|
||||||
logger.info("Password \(success ? "saved to" : "failed to save to") keychain")
|
logger.info("Password \(success ? "saved to" : "failed to save to") keychain")
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if success {
|
if success {
|
||||||
logger
|
logger
|
||||||
.info(
|
.info(
|
||||||
"Debug mode: Password saved to keychain but will not persist across app restarts. The password will only be available during this session to avoid keychain authorization dialogs during development."
|
"Debug mode: Password saved to keychain but will not persist across app restarts. The password will only be available during this session to avoid keychain authorization dialogs during development."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ final class NgrokService: NgrokTunnelProtocol {
|
||||||
seconds: TimeInterval,
|
seconds: TimeInterval,
|
||||||
operation: @Sendable @escaping () async throws -> T
|
operation: @Sendable @escaping () async throws -> T
|
||||||
)
|
)
|
||||||
async throws -> T
|
async throws -> T
|
||||||
{
|
{
|
||||||
try await withThrowingTaskGroup(of: T.self) { group in
|
try await withThrowingTaskGroup(of: T.self) { group in
|
||||||
group.addTask {
|
group.addTask {
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class ServerManager {
|
||||||
get {
|
get {
|
||||||
let mode = DashboardAccessMode(rawValue: UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? ""
|
let mode = DashboardAccessMode(rawValue: UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? ""
|
||||||
) ??
|
) ??
|
||||||
.localhost
|
.localhost
|
||||||
return mode.bindAddress
|
return mode.bindAddress
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
|
|
|
||||||
|
|
@ -52,38 +52,38 @@ public final class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate {
|
||||||
|
|
||||||
// Initialize Sparkle with standard configuration
|
// Initialize Sparkle with standard configuration
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// In debug mode, start the updater for testing
|
// In debug mode, start the updater for testing
|
||||||
updaterController = SPUStandardUpdaterController(
|
updaterController = SPUStandardUpdaterController(
|
||||||
startingUpdater: true,
|
startingUpdater: true,
|
||||||
updaterDelegate: self,
|
updaterDelegate: self,
|
||||||
userDriverDelegate: userDriverDelegate
|
userDriverDelegate: userDriverDelegate
|
||||||
)
|
)
|
||||||
#else
|
#else
|
||||||
updaterController = SPUStandardUpdaterController(
|
updaterController = SPUStandardUpdaterController(
|
||||||
startingUpdater: true,
|
startingUpdater: true,
|
||||||
updaterDelegate: self,
|
updaterDelegate: self,
|
||||||
userDriverDelegate: userDriverDelegate
|
userDriverDelegate: userDriverDelegate
|
||||||
)
|
)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Configure automatic updates
|
// Configure automatic updates
|
||||||
if let updater = updaterController?.updater {
|
if let updater = updaterController?.updater {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// Enable automatic checks in debug too
|
// Enable automatic checks in debug too
|
||||||
updater.automaticallyChecksForUpdates = true
|
updater.automaticallyChecksForUpdates = true
|
||||||
updater.automaticallyDownloadsUpdates = false
|
updater.automaticallyDownloadsUpdates = false
|
||||||
logger.info("Sparkle updater initialized in DEBUG mode - automatic updates enabled for testing")
|
logger.info("Sparkle updater initialized in DEBUG mode - automatic updates enabled for testing")
|
||||||
#else
|
#else
|
||||||
// Enable automatic checking for updates
|
// Enable automatic checking for updates
|
||||||
updater.automaticallyChecksForUpdates = true
|
updater.automaticallyChecksForUpdates = true
|
||||||
|
|
||||||
// Enable automatic downloading of updates
|
// Enable automatic downloading of updates
|
||||||
updater.automaticallyDownloadsUpdates = true
|
updater.automaticallyDownloadsUpdates = true
|
||||||
|
|
||||||
// Set update check interval to 24 hours
|
// Set update check interval to 24 hours
|
||||||
updater.updateCheckInterval = 86_400
|
updater.updateCheckInterval = 86_400
|
||||||
|
|
||||||
logger.info("Sparkle updater initialized successfully with automatic downloads enabled")
|
logger.info("Sparkle updater initialized successfully with automatic downloads enabled")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Start the updater for both debug and release builds
|
// Start the updater for both debug and release builds
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ final class SparkleUserDriverDelegate: NSObject, @preconcurrency SPUStandardUser
|
||||||
_ update: SUAppcastItem,
|
_ update: SUAppcastItem,
|
||||||
andInImmediateFocus immediateFocus: Bool
|
andInImmediateFocus immediateFocus: Bool
|
||||||
)
|
)
|
||||||
-> Bool
|
-> Bool
|
||||||
{
|
{
|
||||||
logger.info("Should handle showing update: \(update.displayVersionString), immediate: \(immediateFocus)")
|
logger.info("Should handle showing update: \(update.displayVersionString), immediate: \(immediateFocus)")
|
||||||
|
|
||||||
|
|
@ -211,7 +211,7 @@ final class SparkleUserDriverDelegate: NSObject, @preconcurrency SPUStandardUser
|
||||||
|
|
||||||
case "LATER_ACTION":
|
case "LATER_ACTION":
|
||||||
logger.info("User tapped 'Remind Me Later' in notification")
|
logger.info("User tapped 'Remind Me Later' in notification")
|
||||||
// The next reminder is already scheduled
|
// The next reminder is already scheduled
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ actor TerminalManager {
|
||||||
seconds: TimeInterval,
|
seconds: TimeInterval,
|
||||||
operation: @escaping @Sendable () async throws -> T
|
operation: @escaping @Sendable () async throws -> T
|
||||||
)
|
)
|
||||||
async throws -> T
|
async throws -> T
|
||||||
{
|
{
|
||||||
try await withThrowingTaskGroup(of: T.self) { group in
|
try await withThrowingTaskGroup(of: T.self) { group in
|
||||||
group.addTask {
|
group.addTask {
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ final class WindowTracker {
|
||||||
tabReference: String?,
|
tabReference: String?,
|
||||||
tabID: String?
|
tabID: String?
|
||||||
)
|
)
|
||||||
-> WindowInfo?
|
-> WindowInfo?
|
||||||
{
|
{
|
||||||
let allWindows = Self.getAllTerminalWindows()
|
let allWindows = Self.getAllTerminalWindows()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ extension View {
|
||||||
error: Binding<Error?>,
|
error: Binding<Error?>,
|
||||||
onDismiss: (() -> Void)? = nil
|
onDismiss: (() -> Void)? = nil
|
||||||
)
|
)
|
||||||
-> some View
|
-> some View
|
||||||
{
|
{
|
||||||
modifier(ErrorAlertModifier(error: error, title: title, onDismiss: onDismiss))
|
modifier(ErrorAlertModifier(error: error, title: title, onDismiss: onDismiss))
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +49,7 @@ extension Task where Failure == Error {
|
||||||
errorBinding: Binding<Error?>,
|
errorBinding: Binding<Error?>,
|
||||||
operation: @escaping () async throws -> T
|
operation: @escaping () async throws -> T
|
||||||
)
|
)
|
||||||
-> Task<T, Error>
|
-> Task<T, Error>
|
||||||
{
|
{
|
||||||
Task<T, Error>(priority: priority) {
|
Task<T, Error>(priority: priority) {
|
||||||
do {
|
do {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ extension View {
|
||||||
horizontal: CGFloat = 16,
|
horizontal: CGFloat = 16,
|
||||||
vertical: CGFloat = 14
|
vertical: CGFloat = 14
|
||||||
)
|
)
|
||||||
-> some View
|
-> some View
|
||||||
{
|
{
|
||||||
self
|
self
|
||||||
.padding(.horizontal, horizontal)
|
.padding(.horizontal, horizontal)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ struct MenuBarView: View {
|
||||||
// Show Tutorial
|
// Show Tutorial
|
||||||
Button(action: {
|
Button(action: {
|
||||||
#if !SWIFT_PACKAGE
|
#if !SWIFT_PACKAGE
|
||||||
AppDelegate.showWelcomeScreen()
|
AppDelegate.showWelcomeScreen()
|
||||||
#endif
|
#endif
|
||||||
}, label: {
|
}, label: {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
||||||
|
|
@ -327,7 +327,7 @@ private struct DeveloperToolsSection: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Show Welcome") {
|
Button("Show Welcome") {
|
||||||
#if !SWIFT_PACKAGE
|
#if !SWIFT_PACKAGE
|
||||||
AppDelegate.showWelcomeScreen()
|
AppDelegate.showWelcomeScreen()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ struct GlowingAppIcon: View {
|
||||||
enableInteraction: true,
|
enableInteraction: true,
|
||||||
glowIntensity: 0.3
|
glowIntensity: 0.3
|
||||||
) {
|
) {
|
||||||
print("Icon clicked!")
|
// Icon clicked - action handled here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ struct ProtectDashboardPageView: View {
|
||||||
|
|
||||||
// When password is set for the first time, automatically switch to network mode
|
// When password is set for the first time, automatically switch to network mode
|
||||||
let currentMode = DashboardAccessMode(rawValue: UserDefaults.standard
|
let currentMode = DashboardAccessMode(rawValue: UserDefaults.standard
|
||||||
.string(forKey: "dashboardAccessMode") ?? ""
|
.string(forKey: "dashboardAccessMode") ?? ""
|
||||||
) ?? .localhost
|
) ?? .localhost
|
||||||
if currentMode == .localhost {
|
if currentMode == .localhost {
|
||||||
UserDefaults.standard.set(DashboardAccessMode.network.rawValue, forKey: "dashboardAccessMode")
|
UserDefaults.standard.set(DashboardAccessMode.network.rawValue, forKey: "dashboardAccessMode")
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,8 @@ final class ApplicationMover {
|
||||||
logger.debug("ApplicationMover: hdiutil returned \(data.count) bytes")
|
logger.debug("ApplicationMover: hdiutil returned \(data.count) bytes")
|
||||||
|
|
||||||
guard let plist = try PropertyListSerialization
|
guard let plist = try PropertyListSerialization
|
||||||
.propertyList(from: data, options: [], format: nil) as? [String: Any],
|
.propertyList(from: data, options: [], format: nil) as? [String: Any],
|
||||||
let images = plist["images"] as? [[String: Any]]
|
let images = plist["images"] as? [[String: Any]]
|
||||||
else {
|
else {
|
||||||
logger.debug("ApplicationMover: No disk images found in hdiutil output")
|
logger.debug("ApplicationMover: No disk images found in hdiutil output")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ final class TerminalLauncher {
|
||||||
var runningTerminals: [Terminal] = []
|
var runningTerminals: [Terminal] = []
|
||||||
|
|
||||||
for terminal in Terminal.allCases
|
for terminal in Terminal.allCases
|
||||||
where runningApps.contains(where: { $0.bundleIdentifier == terminal.bundleIdentifier })
|
where runningApps.contains(where: { $0.bundleIdentifier == terminal.bundleIdentifier })
|
||||||
{
|
{
|
||||||
runningTerminals.append(terminal)
|
runningTerminals.append(terminal)
|
||||||
logger.debug("Detected running terminal: \(terminal.rawValue)")
|
logger.debug("Detected running terminal: \(terminal.rawValue)")
|
||||||
|
|
@ -416,7 +416,7 @@ final class TerminalLauncher {
|
||||||
_ config: TerminalLaunchConfig,
|
_ config: TerminalLaunchConfig,
|
||||||
sessionId: String? = nil
|
sessionId: String? = nil
|
||||||
)
|
)
|
||||||
throws -> TerminalLaunchResult
|
throws -> TerminalLaunchResult
|
||||||
{
|
{
|
||||||
logger.debug("Launch config - command: \(config.command)")
|
logger.debug("Launch config - command: \(config.command)")
|
||||||
logger.debug("Launch config - fullCommand: \(config.fullCommand)")
|
logger.debug("Launch config - fullCommand: \(config.fullCommand)")
|
||||||
|
|
@ -519,7 +519,7 @@ final class TerminalLauncher {
|
||||||
|
|
||||||
if process.terminationStatus != 0 {
|
if process.terminationStatus != 0 {
|
||||||
throw TerminalLauncherError
|
throw TerminalLauncherError
|
||||||
.processLaunchFailed("Process exited with status \(process.terminationStatus)")
|
.processLaunchFailed("Process exited with status \(process.terminationStatus)")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to launch terminal: \(error.localizedDescription)")
|
logger.error("Failed to launch terminal: \(error.localizedDescription)")
|
||||||
|
|
@ -676,7 +676,7 @@ final class TerminalLauncher {
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
vibetunnelPath: String? = nil
|
vibetunnelPath: String? = nil
|
||||||
)
|
)
|
||||||
throws
|
throws
|
||||||
{
|
{
|
||||||
// Expand tilde in working directory path
|
// Expand tilde in working directory path
|
||||||
let expandedWorkingDir = (workingDirectory as NSString).expandingTildeInPath
|
let expandedWorkingDir = (workingDirectory as NSString).expandingTildeInPath
|
||||||
|
|
@ -801,7 +801,7 @@ final class TerminalLauncher {
|
||||||
workingDir: String,
|
workingDir: String,
|
||||||
sessionId: String? = nil
|
sessionId: String? = nil
|
||||||
)
|
)
|
||||||
-> String
|
-> String
|
||||||
{
|
{
|
||||||
// Bun executable has fwd command built-in
|
// Bun executable has fwd command built-in
|
||||||
logger.info("Using Bun executable for session creation")
|
logger.info("Using Bun executable for session creation")
|
||||||
|
|
|
||||||
|
|
@ -21,81 +21,81 @@ struct VibeTunnelApp: App {
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// Hidden WindowGroup to make Settings work in MenuBarExtra-only apps
|
// Hidden WindowGroup to make Settings work in MenuBarExtra-only apps
|
||||||
// This is a workaround for FB10184971
|
// This is a workaround for FB10184971
|
||||||
WindowGroup("HiddenWindow") {
|
WindowGroup("HiddenWindow") {
|
||||||
HiddenWindowView()
|
HiddenWindowView()
|
||||||
}
|
}
|
||||||
.windowResizability(.contentSize)
|
.windowResizability(.contentSize)
|
||||||
.defaultSize(width: 1, height: 1)
|
.defaultSize(width: 1, height: 1)
|
||||||
.windowStyle(.hiddenTitleBar)
|
.windowStyle(.hiddenTitleBar)
|
||||||
|
|
||||||
// Welcome Window
|
// Welcome Window
|
||||||
WindowGroup("Welcome", id: "welcome") {
|
WindowGroup("Welcome", id: "welcome") {
|
||||||
WelcomeView()
|
WelcomeView()
|
||||||
|
.environment(sessionMonitor)
|
||||||
|
.environment(serverManager)
|
||||||
|
.environment(ngrokService)
|
||||||
|
.environment(permissionManager)
|
||||||
|
.environment(terminalLauncher)
|
||||||
|
}
|
||||||
|
.windowResizability(.contentSize)
|
||||||
|
.defaultSize(width: 580, height: 480)
|
||||||
|
.windowStyle(.hiddenTitleBar)
|
||||||
|
|
||||||
|
// Session Detail Window
|
||||||
|
WindowGroup("Session Details", id: "session-detail", for: String.self) { $sessionId in
|
||||||
|
if let sessionId,
|
||||||
|
let session = sessionMonitor.sessions[sessionId]
|
||||||
|
{
|
||||||
|
SessionDetailView(session: session)
|
||||||
.environment(sessionMonitor)
|
.environment(sessionMonitor)
|
||||||
.environment(serverManager)
|
.environment(serverManager)
|
||||||
.environment(ngrokService)
|
.environment(ngrokService)
|
||||||
.environment(permissionManager)
|
.environment(permissionManager)
|
||||||
.environment(terminalLauncher)
|
.environment(terminalLauncher)
|
||||||
|
} else {
|
||||||
|
Text("Session not found")
|
||||||
|
.frame(width: 400, height: 300)
|
||||||
}
|
}
|
||||||
.windowResizability(.contentSize)
|
}
|
||||||
.defaultSize(width: 580, height: 480)
|
.windowResizability(.contentSize)
|
||||||
.windowStyle(.hiddenTitleBar)
|
|
||||||
|
|
||||||
// Session Detail Window
|
Settings {
|
||||||
WindowGroup("Session Details", id: "session-detail", for: String.self) { $sessionId in
|
SettingsView()
|
||||||
if let sessionId,
|
.environment(sessionMonitor)
|
||||||
let session = sessionMonitor.sessions[sessionId]
|
.environment(serverManager)
|
||||||
{
|
.environment(ngrokService)
|
||||||
SessionDetailView(session: session)
|
.environment(permissionManager)
|
||||||
.environment(sessionMonitor)
|
.environment(terminalLauncher)
|
||||||
.environment(serverManager)
|
}
|
||||||
.environment(ngrokService)
|
.commands {
|
||||||
.environment(permissionManager)
|
CommandGroup(after: .appInfo) {
|
||||||
.environment(terminalLauncher)
|
Button("About VibeTunnel") {
|
||||||
} else {
|
SettingsOpener.openSettings()
|
||||||
Text("Session not found")
|
// Navigate to About tab after settings opens
|
||||||
.frame(width: 400, height: 300)
|
Task {
|
||||||
}
|
try? await Task.sleep(for: .milliseconds(100))
|
||||||
}
|
NotificationCenter.default.post(
|
||||||
.windowResizability(.contentSize)
|
name: .openSettingsTab,
|
||||||
|
object: SettingsTab.about
|
||||||
Settings {
|
)
|
||||||
SettingsView()
|
|
||||||
.environment(sessionMonitor)
|
|
||||||
.environment(serverManager)
|
|
||||||
.environment(ngrokService)
|
|
||||||
.environment(permissionManager)
|
|
||||||
.environment(terminalLauncher)
|
|
||||||
}
|
|
||||||
.commands {
|
|
||||||
CommandGroup(after: .appInfo) {
|
|
||||||
Button("About VibeTunnel") {
|
|
||||||
SettingsOpener.openSettings()
|
|
||||||
// Navigate to About tab after settings opens
|
|
||||||
Task {
|
|
||||||
try? await Task.sleep(for: .milliseconds(100))
|
|
||||||
NotificationCenter.default.post(
|
|
||||||
name: .openSettingsTab,
|
|
||||||
object: SettingsTab.about
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MenuBarExtra {
|
MenuBarExtra {
|
||||||
MenuBarView()
|
MenuBarView()
|
||||||
.environment(sessionMonitor)
|
.environment(sessionMonitor)
|
||||||
.environment(serverManager)
|
.environment(serverManager)
|
||||||
.environment(ngrokService)
|
.environment(ngrokService)
|
||||||
.environment(permissionManager)
|
.environment(permissionManager)
|
||||||
.environment(terminalLauncher)
|
.environment(terminalLauncher)
|
||||||
} label: {
|
} label: {
|
||||||
Image("menubar")
|
Image("menubar")
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -121,25 +121,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
|
||||||
NSClassFromString("XCTestCase") != nil
|
NSClassFromString("XCTestCase") != nil
|
||||||
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
let isRunningInDebug = true
|
let isRunningInDebug = true
|
||||||
#else
|
#else
|
||||||
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?
|
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?
|
||||||
.contains("libMainThreadChecker.dylib") ?? false ||
|
.contains("libMainThreadChecker.dylib") ?? false ||
|
||||||
processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil
|
processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Handle single instance check before doing anything else
|
// Handle single instance check before doing anything else
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// Skip single instance check in debug builds
|
// Skip single instance check in debug builds
|
||||||
#else
|
#else
|
||||||
if !isRunningInPreview && !isRunningInTests && !isRunningInDebug {
|
if !isRunningInPreview && !isRunningInTests && !isRunningInDebug {
|
||||||
handleSingleInstanceCheck()
|
handleSingleInstanceCheck()
|
||||||
registerForDistributedNotifications()
|
registerForDistributedNotifications()
|
||||||
|
|
||||||
// Check if app needs to be moved to Applications folder
|
// Check if app needs to be moved to Applications folder
|
||||||
let applicationMover = ApplicationMover()
|
let applicationMover = ApplicationMover()
|
||||||
applicationMover.checkAndOfferToMoveToApplications()
|
applicationMover.checkAndOfferToMoveToApplications()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Initialize Sparkle updater manager
|
// Initialize Sparkle updater manager
|
||||||
|
|
@ -340,11 +340,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
|
||||||
NSClassFromString("XCTestCase") != nil
|
NSClassFromString("XCTestCase") != nil
|
||||||
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
let isRunningInDebug = true
|
let isRunningInDebug = true
|
||||||
#else
|
#else
|
||||||
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?
|
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?
|
||||||
.contains("libMainThreadChecker.dylib") ?? false ||
|
.contains("libMainThreadChecker.dylib") ?? false ||
|
||||||
processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil
|
processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Skip cleanup during tests
|
// Skip cleanup during tests
|
||||||
|
|
@ -374,13 +374,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// Skip removing observer in debug builds
|
// Skip removing observer in debug builds
|
||||||
#else
|
#else
|
||||||
if !isRunningInPreview && !isRunningInTests && !isRunningInDebug {
|
if !isRunningInPreview && !isRunningInTests && !isRunningInDebug {
|
||||||
DistributedNotificationCenter.default().removeObserver(
|
DistributedNotificationCenter.default().removeObserver(
|
||||||
self,
|
self,
|
||||||
name: Self.showSettingsNotification,
|
name: Self.showSettingsNotification,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Remove update check notification observer
|
// Remove update check notification observer
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue