From f4ddddc4b797e5bc185bbfb486f74231a331dfff Mon Sep 17 00:00:00 2001 From: Joseph Martinsen Date: Sat, 21 Feb 2026 16:51:32 -0600 Subject: [PATCH] macos: refactor command finish notification duration handling --- macos/Sources/Ghostty/Ghostty.App.swift | 50 +++++++++------------- macos/Sources/Ghostty/Ghostty.Config.swift | 11 ++--- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index f864e1033..913bb36f9 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -638,7 +638,6 @@ extension Ghostty { case GHOSTTY_ACTION_PRESENT_TERMINAL: return presentTerminal(app, target: target) - case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: fallthrough case GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS: @@ -1403,15 +1402,22 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - guard let appState = (NSApplication.shared.delegate as? AppDelegate)?.ghostty else { return } - let config = appState.config + // Determine if we even care about command finish notifications + guard let config = (NSApplication.shared.delegate as? AppDelegate)?.ghostty.config else { return } + switch config.notifyOnCommandFinish { + case .never: + return - let mode = config.notifyOnCommandFinish - if mode == .never { return } - if mode == .unfocused && surfaceView.focused { return } + case .unfocused: + if surfaceView.focused { return } - let durationMs = v.duration / 1_000_000 - if durationMs < config.notifyOnCommandFinishAfter { return } + case .always: + break + } + + // Determine if the command was slow enough + let duration = Duration.nanoseconds(v.duration) + guard Duration.nanoseconds(v.duration) >= config.notifyOnCommandFinishAfter else { return } let actions = config.notifyOnCommandFinishAction @@ -1433,7 +1439,13 @@ extension Ghostty { } let body: String - let formattedDuration = Self.formatDuration(ns: v.duration) + let formattedDuration = duration.formatted( + .units( + allowed: [.hours, .minutes, .seconds, .milliseconds], + width: .abbreviated, + fractionalPart: .hide + ) + ) if v.exit_code < 0 { body = "Command took \(formattedDuration)." } else { @@ -1448,26 +1460,6 @@ extension Ghostty { } } - private static func formatDuration(ns: UInt64) -> String { - let totalSeconds = ns / 1_000_000_000 - let ms = (ns / 1_000_000) % 1000 - - if totalSeconds == 0 { - return "\(ms)ms" - } - - let seconds = totalSeconds % 60 - let minutes = (totalSeconds / 60) % 60 - let hours = totalSeconds / 3600 - - var parts: [String] = [] - if hours > 0 { parts.append("\(hours)h") } - if minutes > 0 { parts.append("\(minutes)m") } - if seconds > 0 || (hours == 0 && minutes == 0) { parts.append("\(seconds)s") } - - return parts.joined(separator: " ") - } - private static func toggleFloatWindow( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 11d1d4d8e..a26f41038 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -144,19 +144,20 @@ extension Ghostty { } var notifyOnCommandFinishAction: NotifyOnCommandFinishAction { - guard let config = self.config else { return .bell } + let defaultValue = NotifyOnCommandFinishAction.bell + guard let config = self.config else { return defaultValue } var v: CUnsignedInt = 0 let key = "notify-on-command-finish-action" - guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .bell } + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } return .init(rawValue: v) } - var notifyOnCommandFinishAfter: UInt { - guard let config = self.config else { return 5000 } + var notifyOnCommandFinishAfter: Duration { + guard let config = self.config else { return .seconds(5) } var v: UInt = 0 let key = "notify-on-command-finish-after" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) - return v + return .milliseconds(v) } var splitPreserveZoom: SplitPreserveZoom {