mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-24 05:40:15 +00:00
macOS: add bottom bar when child exits (#12251)
### Closes #7649 The bar lives alongside URL Hover in VStack at the bottom. The current body of SurfaceView is becoming rather long and complicated, so this pr also contains some refactors: - Move URL Hover to a separate file > The text is copied from previous input string to keep it consistent, also I’m confused with text on GTK so this is my first choice, but it can be changed as the same as GTK. Separate prs will be opened for: 1. Set to Read-only after exits 2. Hide cursor when in Read-only ### Preview https://github.com/user-attachments/assets/eb44e211-eac5-4f40-836c-4912b18dfb01
This commit is contained in:
@@ -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 {
|
||||
|
||||
23
macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift
Normal file
23
macos/Sources/Ghostty/Ghostty.ChildExitedMessage.swift
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
|
||||
@@ -47,9 +47,6 @@ extension Ghostty {
|
||||
// Maintain whether our window has focus (is key) or not
|
||||
@State private var windowFocus: Bool = true
|
||||
|
||||
// True if we're hovering over the left URL view, so we can show it on the right.
|
||||
@State private var isHoveringURLLeft: Bool = false
|
||||
|
||||
#if canImport(AppKit)
|
||||
// Observe SecureInput to detect when its enabled
|
||||
@ObservedObject private var secureInput = SecureInput.shared
|
||||
@@ -135,49 +132,19 @@ extension Ghostty {
|
||||
)
|
||||
#endif
|
||||
|
||||
// If we have a URL from hovering a link, we show that.
|
||||
if let url = surfaceView.hoverUrl {
|
||||
let padding: CGFloat = 5
|
||||
let cornerRadius: CGFloat = 9
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
VStack(spacing: 0) {
|
||||
// If we have a URL from hovering a link, we show that.
|
||||
if let url = surfaceView.hoverUrl {
|
||||
URLHoverBanner(url: url)
|
||||
}
|
||||
|
||||
Text(verbatim: url)
|
||||
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
|
||||
.background(
|
||||
UnevenRoundedRectangle(cornerRadii: .init(topLeading: cornerRadius))
|
||||
.fill(.background)
|
||||
)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.opacity(isHoveringURLLeft ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
|
||||
Text(verbatim: url)
|
||||
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
|
||||
.background(
|
||||
UnevenRoundedRectangle(cornerRadii: .init(topTrailing: cornerRadius))
|
||||
.fill(.background)
|
||||
)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.opacity(isHoveringURLLeft ? 0 : 1)
|
||||
.onHover(perform: { hovering in
|
||||
isHoveringURLLeft = hovering
|
||||
})
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
// 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)
|
||||
|
||||
#if canImport(AppKit)
|
||||
// If we have secure input enabled and we're the focused surface and window
|
||||
@@ -242,7 +209,6 @@ extension Ghostty {
|
||||
SurfaceGrabHandle(surfaceView: surfaceView)
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
49
macos/Sources/Helpers/URLHoverBanner.swift
Normal file
49
macos/Sources/Helpers/URLHoverBanner.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
import SwiftUI
|
||||
|
||||
struct URLHoverBanner: View {
|
||||
// True if we're hovering over the left URL view, so we can show it on the right.
|
||||
@State private var isHoveringURLLeft: Bool = false
|
||||
let padding: CGFloat = 5
|
||||
let cornerRadius: CGFloat = 9
|
||||
let url: String
|
||||
var body: some View {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
|
||||
Text(verbatim: url)
|
||||
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
|
||||
.background(
|
||||
UnevenRoundedRectangle(cornerRadii: .init(topLeading: cornerRadius))
|
||||
.fill(.background)
|
||||
)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.opacity(isHoveringURLLeft ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
|
||||
Text(verbatim: url)
|
||||
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
|
||||
.background(
|
||||
UnevenRoundedRectangle(cornerRadii: .init(topTrailing: cornerRadius))
|
||||
.fill(.background)
|
||||
)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.opacity(isHoveringURLLeft ? 0 : 1)
|
||||
.onHover(perform: { hovering in
|
||||
isHoveringURLLeft = hovering
|
||||
})
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user