diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 6a9f9fd6..6b6eeb1d 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -6437,6 +6437,10 @@ private struct TabItemView: View { usesInvertedActiveForeground ? Color.white.opacity(0.8) : cmuxAccentColor() } + private var activeSelectionColor: Color { + resolvedCustomTabColor ?? Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme)) + } + private var shortcutHintEmphasis: Double { usesInvertedActiveForeground ? 1.0 : 0.9 } @@ -6491,6 +6495,7 @@ private struct TabItemView: View { .foregroundColor(activeSecondaryColor(0.8)) } + Text(tab.title) .font(.system(size: 12.5, weight: titleFontWeight)) .foregroundColor(activePrimaryTextColor) @@ -6948,11 +6953,11 @@ private struct TabItemView: View { private var backgroundColor: Color { switch activeTabIndicatorStyle { case .leftRail: - if isActive { return Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme)) } + if isActive { return activeSelectionColor } if isMultiSelected { return cmuxAccentColor().opacity(0.25) } return Color.clear case .solidFill: - if isActive { return Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme)) } + if isActive { return activeSelectionColor } if let custom = resolvedCustomTabColor { if isMultiSelected { return custom.opacity(0.35) } return custom.opacity(0.7) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index efdb0fe4..ee1a7616 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -3951,11 +3951,11 @@ final class GhosttySurfaceScrollView: NSView { notificationRingOverlayView.layer?.masksToBounds = false notificationRingOverlayView.autoresizingMask = [.width, .height] notificationRingLayer.fillColor = NSColor.clear.cgColor - notificationRingLayer.strokeColor = NSColor.systemBlue.cgColor + notificationRingLayer.strokeColor = cmuxAccentNSColor().cgColor notificationRingLayer.lineWidth = 2.5 notificationRingLayer.lineJoin = .round notificationRingLayer.lineCap = .round - notificationRingLayer.shadowColor = NSColor.systemBlue.cgColor + notificationRingLayer.shadowColor = cmuxAccentNSColor().cgColor notificationRingLayer.shadowOpacity = 0.35 notificationRingLayer.shadowRadius = 3 notificationRingLayer.shadowOffset = .zero @@ -3968,11 +3968,11 @@ final class GhosttySurfaceScrollView: NSView { flashOverlayView.layer?.masksToBounds = false flashOverlayView.autoresizingMask = [.width, .height] flashLayer.fillColor = NSColor.clear.cgColor - flashLayer.strokeColor = NSColor.systemBlue.cgColor + flashLayer.strokeColor = cmuxAccentNSColor().cgColor flashLayer.lineWidth = 3 flashLayer.lineJoin = .round flashLayer.lineCap = .round - flashLayer.shadowColor = NSColor.systemBlue.cgColor + flashLayer.shadowColor = cmuxAccentNSColor().cgColor flashLayer.shadowOpacity = 0.6 flashLayer.shadowRadius = 6 flashLayer.shadowOffset = .zero @@ -4145,6 +4145,27 @@ final class GhosttySurfaceScrollView: NSView { surfaceView.onTriggerFlash = handler } + private func resolvedWorkspaceFocusFlashColor() -> NSColor { + guard let tabId = surfaceView.tabId, + let app = AppDelegate.shared, + let manager = app.tabManagerFor(tabId: tabId) ?? app.tabManager, + let workspace = manager.tabs.first(where: { $0.id == tabId }) else { + return cmuxAccentNSColor() + } + return Workspace.resolvedFocusFlashColor(customColorHex: workspace.customColor) + } + + private func setFocusFlashColor(_ color: NSColor?) { + let resolved = color ?? resolvedWorkspaceFocusFlashColor() + CATransaction.begin() + CATransaction.setDisableActions(true) + notificationRingLayer.strokeColor = resolved.cgColor + notificationRingLayer.shadowColor = resolved.cgColor + flashLayer.strokeColor = resolved.cgColor + flashLayer.shadowColor = resolved.cgColor + CATransaction.commit() + } + func setBackgroundColor(_ color: NSColor) { guard let layer = backgroundView.layer else { return } CATransaction.begin() @@ -4170,6 +4191,7 @@ final class GhosttySurfaceScrollView: NSView { return } + setFocusFlashColor(nil) CATransaction.begin() CATransaction.setDisableActions(true) notificationRingOverlayView.isHidden = !visible @@ -4392,7 +4414,7 @@ final class GhosttySurfaceScrollView: NSView { } #endif - func triggerFlash() { + func triggerFlash(color: NSColor? = nil) { DispatchQueue.main.async { [weak self] in guard let self else { return } #if DEBUG @@ -4400,6 +4422,7 @@ final class GhosttySurfaceScrollView: NSView { Self.recordFlash(for: surfaceId) } #endif + self.setFocusFlashColor(color) self.updateFlashPath() self.flashLayer.removeAllAnimations() self.flashLayer.opacity = 0 diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 9188d093..7135ffe3 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -1313,6 +1313,7 @@ final class BrowserPanel: Panel, ObservableObject { /// Increment to request a UI-only flash highlight (e.g. from a keyboard shortcut). @Published private(set) var focusFlashToken: Int = 0 + @Published private(set) var focusFlashColor: NSColor = cmuxAccentNSColor() /// Sticky omnibar-focus intent. This survives view mount timing races and is /// cleared only after BrowserPanelView acknowledges handling it. @@ -1520,7 +1521,8 @@ final class BrowserPanel: Panel, ObservableObject { workspaceId = newWorkspaceId } - func triggerFlash() { + func triggerFlash(color: NSColor? = nil) { + focusFlashColor = color ?? cmuxAccentNSColor() focusFlashToken &+= 1 } diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index ea282f33..fbcc815a 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -296,6 +296,10 @@ struct BrowserPanelView: View { ) } + private var focusFlashColor: Color { + Color(nsColor: panel.focusFlashColor) + } + var body: some View { VStack(spacing: 0) { addressBar @@ -303,8 +307,8 @@ struct BrowserPanelView: View { } .overlay { RoundedRectangle(cornerRadius: FocusFlashPattern.ringCornerRadius) - .stroke(cmuxAccentColor().opacity(focusFlashOpacity), lineWidth: 3) - .shadow(color: cmuxAccentColor().opacity(focusFlashOpacity * 0.35), radius: 10) + .stroke(focusFlashColor.opacity(focusFlashOpacity), lineWidth: 3) + .shadow(color: focusFlashColor.opacity(focusFlashOpacity * 0.35), radius: 10) .padding(FocusFlashPattern.ringInset) .allowsHitTesting(false) } diff --git a/Sources/Panels/Panel.swift b/Sources/Panels/Panel.swift index a0a719c4..b3dac0fe 100644 --- a/Sources/Panels/Panel.swift +++ b/Sources/Panels/Panel.swift @@ -1,5 +1,6 @@ import Foundation import Combine +import AppKit /// Type of panel content public enum PanelType: String, Codable, Sendable { @@ -70,7 +71,8 @@ public protocol Panel: AnyObject, Identifiable, ObservableObject where ID == UUI func unfocus() /// Trigger a focus flash animation for this panel. - func triggerFlash() + /// - Parameter color: Optional override color for this flash. + func triggerFlash(color: NSColor?) } /// Extension providing default implementations diff --git a/Sources/Panels/TerminalPanel.swift b/Sources/Panels/TerminalPanel.swift index c8ba8507..08104709 100644 --- a/Sources/Panels/TerminalPanel.swift +++ b/Sources/Panels/TerminalPanel.swift @@ -166,8 +166,8 @@ final class TerminalPanel: Panel, ObservableObject { surface.needsConfirmClose() } - func triggerFlash() { - hostedView.triggerFlash() + func triggerFlash(color: NSColor? = nil) { + hostedView.triggerFlash(color: color) } func applyWindowBackgroundIfActive() { diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 4844d501..fc43ab46 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -2934,8 +2934,23 @@ final class Workspace: Identifiable, ObservableObject { // MARK: - Flash/Notification Support + nonisolated static func resolvedFocusFlashColor( + customColorHex: String?, + fallback: NSColor = cmuxAccentNSColor() + ) -> NSColor { + guard let normalizedHex = customColorHex.flatMap(WorkspaceTabColorSettings.normalizedHex), + let color = NSColor(hex: normalizedHex) else { + return fallback + } + return color + } + + private var focusFlashColor: NSColor { + Self.resolvedFocusFlashColor(customColorHex: customColor) + } + func triggerFocusFlash(panelId: UUID) { - panels[panelId]?.triggerFlash() + panels[panelId]?.triggerFlash(color: focusFlashColor) } func triggerNotificationFocusFlash( @@ -2951,7 +2966,7 @@ final class Workspace: Identifiable, ObservableObject { if requiresSplit && !isSplit { return } - terminalPanel.triggerFlash() + terminalPanel.triggerFlash(color: focusFlashColor) } func triggerDebugFlash(panelId: UUID) { diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 0677a847..105493f1 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -2848,6 +2848,26 @@ final class WorkspaceTabColorSettingsTests: XCTestCase { XCTAssertNotEqual(rendered.hexString(), originalHex) XCTAssertGreaterThan(rendered.luminance, base.luminance) } + + func testWorkspaceFocusFlashColorUsesCustomHexWhenValid() { + let fallback = NSColor(hex: "#112233")! + let resolved = Workspace.resolvedFocusFlashColor( + customColorHex: " c0392b ", + fallback: fallback + ) + + XCTAssertEqual(resolved.hexString(), "#C0392B") + } + + func testWorkspaceFocusFlashColorFallsBackWhenHexIsInvalid() { + let fallback = NSColor(hex: "#112233")! + let resolved = Workspace.resolvedFocusFlashColor( + customColorHex: "not-a-color", + fallback: fallback + ) + + XCTAssertEqual(resolved.hexString(), "#112233") + } } final class WorkspaceAutoReorderSettingsTests: XCTestCase {