diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 992c5efb5..8dbca3b57 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1649,7 +1649,8 @@ extension Ghostty { // We handle this when the window is visible and timetime_ms is greater than 0, // which will rule out exit codes on launch guard surfaceView.window != nil, v.timetime_ms > 0 else { return false } - surfaceView.setChildExitedMessage(.init(v)) + guard let config = (NSApplication.shared.delegate as? AppDelegate)?.ghostty.config else { return false } + surfaceView.setChildExitedMessage(.init(v, threshold: config.abnormalCommandExitRuntime)) return true default: return false diff --git a/macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift b/macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift index f78e69824..e21a79dbe 100644 --- a/macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift +++ b/macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift @@ -10,14 +10,32 @@ extension Ghostty { let text: String let level: Level - init(_ message: ghostty_surface_message_childexited_s) { + init(_ message: ghostty_surface_message_childexited_s, threshold abnormalCommandExitRuntime: Duration) { + var level: Level switch Int(message.exit_code) { case Int(EXIT_SUCCESS): level = .success default: level = .error } - text = "Process exited. Press any key to close the terminal." + // See: Surface.zig/childExited + // If our runtime was below some threshold then we assume that this + // was an abnormal exit and we show an error message. + if abnormalCommandExitRuntime >= .milliseconds(message.timetime_ms) { + level = .error + let measure = Measurement.init(value: Double(message.timetime_ms), unit: UnitDuration.milliseconds) + let formatter = MeasurementFormatter() + if message.timetime_ms > 1000 { + formatter.unitOptions = .naturalScale + } else { + formatter.unitOptions = .providedUnit + } + formatter.locale = .init(identifier: "en_US") + text = "Process exited after **`\(formatter.string(from: measure))`**. Press any key to close the terminal." + } else { + text = "Process exited. Press any key to close the terminal." + } + self.level = level } } } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 743ebfa2f..9b094f21c 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -709,6 +709,16 @@ extension Ghostty { return MacShortcuts(rawValue: str) ?? defaultValue } + var abnormalCommandExitRuntime: Duration { + let defaultValue: Duration = .milliseconds(250) + guard let config = self.config else { return defaultValue } + var v: UInt32? + let key = "abnormal-command-exit-runtime" + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } + guard let v else { return defaultValue } + return .milliseconds(v) + } + var scrollbar: Scrollbar { let defaultValue = Scrollbar.system guard let config = self.config else { return defaultValue } diff --git a/macos/Sources/Ghostty/Surface View/ChildExitedMessageBar.swift b/macos/Sources/Ghostty/Surface View/ChildExitedMessageBar.swift index 4139b4742..a7e522849 100644 --- a/macos/Sources/Ghostty/Surface View/ChildExitedMessageBar.swift +++ b/macos/Sources/Ghostty/Surface View/ChildExitedMessageBar.swift @@ -6,8 +6,7 @@ struct ChildExitedMessageBar: View { var body: some View { HStack(spacing: 6) { - Text(msg.text) - .fontWeight(.medium) + Text(message) .lineLimit(1) .truncationMode(.tail) } @@ -28,6 +27,10 @@ struct ChildExitedMessageBar: View { } } } + + private var message: AttributedString { + (try? AttributedString(markdown: msg.text)) ?? AttributedString(msg.text) + } } private extension Ghostty.ChildExitedMessage.Level {