A macOS utility to track home-manager JJ repo status

Fix Ghostty open command execution path #4

merged opened by nolith.dev targeting main from ac/push-lxuswxvmqnqq

Run the requested Ghostty command via /bin/sh -lc so cd/exec expressions are interpreted correctly and avoid malformed login invocation when opening a new window.

AI-assisted: OpenCode (Openai GPT-5.3 Codex)

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:nzep3slobztdph3kxswzbing/sh.tangled.repo.pull/3mgtzkkxlzz22
+100 -25
Diff #1
+100 -25
Sources/HomeManagerStatus/HomeManagerStatusApp.swift
··· 46 46 47 47 struct MenuContent: View { 48 48 @ObservedObject var checker: AppStatusChecker 49 + @State private var ghosttyLaunchErrorMessage: String? 50 + 51 + private let ghosttyLauncher = GhosttyLauncher() 49 52 50 53 private var formattedTime: String { 51 54 checker.status.checkedAt.formatted(date: .omitted, time: .standard) 52 55 } 53 56 54 57 var body: some View { 55 - // Status section 56 - Section { 57 - ForEach( 58 - checker.status.statusDescription.components(separatedBy: "\n"), id: \.self 59 - ) { line in 60 - Text(line) 58 + Group { 59 + // Status section 60 + Section { 61 + ForEach( 62 + checker.status.statusDescription.components(separatedBy: "\n"), id: \.self 63 + ) { line in 64 + Text(line) 65 + } 66 + 67 + Text("Last checked: \(formattedTime)") 68 + .foregroundStyle(.secondary) 61 69 } 62 70 63 - Text("Last checked: \(formattedTime)") 64 - .foregroundStyle(.secondary) 65 - } 71 + Divider() 66 72 67 - Divider() 73 + // Actions 74 + Section { 75 + Button("Refresh Now") { 76 + Task { await checker.refresh() } 77 + } 78 + .keyboardShortcut("r") 68 79 69 - // Actions 70 - Section { 71 - Button("Refresh Now") { 72 - Task { await checker.refresh() } 80 + Button("Open in Ghostty") { 81 + openInGhostty(path: checker.repoPath) 82 + } 83 + .keyboardShortcut("t") 73 84 } 74 - .keyboardShortcut("r") 75 85 76 - Button("Open in Ghostty") { 77 - openInGhostty(path: checker.repoPath) 86 + Divider() 87 + 88 + Button("Quit") { 89 + NSApplication.shared.terminate(nil) 78 90 } 79 - .keyboardShortcut("t") 91 + .keyboardShortcut("q") 80 92 } 93 + .alert( 94 + "Unable to Open Ghostty", 95 + isPresented: Binding( 96 + get: { ghosttyLaunchErrorMessage != nil }, 97 + set: { isPresented in 98 + if !isPresented { 99 + ghosttyLaunchErrorMessage = nil 100 + } 101 + } 102 + ) 103 + ) { 104 + Button("OK", role: .cancel) {} 105 + } message: { 106 + Text(ghosttyLaunchErrorMessage ?? "Unknown error") 107 + } 108 + } 81 109 82 - Divider() 110 + private func openInGhostty(path: String) { 111 + do { 112 + try ghosttyLauncher.openNewWindow(path: path) 113 + } catch { 114 + ghosttyLaunchErrorMessage = error.localizedDescription 115 + } 116 + } 117 + } 83 118 84 - Button("Quit") { 85 - NSApplication.shared.terminate(nil) 119 + struct GhosttyLauncher { 120 + enum LaunchError: LocalizedError { 121 + case openCommandExecutionFailed(String) 122 + case openCommandFailed(String) 123 + 124 + var errorDescription: String? { 125 + switch self { 126 + case .openCommandExecutionFailed(let details): 127 + return "Could not run open for Ghostty. \(details)" 128 + case .openCommandFailed(let details): 129 + if details.isEmpty { 130 + return "Ghostty could not be opened. Make sure Ghostty is installed." 131 + } 132 + return "Ghostty could not be opened. \(details)" 133 + } 86 134 } 87 - .keyboardShortcut("q") 88 135 } 89 136 90 - private func openInGhostty(path: String) { 137 + func makeOpenArguments(path: String) -> [String] { 138 + let shell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh" 139 + let shellCommand = "cd \(shellQuoted(path)) && exec \(shellQuoted(shell))" 140 + return ["-n", "-a", "Ghostty", "--args", "-e", "/bin/sh", "-lc", shellCommand] 141 + } 142 + 143 + func openNewWindow(path: String) throws { 91 144 let process = Process() 92 145 process.executableURL = URL(fileURLWithPath: "/usr/bin/open") 93 - process.arguments = ["-a", "Ghostty", "--args", "-e", "cd \(path) && exec $SHELL"] 94 - try? process.run() 146 + process.arguments = makeOpenArguments(path: path) 147 + 148 + let stderr = Pipe() 149 + process.standardError = stderr 150 + 151 + do { 152 + try process.run() 153 + } catch { 154 + throw LaunchError.openCommandExecutionFailed(error.localizedDescription) 155 + } 156 + 157 + process.waitUntilExit() 158 + 159 + guard process.terminationStatus == 0 else { 160 + let data = stderr.fileHandleForReading.readDataToEndOfFile() 161 + let output = 162 + String(data: data, encoding: .utf8)? 163 + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" 164 + throw LaunchError.openCommandFailed(output) 165 + } 166 + } 167 + 168 + private func shellQuoted(_ value: String) -> String { 169 + "'\(value.replacingOccurrences(of: "'", with: "'\\''"))'" 95 170 } 96 171 }

History

2 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
Fix Ghostty open command execution path
1/2 failed, 1/2 success
expand
expand 0 comments
pull request successfully merged
1 commit
expand
Fix Ghostty open command execution path
1/1 success
expand
expand 0 comments