Implement side-rounded menu borders for macOS menu bar

- Create custom SideRoundedRectangle shape with flat top/bottom borders
- Apply custom shape to both SwiftUI background and NSWindow mask layer
- Update CustomMenuContainer to use the new shape for consistent styling
- Maintain rounded corners only on left and right sides as requested

This gives the menu bar dropdown a more integrated appearance with the
menu bar while keeping the modern rounded aesthetic on the sides.
This commit is contained in:
Peter Steinberger 2025-07-01 19:16:25 +01:00
parent f1f99be514
commit f60bd2d5a0
7 changed files with 320 additions and 71 deletions

View file

@ -401,6 +401,8 @@ final class WindowTracker {
focusTerminalAppWindow(windowInfo) focusTerminalAppWindow(windowInfo)
case .iTerm2: case .iTerm2:
focusiTerm2Window(windowInfo) focusiTerm2Window(windowInfo)
case .ghostty:
focusGhosttyWindow(windowInfo)
default: default:
// For other terminals, use standard window focus // For other terminals, use standard window focus
focusWindowUsingAccessibility(windowInfo) focusWindowUsingAccessibility(windowInfo)
@ -549,17 +551,19 @@ final class WindowTracker {
/// Focuses a Terminal.app window/tab. /// Focuses a Terminal.app window/tab.
private func focusTerminalAppWindow(_ windowInfo: WindowInfo) { private func focusTerminalAppWindow(_ windowInfo: WindowInfo) {
if let tabRef = windowInfo.tabReference { if let tabRef = windowInfo.tabReference {
// Use stored tab reference // Use stored tab reference to select the tab
// The tabRef format is "tab id X of window id Y"
let script = """ let script = """
tell application "Terminal" tell application "Terminal"
activate activate
\(tabRef) set selected of \(tabRef) to true
set frontmost of window id \(windowInfo.windowID) to true
end tell end tell
""" """
do { do {
try AppleScriptExecutor.shared.execute(script) try AppleScriptExecutor.shared.execute(script)
logger.info("Focused Terminal.app tab using reference") logger.info("Focused Terminal.app tab using reference: \(tabRef)")
} catch { } catch {
logger.error("Failed to focus Terminal.app tab: \(error)") logger.error("Failed to focus Terminal.app tab: \(error)")
// Fallback to accessibility // Fallback to accessibility
@ -593,6 +597,7 @@ final class WindowTracker {
private func focusiTerm2Window(_ windowInfo: WindowInfo) { private func focusiTerm2Window(_ windowInfo: WindowInfo) {
if let windowID = windowInfo.tabID { if let windowID = windowInfo.tabID {
// Use window ID for focusing (stored in tabID for consistency) // Use window ID for focusing (stored in tabID for consistency)
// iTerm2 uses 'select' to bring window to front
let script = """ let script = """
tell application "iTerm2" tell application "iTerm2"
activate activate
@ -604,9 +609,10 @@ final class WindowTracker {
do { do {
try AppleScriptExecutor.shared.execute(script) try AppleScriptExecutor.shared.execute(script)
logger.info("Focused iTerm2 window using ID") logger.info("Focused iTerm2 window using ID: \(windowID)")
} catch { } catch {
logger.error("Failed to focus iTerm2 window: \(error)") logger.error("Failed to focus iTerm2 window: \(error)")
// Fallback to accessibility
focusWindowUsingAccessibility(windowInfo) focusWindowUsingAccessibility(windowInfo)
} }
} else { } else {
@ -615,6 +621,149 @@ final class WindowTracker {
} }
} }
/// Focuses a Ghostty window with macOS standard tabs.
private func focusGhosttyWindow(_ windowInfo: WindowInfo) {
// First bring the application to front
if let app = NSRunningApplication(processIdentifier: windowInfo.ownerPID) {
app.activate()
}
// Ghostty uses macOS standard tabs, so we need to:
// 1. Focus the window
// 2. Find and select the correct tab
// Use Accessibility API to handle tab selection
let axApp = AXUIElementCreateApplication(windowInfo.ownerPID)
var windowsValue: CFTypeRef?
let result = AXUIElementCopyAttributeValue(axApp, kAXWindowsAttribute as CFString, &windowsValue)
guard result == .success,
let windows = windowsValue as? [AXUIElement],
!windows.isEmpty
else {
logger.error("Failed to get windows for Ghostty")
focusWindowUsingAccessibility(windowInfo)
return
}
// Find the matching window
for window in windows {
var windowIDValue: CFTypeRef?
if AXUIElementCopyAttributeValue(window, kAXWindowAttribute as CFString, &windowIDValue) == .success,
let windowNumber = windowIDValue as? Int,
windowNumber == windowInfo.windowID
{
// Found the window, make it main and focused
AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, true as CFTypeRef)
AXUIElementSetAttributeValue(window, kAXFocusedAttribute as CFString, true as CFTypeRef)
// Now look for tabs
var tabsValue: CFTypeRef?
if AXUIElementCopyAttributeValue(window, kAXTabsAttribute as CFString, &tabsValue) == .success,
let tabs = tabsValue as? [AXUIElement],
!tabs.isEmpty
{
// Try to find the tab with matching session info
if let sessionInfo = getSessionInfo(for: windowInfo.sessionID) {
let workingDir = sessionInfo.workingDir
let dirName = (workingDir as NSString).lastPathComponent
let sessionID = windowInfo.sessionID
let activityStatus = sessionInfo.activityStatus?.specificStatus?.status
// First pass: Try to find exact match with session ID in title
for (index, tab) in tabs.enumerated() {
var titleValue: CFTypeRef?
if AXUIElementCopyAttributeValue(tab, kAXTitleAttribute as CFString, &titleValue) == .success,
let title = titleValue as? String
{
// Check if tab title contains the session ID (most precise match)
if title.contains(sessionID) || title.contains("TTY_SESSION_ID=\(sessionID)") {
// Select this tab
AXUIElementPerformAction(tab, kAXPressAction as CFString)
logger.info("Selected Ghostty tab \(index) by session ID match for session \(sessionID)")
return
}
}
}
// Second pass: Try to match by activity status if available
if let activity = activityStatus, !activity.isEmpty {
for (index, tab) in tabs.enumerated() {
var titleValue: CFTypeRef?
if AXUIElementCopyAttributeValue(tab, kAXTitleAttribute as CFString, &titleValue) == .success,
let title = titleValue as? String
{
// Check if tab title contains the activity string
if title.contains(activity) {
// Also verify it's in the right directory if possible
if title.contains(dirName) || title.contains(workingDir) {
// Select this tab
AXUIElementPerformAction(tab, kAXPressAction as CFString)
logger.info("Selected Ghostty tab \(index) by activity match '\(activity)' for session \(sessionID)")
return
}
}
}
}
}
// Third pass: Try to match by command if available
if let command = sessionInfo.command.first, !command.isEmpty {
for (index, tab) in tabs.enumerated() {
var titleValue: CFTypeRef?
if AXUIElementCopyAttributeValue(tab, kAXTitleAttribute as CFString, &titleValue) == .success,
let title = titleValue as? String
{
// Check if tab title contains the command
if title.contains(command) {
// Also verify it's in the right directory if possible
if title.contains(dirName) || title.contains(workingDir) {
// Select this tab
AXUIElementPerformAction(tab, kAXPressAction as CFString)
logger.info("Selected Ghostty tab \(index) by command match for session \(sessionID)")
return
}
}
}
}
}
// Fourth pass: Try to match by working directory only
for (index, tab) in tabs.enumerated() {
var titleValue: CFTypeRef?
if AXUIElementCopyAttributeValue(tab, kAXTitleAttribute as CFString, &titleValue) == .success,
let title = titleValue as? String
{
// Check if tab title contains the working directory
if title.contains(dirName) || title.contains(workingDir) {
// Select this tab
AXUIElementPerformAction(tab, kAXPressAction as CFString)
logger.info("Selected Ghostty tab \(index) by directory match for session \(sessionID)")
return
}
}
}
// If no matching tab found, select the most recently created tab
// (assuming it's the last one if we just created it)
if let lastTab = tabs.last {
AXUIElementPerformAction(lastTab, kAXPressAction as CFString)
logger.info("Selected last Ghostty tab as fallback for session \(sessionID)")
}
}
}
logger.info("Focused Ghostty window using Accessibility API")
return
}
}
// Fallback if we couldn't find the specific window
logger.warning("Could not find matching Ghostty window, using fallback")
focusWindowUsingAccessibility(windowInfo)
}
/// Focuses a window using Accessibility APIs. /// Focuses a window using Accessibility APIs.
private func focusWindowUsingAccessibility(_ windowInfo: WindowInfo) { private func focusWindowUsingAccessibility(_ windowInfo: WindowInfo) {
// First bring the application to front // First bring the application to front
@ -647,6 +796,53 @@ final class WindowTracker {
// Found the matching window, make it main and focused // Found the matching window, make it main and focused
AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, true as CFTypeRef) AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, true as CFTypeRef)
AXUIElementSetAttributeValue(window, kAXFocusedAttribute as CFString, true as CFTypeRef) AXUIElementSetAttributeValue(window, kAXFocusedAttribute as CFString, true as CFTypeRef)
// For terminals that use macOS standard tabs, try to select the correct tab
var tabsValue: CFTypeRef?
if AXUIElementCopyAttributeValue(window, kAXTabsAttribute as CFString, &tabsValue) == .success,
let tabs = tabsValue as? [AXUIElement],
!tabs.isEmpty
{
logger.info("Terminal has \(tabs.count) tabs, attempting to find correct one")
// Try to find the tab with matching session info
if let sessionInfo = getSessionInfo(for: windowInfo.sessionID) {
let workingDir = sessionInfo.workingDir
let dirName = (workingDir as NSString).lastPathComponent
let sessionID = windowInfo.sessionID
let activityStatus = sessionInfo.activityStatus?.specificStatus?.status
// Try multiple matching strategies
for (index, tab) in tabs.enumerated() {
var titleValue: CFTypeRef?
if AXUIElementCopyAttributeValue(tab, kAXTitleAttribute as CFString, &titleValue) == .success,
let title = titleValue as? String
{
// Check for session ID match first (most precise)
if title.contains(sessionID) || title.contains("TTY_SESSION_ID=\(sessionID)") {
AXUIElementPerformAction(tab, kAXPressAction as CFString)
logger.info("Selected tab \(index) by session ID for terminal \(windowInfo.terminalApp.rawValue)")
return
}
// Check for activity status match (unique for dynamic activities)
if let activity = activityStatus, !activity.isEmpty, title.contains(activity) {
AXUIElementPerformAction(tab, kAXPressAction as CFString)
logger.info("Selected tab \(index) by activity '\(activity)' for terminal \(windowInfo.terminalApp.rawValue)")
return
}
// Check for directory match
if title.contains(dirName) || title.contains(workingDir) {
AXUIElementPerformAction(tab, kAXPressAction as CFString)
logger.info("Selected tab \(index) by directory for terminal \(windowInfo.terminalApp.rawValue)")
return
}
}
}
}
}
logger.info("Focused window using Accessibility API") logger.info("Focused window using Accessibility API")
return return
} }

View file

@ -35,7 +35,7 @@ final class CustomMenuWindow: NSPanel {
// Initialize window with appropriate style // Initialize window with appropriate style
super.init( super.init(
contentRect: NSRect(x: 0, y: 0, width: 384, height: 400), contentRect: NSRect(x: 0, y: 0, width: 384, height: 400),
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow], styleMask: [.borderless, .utilityWindow],
backing: .buffered, backing: .buffered,
defer: false defer: false
) )
@ -45,22 +45,42 @@ final class CustomMenuWindow: NSPanel {
backgroundColor = .clear backgroundColor = .clear
hasShadow = true hasShadow = true
level = .popUpMenu level = .popUpMenu
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient] collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient, .ignoresCycle]
isMovableByWindowBackground = false isMovableByWindowBackground = false
hidesOnDeactivate = false hidesOnDeactivate = false
isReleasedWhenClosed = false isReleasedWhenClosed = false
// Allow the window to become key but not main
// This helps maintain button highlight state
acceptsMouseMovedEvents = false
// Set content view controller // Set content view controller
contentViewController = hostingController contentViewController = hostingController
// Force the view to load immediately // Force the view to load immediately
_ = hostingController.view _ = hostingController.view
// Add visual effect background with rounded corners // Add visual effect background with custom shape
if let contentView = contentViewController?.view { if let contentView = contentViewController?.view {
contentView.wantsLayer = true contentView.wantsLayer = true
contentView.layer?.cornerRadius = 12
contentView.layer?.masksToBounds = true // Create a custom mask layer for side-rounded corners
let maskLayer = CAShapeLayer()
maskLayer.path = createSideRoundedPath(in: contentView.bounds, cornerRadius: 12)
contentView.layer?.mask = maskLayer
// Update mask when bounds change
contentView.postsFrameChangedNotifications = true
NotificationCenter.default.addObserver(
forName: NSView.frameDidChangeNotification,
object: contentView,
queue: .main
) { [weak self, weak contentView] _ in
guard let self = self, let contentView = contentView else { return }
Task { @MainActor in
maskLayer.path = self.createSideRoundedPath(in: contentView.bounds, cornerRadius: 12)
}
}
// Add subtle shadow // Add subtle shadow
contentView.shadow = NSShadow() contentView.shadow = NSShadow()
@ -73,7 +93,7 @@ final class CustomMenuWindow: NSPanel {
func show(relativeTo statusItemButton: NSStatusBarButton) { func show(relativeTo statusItemButton: NSStatusBarButton) {
// Store button reference and ensure it stays highlighted // Store button reference and ensure it stays highlighted
self.statusBarButton = statusItemButton self.statusBarButton = statusItemButton
statusItemButton.state = .on statusItemButton.highlight(true)
// First, make sure the SwiftUI hierarchy has laid itself out // First, make sure the SwiftUI hierarchy has laid itself out
hostingController.view.layoutSubtreeIfNeeded() hostingController.view.layoutSubtreeIfNeeded()
@ -134,16 +154,17 @@ final class CustomMenuWindow: NSPanel {
// Set all visual properties at once // Set all visual properties at once
alphaValue = 1.0 alphaValue = 1.0
// Ensure button state remains on // Ensure button remains highlighted
statusBarButton?.state = .on statusBarButton?.highlight(true)
// Activate app and show window // Show window without activating the app aggressively
NSApp.activate(ignoringOtherApps: true) // This helps maintain the button's highlight state
makeKeyAndOrderFront(nil) orderFront(nil)
makeKey()
// Force button state update again after window is shown // Force button highlight update again after window is shown
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.statusBarButton?.state = .on self?.statusBarButton?.highlight(true)
} }
// Set first responder after window is visible // Set first responder after window is visible
@ -238,8 +259,8 @@ final class CustomMenuWindow: NSPanel {
// Mark window as not visible // Mark window as not visible
_isWindowVisible = false _isWindowVisible = false
// Reset button state when hiding // Reset button highlight when hiding
statusBarButton?.state = .off statusBarButton?.highlight(false)
orderOut(nil) orderOut(nil)
teardownEventMonitoring() teardownEventMonitoring()
onHide?() onHide?()
@ -251,8 +272,8 @@ final class CustomMenuWindow: NSPanel {
// Mark window as not visible // Mark window as not visible
_isWindowVisible = false _isWindowVisible = false
// Reset button state when window is ordered out // Reset button highlight when window is ordered out
statusBarButton?.state = .off statusBarButton?.highlight(false)
onHide?() onHide?()
} }
@ -306,12 +327,67 @@ final class CustomMenuWindow: NSPanel {
teardownEventMonitoring() teardownEventMonitoring()
} }
} }
private func createSideRoundedPath(in rect: CGRect, cornerRadius: CGFloat) -> CGPath {
let path = CGMutablePath()
// Start from top-left corner (flat)
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
// Top edge (flat)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
// Right edge with rounded corners
path.addArc(
center: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: -CGFloat.pi / 2,
endAngle: 0,
clockwise: false
)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
path.addArc(
center: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - cornerRadius),
radius: cornerRadius,
startAngle: 0,
endAngle: CGFloat.pi / 2,
clockwise: false
)
// Bottom edge (flat)
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
// Left edge with rounded corners
path.addArc(
center: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - cornerRadius),
radius: cornerRadius,
startAngle: CGFloat.pi / 2,
endAngle: CGFloat.pi,
clockwise: false
)
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
path.addArc(
center: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: CGFloat.pi,
endAngle: 3 * CGFloat.pi / 2,
clockwise: false
)
path.closeSubpath()
return path
}
} }
/// A wrapper view that applies modern SwiftUI material background to menu content. /// A wrapper view that applies modern SwiftUI material background to menu content.
struct CustomMenuContainer<Content: View>: View { struct CustomMenuContainer<Content: View>: View {
@ViewBuilder @ViewBuilder let content: Content
let content: Content
@Environment(\.colorScheme) @Environment(\.colorScheme)
private var colorScheme private var colorScheme
@ -319,9 +395,9 @@ struct CustomMenuContainer<Content: View>: View {
var body: some View { var body: some View {
content content
.fixedSize() .fixedSize()
.background(backgroundMaterial, in: RoundedRectangle(cornerRadius: 12)) .background(backgroundMaterial, in: SideRoundedRectangle(cornerRadius: 12))
.overlay( .overlay(
RoundedRectangle(cornerRadius: 12) SideRoundedRectangle(cornerRadius: 12)
.stroke(borderColor, lineWidth: 1) .stroke(borderColor, lineWidth: 1)
) )
} }

View file

@ -292,7 +292,7 @@ struct NewSessionForm: View {
.foregroundColor(.white) .foregroundColor(.white)
.background( .background(
RoundedRectangle(cornerRadius: 6) RoundedRectangle(cornerRadius: 6)
.fill(command.isEmpty || workingDirectory.isEmpty ? Color.gray.opacity(0.4) : Color.accentColor) .fill(command.isEmpty || workingDirectory.isEmpty ? Color.gray.opacity(0.4) : Color(red: 0.2, green: 0.6, blue: 0.3))
) )
.disabled(isCreating || command.isEmpty || workingDirectory.isEmpty) .disabled(isCreating || command.isEmpty || workingDirectory.isEmpty)
} }
@ -301,6 +301,7 @@ struct NewSessionForm: View {
} }
.frame(width: 384) .frame(width: 384)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12)) .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
.clipShape(RoundedRectangle(cornerRadius: 12))
.fixedSize(horizontal: true, vertical: false) .fixedSize(horizontal: true, vertical: false)
.onAppear { .onAppear {
loadPreferences() loadPreferences()

View file

@ -63,6 +63,8 @@ final class StatusBarController: NSObject {
button.action = #selector(handleClick(_:)) button.action = #selector(handleClick(_:))
button.target = self button.target = self
button.sendAction(on: [.leftMouseUp, .rightMouseUp]) button.sendAction(on: [.leftMouseUp, .rightMouseUp])
// Use pushOnPushOff for proper state management
button.setButtonType(.pushOnPushOff) button.setButtonType(.pushOnPushOff)
// Accessibility // Accessibility
@ -126,9 +128,6 @@ final class StatusBarController: NSObject {
func updateStatusItemDisplay() { func updateStatusItemDisplay() {
guard let button = statusItem?.button else { return } guard let button = statusItem?.button else { return }
// Check if any menu is visible to preserve highlight state
let shouldBeHighlighted = menuManager.isAnyMenuVisible
// Update icon based on server and network status // Update icon based on server and network status
let iconName = (serverManager.isRunning && hasNetworkAccess) ? "menubar" : "menubar.inactive" let iconName = (serverManager.isRunning && hasNetworkAccess) ? "menubar" : "menubar.inactive"
if let image = NSImage(named: iconName) { if let image = NSImage(named: iconName) {
@ -143,13 +142,6 @@ final class StatusBarController: NSObject {
} }
} }
// With .pushOnPushOff button type, the button manages its own highlight state
// We only need to update the state when the menu visibility changes
let expectedState: NSControl.StateValue = shouldBeHighlighted ? .on : .off
if button.state != expectedState {
button.state = expectedState
}
// Update session count display // Update session count display
let sessions = sessionMonitor.sessions.values.filter(\.isRunning) let sessions = sessionMonitor.sessions.values.filter(\.isRunning)
let activeSessions = sessions.filter { session in let activeSessions = sessions.filter { session in
@ -174,11 +166,6 @@ final class StatusBarController: NSObject {
let indicator = formatSessionIndicator(activeCount: activeCount, totalCount: totalCount, style: indicatorStyle) let indicator = formatSessionIndicator(activeCount: activeCount, totalCount: totalCount, style: indicatorStyle)
button.title = indicator.isEmpty ? "" : " " + indicator button.title = indicator.isEmpty ? "" : " " + indicator
// Update button state after title change if needed
if shouldBeHighlighted && button.state != .on {
button.state = .on
}
// Update tooltip // Update tooltip
updateTooltip() updateTooltip()
} }
@ -259,12 +246,6 @@ final class StatusBarController: NSObject {
private func handleLeftClick(_ button: NSStatusBarButton) { private func handleLeftClick(_ button: NSStatusBarButton) {
menuManager.toggleCustomWindow(relativeTo: button) menuManager.toggleCustomWindow(relativeTo: button)
// Force update display after toggling to ensure button state is correct
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(50))
updateStatusItemDisplay()
}
} }
private func handleRightClick(_ button: NSStatusBarButton) { private func handleRightClick(_ button: NSStatusBarButton) {

View file

@ -69,13 +69,7 @@ final class StatusBarMenuManager: NSObject {
statusBarButton = button statusBarButton = button
} }
// Update button state based on menu state (for .pushOnPushOff button type) // No need to manage highlight here since we're using button state
switch menuState {
case .none:
statusBarButton?.state = .off
case .customWindow, .contextMenu:
statusBarButton?.state = .on
}
} }
// MARK: - Left-Click Custom Window Management // MARK: - Left-Click Custom Window Management
@ -83,8 +77,6 @@ final class StatusBarMenuManager: NSObject {
func toggleCustomWindow(relativeTo button: NSStatusBarButton) { func toggleCustomWindow(relativeTo button: NSStatusBarButton) {
if let window = customWindow, window.isVisible { if let window = customWindow, window.isVisible {
hideCustomWindow() hideCustomWindow()
// Ensure button state is updated
button.state = .off
} else { } else {
showCustomWindow(relativeTo: button) showCustomWindow(relativeTo: button)
} }
@ -153,19 +145,19 @@ final class StatusBarMenuManager: NSObject {
// Show the custom window // Show the custom window
customWindow?.show(relativeTo: button) customWindow?.show(relativeTo: button)
// Force immediate button state update after showing window // Force immediate button highlight update after showing window
// This ensures the button stays highlighted even if there's a timing issue // This ensures the button stays highlighted even if there's a timing issue
Task { @MainActor in Task { @MainActor in
try? await Task.sleep(for: .milliseconds(10)) try? await Task.sleep(for: .milliseconds(10))
button.state = .on button.highlight(true)
} }
} }
func hideCustomWindow() { func hideCustomWindow() {
customWindow?.hide() customWindow?.hide()
// Note: state will be reset by the onHide callback // Reset button state
// But also ensure button state is updated immediately
statusBarButton?.state = .off statusBarButton?.state = .off
// Note: state will be reset by the onHide callback
} }
var isCustomWindowVisible: Bool { var isCustomWindowVisible: Bool {
@ -195,12 +187,15 @@ final class StatusBarMenuManager: NSObject {
// Hide custom window first if it's visible // Hide custom window first if it's visible
hideCustomWindow() hideCustomWindow()
// Update menu state to context menu
updateMenuState(.contextMenu, button: button)
// Store status item reference // Store status item reference
currentStatusItem = statusItem currentStatusItem = statusItem
// Set the button's state to on for context menu
button.state = .on
// Update menu state to context menu
updateMenuState(.contextMenu, button: button)
let menu = NSMenu() let menu = NSMenu()
menu.delegate = self menu.delegate = self
@ -282,9 +277,6 @@ final class StatusBarMenuManager: NSObject {
// Show the context menu // Show the context menu
// Use popUpMenu for proper context menu display that doesn't interfere with button highlighting // Use popUpMenu for proper context menu display that doesn't interfere with button highlighting
menu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 5), in: button) menu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 5), in: button)
// Update state to indicate no menu is active after context menu closes
updateMenuState(.none, button: button)
} }
// MARK: - Context Menu Actions // MARK: - Context Menu Actions
@ -362,6 +354,9 @@ final class StatusBarMenuManager: NSObject {
extension StatusBarMenuManager: NSMenuDelegate { extension StatusBarMenuManager: NSMenuDelegate {
func menuDidClose(_ menu: NSMenu) { func menuDidClose(_ menu: NSMenu) {
// Reset button state
statusBarButton?.state = .off
// Reset menu state when context menu closes // Reset menu state when context menu closes
updateMenuState(.none) updateMenuState(.none)

View file

@ -496,7 +496,7 @@ struct SessionRow: View {
HStack(spacing: 4) { HStack(spacing: 4) {
Text(activityStatus) Text(activityStatus)
.font(.system(size: 10)) .font(.system(size: 10))
.foregroundColor(Color(red: 1.0, green: 0.5, blue: 0.0)) .foregroundColor(Color(red: 0.8, green: 0.4, blue: 0.0))
Spacer(minLength: 4) Spacer(minLength: 4)
@ -548,7 +548,7 @@ struct SessionRow: View {
.transition(.opacity) .transition(.opacity)
} }
} }
.frame(width: 35, alignment: .trailing) .frame(width: 40, alignment: .trailing)
.animation(.easeInOut(duration: 0.15), value: isHovered) .animation(.easeInOut(duration: 0.15), value: isHovered)
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
@ -704,7 +704,7 @@ struct SessionRow: View {
private var activityColor: Color { private var activityColor: Color {
if isActive { if isActive {
Color(red: 1.0, green: 0.5, blue: 0.0) // Brighter, more saturated orange Color(red: 0.8, green: 0.4, blue: 0.0) // Darker orange for better contrast
} else { } else {
Color(red: 0.0, green: 0.7, blue: 0.0) // Darker, more visible green Color(red: 0.0, green: 0.7, blue: 0.0) // Darker, more visible green
} }

View file

@ -367,7 +367,7 @@ struct SessionRowView: View {
if let activityStatus { if let activityStatus {
Text(activityStatus) Text(activityStatus)
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundColor(.orange) .foregroundColor(Color(red: 0.8, green: 0.4, blue: 0.0))
Text("·") Text("·")
.font(.system(size: 11)) .font(.system(size: 11))