mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
show welcome dialog on update + lint
This commit is contained in:
parent
d9d134ff2b
commit
daf455ec9a
17 changed files with 445 additions and 369 deletions
13
VibeTunnel/Core/Models/AppConstants.swift
Normal file
13
VibeTunnel/Core/Models/AppConstants.swift
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import Foundation
|
||||
|
||||
/// Central location for app-wide constants and configuration values
|
||||
enum AppConstants {
|
||||
/// Current version of the welcome dialog
|
||||
/// Increment this when significant changes require re-showing the welcome flow
|
||||
static let currentWelcomeVersion = 2
|
||||
|
||||
/// UserDefaults keys
|
||||
enum UserDefaultsKeys {
|
||||
static let welcomeVersion = "welcomeVersion"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import AppKit
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
/// Manages Accessibility permissions required for sending keystrokes.
|
||||
|
|
@ -39,7 +39,9 @@ final class AccessibilityPermissionManager {
|
|||
logger.info("Accessibility permission dialog triggered")
|
||||
// After a short delay, also open System Settings as a fallback
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
||||
if let url =
|
||||
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")
|
||||
{
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
@preconcurrency import AppKit
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
/// Sendable wrapper for NSAppleEventDescriptor
|
||||
|
|
@ -49,7 +49,7 @@ final class AppleScriptExecutor {
|
|||
|
||||
let result = scriptObject.executeAndReturnError(&error)
|
||||
|
||||
if let error = error {
|
||||
if let error {
|
||||
let errorMessage = error["NSAppleScriptErrorMessage"] as? String ?? "Unknown error"
|
||||
let errorNumber = error["NSAppleScriptErrorNumber"] as? Int
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ final class AppleScriptExecutor {
|
|||
|
||||
DispatchQueue.main.sync {
|
||||
do {
|
||||
result = .success(try execute(script, timeout: timeout))
|
||||
result = try .success(execute(script, timeout: timeout))
|
||||
} catch {
|
||||
result = .failure(error)
|
||||
}
|
||||
|
|
@ -147,7 +147,7 @@ final class AppleScriptExecutor {
|
|||
|
||||
let result = scriptObject.executeAndReturnError(&error)
|
||||
|
||||
if let error = error {
|
||||
if let error {
|
||||
let errorMessage = error["NSAppleScriptErrorMessage"] as? String ?? "Unknown error"
|
||||
let errorNumber = error["NSAppleScriptErrorNumber"] as? Int
|
||||
|
||||
|
|
@ -225,17 +225,17 @@ enum AppleScriptError: LocalizedError {
|
|||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .scriptCreationFailed:
|
||||
return "Failed to create AppleScript object"
|
||||
"Failed to create AppleScript object"
|
||||
case .executionFailed(let message, let errorCode):
|
||||
if let code = errorCode {
|
||||
return "AppleScript error \(code): \(message)"
|
||||
"AppleScript error \(code): \(message)"
|
||||
} else {
|
||||
return "AppleScript error: \(message)"
|
||||
"AppleScript error: \(message)"
|
||||
}
|
||||
case .permissionDenied:
|
||||
return "AppleScript permission denied. Please grant permission in System Settings."
|
||||
"AppleScript permission denied. Please grant permission in System Settings."
|
||||
case .timeout:
|
||||
return "AppleScript execution timed out"
|
||||
"AppleScript execution timed out"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,13 +246,13 @@ enum AppleScriptError: LocalizedError {
|
|||
case .executionFailed(_, let errorCode):
|
||||
if let code = errorCode {
|
||||
switch code {
|
||||
case -1743:
|
||||
case -1_743:
|
||||
return "User permission is required to control other applications."
|
||||
case -1728:
|
||||
case -1_728:
|
||||
return "The application is not running or cannot be controlled."
|
||||
case -1708:
|
||||
case -1_708:
|
||||
return "The event was not handled by the target application."
|
||||
case -2741:
|
||||
case -2_741:
|
||||
return "AppleScript syntax error - check for unescaped quotes or invalid identifiers."
|
||||
default:
|
||||
return nil
|
||||
|
|
@ -268,11 +268,11 @@ enum AppleScriptError: LocalizedError {
|
|||
var isPermissionError: Bool {
|
||||
switch self {
|
||||
case .permissionDenied:
|
||||
return true
|
||||
true
|
||||
case .executionFailed(_, let errorCode):
|
||||
return errorCode == -1743
|
||||
errorCode == -1_743
|
||||
default:
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
/// Manages AppleScript automation permissions for VibeTunnel.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ final class DashboardKeychain {
|
|||
func getPassword() -> String? {
|
||||
#if DEBUG
|
||||
// In debug builds, skip keychain access to avoid authorization dialogs
|
||||
logger.info("Debug mode: Skipping keychain password retrieval. Password will only persist during current app session.")
|
||||
logger
|
||||
.info(
|
||||
"Debug mode: Skipping keychain password retrieval. Password will only persist during current app session."
|
||||
)
|
||||
return nil
|
||||
#else
|
||||
let query: [String: Any] = [
|
||||
|
|
@ -101,7 +104,10 @@ final class DashboardKeychain {
|
|||
|
||||
#if DEBUG
|
||||
if success {
|
||||
logger.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.")
|
||||
logger
|
||||
.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."
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ final class TerminalSpawnService: @unchecked Sendable {
|
|||
defer { close(clientSocket) }
|
||||
|
||||
// Read request data
|
||||
var buffer = [UInt8](repeating: 0, count: 65536)
|
||||
var buffer = [UInt8](repeating: 0, count: 65_536)
|
||||
let bytesRead = recv(clientSocket, &buffer, buffer.count, 0)
|
||||
|
||||
guard bytesRead > 0 else {
|
||||
|
|
|
|||
|
|
@ -716,7 +716,7 @@ public final class TunnelServer {
|
|||
let command: [String]
|
||||
let workingDir: String?
|
||||
let term: String?
|
||||
let spawn_terminal: Bool?
|
||||
let spawnTerminal: Bool?
|
||||
}
|
||||
|
||||
let sessionRequest = try JSONDecoder().decode(CreateSessionRequest.self, from: requestData)
|
||||
|
|
@ -726,11 +726,14 @@ public final class TunnelServer {
|
|||
}
|
||||
|
||||
// Handle terminal spawning if requested
|
||||
if sessionRequest.spawn_terminal ?? false {
|
||||
if sessionRequest.spawnTerminal ?? false {
|
||||
logger.info("Spawn terminal requested for command: \(sessionRequest.command.joined(separator: " "))")
|
||||
|
||||
let sessionId = UUID().uuidString
|
||||
let workingDir = resolvePath(sessionRequest.workingDir ?? "~/", fallback: FileManager.default.homeDirectoryForCurrentUser.path)
|
||||
let workingDir = resolvePath(
|
||||
sessionRequest.workingDir ?? "~/",
|
||||
fallback: FileManager.default.homeDirectoryForCurrentUser.path
|
||||
)
|
||||
_ = sessionRequest.command.joined(separator: " ")
|
||||
|
||||
// Connect to the terminal spawn service via Unix socket
|
||||
|
|
@ -739,7 +742,10 @@ public final class TunnelServer {
|
|||
|
||||
guard socketFd >= 0 else {
|
||||
logger.error("Failed to create socket")
|
||||
return errorResponse(message: "Failed to create socket for terminal spawning", status: .internalServerError)
|
||||
return errorResponse(
|
||||
message: "Failed to create socket for terminal spawning",
|
||||
status: .internalServerError
|
||||
)
|
||||
}
|
||||
|
||||
defer { close(socketFd) }
|
||||
|
|
@ -778,18 +784,31 @@ public final class TunnelServer {
|
|||
|
||||
do {
|
||||
let requestData = try JSONEncoder().encode(spawnRequest)
|
||||
let sendResult = send(socketFd, requestData.withUnsafeBytes { $0.baseAddress }, requestData.count, 0)
|
||||
let sendResult = send(
|
||||
socketFd,
|
||||
requestData.withUnsafeBytes { $0.baseAddress },
|
||||
requestData.count,
|
||||
0
|
||||
)
|
||||
|
||||
guard sendResult == requestData.count else {
|
||||
throw NSError(domain: "TerminalSpawn", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to send complete request"])
|
||||
throw NSError(
|
||||
domain: "TerminalSpawn",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to send complete request"]
|
||||
)
|
||||
}
|
||||
|
||||
// Read the response
|
||||
var responseBuffer = [UInt8](repeating: 0, count: 4096)
|
||||
var responseBuffer = [UInt8](repeating: 0, count: 4_096)
|
||||
let bytesRead = recv(socketFd, &responseBuffer, responseBuffer.count, 0)
|
||||
|
||||
guard bytesRead > 0 else {
|
||||
throw NSError(domain: "TerminalSpawn", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to read response"])
|
||||
throw NSError(
|
||||
domain: "TerminalSpawn",
|
||||
code: 2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to read response"]
|
||||
)
|
||||
}
|
||||
|
||||
let responseData = Data(bytes: responseBuffer, count: bytesRead)
|
||||
|
|
@ -827,11 +846,17 @@ public final class TunnelServer {
|
|||
} else {
|
||||
let errorMsg = spawnResponse.error ?? "Unknown error"
|
||||
logger.error("Failed to spawn terminal: \(errorMsg)")
|
||||
return errorResponse(message: "Failed to spawn terminal: \(errorMsg)", status: .internalServerError)
|
||||
return errorResponse(
|
||||
message: "Failed to spawn terminal: \(errorMsg)",
|
||||
status: .internalServerError
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to communicate with terminal spawn service: \(error)")
|
||||
return errorResponse(message: "Failed to spawn terminal: \(error.localizedDescription)", status: .internalServerError)
|
||||
return errorResponse(
|
||||
message: "Failed to spawn terminal: \(error.localizedDescription)",
|
||||
status: .internalServerError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -171,28 +171,35 @@ private struct TerminalPreferenceSection: View {
|
|||
switch terminalError {
|
||||
case .appleScriptPermissionDenied:
|
||||
errorTitle = "Permission Denied"
|
||||
errorMessage = "VibeTunnel needs permission to control terminal applications.\n\nPlease grant Automation permission in System Settings > Privacy & Security > Automation."
|
||||
errorMessage =
|
||||
"VibeTunnel needs permission to control terminal applications.\n\nPlease grant Automation permission in System Settings > Privacy & Security > Automation."
|
||||
case .accessibilityPermissionDenied:
|
||||
errorTitle = "Accessibility Permission Required"
|
||||
errorMessage = "VibeTunnel needs Accessibility permission to send keystrokes to \(Terminal(rawValue: preferredTerminal)?.displayName ?? "terminal").\n\nPlease grant permission in System Settings > Privacy & Security > Accessibility."
|
||||
errorMessage =
|
||||
"VibeTunnel needs Accessibility permission to send keystrokes to \(Terminal(rawValue: preferredTerminal)?.displayName ?? "terminal").\n\nPlease grant permission in System Settings > Privacy & Security > Accessibility."
|
||||
case .terminalNotFound:
|
||||
errorTitle = "Terminal Not Found"
|
||||
errorMessage = "The selected terminal application could not be found. Please select a different terminal."
|
||||
errorMessage =
|
||||
"The selected terminal application could not be found. Please select a different terminal."
|
||||
case .appleScriptExecutionFailed(let details, let errorCode):
|
||||
if let code = errorCode {
|
||||
switch code {
|
||||
case -1_743:
|
||||
errorTitle = "Permission Denied"
|
||||
errorMessage = "VibeTunnel needs permission to control terminal applications.\n\nPlease grant Automation permission in System Settings > Privacy & Security > Automation."
|
||||
errorMessage =
|
||||
"VibeTunnel needs permission to control terminal applications.\n\nPlease grant Automation permission in System Settings > Privacy & Security > Automation."
|
||||
case -1_728:
|
||||
errorTitle = "Terminal Not Available"
|
||||
errorMessage = "The terminal application is not running or cannot be controlled.\n\nDetails: \(details)"
|
||||
errorMessage =
|
||||
"The terminal application is not running or cannot be controlled.\n\nDetails: \(details)"
|
||||
case -1_708:
|
||||
errorTitle = "Terminal Communication Error"
|
||||
errorMessage = "The terminal did not respond to the command.\n\nDetails: \(details)"
|
||||
case -25211:
|
||||
errorMessage =
|
||||
"The terminal did not respond to the command.\n\nDetails: \(details)"
|
||||
case -25_211:
|
||||
errorTitle = "Accessibility Permission Required"
|
||||
errorMessage = "System Events requires Accessibility permission to send keystrokes.\n\nPlease grant permission in System Settings > Privacy & Security > Accessibility."
|
||||
errorMessage =
|
||||
"System Events requires Accessibility permission to send keystrokes.\n\nPlease grant permission in System Settings > Privacy & Security > Accessibility."
|
||||
default:
|
||||
errorTitle = "Terminal Launch Failed"
|
||||
errorMessage = "AppleScript error \(code): \(details)"
|
||||
|
|
@ -235,7 +242,9 @@ private struct TerminalPreferenceSection: View {
|
|||
Button("OK") {}
|
||||
if errorTitle == "Permission Denied" {
|
||||
Button("Open System Settings") {
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation") {
|
||||
if let url =
|
||||
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation")
|
||||
{
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -822,4 +822,3 @@ private struct NgrokErrorView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -369,7 +369,9 @@ private struct ServerSection: View {
|
|||
Text("HTTP Server")
|
||||
.font(.headline)
|
||||
} footer: {
|
||||
Text("The HTTP server provides REST API endpoints for terminal session management. Choose between the built-in Swift Hummingbird server or the Rust tty-fwd binary.")
|
||||
Text(
|
||||
"The HTTP server provides REST API endpoints for terminal session management. Choose between the built-in Swift Hummingbird server or the Rust tty-fwd binary."
|
||||
)
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -609,4 +611,3 @@ private struct DeveloperToolsSection: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import SwiftUI
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
/// General settings tab for basic app preferences
|
||||
struct GeneralSettingsView: View {
|
||||
|
|
@ -210,7 +210,9 @@ private struct PermissionsSection: View {
|
|||
Text("Permissions")
|
||||
.font(.headline)
|
||||
} footer: {
|
||||
Text("Terminal automation is required for all terminals. Accessibility is only needed for terminals that simulate keyboard input.")
|
||||
Text(
|
||||
"Terminal automation is required for all terminals. Accessibility is only needed for terminals that simulate keyboard input."
|
||||
)
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ struct WelcomeView: View {
|
|||
@State private var currentPage = 0
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
@AppStorage("hasSeenWelcome")
|
||||
private var hasSeenWelcome = false
|
||||
@AppStorage(AppConstants.UserDefaultsKeys.welcomeVersion)
|
||||
private var welcomeVersion = 0
|
||||
@State private var cliInstaller = CLIInstaller()
|
||||
@StateObject private var permissionManager = AppleScriptPermissionManager.shared
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ struct WelcomeView: View {
|
|||
}
|
||||
} else {
|
||||
// Finish action - open Settings
|
||||
hasSeenWelcome = true
|
||||
welcomeVersion = AppConstants.currentWelcomeVersion
|
||||
dismiss()
|
||||
SettingsOpener.openSettings()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import AppKit
|
||||
import Foundation
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import os.log
|
||||
import SwiftUI
|
||||
|
||||
/// Service responsible for creating symlinks to command line tools with sudo authentication.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -88,7 +88,9 @@ enum SettingsOpener {
|
|||
guard let window = notification.object as? NSWindow else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
guard window.title.contains("Settings") || window.identifier?.rawValue.contains(settingsWindowIdentifier) == true else {
|
||||
guard window.title.contains("Settings") || window.identifier?.rawValue
|
||||
.contains(settingsWindowIdentifier) == true
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +110,7 @@ enum SettingsOpener {
|
|||
/// Finds the settings window using multiple detection methods
|
||||
static func findSettingsWindow() -> NSWindow? {
|
||||
// Try multiple methods to find the window
|
||||
return NSApp.windows.first { window in
|
||||
NSApp.windows.first { window in
|
||||
// Check by identifier
|
||||
if window.identifier?.rawValue == settingsWindowIdentifier {
|
||||
return true
|
||||
|
|
@ -117,13 +119,16 @@ enum SettingsOpener {
|
|||
// Check by title
|
||||
if window.isVisible && window.styleMask.contains(.titled) &&
|
||||
(window.title.localizedCaseInsensitiveContains("settings") ||
|
||||
window.title.localizedCaseInsensitiveContains("preferences")) {
|
||||
window.title.localizedCaseInsensitiveContains("preferences")
|
||||
)
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
// Check by content view controller type
|
||||
if let contentVC = window.contentViewController,
|
||||
String(describing: type(of: contentVC)).contains("Settings") {
|
||||
String(describing: type(of: contentVC)).contains("Settings")
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -131,8 +136,6 @@ enum SettingsOpener {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Opens the Settings window and navigates to a specific tab
|
||||
static func openSettingsTab(_ tab: SettingsTab) {
|
||||
openSettings()
|
||||
|
|
@ -168,4 +171,3 @@ struct HiddenWindowView: View {
|
|||
extension Notification.Name {
|
||||
static let openSettingsRequest = Notification.Name("openSettingsRequest")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import AppKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import os.log
|
||||
import SwiftUI
|
||||
|
||||
/// Terminal launch configuration
|
||||
struct TerminalLaunchConfig {
|
||||
|
|
@ -10,7 +10,7 @@ struct TerminalLaunchConfig {
|
|||
let terminal: Terminal
|
||||
|
||||
var fullCommand: String {
|
||||
guard let workingDirectory = workingDirectory else {
|
||||
guard let workingDirectory else {
|
||||
return command
|
||||
}
|
||||
let escapedDir = workingDirectory.replacingOccurrences(of: "\"", with: "\\\"")
|
||||
|
|
@ -85,13 +85,13 @@ enum Terminal: String, CaseIterable {
|
|||
/// Priority for auto-detection (higher is better, based on popularity)
|
||||
var detectionPriority: Int {
|
||||
switch self {
|
||||
case .terminal: return 100 // Highest - macOS default, most popular
|
||||
case .iTerm2: return 95 // Very popular among developers
|
||||
case .warp: return 85 // Popular modern terminal
|
||||
case .ghostty: return 80 // New but gaining popularity
|
||||
case .alacritty: return 70 // Popular among power users
|
||||
case .wezterm: return 60 // Less common but powerful
|
||||
case .hyper: return 50 // Less popular Electron-based
|
||||
case .terminal: 100 // Highest - macOS default, most popular
|
||||
case .iTerm2: 95 // Very popular among developers
|
||||
case .warp: 85 // Popular modern terminal
|
||||
case .ghostty: 80 // New but gaining popularity
|
||||
case .alacritty: 70 // Popular among power users
|
||||
case .wezterm: 60 // Less common but powerful
|
||||
case .hyper: 50 // Less popular Electron-based
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,51 +155,51 @@ enum Terminal: String, CaseIterable {
|
|||
switch self {
|
||||
case .terminal:
|
||||
// Use unified AppleScript approach for consistency
|
||||
return .appleScript(script: unifiedAppleScript(for: config))
|
||||
.appleScript(script: unifiedAppleScript(for: config))
|
||||
|
||||
case .iTerm2:
|
||||
// Use unified AppleScript approach for consistency
|
||||
return .appleScript(script: unifiedAppleScript(for: config))
|
||||
.appleScript(script: unifiedAppleScript(for: config))
|
||||
|
||||
case .ghostty:
|
||||
// Use unified AppleScript approach
|
||||
return .appleScript(script: unifiedAppleScript(for: config))
|
||||
.appleScript(script: unifiedAppleScript(for: config))
|
||||
|
||||
case .alacritty:
|
||||
// Use unified AppleScript approach for consistency
|
||||
return .appleScript(script: unifiedAppleScript(for: config))
|
||||
.appleScript(script: unifiedAppleScript(for: config))
|
||||
|
||||
case .warp:
|
||||
// Use unified AppleScript approach
|
||||
return .appleScript(script: unifiedAppleScript(for: config))
|
||||
.appleScript(script: unifiedAppleScript(for: config))
|
||||
|
||||
case .hyper:
|
||||
// Use unified AppleScript approach
|
||||
return .appleScript(script: unifiedAppleScript(for: config))
|
||||
.appleScript(script: unifiedAppleScript(for: config))
|
||||
|
||||
case .wezterm:
|
||||
// Use unified AppleScript approach for consistency
|
||||
return .appleScript(script: unifiedAppleScript(for: config))
|
||||
.appleScript(script: unifiedAppleScript(for: config))
|
||||
}
|
||||
}
|
||||
|
||||
/// Process name for AppleScript typing
|
||||
var processName: String {
|
||||
switch self {
|
||||
case .terminal: return "Terminal"
|
||||
case .iTerm2: return "iTerm"
|
||||
case .ghostty: return "Ghostty"
|
||||
case .warp: return "Warp"
|
||||
case .alacritty: return "Alacritty"
|
||||
case .hyper: return "Hyper"
|
||||
case .wezterm: return "WezTerm"
|
||||
case .terminal: "Terminal"
|
||||
case .iTerm2: "iTerm"
|
||||
case .ghostty: "Ghostty"
|
||||
case .warp: "Warp"
|
||||
case .alacritty: "Alacritty"
|
||||
case .hyper: "Hyper"
|
||||
case .wezterm: "WezTerm"
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this terminal requires keystroke-based input (needs Accessibility permission)
|
||||
var requiresKeystrokeInput: Bool {
|
||||
// All terminals now use keystroke-based input
|
||||
return true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,19 +217,19 @@ enum TerminalLauncherError: LocalizedError {
|
|||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .terminalNotFound:
|
||||
return "Selected terminal application not found"
|
||||
"Selected terminal application not found"
|
||||
case .appleScriptPermissionDenied:
|
||||
return "AppleScript permission denied. Please grant permission in System Settings."
|
||||
"AppleScript permission denied. Please grant permission in System Settings."
|
||||
case .accessibilityPermissionDenied:
|
||||
return "Accessibility permission required to send keystrokes. Please grant permission in System Settings."
|
||||
"Accessibility permission required to send keystrokes. Please grant permission in System Settings."
|
||||
case .appleScriptExecutionFailed(let message, let errorCode):
|
||||
if let code = errorCode {
|
||||
return "AppleScript error \(code): \(message)"
|
||||
"AppleScript error \(code): \(message)"
|
||||
} else {
|
||||
return "AppleScript error: \(message)"
|
||||
"AppleScript error: \(message)"
|
||||
}
|
||||
case .processLaunchFailed(let message):
|
||||
return "Failed to launch process: \(message)"
|
||||
"Failed to launch process: \(message)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ enum TerminalLauncherError: LocalizedError {
|
|||
return "The application is not running or cannot be controlled."
|
||||
case -1_708:
|
||||
return "The event was not handled by the target application."
|
||||
case -25211:
|
||||
case -25_211:
|
||||
return "Accessibility permission is required to send keystrokes."
|
||||
default:
|
||||
return nil
|
||||
|
|
@ -281,14 +281,12 @@ final class TerminalLauncher {
|
|||
logger.info("TerminalLauncher initialized successfully")
|
||||
}
|
||||
|
||||
|
||||
func launchCommand(_ command: String) throws {
|
||||
let terminal = getValidTerminal()
|
||||
let config = TerminalLaunchConfig(command: command, workingDirectory: nil, terminal: terminal)
|
||||
try launchWithConfig(config)
|
||||
}
|
||||
|
||||
|
||||
func verifyPreferredTerminal() {
|
||||
let terminal = Terminal(rawValue: preferredTerminal) ?? .terminal
|
||||
if !terminal.isInstalled {
|
||||
|
|
@ -313,7 +311,10 @@ final class TerminalLauncher {
|
|||
let installedTerminals = Terminal.installed.filter { $0 != .terminal }
|
||||
if let bestTerminal = installedTerminals.max(by: { $0.detectionPriority < $1.detectionPriority }) {
|
||||
preferredTerminal = bestTerminal.rawValue
|
||||
logger.info("No running terminals found, set preferred terminal to most popular installed: \(bestTerminal.rawValue)")
|
||||
logger
|
||||
.info(
|
||||
"No running terminals found, set preferred terminal to most popular installed: \(bestTerminal.rawValue)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -344,7 +345,10 @@ final class TerminalLauncher {
|
|||
if actualTerminal != terminal {
|
||||
// Update preference to fallback
|
||||
preferredTerminal = actualTerminal.rawValue
|
||||
logger.warning("Preferred terminal \(terminal.rawValue) not installed, falling back to \(actualTerminal.rawValue)")
|
||||
logger
|
||||
.warning(
|
||||
"Preferred terminal \(terminal.rawValue) not installed, falling back to \(actualTerminal.rawValue)"
|
||||
)
|
||||
}
|
||||
|
||||
return actualTerminal
|
||||
|
|
@ -414,7 +418,8 @@ final class TerminalLauncher {
|
|||
process.waitUntilExit()
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
throw TerminalLauncherError.processLaunchFailed("Process exited with status \(process.terminationStatus)")
|
||||
throw TerminalLauncherError
|
||||
.processLaunchFailed("Process exited with status \(process.terminationStatus)")
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to launch terminal: \(error.localizedDescription)")
|
||||
|
|
@ -436,15 +441,19 @@ final class TerminalLauncher {
|
|||
} catch let error as AppleScriptError {
|
||||
// Check if this is a permission error
|
||||
if case .executionFailed(_, let errorCode) = error,
|
||||
let code = errorCode {
|
||||
let code = errorCode
|
||||
{
|
||||
switch code {
|
||||
case -25211, -1719:
|
||||
case -25_211, -1_719:
|
||||
// These error codes indicate accessibility permission issues
|
||||
throw TerminalLauncherError.accessibilityPermissionDenied
|
||||
case -2741:
|
||||
case -2_741:
|
||||
// This is a syntax error: "Expected end of line but found identifier"
|
||||
// It usually means the AppleScript has unescaped quotes or other syntax issues
|
||||
throw TerminalLauncherError.appleScriptExecutionFailed("AppleScript syntax error - likely unescaped quotes in command", errorCode: code)
|
||||
throw TerminalLauncherError.appleScriptExecutionFailed(
|
||||
"AppleScript syntax error - likely unescaped quotes in command",
|
||||
errorCode: code
|
||||
)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -457,11 +466,9 @@ final class TerminalLauncher {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Terminal Session Launching
|
||||
|
||||
func launchTerminalSession(workingDirectory: String, command: String, sessionId: String) throws {
|
||||
|
||||
// Find tty-fwd binary path
|
||||
let ttyFwdPath = findTTYFwdBinary()
|
||||
|
||||
|
|
@ -473,7 +480,8 @@ final class TerminalLauncher {
|
|||
|
||||
// Construct the full command with cd && tty-fwd && exit pattern
|
||||
// tty-fwd will use TTY_SESSION_ID from environment or generate one
|
||||
let fullCommand = "cd \"\(escapedWorkingDir)\" && TTY_SESSION_ID=\"\(sessionId)\" \(ttyFwdPath) -- \(command) && exit"
|
||||
let fullCommand =
|
||||
"cd \"\(escapedWorkingDir)\" && TTY_SESSION_ID=\"\(sessionId)\" \(ttyFwdPath) -- \(command) && exit"
|
||||
|
||||
// Get the preferred terminal or fallback
|
||||
let terminal = getValidTerminal()
|
||||
|
|
@ -488,7 +496,14 @@ final class TerminalLauncher {
|
|||
}
|
||||
|
||||
/// Optimized terminal session launching that receives pre-formatted command from Rust
|
||||
func launchOptimizedTerminalSession(workingDirectory: String, command: String, sessionId: String, ttyFwdPath: String? = nil) throws {
|
||||
func launchOptimizedTerminalSession(
|
||||
workingDirectory: String,
|
||||
command: String,
|
||||
sessionId: String,
|
||||
ttyFwdPath: String? = nil
|
||||
)
|
||||
throws
|
||||
{
|
||||
// Expand tilde in working directory path
|
||||
let expandedWorkingDir = (workingDirectory as NSString).expandingTildeInPath
|
||||
|
||||
|
|
|
|||
|
|
@ -103,9 +103,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
let showInDock = UserDefaults.standard.bool(forKey: "showInDock")
|
||||
NSApp.setActivationPolicy(showInDock ? .regular : .accessory)
|
||||
|
||||
// Show welcome screen on first launch
|
||||
let hasSeenWelcome = UserDefaults.standard.bool(forKey: "hasSeenWelcome")
|
||||
if !hasSeenWelcome && !isRunningInTests && !isRunningInPreview {
|
||||
// Show welcome screen when version changes
|
||||
let storedWelcomeVersion = UserDefaults.standard.integer(forKey: AppConstants.UserDefaultsKeys.welcomeVersion)
|
||||
|
||||
// Show welcome if version is different from current
|
||||
if storedWelcomeVersion < AppConstants.currentWelcomeVersion && !isRunningInTests && !isRunningInPreview {
|
||||
showWelcomeScreen()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue