mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Add terminal logic
This commit is contained in:
parent
f4a4f6b16b
commit
3e3bca0fb2
3 changed files with 208 additions and 4 deletions
|
|
@ -65,6 +65,8 @@ struct DebugSettingsView: View {
|
||||||
logLevel: $logLevel
|
logLevel: $logLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TerminalPreferenceSection()
|
||||||
|
|
||||||
DeveloperToolsSection(
|
DeveloperToolsSection(
|
||||||
showPurgeConfirmation: $showPurgeConfirmation,
|
showPurgeConfirmation: $showPurgeConfirmation,
|
||||||
showServerConsole: showServerConsole,
|
showServerConsole: showServerConsole,
|
||||||
|
|
@ -100,7 +102,9 @@ struct DebugSettingsView: View {
|
||||||
purgeAllUserDefaults()
|
purgeAllUserDefaults()
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text("This will remove all stored preferences and reset the app to its default state. The app will quit after purging.")
|
Text(
|
||||||
|
"This will remove all stored preferences and reset the app to its default state. The app will quit after purging."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,13 +224,13 @@ struct DebugSettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getCurrentServerMode() -> String {
|
private func getCurrentServerMode() -> String {
|
||||||
// If server is switching, show transitioning state
|
// If server is switching, show transitioning state
|
||||||
if serverManager.isSwitching {
|
if serverManager.isSwitching {
|
||||||
return "Switching..."
|
return "Switching..."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always use the configured mode from settings to ensure immediate UI update
|
// Always use the configured mode from settings to ensure immediate UI update
|
||||||
return ServerMode(rawValue: serverModeString)?.displayName ?? "None"
|
return ServerMode(rawValue: serverModeString)?.displayName ?? "None"
|
||||||
}
|
}
|
||||||
|
|
@ -621,4 +625,49 @@ private struct DeveloperToolsSection: View {
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Terminal Preference Section
|
||||||
|
|
||||||
|
private struct TerminalPreferenceSection: View {
|
||||||
|
@AppStorage("preferredTerminal") private var preferredTerminal = Terminal.terminal.rawValue
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("Preferred Terminal")
|
||||||
|
Spacer()
|
||||||
|
Picker("", selection: $preferredTerminal) {
|
||||||
|
ForEach(Terminal.installed, id: \.rawValue) { terminal in
|
||||||
|
HStack {
|
||||||
|
if let icon = terminal.appIcon {
|
||||||
|
Image(nsImage: icon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
}
|
||||||
|
Text(terminal.displayName)
|
||||||
|
}
|
||||||
|
.tag(terminal.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
Text("Select which terminal application to use when creating new sessions")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Terminal Preference")
|
||||||
|
.font(.headline)
|
||||||
|
} footer: {
|
||||||
|
Text(
|
||||||
|
"VibeTunnel will use this terminal when launching new terminal sessions. Falls back to Terminal if the selected app is not available."
|
||||||
|
)
|
||||||
|
.font(.caption)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
152
VibeTunnel/Utilities/TerminalLauncher.swift
Normal file
152
VibeTunnel/Utilities/TerminalLauncher.swift
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum Terminal: String, CaseIterable {
|
||||||
|
case terminal = "Terminal"
|
||||||
|
case iTerm2 = "iTerm2"
|
||||||
|
case ghostty = "Ghostty"
|
||||||
|
|
||||||
|
var bundleIdentifier: String {
|
||||||
|
switch self {
|
||||||
|
case .terminal:
|
||||||
|
"com.apple.Terminal"
|
||||||
|
case .iTerm2:
|
||||||
|
"com.googlecode.iterm2"
|
||||||
|
case .ghostty:
|
||||||
|
"com.mitchellh.ghostty"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var isInstalled: Bool {
|
||||||
|
if self == .terminal {
|
||||||
|
return true // Terminal is always installed
|
||||||
|
}
|
||||||
|
return NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var appIcon: NSImage? {
|
||||||
|
guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NSWorkspace.shared.icon(forFile: appURL.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var installed: [Self] {
|
||||||
|
Self.allCases.filter(\.isInstalled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TerminalLauncherError: LocalizedError {
|
||||||
|
case terminalNotFound
|
||||||
|
case appleScriptPermissionDenied
|
||||||
|
case appleScriptExecutionFailed(String)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .terminalNotFound:
|
||||||
|
"Selected terminal application not found"
|
||||||
|
case .appleScriptPermissionDenied:
|
||||||
|
"AppleScript permission denied. Please grant permission in System Settings."
|
||||||
|
case .appleScriptExecutionFailed(let message):
|
||||||
|
"Failed to execute AppleScript: \(message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class TerminalLauncher {
|
||||||
|
static let shared = TerminalLauncher()
|
||||||
|
|
||||||
|
@AppStorage("preferredTerminal")
|
||||||
|
private var preferredTerminal = Terminal.terminal.rawValue
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func launchCommand(_ command: String) throws {
|
||||||
|
let terminal = Terminal(rawValue: preferredTerminal) ?? .terminal
|
||||||
|
|
||||||
|
// Verify terminal is still installed, fallback to Terminal if not
|
||||||
|
let actualTerminal = terminal.isInstalled ? terminal : .terminal
|
||||||
|
|
||||||
|
if actualTerminal != terminal {
|
||||||
|
// Update preference to fallback
|
||||||
|
preferredTerminal = actualTerminal.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
try launchCommand(command, in: actualTerminal)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func launchCommand(_ command: String, in terminal: Terminal) throws {
|
||||||
|
let escapedCommand = command.replacingOccurrences(of: "\"", with: "\\\"")
|
||||||
|
|
||||||
|
let appleScript: String
|
||||||
|
switch terminal {
|
||||||
|
case .terminal:
|
||||||
|
appleScript = """
|
||||||
|
tell application "Terminal"
|
||||||
|
activate
|
||||||
|
do script "\(escapedCommand)"
|
||||||
|
end tell
|
||||||
|
"""
|
||||||
|
|
||||||
|
case .iTerm2:
|
||||||
|
appleScript = """
|
||||||
|
tell application "iTerm"
|
||||||
|
activate
|
||||||
|
create window with default profile
|
||||||
|
tell current session of current window
|
||||||
|
write text "\(escapedCommand)"
|
||||||
|
end tell
|
||||||
|
end tell
|
||||||
|
"""
|
||||||
|
|
||||||
|
case .ghostty:
|
||||||
|
// Ghostty doesn't have AppleScript support, so we use open command
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/open")
|
||||||
|
process.arguments = ["-b", terminal.bundleIdentifier, "--args", "-e", command]
|
||||||
|
|
||||||
|
do {
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
if process.terminationStatus != 0 {
|
||||||
|
throw TerminalLauncherError.appleScriptExecutionFailed("Failed to launch Ghostty")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
throw TerminalLauncherError.appleScriptExecutionFailed(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute AppleScript for Terminal and iTerm2
|
||||||
|
var error: NSDictionary?
|
||||||
|
if let scriptObject = NSAppleScript(source: appleScript) {
|
||||||
|
_ = scriptObject.executeAndReturnError(&error)
|
||||||
|
|
||||||
|
if let error {
|
||||||
|
let errorMessage = error["NSAppleScriptErrorMessage"] as? String ?? "Unknown error"
|
||||||
|
let errorNumber = error["NSAppleScriptErrorNumber"] as? Int ?? 0
|
||||||
|
|
||||||
|
// Check for permission errors
|
||||||
|
if errorNumber == -1_743 {
|
||||||
|
throw TerminalLauncherError.appleScriptPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
throw TerminalLauncherError.appleScriptExecutionFailed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyPreferredTerminal() {
|
||||||
|
let terminal = Terminal(rawValue: preferredTerminal) ?? .terminal
|
||||||
|
if !terminal.isInstalled {
|
||||||
|
preferredTerminal = Terminal.terminal.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -107,6 +107,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
showWelcomeScreen()
|
showWelcomeScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify preferred terminal is still available
|
||||||
|
TerminalLauncher.shared.verifyPreferredTerminal()
|
||||||
|
|
||||||
// Listen for update check requests
|
// Listen for update check requests
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue