-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Sync ghostty fork with upstream main #1484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 23 commits
f34f8c1
d768fe6
2667d22
fcdeac5
0609f57
6105c98
d937c39
605fe5c
306b161
40132e5
d33e98e
bfeb362
8ecb149
f363936
09ad5a2
b2f99cd
1b49b41
835ed70
f9fceed
c3df2e2
922b961
ede6ea4
bfd9294
12a8769
8752400
06da3d9
d14c7de
2bb008d
85e0303
ed9da3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -320,6 +320,44 @@ struct TitlebarLayerBackground: NSViewRepresentable { | |
| } | ||
| } | ||
|
|
||
| func cmuxMainWindowTitlebarOpacity( | ||
| backgroundOpacity: Double | ||
| ) -> CGFloat { | ||
| GhosttyBackgroundTheme.compositedChromeOpacity(backgroundOpacity) | ||
| } | ||
|
|
||
| func cmuxResolveGhosttyChromeConfig( | ||
| loadedConfig: GhosttyConfig, | ||
| runtimeBackgroundColor: NSColor, | ||
| runtimeBackgroundOpacity: Double | ||
| ) -> GhosttyConfig { | ||
| let runtimeOpacity = GhosttyBackgroundTheme.clampedOpacity(runtimeBackgroundOpacity) | ||
| let loadedOpacity = GhosttyBackgroundTheme.clampedOpacity(loadedConfig.backgroundOpacity) | ||
|
|
||
| // Newer Ghostty builds can drive the window background fully clear at runtime for | ||
| // translucent terminals. That clear backing color should not replace cmux chrome. | ||
| guard runtimeOpacity > 0.001 || loadedOpacity <= 0.001 else { | ||
| return loadedConfig | ||
| } | ||
|
|
||
| var resolved = loadedConfig | ||
| resolved.backgroundColor = runtimeBackgroundColor | ||
| resolved.backgroundOpacity = runtimeBackgroundOpacity | ||
| return resolved | ||
| } | ||
|
|
||
| func cmuxResolveGhosttyChromeConfig( | ||
| loadConfig: () -> GhosttyConfig = { GhosttyConfig.load() }, | ||
| runtimeBackgroundColor: () -> NSColor = { GhosttyApp.shared.defaultBackgroundColor }, | ||
| runtimeBackgroundOpacity: () -> Double = { GhosttyApp.shared.defaultBackgroundOpacity } | ||
| ) -> GhosttyConfig { | ||
| cmuxResolveGhosttyChromeConfig( | ||
| loadedConfig: loadConfig(), | ||
| runtimeBackgroundColor: runtimeBackgroundColor(), | ||
| runtimeBackgroundOpacity: runtimeBackgroundOpacity() | ||
| ) | ||
| } | ||
|
|
||
| final class SidebarState: ObservableObject { | ||
| @Published var isVisible: Bool | ||
| @Published var persistedWidth: CGFloat | ||
|
|
@@ -1457,6 +1495,7 @@ struct ContentView: View { | |
| @State private var workspaceHandoffFallbackTask: Task<Void, Never>? | ||
| @State private var didApplyUITestSidebarSelection = false | ||
| @State private var titlebarThemeGeneration: UInt64 = 0 | ||
| @State private var titlebarThemeConfig = cmuxResolveGhosttyChromeConfig() | ||
| @State private var sidebarDraggedTabId: UUID? | ||
| @State private var titlebarTextUpdateCoalescer = NotificationBurstCoalescer(delay: 1.0 / 30.0) | ||
| @State private var sidebarResizerCursorReleaseWorkItem: DispatchWorkItem? | ||
|
|
@@ -2183,8 +2222,7 @@ struct ContentView: View { | |
| @State private var titlebarLeadingInset: CGFloat = 12 | ||
| private var windowIdentifier: String { "cmux.main.\(windowId.uuidString)" } | ||
| private var fakeTitlebarTextColor: Color { | ||
| _ = titlebarThemeGeneration | ||
| let ghosttyBackground = GhosttyApp.shared.defaultBackgroundColor | ||
| let ghosttyBackground = titlebarThemeConfig.backgroundColor | ||
| return ghosttyBackground.isLightColor | ||
| ? Color.black.opacity(0.78) | ||
| : Color.white.opacity(0.82) | ||
|
|
@@ -2242,14 +2280,11 @@ struct ContentView: View { | |
| .frame(maxWidth: .infinity) | ||
| .contentShape(Rectangle()) | ||
| .background({ | ||
| // The terminal area has two stacked semi-transparent layers: the Bonsplit | ||
| // container chrome background plus Ghostty's own Metal-rendered background. | ||
| // Compute the effective composited opacity so the titlebar matches visually. | ||
| let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity) | ||
| let effective = alpha >= 0.999 ? alpha : 1.0 - pow(1.0 - alpha, 2) | ||
| return TitlebarLayerBackground( | ||
| backgroundColor: GhosttyApp.shared.defaultBackgroundColor, | ||
| opacity: effective | ||
| backgroundColor: titlebarThemeConfig.backgroundColor, | ||
| opacity: cmuxMainWindowTitlebarOpacity( | ||
| backgroundOpacity: titlebarThemeConfig.backgroundOpacity | ||
| ) | ||
| ) | ||
| }()) | ||
| .overlay(alignment: .bottom) { | ||
|
|
@@ -2408,6 +2443,7 @@ struct ContentView: View { | |
| syncSidebarSelectedWorkspaceIds() | ||
| applyUITestSidebarSelectionIfNeeded(tabs: tabManager.tabs) | ||
| updateTitlebarText() | ||
| titlebarThemeConfig = cmuxResolveGhosttyChromeConfig() | ||
|
|
||
| // Startup recovery (#399): if session restore or a race condition leaves the | ||
| // view in a broken state (empty tabs, no selection, unmounted workspaces), | ||
|
|
@@ -2529,9 +2565,10 @@ struct ContentView: View { | |
| }) | ||
|
|
||
| view = AnyView(view.onChange(of: titlebarThemeGeneration) { oldValue, newValue in | ||
| titlebarThemeConfig = cmuxResolveGhosttyChromeConfig() | ||
|
Comment on lines
2567
to
+2568
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Updating Useful? React with 👍 / 👎. |
||
| guard GhosttyApp.shared.backgroundLogEnabled else { return } | ||
| GhosttyApp.shared.logBackground( | ||
| "titlebar theme refresh applied oldGeneration=\(oldValue) generation=\(newValue) appBg=\(GhosttyApp.shared.defaultBackgroundColor.hexString()) appOpacity=\(String(format: "%.3f", GhosttyApp.shared.defaultBackgroundOpacity))" | ||
| "titlebar theme refresh applied oldGeneration=\(oldValue) generation=\(newValue) titlebarBg=\(titlebarThemeConfig.backgroundColor.hexString()) titlebarOpacity=\(String(format: "%.3f", titlebarThemeConfig.backgroundOpacity)) appBg=\(GhosttyApp.shared.defaultBackgroundColor.hexString()) appOpacity=\(String(format: "%.3f", GhosttyApp.shared.defaultBackgroundOpacity))" | ||
| ) | ||
| }) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,44 @@ struct GhosttyConfig { | |
| case dark | ||
| } | ||
|
|
||
| enum BackgroundBlur: Hashable { | ||
| case disabled | ||
| case enabled | ||
| case macosGlassRegular | ||
| case macosGlassClear | ||
|
|
||
| var isGlassStyle: Bool { | ||
| switch self { | ||
| case .macosGlassRegular, .macosGlassClear: | ||
| return true | ||
| case .disabled, .enabled: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| static func parse(_ rawValue: String) -> Self? { | ||
| let normalized = rawValue | ||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||
| .lowercased() | ||
|
|
||
| switch normalized { | ||
| case "false", "0", "off", "disabled": | ||
| return .disabled | ||
| case "true", "1", "on": | ||
| return .enabled | ||
| case "macos-glass-regular": | ||
| return .macosGlassRegular | ||
| case "macos-glass-clear": | ||
| return .macosGlassClear | ||
| default: | ||
| if Double(normalized) != nil { | ||
| return .enabled | ||
|
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| } | ||
| return nil | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static let cmuxReleaseBundleIdentifier = "com.cmuxterm.app" | ||
| private static let loadCacheLock = NSLock() | ||
| private static var cachedConfigsByColorScheme: [ColorSchemePreference: GhosttyConfig] = [:] | ||
|
|
@@ -23,6 +61,7 @@ struct GhosttyConfig { | |
| // Colors (from theme or config) | ||
| var backgroundColor: NSColor = NSColor(hex: "#272822")! | ||
| var backgroundOpacity: Double = 1.0 | ||
| var backgroundBlur: BackgroundBlur = .disabled | ||
| var foregroundColor: NSColor = NSColor(hex: "#fdfff1")! | ||
| var cursorColor: NSColor = NSColor(hex: "#c0c1b5")! | ||
| var cursorTextColor: NSColor = NSColor(hex: "#8d8e82")! | ||
|
|
@@ -258,6 +297,10 @@ struct GhosttyConfig { | |
| if let opacity = Double(value) { | ||
| backgroundOpacity = opacity | ||
| } | ||
| case "background-blur": | ||
| if let blur = BackgroundBlur.parse(value) { | ||
| backgroundBlur = blur | ||
| } | ||
| case "foreground": | ||
| if let color = NSColor(hex: value) { | ||
| foregroundColor = color | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,68 @@ private func cmuxTransparentWindowBaseColor() -> NSColor { | |
| // avoids visual artifacts that can happen with a fully clear window background. | ||
| NSColor.white.withAlphaComponent(0.001) | ||
| } | ||
|
|
||
| enum CmuxPendingGhosttySearchRequest: Equatable { | ||
| case startSearch | ||
| case searchSelection | ||
| } | ||
|
|
||
| enum CmuxGhosttyStartSearchResolution: Equatable { | ||
| case ignore | ||
| case focusExisting | ||
| case updateExistingNeedle(String) | ||
| case createEmpty | ||
| case createWithNeedle(String) | ||
| } | ||
|
|
||
| func cmuxDecodeGhosttyOptionalCString(_ pointer: UnsafePointer<CChar>?) -> String? { | ||
| guard let pointer else { return nil } | ||
|
|
||
| // Guard obviously invalid low addresses so Ghostty callback corruption cannot crash us | ||
| // while handling an empty/open-search notification. | ||
| let address = UInt(bitPattern: pointer) | ||
| guard address >= 0x1000 else { return nil } | ||
|
|
||
| return String(validatingUTF8: pointer) | ||
| } | ||
|
|
||
| func cmuxResolveGhosttyStartSearch( | ||
| existingSearchState: Bool, | ||
| pendingRequest: CmuxPendingGhosttySearchRequest?, | ||
| needle: String? | ||
| ) -> CmuxGhosttyStartSearchResolution { | ||
| if existingSearchState { | ||
| if let needle, !needle.isEmpty { | ||
| return .updateExistingNeedle(needle) | ||
| } | ||
| return .focusExisting | ||
| } | ||
|
|
||
| switch pendingRequest { | ||
| case .startSearch: | ||
| return .createEmpty | ||
| case .searchSelection: | ||
| if let needle, !needle.isEmpty { | ||
| return .createWithNeedle(needle) | ||
| } | ||
| return .ignore | ||
|
Comment on lines
+71
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| case nil: | ||
| return .ignore | ||
|
Comment on lines
+75
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| func cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded( | ||
| _ terminalSurface: TerminalSurface, | ||
| searchFocusNotifier: @escaping (TerminalSurface) -> Void = { | ||
| NotificationCenter.default.post(name: .ghosttySearchFocus, object: $0) | ||
| } | ||
| ) { | ||
| guard terminalSurface.pendingGhosttySearchActivationRequest == .startSearch else { return } | ||
| terminalSurface.clearGhosttySearchActivationRequest() | ||
| guard terminalSurface.searchState == nil else { return } | ||
| terminalSurface.searchState = TerminalSurface.SearchState() | ||
| searchFocusNotifier(terminalSurface) | ||
| } | ||
| #endif | ||
|
|
||
| #if DEBUG | ||
|
|
@@ -2131,15 +2193,41 @@ class GhosttyApp { | |
| return true | ||
| case GHOSTTY_ACTION_START_SEARCH: | ||
| guard let terminalSurface = surfaceView.terminalSurface else { return true } | ||
| let needle = action.action.start_search.needle.flatMap { String(cString: $0) } | ||
| let pendingRequest = terminalSurface.consumeGhosttySearchActivationRequest() | ||
| let shouldDecodeNeedle = | ||
| terminalSurface.searchState != nil || pendingRequest != .startSearch | ||
|
Comment on lines
+2196
to
+2198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| let needle = shouldDecodeNeedle | ||
|
Comment on lines
+2197
to
+2199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| ? cmuxDecodeGhosttyOptionalCString(action.action.start_search.needle) | ||
| : nil | ||
| DispatchQueue.main.async { | ||
| if let searchState = terminalSurface.searchState { | ||
| if let needle, !needle.isEmpty { | ||
| let resolution = cmuxResolveGhosttyStartSearch( | ||
| existingSearchState: terminalSurface.searchState != nil, | ||
| pendingRequest: pendingRequest, | ||
| needle: needle | ||
| ) | ||
|
|
||
| switch resolution { | ||
| case .ignore: | ||
| #if DEBUG | ||
| dlog( | ||
| "find.startSearch ignored tab=\(terminalSurface.tabId.uuidString.prefix(5)) " + | ||
| "surface=\(terminalSurface.id.uuidString.prefix(5)) " + | ||
| "pending=\(String(describing: pendingRequest))" | ||
| ) | ||
| #endif | ||
| return | ||
| case .focusExisting: | ||
| break | ||
| case .updateExistingNeedle(let needle): | ||
| if let searchState = terminalSurface.searchState { | ||
| searchState.needle = needle | ||
| } | ||
| } else { | ||
| terminalSurface.searchState = TerminalSurface.SearchState(needle: needle ?? "") | ||
| case .createEmpty: | ||
| terminalSurface.searchState = TerminalSurface.SearchState() | ||
| case .createWithNeedle(let needle): | ||
| terminalSurface.searchState = TerminalSurface.SearchState(needle: needle) | ||
| } | ||
|
|
||
| NotificationCenter.default.post(name: .ghosttySearchFocus, object: terminalSurface) | ||
| } | ||
| return true | ||
|
|
@@ -2590,6 +2678,7 @@ final class TerminalSurface: Identifiable, ObservableObject { | |
| private var portalLifecycleState: PortalLifecycleState = .live | ||
| private var portalLifecycleGeneration: UInt64 = 1 | ||
| private var activePortalHostLease: PortalHostLease? | ||
| private var pendingGhosttySearchRequest: CmuxPendingGhosttySearchRequest? | ||
| @Published var searchState: SearchState? = nil { | ||
| didSet { | ||
| if let searchState { | ||
|
|
@@ -3629,6 +3718,24 @@ final class TerminalSurface: Identifiable, ObservableObject { | |
| } | ||
| } | ||
|
|
||
| func requestGhosttySearchActivation(_ request: CmuxPendingGhosttySearchRequest) { | ||
| pendingGhosttySearchRequest = request | ||
| } | ||
|
Comment on lines
+3721
to
+3723
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎.
Comment on lines
+3721
to
+3723
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
|
|
||
| var pendingGhosttySearchActivationRequest: CmuxPendingGhosttySearchRequest? { | ||
| pendingGhosttySearchRequest | ||
| } | ||
|
|
||
| func consumeGhosttySearchActivationRequest() -> CmuxPendingGhosttySearchRequest? { | ||
| let request = pendingGhosttySearchRequest | ||
| pendingGhosttySearchRequest = nil | ||
| return request | ||
| } | ||
|
|
||
| func clearGhosttySearchActivationRequest() { | ||
| pendingGhosttySearchRequest = nil | ||
| } | ||
|
|
||
| @discardableResult | ||
| func toggleKeyboardCopyMode() -> Bool { | ||
| let handled = surfaceView.toggleKeyboardCopyMode() | ||
|
|
@@ -4536,7 +4643,20 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { | |
| _ = performBindingAction("jump_to_prompt:\(delta * count)") | ||
| refreshKeyboardCopyModeViewportRowFromVisibleAnchor(surface: surface) | ||
| case .startSearch: | ||
| _ = performBindingAction("start_search") | ||
| if let terminalSurface, terminalSurface.searchState != nil { | ||
| NotificationCenter.default.post(name: .ghosttySearchFocus, object: terminalSurface) | ||
| break | ||
| } | ||
| terminalSurface?.requestGhosttySearchActivation(.startSearch) | ||
| if performBindingAction("start_search") { | ||
| if let terminalSurface { | ||
| DispatchQueue.main.async { | ||
| cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(terminalSurface) | ||
| } | ||
| } | ||
| } else { | ||
| terminalSurface?.clearGhosttySearchActivationRequest() | ||
| } | ||
| case .searchNext: | ||
| performBindingAction("navigate_search:next", repeatCount: count) | ||
| refreshKeyboardCopyModeViewportRowFromVisibleAnchor(surface: surface) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid run-loop deferral for search fallback state initialization.
At Line 1743, forcing
cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(...)ontoDispatchQueue.main.asynccreates a timing gap where this function returnstruewhilesearchStatecan still benil. That can desync immediate search-active checks and overlay activation paths.Suggested fix
🤖 Prompt for AI Agents