macos: show a highlight animation when a terminal is presented

pull/9945/head
Mitchell Hashimoto 2025-12-17 09:57:33 -08:00
parent d23f7e051f
commit e1356538ac
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 75 additions and 1 deletions

View File

@ -715,6 +715,9 @@ class BaseTerminalController: NSWindowController,
// We use a small delay to ensure this runs after any UI cleanup
// (e.g., command palette restoring focus to its original surface).
Ghostty.moveFocus(to: target, delay: 0.1)
// Show a brief highlight to help the user locate the presented terminal.
target.highlight()
}
// MARK: Local Events

View File

@ -49,7 +49,7 @@ extension Ghostty {
// 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
@ -219,6 +219,9 @@ extension Ghostty {
BellBorderOverlay(bell: surfaceView.bell)
}
// Show a highlight effect when this surface needs attention
HighlightOverlay(highlighted: surfaceView.highlighted)
// If our surface is not healthy, then we render an error view over it.
if (!surfaceView.healthy) {
Rectangle().fill(ghostty.config.backgroundColor)
@ -242,6 +245,7 @@ extension Ghostty {
}
}
}
}
}
@ -764,6 +768,62 @@ extension Ghostty {
}
}
/// Visual overlay that briefly highlights a surface to draw attention to it.
/// Uses a soft, soothing highlight with a pulsing border effect.
struct HighlightOverlay: View {
let highlighted: Bool
@State private var borderPulse: Bool = false
var body: some View {
ZStack {
Rectangle()
.fill(
RadialGradient(
gradient: Gradient(colors: [
Color.accentColor.opacity(0.12),
Color.accentColor.opacity(0.03),
Color.clear
]),
center: .center,
startRadius: 0,
endRadius: 2000
)
)
Rectangle()
.strokeBorder(
LinearGradient(
gradient: Gradient(colors: [
Color.accentColor.opacity(0.8),
Color.accentColor.opacity(0.5),
Color.accentColor.opacity(0.8)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: borderPulse ? 4 : 2
)
.shadow(color: Color.accentColor.opacity(borderPulse ? 0.8 : 0.6), radius: borderPulse ? 12 : 8, x: 0, y: 0)
.shadow(color: Color.accentColor.opacity(borderPulse ? 0.5 : 0.3), radius: borderPulse ? 24 : 16, x: 0, y: 0)
}
.allowsHitTesting(false)
.opacity(highlighted ? 1.0 : 0.0)
.animation(.easeOut(duration: 0.4), value: highlighted)
.onChange(of: highlighted) { newValue in
if newValue {
withAnimation(.easeInOut(duration: 0.4).repeatForever(autoreverses: true)) {
borderPulse = true
}
} else {
withAnimation(.easeOut(duration: 0.4)) {
borderPulse = false
}
}
}
}
}
// MARK: Readonly Badge
/// A badge overlay that indicates a surface is in readonly mode.

View File

@ -126,6 +126,9 @@ extension Ghostty {
/// True when the surface is in readonly mode.
@Published private(set) var readonly: Bool = false
/// True when the surface should show a highlight effect (e.g., when presented via goto_split).
@Published private(set) var highlighted: Bool = false
// An initial size to request for a window. This will only affect
// then the view is moved to a new window.
var initialSize: NSSize? = nil
@ -1523,6 +1526,14 @@ extension Ghostty {
}
}
/// Triggers a brief highlight animation on this surface.
func highlight() {
highlighted = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
self?.highlighted = false
}
}
@IBAction func splitRight(_ sender: Any) {
guard let surface = self.surface else { return }
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT)