macOS: add bottom bar when child exits

This commit is contained in:
Lukas
2026-04-12 11:42:58 +02:00
parent c2a93db591
commit 38e64c3706
5 changed files with 102 additions and 2 deletions

View File

@@ -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 {

View File

@@ -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."
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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) {}

View File

@@ -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)