Add customizable sidebar selection highlight color (#1824)

* Add customizable sidebar selection highlight color

Expose a `sidebarSelectionColorHex` user default that overrides the
hardcoded blue (#0091FF) selection highlight in the sidebar. Add a
"Selection Highlight" color picker in Settings > Workspace Colors,
following the same pattern as existing tint color pickers. Falls back
to the default accent color when no custom color is set.

Closes #1753

* Fix review feedback: reactivity, reset button, localization

- Add @AppStorage subscription in TabItemView so sidebar selection
  color updates reactively when changed in Settings
- Add Reset button in Settings > Workspace Colors > Selection Highlight
- Localize debug panel strings for Selection Color picker
- Clear sidebarSelectionColorHex in resetAllSettings()

* Add customizable notification badge color in sidebar

Add `sidebarNotificationBadgeColorHex` user default to override the
unread notification badge color on workspace tabs. Add a "Notification
Badge" color picker in Settings > Workspace Colors, following the same
pattern as the selection highlight picker. Falls back to the default
accent color when no custom color is set.
This commit is contained in:
Yinbo Wang 2026-03-28 11:18:36 +08:00 committed by GitHub
parent 63904811f9
commit 609a02c3f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 140 additions and 4 deletions

View file

@ -113,7 +113,11 @@ enum SidebarRemoteErrorCopySupport {
}
func sidebarSelectedWorkspaceBackgroundNSColor(for colorScheme: ColorScheme) -> NSColor {
cmuxAccentNSColor(for: colorScheme)
if let hex = UserDefaults.standard.string(forKey: "sidebarSelectionColorHex"),
let parsed = NSColor(hex: hex) {
return parsed
}
return cmuxAccentNSColor(for: colorScheme)
}
func sidebarSelectedWorkspaceForegroundNSColor(opacity: CGFloat) -> NSColor {
@ -11026,6 +11030,8 @@ private struct TabItemView: View, Equatable {
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
private var activeTabIndicatorStyleRaw = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
@AppStorage("sidebarSelectionColorHex") private var sidebarSelectionColorHex: String?
@AppStorage("sidebarNotificationBadgeColorHex") private var sidebarNotificationBadgeColorHex: String?
var isMultiSelected: Bool {
selectedTabIds.contains(tab.id)
@ -11083,7 +11089,10 @@ private struct TabItemView: View, Equatable {
}
private var activeUnreadBadgeFillColor: Color {
usesInvertedActiveForeground ? Color.white.opacity(0.25) : cmuxAccentColor()
if let hex = sidebarNotificationBadgeColorHex, let nsColor = NSColor(hex: hex) {
return Color(nsColor: nsColor)
}
return usesInvertedActiveForeground ? Color.white.opacity(0.25) : cmuxAccentColor()
}
private var activeProgressTrackColor: Color {
@ -11828,14 +11837,21 @@ private struct TabItemView: View, Equatable {
.disabled(!hasReadNotifications(in: targetIds))
}
private var selectionBackgroundColor: NSColor {
if let hex = sidebarSelectionColorHex, let parsed = NSColor(hex: hex) {
return parsed
}
return cmuxAccentNSColor(for: colorScheme)
}
private var backgroundColor: Color {
switch activeTabIndicatorStyle {
case .leftRail:
if isActive { return Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme)) }
if isActive { return Color(nsColor: selectionBackgroundColor) }
if isMultiSelected { return cmuxAccentColor().opacity(0.25) }
return Color.clear
case .solidFill:
if isActive { return Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme)) }
if isActive { return Color(nsColor: selectionBackgroundColor) }
if let custom = resolvedCustomTabColor {
if isMultiSelected { return custom.opacity(0.35) }
return custom.opacity(0.7)

View file

@ -2885,6 +2885,7 @@ private struct SidebarDebugView: View {
private var showSidebarDevBuildBanner = DevBuildBannerDebugSettings.defaultShowSidebarBanner
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
@AppStorage("sidebarSelectionColorHex") private var sidebarSelectionColorHex: String?
private var selectedSidebarIndicatorStyle: SidebarActiveTabIndicatorStyle {
SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle)
@ -2897,6 +2898,21 @@ private struct SidebarDebugView: View {
)
}
private var selectionColorBinding: Binding<Color> {
Binding(
get: {
if let hex = sidebarSelectionColorHex, let nsColor = NSColor(hex: hex) {
return Color(nsColor: nsColor)
}
return cmuxAccentColor()
},
set: { newColor in
let nsColor = NSColor(newColor)
sidebarSelectionColorHex = nsColor.hexString()
}
)
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 14) {
@ -3004,6 +3020,15 @@ private struct SidebarDebugView: View {
Text(style.displayName).tag(style.rawValue)
}
}
ColorPicker(String(localized: "sidebar.debug.selectionColor", defaultValue: "Selection Color"), selection: selectionColorBinding, supportsOpacity: false)
if sidebarSelectionColorHex != nil {
Button(String(localized: "sidebar.debug.resetSelectionColor", defaultValue: "Reset to Default")) {
sidebarSelectionColorHex = nil
}
.font(.caption)
}
}
.padding(.top, 2)
}
@ -3039,6 +3064,7 @@ private struct SidebarDebugView: View {
}
Button("Reset Active Indicator") {
sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
sidebarSelectionColorHex = nil
}
}
@ -3857,6 +3883,8 @@ struct SettingsView: View {
@AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
@AppStorage("sidebarSelectionColorHex") private var sidebarSelectionColorHex: String?
@AppStorage("sidebarNotificationBadgeColorHex") private var sidebarNotificationBadgeColorHex: String?
@AppStorage("sidebarShowBranchDirectory") private var sidebarShowBranchDirectory = true
@AppStorage("sidebarShowPullRequest") private var sidebarShowPullRequest = true
@AppStorage(BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowserKey)
@ -3969,6 +3997,36 @@ struct SettingsView: View {
)
}
private var selectionColorBinding: Binding<Color> {
Binding(
get: {
if let hex = sidebarSelectionColorHex, let nsColor = NSColor(hex: hex) {
return Color(nsColor: nsColor)
}
return cmuxAccentColor()
},
set: { newColor in
let nsColor = NSColor(newColor)
sidebarSelectionColorHex = nsColor.hexString()
}
)
}
private var notificationBadgeColorBinding: Binding<Color> {
Binding(
get: {
if let hex = sidebarNotificationBadgeColorHex, let nsColor = NSColor(hex: hex) {
return Color(nsColor: nsColor)
}
return cmuxAccentColor()
},
set: { newColor in
let nsColor = NSColor(newColor)
sidebarNotificationBadgeColorHex = nsColor.hexString()
}
)
}
private var selectedSocketControlMode: SocketControlMode {
SocketControlSettings.migrateMode(socketControlMode)
}
@ -4836,6 +4894,66 @@ struct SettingsView: View {
SettingsCardDivider()
SettingsCardRow(
String(localized: "settings.workspaceColors.selectionColor", defaultValue: "Selection Highlight"),
subtitle: String(localized: "settings.workspaceColors.selectionColor.subtitle", defaultValue: "Background color of the selected workspace in the sidebar.")
) {
HStack(spacing: 8) {
if sidebarSelectionColorHex != nil {
Button(String(localized: "settings.workspaceColors.selectionColor.reset", defaultValue: "Reset")) {
sidebarSelectionColorHex = nil
}
.buttonStyle(.bordered)
.controlSize(.small)
}
ColorPicker(
"",
selection: selectionColorBinding,
supportsOpacity: false
)
.labelsHidden()
.frame(width: 38)
Text(sidebarSelectionColorHex ?? String(localized: "settings.sidebarAppearance.defaultLabel", defaultValue: "Default"))
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
.frame(width: 76, alignment: .trailing)
}
}
SettingsCardDivider()
SettingsCardRow(
String(localized: "settings.workspaceColors.notificationBadgeColor", defaultValue: "Notification Badge"),
subtitle: String(localized: "settings.workspaceColors.notificationBadgeColor.subtitle", defaultValue: "Color of the unread notification badge on workspace tabs.")
) {
HStack(spacing: 8) {
if sidebarNotificationBadgeColorHex != nil {
Button(String(localized: "settings.workspaceColors.notificationBadgeColor.reset", defaultValue: "Reset")) {
sidebarNotificationBadgeColorHex = nil
}
.buttonStyle(.bordered)
.controlSize(.small)
}
ColorPicker(
"",
selection: notificationBadgeColorBinding,
supportsOpacity: false
)
.labelsHidden()
.frame(width: 38)
Text(sidebarNotificationBadgeColorHex ?? String(localized: "settings.sidebarAppearance.defaultLabel", defaultValue: "Default"))
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
.frame(width: 76, alignment: .trailing)
}
}
SettingsCardDivider()
SettingsCardNote(String(localized: "settings.workspaceColors.paletteNote", defaultValue: "Customize the workspace color palette used by Sidebar > Workspace Color. \"Choose Custom Color...\" entries are persisted below."))
ForEach(Array(workspaceTabDefaultEntries.enumerated()), id: \.element.name) { index, entry in
@ -5660,6 +5778,8 @@ struct SettingsView: View {
sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
sidebarSelectionColorHex = nil
sidebarNotificationBadgeColorHex = nil
sidebarShowBranchDirectory = true
sidebarShowPullRequest = true
openSidebarPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPullRequestLinksInCmuxBrowser