From 38e64c3706b9bc12e9191f4deaf6f37a717afe95 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:42:58 +0200 Subject: [PATCH] macOS: add bottom bar when child exits --- macos/Sources/Ghostty/Ghostty.App.swift | 22 ++++++++- .../Ghostty/Ghostty.ChildExitedMessage.swift | 23 ++++++++++ .../Surface View/ChildExitedMessageBar.swift | 46 +++++++++++++++++++ .../Ghostty/Surface View/OSSurfaceView.swift | 7 +++ .../Ghostty/Surface View/SurfaceView.swift | 6 +++ 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift create mode 100644 macos/Sources/Ghostty/Surface View/ChildExitedMessageBar.swift diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 2f0644b93..cc8af71cf 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -662,8 +662,7 @@ extension Ghostty { case GHOSTTY_ACTION_QUIT_TIMER: fallthrough case GHOSTTY_ACTION_SHOW_CHILD_EXITED: - Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)") - return false + return showChildExited(app, target: target, v: action.action.child_exited) case GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD: return copyTitleToClipboard(app, target: target) default: @@ -1632,6 +1631,25 @@ extension Ghostty { } } + private static func showChildExited( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_surface_message_childexited_s, + ) -> Bool { + switch target.tag { + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + // 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)) + return true + default: + return false + } + } + private static func copyTitleToClipboard( _ app: ghostty_app_t, target: ghostty_target_s) -> Bool { diff --git a/macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift b/macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift new file mode 100644 index 000000000..f78e69824 --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift @@ -0,0 +1,23 @@ +import Foundation +import GhosttyKit +import SwiftUI + +extension Ghostty { + struct ChildExitedMessage { + enum Level { + case success, error + } + let text: String + let level: Level + + init(_ message: ghostty_surface_message_childexited_s) { + switch Int(message.exit_code) { + case Int(EXIT_SUCCESS): + level = .success + default: + level = .error + } + text = "Process exited. Press any key to close the terminal." + } + } +} diff --git a/macos/Sources/Ghostty/Surface View/ChildExitedMessageBar.swift b/macos/Sources/Ghostty/Surface View/ChildExitedMessageBar.swift new file mode 100644 index 000000000..4139b4742 --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/ChildExitedMessageBar.swift @@ -0,0 +1,46 @@ +import SwiftUI + +struct ChildExitedMessageBar: View { + let msg: Ghostty.ChildExitedMessage + @State private var isHovered: Bool = false + + var body: some View { + HStack(spacing: 6) { + Text(msg.text) + .fontWeight(.medium) + .lineLimit(1) + .truncationMode(.tail) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .frame(maxWidth: .infinity, alignment: .center) + .background(msg.level.backgroundStyle) + .foregroundColor(msg.level.foregroundColor) + .contentShape(.rect) + .accessibilityLabel(msg.text) + .transition(.move(edge: .bottom)) + .opacity(isHovered ? 0 : 1) + .allowsHitTesting(false) + .overlay { + Color.clear + .onHover { + isHovered = $0 + } + } + } +} + +private extension Ghostty.ChildExitedMessage.Level { + var foregroundColor: Color { + .primary + } + + var backgroundStyle: AnyShapeStyle { + switch self { + case .success: + AnyShapeStyle(.background) + case .error: + AnyShapeStyle(.red.opacity(0.5)) + } + } +} diff --git a/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift b/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift index 0a26425a6..8553e563f 100644 --- a/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift @@ -51,6 +51,9 @@ extension Ghostty { /// True when the surface should show a highlight effect (e.g., when presented via goto_split). @Published private(set) var highlighted: Bool = false + /// A message sent from `ghostty_surface_t` when a child process exited + @Published private(set) var childExitedMessage: ChildExitedMessage? + var surface: ghostty_surface_t? { nil } @@ -92,6 +95,10 @@ extension Ghostty { } } + func setChildExitedMessage(_ message: ChildExitedMessage) { + self.childExitedMessage = message + } + // MARK: - Placeholders func focusDidChange(_ focused: Bool) {} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 8d8239c6d..22abf1e44 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -137,6 +137,12 @@ extension Ghostty { if let url = surfaceView.hoverUrl { URLHoverBanner(url: url) } + + // Show a bar to indicate a child process has exited. + if let msg = surfaceView.childExitedMessage { + ChildExitedMessageBar(msg: msg) + .font(.system(size: min(surfaceView.cellSize.height * 0.8, 30))) + } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)