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

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
nolith.dev submitted #0
1 commit
expand
Fix Ghostty open command execution path
1/1 success
expand
expand 0 comments