Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6733393
chore(terminal): delete disabled SurfacePool machinery
arzafran Jul 9, 2026
b13bf09
refactor(contentview): extract TabItemView.swift (mechanical move)
arzafran Jul 9, 2026
3a6620d
refactor(contentview): extract FileDropOverlayView.swift (mechanical …
arzafran Jul 9, 2026
3938784
refactor(contentview): extract CommandPaletteSearchEngine.swift (mech…
arzafran Jul 9, 2026
dd1fe83
fix(terminal): fix lost Ghostty focus push after reparent resume
arzafran Jul 9, 2026
4b17278
refactor(terminal): dedupe triplicated hide/retry block in synchroniz…
arzafran Jul 9, 2026
5b236db
refactor: split free-standing types out of AppDelegate.swift
arzafran Jul 9, 2026
fde4223
fix(workspace): unify remote-state reset between configure and discon…
arzafran Jul 9, 2026
e8d4c2a
refactor(workspace-remote): dedup SSH quoting, connection policy, and…
arzafran Jul 9, 2026
32e8c20
refactor: extract handleCustomShortcut app-shortcut dispatch table
arzafran Jul 9, 2026
d161e9f
fix(contentview): widen access for symbols split across the #94 file …
arzafran Jul 9, 2026
33132b1
refactor(workspace-remote): split WorkspaceRemoteDaemon.swift by type
arzafran Jul 9, 2026
2140f5e
refactor(terminal): split IME and SwiftUI wrapper out of GhosttyTermi…
arzafran Jul 9, 2026
5294e0c
refactor: consolidate command-palette per-window state into one dicti…
arzafran Jul 9, 2026
5f44a87
refactor(contentview): extract SidebarDragDrop.swift and SidebarVisua…
arzafran Jul 9, 2026
e9c9ee8
refactor(terminal): split RenderStats debug introspection out of Ghos…
arzafran Jul 9, 2026
593ea63
refactor(workspace-remote): split WorkspaceRemoteSessionController by…
arzafran Jul 9, 2026
aac4826
refactor(contentview): extract CommandPaletteController for palette s…
arzafran Jul 9, 2026
0d6e802
refactor(workspace): extract Persistence/Layout/Bonsplit from Workspa…
arzafran Jul 9, 2026
8ad5fef
Merge remote-tracking branch 'origin/main' into train-cv
arzafran Jul 9, 2026
32a95f9
Merge branch 'train-cv' into train-ad
arzafran Jul 9, 2026
b5e2c7c
Merge branch 'train-ad' into train-gv
arzafran Jul 9, 2026
6d0a63a
Merge branch 'train-gv' into train-ws
arzafran Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 132 additions & 8 deletions GhosttyTabs.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

17,649 changes: 7,376 additions & 10,273 deletions Sources/AppDelegate.swift

Large diffs are not rendered by default.

324 changes: 324 additions & 0 deletions Sources/CLIInstaller.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
import AppKit
import SwiftUI
import Bonsplit
import CoreServices
import UserNotifications
import WebKit
import Combine
import ObjectiveC.runtime
import Darwin

struct ProgramaCLIPathInstaller {
struct InstallOutcome {
let usedAdministratorPrivileges: Bool
let destinationURL: URL
let sourceURL: URL
}

struct UninstallOutcome {
let usedAdministratorPrivileges: Bool
let destinationURL: URL
let removedExistingEntry: Bool
}

enum InstallerError: LocalizedError {
case bundledCLIMissing(expectedPath: String)
case destinationParentNotDirectory(path: String)
case destinationIsDirectory(path: String)
case installVerificationFailed(path: String)
case uninstallVerificationFailed(path: String)
case privilegedCommandFailed(message: String)

var errorDescription: String? {
switch self {
case .bundledCLIMissing(let expectedPath):
return "Bundled Programa CLI was not found at \(expectedPath)."
case .destinationParentNotDirectory(let path):
return "Expected \(path) to be a directory."
case .destinationIsDirectory(let path):
return "\(path) is a directory. Remove or rename it and try again."
case .installVerificationFailed(let path):
return "Installed symlink at \(path) did not point to the bundled programa CLI."
case .uninstallVerificationFailed(let path):
return "Failed to remove \(path)."
case .privilegedCommandFailed(let message):
return "Administrator action failed: \(message)"
}
}
}

typealias PrivilegedInstallHandler = (_ sourceURL: URL, _ destinationURL: URL) throws -> Void
typealias PrivilegedUninstallHandler = (_ destinationURL: URL) throws -> Void

let fileManager: FileManager
let destinationURL: URL
private let bundledCLIURLProvider: () -> URL?
private let expectedBundledCLIPath: String
private let privilegedInstaller: PrivilegedInstallHandler
private let privilegedUninstaller: PrivilegedUninstallHandler

init(
fileManager: FileManager = .default,
destinationURL: URL = URL(fileURLWithPath: "/usr/local/bin/programa"),
bundledCLIURLProvider: @escaping () -> URL? = {
ProgramaCLIPathInstaller.defaultBundledCLIURL()
},
expectedBundledCLIPath: String = ProgramaCLIPathInstaller.defaultBundledCLIExpectedPath(),
privilegedInstaller: PrivilegedInstallHandler? = nil,
privilegedUninstaller: PrivilegedUninstallHandler? = nil
) {
self.fileManager = fileManager
self.destinationURL = destinationURL
self.bundledCLIURLProvider = bundledCLIURLProvider
self.expectedBundledCLIPath = expectedBundledCLIPath
self.privilegedInstaller = privilegedInstaller ?? Self.installWithAdministratorPrivileges(sourceURL:destinationURL:)
self.privilegedUninstaller = privilegedUninstaller ?? Self.uninstallWithAdministratorPrivileges(destinationURL:)
}

var destinationPath: String {
destinationURL.path
}

func install() throws -> InstallOutcome {
let sourceURL = try resolveBundledCLIURL()
do {
try installWithoutAdministratorPrivileges(sourceURL: sourceURL)
return InstallOutcome(
usedAdministratorPrivileges: false,
destinationURL: destinationURL,
sourceURL: sourceURL
)
} catch {
guard Self.isPermissionDenied(error) else { throw error }
try ensureDestinationIsNotDirectory()
try privilegedInstaller(sourceURL, destinationURL)
try verifyInstalledSymlinkTarget(sourceURL: sourceURL)
return InstallOutcome(
usedAdministratorPrivileges: true,
destinationURL: destinationURL,
sourceURL: sourceURL
)
}
}

func uninstall() throws -> UninstallOutcome {
do {
let removedExistingEntry = try uninstallWithoutAdministratorPrivileges()
return UninstallOutcome(
usedAdministratorPrivileges: false,
destinationURL: destinationURL,
removedExistingEntry: removedExistingEntry
)
} catch {
guard Self.isPermissionDenied(error) else { throw error }
try ensureDestinationIsNotDirectory()
let removedExistingEntry = destinationEntryExists()
try privilegedUninstaller(destinationURL)
if destinationEntryExists() {
throw InstallerError.uninstallVerificationFailed(path: destinationURL.path)
}
return UninstallOutcome(
usedAdministratorPrivileges: true,
destinationURL: destinationURL,
removedExistingEntry: removedExistingEntry
)
}
}

func isInstalled() -> Bool {
guard let sourceURL = bundledCLIURLProvider()?.standardizedFileURL else { return false }
guard let installedTargetURL = symlinkDestinationURL() else { return false }
return installedTargetURL == sourceURL
}

private func resolveBundledCLIURL() throws -> URL {
guard let sourceURL = bundledCLIURLProvider()?.standardizedFileURL else {
throw InstallerError.bundledCLIMissing(expectedPath: expectedBundledCLIPath)
}

var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: sourceURL.path, isDirectory: &isDirectory), !isDirectory.boolValue else {
throw InstallerError.bundledCLIMissing(expectedPath: sourceURL.path)
}
return sourceURL
}

private func installWithoutAdministratorPrivileges(sourceURL: URL) throws {
try ensureDestinationParentDirectoryExists()
try ensureDestinationIsNotDirectory()
if destinationEntryExists() {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.createSymbolicLink(at: destinationURL, withDestinationURL: sourceURL)
try verifyInstalledSymlinkTarget(sourceURL: sourceURL)
}

@discardableResult
private func uninstallWithoutAdministratorPrivileges() throws -> Bool {
try ensureDestinationIsNotDirectory()
let existed = destinationEntryExists()
if existed {
try fileManager.removeItem(at: destinationURL)
}
if destinationEntryExists() {
throw InstallerError.uninstallVerificationFailed(path: destinationURL.path)
}
return existed
}

/// Check if the destination path has any filesystem entry (including dangling symlinks).
/// `FileManager.fileExists` follows symlinks, so a dangling symlink returns false.
private func destinationEntryExists() -> Bool {
(try? fileManager.attributesOfItem(atPath: destinationURL.path)) != nil
}

private func verifyInstalledSymlinkTarget(sourceURL: URL) throws {
guard let installedTargetURL = symlinkDestinationURL(),
installedTargetURL == sourceURL.standardizedFileURL else {
throw InstallerError.installVerificationFailed(path: destinationURL.path)
}
}

private func symlinkDestinationURL() -> URL? {
guard fileManager.fileExists(atPath: destinationURL.path) else { return nil }
guard let destinationPath = try? fileManager.destinationOfSymbolicLink(atPath: destinationURL.path) else {
return nil
}
return URL(
fileURLWithPath: destinationPath,
relativeTo: destinationURL.deletingLastPathComponent()
).standardizedFileURL
}

private func ensureDestinationParentDirectoryExists() throws {
let parentURL = destinationURL.deletingLastPathComponent()
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: parentURL.path, isDirectory: &isDirectory) {
guard isDirectory.boolValue else {
throw InstallerError.destinationParentNotDirectory(path: parentURL.path)
}
return
}
try fileManager.createDirectory(at: parentURL, withIntermediateDirectories: true)
}

private func ensureDestinationIsNotDirectory() throws {
guard let values = try resourceValuesIfFileExists(
at: destinationURL,
keys: [.isDirectoryKey, .isSymbolicLinkKey]
) else {
return
}

if values.isDirectory == true, values.isSymbolicLink != true {
throw InstallerError.destinationIsDirectory(path: destinationURL.path)
}
}

private func resourceValuesIfFileExists(
at url: URL,
keys: Set<URLResourceKey>
) throws -> URLResourceValues? {
do {
return try url.resourceValues(forKeys: keys)
} catch {
let nsError = error as NSError
if nsError.domain == NSCocoaErrorDomain && nsError.code == NSFileReadNoSuchFileError {
return nil
}
if nsError.domain == NSPOSIXErrorDomain,
POSIXErrorCode(rawValue: Int32(nsError.code)) == .ENOENT {
return nil
}
throw error
}
}

private static func defaultBundledCLIURL(bundle: Bundle = .main) -> URL? {
bundle.resourceURL?.appendingPathComponent("bin/programa", isDirectory: false)
}

private static func defaultBundledCLIExpectedPath(bundle: Bundle = .main) -> String {
bundle.bundleURL
.appendingPathComponent("Contents/Resources/bin/programa", isDirectory: false)
.path
}

private static func installWithAdministratorPrivileges(sourceURL: URL, destinationURL: URL) throws {
let destinationPath = destinationURL.path
let parentPath = destinationURL.deletingLastPathComponent().path
let command = "/bin/mkdir -p \(shellQuoted(parentPath)) && " +
"/bin/rm -f \(shellQuoted(destinationPath)) && " +
"/bin/ln -s \(shellQuoted(sourceURL.path)) \(shellQuoted(destinationPath))"
try runPrivilegedShellCommand(command)
}

private static func uninstallWithAdministratorPrivileges(destinationURL: URL) throws {
let command = "/bin/rm -f \(shellQuoted(destinationURL.path))"
try runPrivilegedShellCommand(command)
}

private static func runPrivilegedShellCommand(_ command: String) throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
process.arguments = [
"-e", "on run argv",
"-e", "do shell script (item 1 of argv) with administrator privileges",
"-e", "end run",
command
]
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
try process.run()
process.waitUntilExit()

guard process.terminationStatus == 0 else {
let stderrText = String(
data: stderr.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let stdoutText = String(
data: stdout.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let details = stderrText.isEmpty ? stdoutText : stderrText
let message = details.isEmpty
? "osascript exited with status \(process.terminationStatus)."
: details
throw InstallerError.privilegedCommandFailed(message: message)
}
}

private static func shellQuoted(_ value: String) -> String {
"'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
}

private static func isPermissionDenied(_ error: Error) -> Bool {
isPermissionDenied(error as NSError)
}

private static func isPermissionDenied(_ error: NSError) -> Bool {
if error.domain == NSPOSIXErrorDomain,
let code = POSIXErrorCode(rawValue: Int32(error.code)),
code == .EACCES || code == .EPERM || code == .EROFS {
return true
}

if error.domain == NSCocoaErrorDomain {
switch error.code {
case NSFileWriteNoPermissionError, NSFileReadNoPermissionError, NSFileWriteVolumeReadOnlyError:
return true
default:
break
}
}

if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
return isPermissionDenied(underlying)
}

return false
}
}
73 changes: 73 additions & 0 deletions Sources/CommandPaletteController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Command-palette state ownership, extracted from ContentView.swift (nuclear-review #88).
//
// CommandPaletteController owns every @State property that used to live on
// ContentView and is exclusively used by the command palette (query, mode,
// search corpus/results, rename/workspace-description drafts, focus-restore
// targets, usage history, etc.). ContentView holds a single
// `@StateObject private var commandPaletteController` and exposes each
// property back to its existing (unqualified) call sites in its body via
// thin computed proxies — this keeps the ~4000 lines of palette orchestration
// code that reads/writes these properties unchanged while genuinely moving
// storage ownership onto the controller (no more @State duplicated per-view).
//
// The two @FocusState properties (isCommandPaletteSearchFocused,
// isCommandPaletteRenameFocused) stay on ContentView: @FocusState is a
// SwiftUI View-only property wrapper and cannot be hosted on an
// ObservableObject.
//
// Access-level widening: CommandPaletteMode, CommandPaletteRestoreFocusTarget,
// CommandPaletteTextSelectionBehavior, and CommandPaletteMultilineTextEditorRepresentable
// were `private` nested types inside ContentView; widened to internal (dropped
// the `private` modifier) so this controller (a different file) can reference
// them. No other behavior change. CommandPaletteCommand, CommandPaletteSearchResult,
// CommandPaletteListScope, CommandPalettePendingActivation, and
// CommandPaletteUsageEntry were already internal nested types; referenced here
// via `ContentView.` qualification since nested-type name lookup requires it
// from outside the enclosing type's lexical scope.

import AppKit
import Combine
import SwiftUI

final class CommandPaletteController: ObservableObject {
@Published var isCommandPalettePresented = false
@Published var commandPaletteQuery: String = ""
@Published var commandPaletteMode: ContentView.CommandPaletteMode = .commands
@Published var commandPaletteRenameDraft: String = ""
@Published var commandPaletteWorkspaceDescriptionDraft: String = ""
@Published var commandPaletteWorkspaceDescriptionHeight: CGFloat = ContentView.CommandPaletteMultilineTextEditorRepresentable.defaultMinimumHeight
@Published var commandPaletteSelectedResultIndex: Int = 0
@Published var commandPaletteSelectionAnchorCommandID: String?
@Published var commandPaletteHoveredResultIndex: Int?
@Published var commandPaletteScrollTargetIndex: Int?
@Published var commandPaletteScrollTargetAnchor: UnitPoint?
@Published var commandPaletteRestoreFocusTarget: ContentView.CommandPaletteRestoreFocusTarget?
@Published var commandPaletteSearchCorpus: [CommandPaletteSearchCorpusEntry<String>] = []
@Published var commandPaletteSearchCorpusByID: [String: CommandPaletteSearchCorpusEntry<String>] = [:]
@Published var commandPaletteSearchCommandsByID: [String: ContentView.CommandPaletteCommand] = [:]
@Published var cachedCommandPaletteResults: [ContentView.CommandPaletteSearchResult] = []
@Published var commandPaletteVisibleResults: [ContentView.CommandPaletteSearchResult] = []
@Published var commandPaletteVisibleResultsScope: ContentView.CommandPaletteListScope?
@Published var commandPaletteVisibleResultsFingerprint: Int?
@Published var cachedCommandPaletteScope: ContentView.CommandPaletteListScope?
@Published var cachedCommandPaletteFingerprint: Int?
@Published var commandPalettePendingDismissFocusTarget: ContentView.CommandPaletteRestoreFocusTarget?
@Published var commandPaletteRestoreTimeoutWorkItem: DispatchWorkItem?
@Published var commandPalettePendingTextSelectionBehavior: ContentView.CommandPaletteTextSelectionBehavior?
@Published var commandPaletteSearchTask: Task<Void, Never>?
@Published var commandPaletteSearchRequestID: UInt64 = 0
@Published var commandPaletteResolvedSearchRequestID: UInt64 = 0
@Published var commandPaletteResolvedSearchScope: ContentView.CommandPaletteListScope?
@Published var commandPaletteResolvedSearchFingerprint: Int?
@Published var commandPaletteResolvedMatchingQuery = ""
@Published var commandPaletteTerminalOpenTargetAvailability: Set<TerminalDirectoryOpenTarget> = []
@Published var isCommandPaletteSearchPending = false
@Published var commandPalettePendingActivation: ContentView.CommandPalettePendingActivation?
@Published var commandPaletteResultsRevision: UInt64 = 0
@Published var commandPaletteUsageHistoryByCommandId: [String: ContentView.CommandPaletteUsageEntry] = [:]
@AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
var commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus
@AppStorage(CommandPaletteSwitcherSearchSettings.searchAllSurfacesKey)
var commandPaletteSearchAllSurfaces = CommandPaletteSwitcherSearchSettings.defaultSearchAllSurfaces
@Published var commandPaletteShouldFocusWorkspaceDescriptionEditor = false
}
Loading
Loading