Add workspace tab color schemes and debug scheme toggle (#324)
* Add tab color feature to sidebar workspaces Lets users assign a custom background color to any sidebar workspace tab via a right-click "Tab Color" submenu. The primary motivation is working across multiple projects simultaneously — coloring tabs by project makes it instant to visually locate the right workspace without reading the title. - Workspace: adds `customColor: String?` (@Published hex string) and `setCustomColor()` setter - TabManager: adds `setTabColor(tabId:color:)` convenience method - ContentView: 16-color dark palette (all luminance < 0.30, white text always readable), `Color(hex:)` extension, `coloredCircleImage(hex:)` helper to render bitmapped NSImage circles (needed because macOS menus strip SwiftUI foregroundColor from SF Symbols), updated `backgroundColor` to use custom color at full/70%/35% opacity for active/inactive/ multi-selected states, "Tab Color" submenu in context menu with "Clear Color" option, and a 1.5pt `Color.primary` border overlay on the active tab for clear selection indication when custom colors are set * Add workspace tab color schemes with settings and debug toggles * Remove Kelly scheme and keep only original tab color palette * Preserve neutral grayscale when brightening tab colors * Harden UpdatePill UI test polling timeouts --------- Co-authored-by: Andreas Fruth <andreas.fruth@gmail.com>
This commit is contained in:
parent
c5c27b678f
commit
0105b6256a
8 changed files with 995 additions and 59 deletions
|
|
@ -935,6 +935,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
private let debugColorWorkspaceTitlePrefix = "Debug Color - "
|
||||||
|
|
||||||
@objc func openDebugScrollbackTab(_ sender: Any?) {
|
@objc func openDebugScrollbackTab(_ sender: Any?) {
|
||||||
guard let tabManager else { return }
|
guard let tabManager else { return }
|
||||||
let tab = tabManager.addTab()
|
let tab = tabManager.addTab()
|
||||||
|
|
@ -958,6 +960,32 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
sendTextWhenReady(payload, to: tab)
|
sendTextWhenReady(payload, to: tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openDebugColorComparisonWorkspaces(_ sender: Any?) {
|
||||||
|
guard let tabManager else { return }
|
||||||
|
|
||||||
|
let palette = WorkspaceTabColorSettings.palette()
|
||||||
|
guard !palette.isEmpty else { return }
|
||||||
|
|
||||||
|
var existingByTitle: [String: Workspace] = [:]
|
||||||
|
for tab in tabManager.tabs {
|
||||||
|
guard let title = tab.customTitle,
|
||||||
|
title.hasPrefix(debugColorWorkspaceTitlePrefix) else { continue }
|
||||||
|
existingByTitle[title] = tab
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in palette {
|
||||||
|
let title = "\(debugColorWorkspaceTitlePrefix)\(entry.name)"
|
||||||
|
let targetTab: Workspace
|
||||||
|
if let existing = existingByTitle[title] {
|
||||||
|
targetTab = existing
|
||||||
|
} else {
|
||||||
|
targetTab = tabManager.addTab()
|
||||||
|
}
|
||||||
|
tabManager.setCustomTitle(tabId: targetTab.id, title: title)
|
||||||
|
tabManager.setTabColor(tabId: targetTab.id, color: entry.hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func sendTextWhenReady(_ text: String, to tab: Tab, attempt: Int = 0) {
|
private func sendTextWhenReady(_ text: String, to tab: Tab, attempt: Int = 0) {
|
||||||
let maxAttempts = 60
|
let maxAttempts = 60
|
||||||
if let terminalPanel = tab.focusedTerminalPanel, terminalPanel.surface.surface != nil {
|
if let terminalPanel = tab.focusedTerminalPanel, terminalPanel.surface.surface != nil {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,29 @@ import ObjectiveC
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import WebKit
|
import WebKit
|
||||||
|
|
||||||
|
private extension Color {
|
||||||
|
init?(hex: String) {
|
||||||
|
let hex = hex.trimmingCharacters(in: .init(charactersIn: "#"))
|
||||||
|
guard hex.count == 6, let value = UInt64(hex, radix: 16) else { return nil }
|
||||||
|
self.init(
|
||||||
|
red: Double((value >> 16) & 0xFF) / 255.0,
|
||||||
|
green: Double((value >> 8) & 0xFF) / 255.0,
|
||||||
|
blue: Double( value & 0xFF) / 255.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func coloredCircleImage(color: NSColor) -> NSImage {
|
||||||
|
let size = NSSize(width: 14, height: 14)
|
||||||
|
let image = NSImage(size: size, flipped: false) { rect in
|
||||||
|
color.setFill()
|
||||||
|
NSBezierPath(ovalIn: rect.insetBy(dx: 1, dy: 1)).fill()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
image.isTemplate = false
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
struct ShortcutHintPillBackground: View {
|
struct ShortcutHintPillBackground: View {
|
||||||
var emphasis: Double = 1.0
|
var emphasis: Double = 1.0
|
||||||
|
|
||||||
|
|
@ -2439,6 +2462,7 @@ private struct SidebarEmptyArea: View {
|
||||||
private struct TabItemView: View {
|
private struct TabItemView: View {
|
||||||
@EnvironmentObject var tabManager: TabManager
|
@EnvironmentObject var tabManager: TabManager
|
||||||
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@ObservedObject var tab: Tab
|
@ObservedObject var tab: Tab
|
||||||
let index: Int
|
let index: Int
|
||||||
let rowSpacing: CGFloat
|
let rowSpacing: CGFloat
|
||||||
|
|
@ -2461,6 +2485,8 @@ private struct TabItemView: View {
|
||||||
@AppStorage("sidebarShowLog") private var sidebarShowLog = true
|
@AppStorage("sidebarShowLog") private var sidebarShowLog = true
|
||||||
@AppStorage("sidebarShowProgress") private var sidebarShowProgress = true
|
@AppStorage("sidebarShowProgress") private var sidebarShowProgress = true
|
||||||
@AppStorage("sidebarShowStatusPills") private var sidebarShowStatusPills = true
|
@AppStorage("sidebarShowStatusPills") private var sidebarShowStatusPills = true
|
||||||
|
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
|
private var activeTabIndicatorStyleRaw = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
||||||
|
|
||||||
var isActive: Bool {
|
var isActive: Bool {
|
||||||
tabManager.selectedTabId == tab.id
|
tabManager.selectedTabId == tab.id
|
||||||
|
|
@ -2474,6 +2500,65 @@ private struct TabItemView: View {
|
||||||
draggedTabId == tab.id
|
draggedTabId == tab.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var activeTabIndicatorStyle: SidebarActiveTabIndicatorStyle {
|
||||||
|
SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: activeTabIndicatorStyleRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var titleFontWeight: Font.Weight {
|
||||||
|
.semibold
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showsLeadingRail: Bool {
|
||||||
|
explicitRailColor != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activeBorderLineWidth: CGFloat {
|
||||||
|
switch activeTabIndicatorStyle {
|
||||||
|
case .leftRail:
|
||||||
|
return 0
|
||||||
|
case .solidFill:
|
||||||
|
return isActive ? 1.5 : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activeBorderColor: Color {
|
||||||
|
guard isActive else { return .clear }
|
||||||
|
switch activeTabIndicatorStyle {
|
||||||
|
case .leftRail:
|
||||||
|
return .clear
|
||||||
|
case .solidFill:
|
||||||
|
return Color.primary.opacity(0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var usesInvertedActiveForeground: Bool {
|
||||||
|
isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activePrimaryTextColor: Color {
|
||||||
|
usesInvertedActiveForeground ? .white : .primary
|
||||||
|
}
|
||||||
|
|
||||||
|
private func activeSecondaryColor(_ opacity: Double = 0.75) -> Color {
|
||||||
|
usesInvertedActiveForeground ? .white.opacity(opacity) : .secondary
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activeUnreadBadgeFillColor: Color {
|
||||||
|
usesInvertedActiveForeground ? Color.white.opacity(0.25) : Color.accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activeProgressTrackColor: Color {
|
||||||
|
usesInvertedActiveForeground ? Color.white.opacity(0.15) : Color.secondary.opacity(0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activeProgressFillColor: Color {
|
||||||
|
usesInvertedActiveForeground ? Color.white.opacity(0.8) : Color.accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shortcutHintEmphasis: Double {
|
||||||
|
usesInvertedActiveForeground ? 1.0 : 0.9
|
||||||
|
}
|
||||||
|
|
||||||
private var workspaceShortcutDigit: Int? {
|
private var workspaceShortcutDigit: Int? {
|
||||||
WorkspaceShortcutMapper.commandDigitForWorkspace(at: index, workspaceCount: tabManager.tabs.count)
|
WorkspaceShortcutMapper.commandDigitForWorkspace(at: index, workspaceCount: tabManager.tabs.count)
|
||||||
}
|
}
|
||||||
|
|
@ -2510,7 +2595,7 @@ private struct TabItemView: View {
|
||||||
if unreadCount > 0 {
|
if unreadCount > 0 {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(isActive ? Color.white.opacity(0.25) : Color.accentColor)
|
.fill(activeUnreadBadgeFillColor)
|
||||||
Text("\(unreadCount)")
|
Text("\(unreadCount)")
|
||||||
.font(.system(size: 9, weight: .semibold))
|
.font(.system(size: 9, weight: .semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
@ -2521,12 +2606,12 @@ private struct TabItemView: View {
|
||||||
if tab.isPinned {
|
if tab.isPinned {
|
||||||
Image(systemName: "pin.fill")
|
Image(systemName: "pin.fill")
|
||||||
.font(.system(size: 9, weight: .semibold))
|
.font(.system(size: 9, weight: .semibold))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.8) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.8))
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(tab.title)
|
Text(tab.title)
|
||||||
.font(.system(size: 12.5, weight: .semibold))
|
.font(.system(size: 12.5, weight: titleFontWeight))
|
||||||
.foregroundColor(isActive ? .white : .primary)
|
.foregroundColor(activePrimaryTextColor)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
|
|
||||||
|
|
@ -2541,7 +2626,7 @@ private struct TabItemView: View {
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: "xmark")
|
Image(systemName: "xmark")
|
||||||
.font(.system(size: 9, weight: .medium))
|
.font(.system(size: 9, weight: .medium))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.7) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.7))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.help(KeyboardShortcutSettings.Action.closeWorkspace.tooltip("Close Workspace"))
|
.help(KeyboardShortcutSettings.Action.closeWorkspace.tooltip("Close Workspace"))
|
||||||
|
|
@ -2555,10 +2640,10 @@ private struct TabItemView: View {
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
.font(.system(size: 10, weight: .semibold, design: .rounded))
|
.font(.system(size: 10, weight: .semibold, design: .rounded))
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundColor(isActive ? .white : .primary)
|
.foregroundColor(activePrimaryTextColor)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.background(ShortcutHintPillBackground(emphasis: isActive ? 1.0 : 0.9))
|
.background(ShortcutHintPillBackground(emphasis: shortcutHintEmphasis))
|
||||||
.offset(
|
.offset(
|
||||||
x: ShortcutHintDebugSettings.clamped(sidebarShortcutHintXOffset),
|
x: ShortcutHintDebugSettings.clamped(sidebarShortcutHintXOffset),
|
||||||
y: ShortcutHintDebugSettings.clamped(sidebarShortcutHintYOffset)
|
y: ShortcutHintDebugSettings.clamped(sidebarShortcutHintYOffset)
|
||||||
|
|
@ -2573,7 +2658,7 @@ private struct TabItemView: View {
|
||||||
if let subtitle = latestNotificationText {
|
if let subtitle = latestNotificationText {
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.8) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.8))
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
|
@ -2585,7 +2670,7 @@ private struct TabItemView: View {
|
||||||
if lhs.timestamp != rhs.timestamp { return lhs.timestamp > rhs.timestamp }
|
if lhs.timestamp != rhs.timestamp { return lhs.timestamp > rhs.timestamp }
|
||||||
return lhs.key < rhs.key
|
return lhs.key < rhs.key
|
||||||
}),
|
}),
|
||||||
isActive: isActive,
|
isActive: usesInvertedActiveForeground,
|
||||||
onFocus: { updateSelection() }
|
onFocus: { updateSelection() }
|
||||||
)
|
)
|
||||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||||
|
|
@ -2596,10 +2681,10 @@ private struct TabItemView: View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: logLevelIcon(latestLog.level))
|
Image(systemName: logLevelIcon(latestLog.level))
|
||||||
.font(.system(size: 8))
|
.font(.system(size: 8))
|
||||||
.foregroundColor(logLevelColor(latestLog.level, isActive: isActive))
|
.foregroundColor(logLevelColor(latestLog.level, isActive: usesInvertedActiveForeground))
|
||||||
Text(latestLog.message)
|
Text(latestLog.message)
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.8) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.8))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
|
|
@ -2612,9 +2697,9 @@ private struct TabItemView: View {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(isActive ? Color.white.opacity(0.15) : Color.secondary.opacity(0.2))
|
.fill(activeProgressTrackColor)
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(isActive ? Color.white.opacity(0.8) : Color.accentColor)
|
.fill(activeProgressFillColor)
|
||||||
.frame(width: max(0, geo.size.width * CGFloat(progress.value)))
|
.frame(width: max(0, geo.size.width * CGFloat(progress.value)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2623,7 +2708,7 @@ private struct TabItemView: View {
|
||||||
if let label = progress.label {
|
if let label = progress.label {
|
||||||
Text(label)
|
Text(label)
|
||||||
.font(.system(size: 9))
|
.font(.system(size: 9))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.6) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.6))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2637,7 +2722,7 @@ private struct TabItemView: View {
|
||||||
if sidebarShowGitBranchIcon, sidebarShowGitBranch, verticalRowsContainBranch {
|
if sidebarShowGitBranchIcon, sidebarShowGitBranch, verticalRowsContainBranch {
|
||||||
Image(systemName: "arrow.triangle.branch")
|
Image(systemName: "arrow.triangle.branch")
|
||||||
.font(.system(size: 9))
|
.font(.system(size: 9))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.6) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.6))
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
ForEach(Array(verticalBranchDirectoryLines.enumerated()), id: \.offset) { _, line in
|
ForEach(Array(verticalBranchDirectoryLines.enumerated()), id: \.offset) { _, line in
|
||||||
|
|
@ -2645,20 +2730,20 @@ private struct TabItemView: View {
|
||||||
if let branch = line.branch {
|
if let branch = line.branch {
|
||||||
Text(branch)
|
Text(branch)
|
||||||
.font(.system(size: 10, design: .monospaced))
|
.font(.system(size: 10, design: .monospaced))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.75) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.75))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
if line.branch != nil, line.directory != nil {
|
if line.branch != nil, line.directory != nil {
|
||||||
Image(systemName: "circle.fill")
|
Image(systemName: "circle.fill")
|
||||||
.font(.system(size: 3))
|
.font(.system(size: 3))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.6) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.6))
|
||||||
.padding(.horizontal, 1)
|
.padding(.horizontal, 1)
|
||||||
}
|
}
|
||||||
if let directory = line.directory {
|
if let directory = line.directory {
|
||||||
Text(directory)
|
Text(directory)
|
||||||
.font(.system(size: 10, design: .monospaced))
|
.font(.system(size: 10, design: .monospaced))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.75) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.75))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
|
|
@ -2672,11 +2757,11 @@ private struct TabItemView: View {
|
||||||
if sidebarShowGitBranch && gitBranchSummaryText != nil && sidebarShowGitBranchIcon {
|
if sidebarShowGitBranch && gitBranchSummaryText != nil && sidebarShowGitBranchIcon {
|
||||||
Image(systemName: "arrow.triangle.branch")
|
Image(systemName: "arrow.triangle.branch")
|
||||||
.font(.system(size: 9))
|
.font(.system(size: 9))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.6) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.6))
|
||||||
}
|
}
|
||||||
Text(dirRow)
|
Text(dirRow)
|
||||||
.font(.system(size: 10, design: .monospaced))
|
.font(.system(size: 10, design: .monospaced))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.75) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.75))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
|
|
@ -2686,7 +2771,7 @@ private struct TabItemView: View {
|
||||||
if sidebarShowPorts, !tab.listeningPorts.isEmpty {
|
if sidebarShowPorts, !tab.listeningPorts.isEmpty {
|
||||||
Text(tab.listeningPorts.map { ":\($0)" }.joined(separator: ", "))
|
Text(tab.listeningPorts.map { ":\($0)" }.joined(separator: ", "))
|
||||||
.font(.system(size: 10, design: .monospaced))
|
.font(.system(size: 10, design: .monospaced))
|
||||||
.foregroundColor(isActive ? .white.opacity(0.75) : .secondary)
|
.foregroundColor(activeSecondaryColor(0.75))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
|
|
@ -2698,6 +2783,20 @@ private struct TabItemView: View {
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 6)
|
RoundedRectangle(cornerRadius: 6)
|
||||||
.fill(backgroundColor)
|
.fill(backgroundColor)
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.strokeBorder(activeBorderColor, lineWidth: activeBorderLineWidth)
|
||||||
|
}
|
||||||
|
.overlay(alignment: .leading) {
|
||||||
|
if showsLeadingRail {
|
||||||
|
Capsule(style: .continuous)
|
||||||
|
.fill(railColor)
|
||||||
|
.frame(width: 3)
|
||||||
|
.padding(.leading, 4)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
.offset(x: -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.background {
|
.background {
|
||||||
|
|
@ -2765,6 +2864,7 @@ private struct TabItemView: View {
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
let targetIds = contextTargetIds()
|
let targetIds = contextTargetIds()
|
||||||
|
let tabColorPalette = WorkspaceTabColorSettings.palette()
|
||||||
let shouldPin = !tab.isPinned
|
let shouldPin = !tab.isPinned
|
||||||
let pinLabel = targetIds.count > 1
|
let pinLabel = targetIds.count > 1
|
||||||
? (shouldPin ? "Pin Workspaces" : "Unpin Workspaces")
|
? (shouldPin ? "Pin Workspaces" : "Unpin Workspaces")
|
||||||
|
|
@ -2800,6 +2900,38 @@ private struct TabItemView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Menu("Tab Color") {
|
||||||
|
if tab.customColor != nil {
|
||||||
|
Button {
|
||||||
|
applyTabColor(nil, targetIds: targetIds)
|
||||||
|
} label: {
|
||||||
|
Label("Clear Color", systemImage: "xmark.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
promptCustomColor(targetIds: targetIds)
|
||||||
|
} label: {
|
||||||
|
Label("Choose Custom Color…", systemImage: "paintpalette")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tabColorPalette.isEmpty {
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(tabColorPalette, id: \.id) { entry in
|
||||||
|
Button {
|
||||||
|
applyTabColor(entry.hex, targetIds: targetIds)
|
||||||
|
} label: {
|
||||||
|
Label {
|
||||||
|
Text(entry.name)
|
||||||
|
} icon: {
|
||||||
|
Image(nsImage: coloredCircleImage(color: tabColorSwatchColor(for: entry.hex)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button("Move Up") {
|
Button("Move Up") {
|
||||||
|
|
@ -2863,13 +2995,50 @@ private struct TabItemView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var backgroundColor: Color {
|
private var backgroundColor: Color {
|
||||||
if isActive {
|
switch activeTabIndicatorStyle {
|
||||||
return Color.accentColor
|
case .leftRail:
|
||||||
|
if isActive { return Color.accentColor }
|
||||||
|
if isMultiSelected { return Color.accentColor.opacity(0.25) }
|
||||||
|
return Color.clear
|
||||||
|
case .solidFill:
|
||||||
|
if let custom = resolvedCustomTabColor {
|
||||||
|
if isActive { return custom }
|
||||||
|
if isMultiSelected { return custom.opacity(0.35) }
|
||||||
|
return custom.opacity(0.7)
|
||||||
|
}
|
||||||
|
if isActive { return Color.accentColor }
|
||||||
|
if isMultiSelected { return Color.accentColor.opacity(0.25) }
|
||||||
|
return Color.clear
|
||||||
}
|
}
|
||||||
if isMultiSelected {
|
}
|
||||||
return Color.accentColor.opacity(0.25)
|
|
||||||
|
private var railColor: Color {
|
||||||
|
explicitRailColor ?? .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
private var explicitRailColor: Color? {
|
||||||
|
guard activeTabIndicatorStyle == .leftRail,
|
||||||
|
let custom = resolvedCustomTabColor else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return Color.clear
|
return custom.opacity(0.95)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resolvedCustomTabColor: Color? {
|
||||||
|
guard let hex = tab.customColor else { return nil }
|
||||||
|
return WorkspaceTabColorSettings.displayColor(
|
||||||
|
hex: hex,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
forceBright: activeTabIndicatorStyle == .leftRail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tabColorSwatchColor(for hex: String) -> NSColor {
|
||||||
|
WorkspaceTabColorSettings.displayNSColor(
|
||||||
|
hex: hex,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
forceBright: activeTabIndicatorStyle == .leftRail
|
||||||
|
) ?? NSColor(hex: hex) ?? .gray
|
||||||
}
|
}
|
||||||
|
|
||||||
private var showsCenteredTopDropIndicator: Bool {
|
private var showsCenteredTopDropIndicator: Bool {
|
||||||
|
|
@ -3142,6 +3311,55 @@ private struct TabItemView: View {
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func applyTabColor(_ hex: String?, targetIds: [UUID]) {
|
||||||
|
for targetId in targetIds {
|
||||||
|
tabManager.setTabColor(tabId: targetId, color: hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func promptCustomColor(targetIds: [UUID]) {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Custom Tab Color"
|
||||||
|
alert.informativeText = "Enter a hex color in the format #RRGGBB."
|
||||||
|
|
||||||
|
let seed = tab.customColor ?? WorkspaceTabColorSettings.customColors().first ?? ""
|
||||||
|
let input = NSTextField(string: seed)
|
||||||
|
input.placeholderString = "#1565C0"
|
||||||
|
input.frame = NSRect(x: 0, y: 0, width: 240, height: 22)
|
||||||
|
alert.accessoryView = input
|
||||||
|
alert.addButton(withTitle: "Apply")
|
||||||
|
alert.addButton(withTitle: "Cancel")
|
||||||
|
|
||||||
|
let alertWindow = alert.window
|
||||||
|
alertWindow.initialFirstResponder = input
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
alertWindow.makeFirstResponder(input)
|
||||||
|
input.selectText(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = alert.runModal()
|
||||||
|
guard response == .alertFirstButtonReturn else { return }
|
||||||
|
guard let normalized = WorkspaceTabColorSettings.addCustomColor(input.stringValue) else {
|
||||||
|
showInvalidColorAlert(input.stringValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applyTabColor(normalized, targetIds: targetIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showInvalidColorAlert(_ value: String) {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.messageText = "Invalid Color"
|
||||||
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.isEmpty {
|
||||||
|
alert.informativeText = "Enter a hex color in the format #RRGGBB."
|
||||||
|
} else {
|
||||||
|
alert.informativeText = "\"\(trimmed)\" is not a valid hex color. Use #RRGGBB."
|
||||||
|
}
|
||||||
|
alert.addButton(withTitle: "OK")
|
||||||
|
_ = alert.runModal()
|
||||||
|
}
|
||||||
|
|
||||||
private func promptRename() {
|
private func promptRename() {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "Rename Workspace"
|
alert.messageText = "Rename Workspace"
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,48 @@ enum SidebarBranchLayoutSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SidebarActiveTabIndicatorStyle: String, CaseIterable, Identifiable {
|
||||||
|
case leftRail
|
||||||
|
case solidFill
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .leftRail:
|
||||||
|
return "Left Rail"
|
||||||
|
case .solidFill:
|
||||||
|
return "Solid Fill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SidebarActiveTabIndicatorSettings {
|
||||||
|
static let styleKey = "sidebarActiveTabIndicatorStyle"
|
||||||
|
static let defaultStyle: SidebarActiveTabIndicatorStyle = .solidFill
|
||||||
|
|
||||||
|
static func resolvedStyle(rawValue: String?) -> SidebarActiveTabIndicatorStyle {
|
||||||
|
guard let rawValue else { return defaultStyle }
|
||||||
|
if let style = SidebarActiveTabIndicatorStyle(rawValue: rawValue) {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy values from earlier iterations map to the closest modern option.
|
||||||
|
switch rawValue {
|
||||||
|
case "rail":
|
||||||
|
return .leftRail
|
||||||
|
case "border", "wash", "lift", "typography", "washRail", "blueWashColorRail":
|
||||||
|
return .solidFill
|
||||||
|
default:
|
||||||
|
return defaultStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func current(defaults: UserDefaults = .standard) -> SidebarActiveTabIndicatorStyle {
|
||||||
|
resolvedStyle(rawValue: defaults.string(forKey: styleKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum WorkspacePlacementSettings {
|
enum WorkspacePlacementSettings {
|
||||||
static let placementKey = "newWorkspacePlacement"
|
static let placementKey = "newWorkspacePlacement"
|
||||||
static let defaultPlacement: NewWorkspacePlacement = .afterCurrent
|
static let defaultPlacement: NewWorkspacePlacement = .afterCurrent
|
||||||
|
|
@ -104,6 +146,213 @@ enum WorkspacePlacementSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct WorkspaceTabColorEntry: Equatable, Identifiable {
|
||||||
|
let name: String
|
||||||
|
let hex: String
|
||||||
|
|
||||||
|
var id: String { "\(name)-\(hex)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WorkspaceTabColorSettings {
|
||||||
|
static let defaultOverridesKey = "workspaceTabColor.defaultOverrides"
|
||||||
|
static let customColorsKey = "workspaceTabColor.customColors"
|
||||||
|
static let maxCustomColors = 24
|
||||||
|
|
||||||
|
private static let originalPRPalette: [WorkspaceTabColorEntry] = [
|
||||||
|
WorkspaceTabColorEntry(name: "Red", hex: "#C0392B"),
|
||||||
|
WorkspaceTabColorEntry(name: "Crimson", hex: "#922B21"),
|
||||||
|
WorkspaceTabColorEntry(name: "Orange", hex: "#A04000"),
|
||||||
|
WorkspaceTabColorEntry(name: "Amber", hex: "#7D6608"),
|
||||||
|
WorkspaceTabColorEntry(name: "Olive", hex: "#4A5C18"),
|
||||||
|
WorkspaceTabColorEntry(name: "Green", hex: "#196F3D"),
|
||||||
|
WorkspaceTabColorEntry(name: "Teal", hex: "#006B6B"),
|
||||||
|
WorkspaceTabColorEntry(name: "Aqua", hex: "#0E6B8C"),
|
||||||
|
WorkspaceTabColorEntry(name: "Blue", hex: "#1565C0"),
|
||||||
|
WorkspaceTabColorEntry(name: "Navy", hex: "#1A5276"),
|
||||||
|
WorkspaceTabColorEntry(name: "Indigo", hex: "#283593"),
|
||||||
|
WorkspaceTabColorEntry(name: "Purple", hex: "#6A1B9A"),
|
||||||
|
WorkspaceTabColorEntry(name: "Magenta", hex: "#AD1457"),
|
||||||
|
WorkspaceTabColorEntry(name: "Rose", hex: "#880E4F"),
|
||||||
|
WorkspaceTabColorEntry(name: "Brown", hex: "#7B3F00"),
|
||||||
|
WorkspaceTabColorEntry(name: "Charcoal", hex: "#3E4B5E"),
|
||||||
|
]
|
||||||
|
|
||||||
|
static var defaultPalette: [WorkspaceTabColorEntry] {
|
||||||
|
originalPRPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
static func palette(defaults: UserDefaults = .standard) -> [WorkspaceTabColorEntry] {
|
||||||
|
defaultPaletteWithOverrides(defaults: defaults) + customColorEntries(defaults: defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func defaultPaletteWithOverrides(defaults: UserDefaults = .standard) -> [WorkspaceTabColorEntry] {
|
||||||
|
let palette = defaultPalette
|
||||||
|
let overrides = defaultOverrideMap(defaults: defaults)
|
||||||
|
return palette.map { entry in
|
||||||
|
WorkspaceTabColorEntry(name: entry.name, hex: overrides[entry.name] ?? entry.hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func defaultColorHex(named name: String, defaults: UserDefaults = .standard) -> String {
|
||||||
|
let palette = defaultPalette
|
||||||
|
guard let entry = palette.first(where: { $0.name == name }) else {
|
||||||
|
return palette.first?.hex ?? "#1565C0"
|
||||||
|
}
|
||||||
|
return defaultOverrideMap(defaults: defaults)[name] ?? entry.hex
|
||||||
|
}
|
||||||
|
|
||||||
|
static func setDefaultColor(named name: String, hex: String, defaults: UserDefaults = .standard) {
|
||||||
|
let palette = defaultPalette
|
||||||
|
guard let entry = palette.first(where: { $0.name == name }),
|
||||||
|
let normalized = normalizedHex(hex) else { return }
|
||||||
|
|
||||||
|
var overrides = defaultOverrideMap(defaults: defaults)
|
||||||
|
if normalized == entry.hex {
|
||||||
|
overrides.removeValue(forKey: name)
|
||||||
|
} else {
|
||||||
|
overrides[name] = normalized
|
||||||
|
}
|
||||||
|
saveDefaultOverrideMap(overrides, defaults: defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func customColors(defaults: UserDefaults = .standard) -> [String] {
|
||||||
|
guard let raw = defaults.array(forKey: customColorsKey) as? [String] else { return [] }
|
||||||
|
var result: [String] = []
|
||||||
|
var seen: Set<String> = []
|
||||||
|
for value in raw {
|
||||||
|
guard let normalized = normalizedHex(value), seen.insert(normalized).inserted else { continue }
|
||||||
|
result.append(normalized)
|
||||||
|
if result.count >= maxCustomColors { break }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
static func customColorEntries(defaults: UserDefaults = .standard) -> [WorkspaceTabColorEntry] {
|
||||||
|
customColors(defaults: defaults).enumerated().map { index, hex in
|
||||||
|
WorkspaceTabColorEntry(name: "Custom \(index + 1)", hex: hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func addCustomColor(_ hex: String, defaults: UserDefaults = .standard) -> String? {
|
||||||
|
guard let normalized = normalizedHex(hex) else { return nil }
|
||||||
|
var colors = customColors(defaults: defaults)
|
||||||
|
colors.removeAll { $0 == normalized }
|
||||||
|
colors.insert(normalized, at: 0)
|
||||||
|
setCustomColors(colors, defaults: defaults)
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
static func removeCustomColor(_ hex: String, defaults: UserDefaults = .standard) {
|
||||||
|
guard let normalized = normalizedHex(hex) else { return }
|
||||||
|
var colors = customColors(defaults: defaults)
|
||||||
|
colors.removeAll { $0 == normalized }
|
||||||
|
setCustomColors(colors, defaults: defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func setCustomColors(_ hexes: [String], defaults: UserDefaults = .standard) {
|
||||||
|
var normalizedColors: [String] = []
|
||||||
|
var seen: Set<String> = []
|
||||||
|
for value in hexes {
|
||||||
|
guard let normalized = normalizedHex(value), seen.insert(normalized).inserted else { continue }
|
||||||
|
normalizedColors.append(normalized)
|
||||||
|
if normalizedColors.count >= maxCustomColors { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalizedColors.isEmpty {
|
||||||
|
defaults.removeObject(forKey: customColorsKey)
|
||||||
|
} else {
|
||||||
|
defaults.set(normalizedColors, forKey: customColorsKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func reset(defaults: UserDefaults = .standard) {
|
||||||
|
defaults.removeObject(forKey: defaultOverridesKey)
|
||||||
|
defaults.removeObject(forKey: customColorsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func normalizedHex(_ raw: String) -> String? {
|
||||||
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return nil }
|
||||||
|
let body = trimmed.hasPrefix("#") ? String(trimmed.dropFirst()) : trimmed
|
||||||
|
guard body.count == 6 else { return nil }
|
||||||
|
guard UInt64(body, radix: 16) != nil else { return nil }
|
||||||
|
return "#" + body.uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func displayColor(
|
||||||
|
hex: String,
|
||||||
|
colorScheme: ColorScheme,
|
||||||
|
forceBright: Bool = false
|
||||||
|
) -> Color? {
|
||||||
|
guard let color = displayNSColor(hex: hex, colorScheme: colorScheme, forceBright: forceBright) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Color(nsColor: color)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func displayNSColor(
|
||||||
|
hex: String,
|
||||||
|
colorScheme: ColorScheme,
|
||||||
|
forceBright: Bool = false
|
||||||
|
) -> NSColor? {
|
||||||
|
guard let normalized = normalizedHex(hex),
|
||||||
|
let baseColor = NSColor(hex: normalized) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceBright || colorScheme == .dark {
|
||||||
|
return brightenedForDarkAppearance(baseColor)
|
||||||
|
}
|
||||||
|
return baseColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func defaultOverrideMap(defaults: UserDefaults) -> [String: String] {
|
||||||
|
guard let raw = defaults.dictionary(forKey: defaultOverridesKey) as? [String: String] else { return [:] }
|
||||||
|
let validNames = Set(defaultPalette.map(\.name))
|
||||||
|
var normalized: [String: String] = [:]
|
||||||
|
for (name, hex) in raw {
|
||||||
|
guard validNames.contains(name),
|
||||||
|
let normalizedHex = normalizedHex(hex) else { continue }
|
||||||
|
normalized[name] = normalizedHex
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func saveDefaultOverrideMap(_ map: [String: String], defaults: UserDefaults) {
|
||||||
|
if map.isEmpty {
|
||||||
|
defaults.removeObject(forKey: defaultOverridesKey)
|
||||||
|
} else {
|
||||||
|
defaults.set(map, forKey: defaultOverridesKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func brightenedForDarkAppearance(_ color: NSColor) -> NSColor {
|
||||||
|
let rgbColor = color.usingColorSpace(.sRGB) ?? color
|
||||||
|
var hue: CGFloat = 0
|
||||||
|
var saturation: CGFloat = 0
|
||||||
|
var brightness: CGFloat = 0
|
||||||
|
var alpha: CGFloat = 0
|
||||||
|
rgbColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
|
||||||
|
|
||||||
|
let boostedBrightness = min(1, max(brightness, 0.62) + ((1 - brightness) * 0.28))
|
||||||
|
// Preserve neutral grays when brightening to avoid introducing hue shifts.
|
||||||
|
let boostedSaturation: CGFloat
|
||||||
|
if saturation <= 0.08 {
|
||||||
|
boostedSaturation = saturation
|
||||||
|
} else {
|
||||||
|
boostedSaturation = min(1, saturation + ((1 - saturation) * 0.12))
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSColor(
|
||||||
|
hue: hue,
|
||||||
|
saturation: boostedSaturation,
|
||||||
|
brightness: boostedBrightness,
|
||||||
|
alpha: alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Coalesces repeated main-thread signals into one callback after a short delay.
|
/// Coalesces repeated main-thread signals into one callback after a short delay.
|
||||||
/// Useful for notification storms where only the latest update matters.
|
/// Useful for notification storms where only the latest update matters.
|
||||||
final class NotificationBurstCoalescer {
|
final class NotificationBurstCoalescer {
|
||||||
|
|
@ -632,6 +881,11 @@ class TabManager: ObservableObject {
|
||||||
setCustomTitle(tabId: tabId, title: nil)
|
setCustomTitle(tabId: tabId, title: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setTabColor(tabId: UUID, color: String?) {
|
||||||
|
guard let tab = tabs.first(where: { $0.id == tabId }) else { return }
|
||||||
|
tab.setCustomColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
func togglePin(tabId: UUID) {
|
func togglePin(tabId: UUID) {
|
||||||
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return }
|
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return }
|
||||||
let tab = tabs[index]
|
let tab = tabs[index]
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,7 @@ final class Workspace: Identifiable, ObservableObject {
|
||||||
@Published var title: String
|
@Published var title: String
|
||||||
@Published var customTitle: String?
|
@Published var customTitle: String?
|
||||||
@Published var isPinned: Bool = false
|
@Published var isPinned: Bool = false
|
||||||
|
@Published var customColor: String? // hex string, e.g. "#C0392B"
|
||||||
@Published var currentDirectory: String
|
@Published var currentDirectory: String
|
||||||
|
|
||||||
/// Ordinal for CMUX_PORT range assignment (monotonically increasing per app session)
|
/// Ordinal for CMUX_PORT range assignment (monotonically increasing per app session)
|
||||||
|
|
@ -755,6 +756,10 @@ final class Workspace: Identifiable, ObservableObject {
|
||||||
self.title = title
|
self.title = title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setCustomColor(_ hex: String?) {
|
||||||
|
customColor = hex
|
||||||
|
}
|
||||||
|
|
||||||
func setCustomTitle(_ title: String?) {
|
func setCustomTitle(_ title: String?) {
|
||||||
let trimmed = title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let trimmed = title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
if trimmed.isEmpty {
|
if trimmed.isEmpty {
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,10 @@ struct cmuxApp: App {
|
||||||
appDelegate.openDebugScrollbackTab(nil)
|
appDelegate.openDebugScrollbackTab(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button("Open Workspaces for All Tab Colors") {
|
||||||
|
appDelegate.openDebugColorComparisonWorkspaces(nil)
|
||||||
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
Menu("Debug Windows") {
|
Menu("Debug Windows") {
|
||||||
Button("Debug Window Controls…") {
|
Button("Debug Window Controls…") {
|
||||||
|
|
@ -1223,6 +1227,7 @@ private enum DebugWindowConfigSnapshot {
|
||||||
sidebarTintOpacity=\(String(format: "%.2f", doubleValue(defaults, key: "sidebarTintOpacity", fallback: 0.18)))
|
sidebarTintOpacity=\(String(format: "%.2f", doubleValue(defaults, key: "sidebarTintOpacity", fallback: 0.18)))
|
||||||
sidebarCornerRadius=\(String(format: "%.1f", doubleValue(defaults, key: "sidebarCornerRadius", fallback: 0.0)))
|
sidebarCornerRadius=\(String(format: "%.1f", doubleValue(defaults, key: "sidebarCornerRadius", fallback: 0.0)))
|
||||||
sidebarBranchVerticalLayout=\(boolValue(defaults, key: SidebarBranchLayoutSettings.key, fallback: SidebarBranchLayoutSettings.defaultVerticalLayout))
|
sidebarBranchVerticalLayout=\(boolValue(defaults, key: SidebarBranchLayoutSettings.key, fallback: SidebarBranchLayoutSettings.defaultVerticalLayout))
|
||||||
|
sidebarActiveTabIndicatorStyle=\(stringValue(defaults, key: SidebarActiveTabIndicatorSettings.styleKey, fallback: SidebarActiveTabIndicatorSettings.defaultStyle.rawValue))
|
||||||
shortcutHintSidebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintXKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintX)))
|
shortcutHintSidebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintXKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintX)))
|
||||||
shortcutHintSidebarYOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintYKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintY)))
|
shortcutHintSidebarYOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintYKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintY)))
|
||||||
shortcutHintTitlebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.titlebarHintXKey, fallback: ShortcutHintDebugSettings.defaultTitlebarHintX)))
|
shortcutHintTitlebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.titlebarHintXKey, fallback: ShortcutHintDebugSettings.defaultTitlebarHintX)))
|
||||||
|
|
@ -1319,6 +1324,8 @@ private struct DebugWindowControlsView: View {
|
||||||
@AppStorage(ShortcutHintDebugSettings.paneHintXKey) private var paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX
|
@AppStorage(ShortcutHintDebugSettings.paneHintXKey) private var paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX
|
||||||
@AppStorage(ShortcutHintDebugSettings.paneHintYKey) private var paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY
|
@AppStorage(ShortcutHintDebugSettings.paneHintYKey) private var paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY
|
||||||
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
||||||
|
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
|
private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
||||||
@AppStorage("debugTitlebarLeadingExtra") private var titlebarLeadingExtra: Double = 0
|
@AppStorage("debugTitlebarLeadingExtra") private var titlebarLeadingExtra: Double = 0
|
||||||
@AppStorage(BrowserDevToolsButtonDebugSettings.iconNameKey) private var browserDevToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue
|
@AppStorage(BrowserDevToolsButtonDebugSettings.iconNameKey) private var browserDevToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue
|
||||||
@AppStorage(BrowserDevToolsButtonDebugSettings.iconColorKey) private var browserDevToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue
|
@AppStorage(BrowserDevToolsButtonDebugSettings.iconColorKey) private var browserDevToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue
|
||||||
|
|
@ -1331,6 +1338,17 @@ private struct DebugWindowControlsView: View {
|
||||||
BrowserDevToolsIconColorOption(rawValue: browserDevToolsIconColorRaw) ?? BrowserDevToolsButtonDebugSettings.defaultColor
|
BrowserDevToolsIconColorOption(rawValue: browserDevToolsIconColorRaw) ?? BrowserDevToolsButtonDebugSettings.defaultColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var selectedSidebarActiveTabIndicatorStyle: SidebarActiveTabIndicatorStyle {
|
||||||
|
SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sidebarIndicatorStyleSelection: Binding<String> {
|
||||||
|
Binding(
|
||||||
|
get: { selectedSidebarActiveTabIndicatorStyle.rawValue },
|
||||||
|
set: { sidebarActiveTabIndicatorStyle = $0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
|
@ -1396,6 +1414,22 @@ private struct DebugWindowControlsView: View {
|
||||||
.padding(.top, 2)
|
.padding(.top, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GroupBox("Active Workspace Indicator") {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Picker("Style", selection: sidebarIndicatorStyleSelection) {
|
||||||
|
ForEach(SidebarActiveTabIndicatorStyle.allCases) { style in
|
||||||
|
Text(style.displayName).tag(style.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
|
||||||
|
Button("Reset Indicator Style") {
|
||||||
|
sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 2)
|
||||||
|
}
|
||||||
|
|
||||||
GroupBox("Titlebar Spacing") {
|
GroupBox("Titlebar Spacing") {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
|
|
@ -1797,6 +1831,19 @@ private struct SidebarDebugView: View {
|
||||||
@AppStorage(ShortcutHintDebugSettings.paneHintXKey) private var paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX
|
@AppStorage(ShortcutHintDebugSettings.paneHintXKey) private var paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX
|
||||||
@AppStorage(ShortcutHintDebugSettings.paneHintYKey) private var paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY
|
@AppStorage(ShortcutHintDebugSettings.paneHintYKey) private var paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY
|
||||||
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
||||||
|
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
|
private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
||||||
|
|
||||||
|
private var selectedSidebarIndicatorStyle: SidebarActiveTabIndicatorStyle {
|
||||||
|
SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sidebarIndicatorStyleSelection: Binding<String> {
|
||||||
|
Binding(
|
||||||
|
get: { selectedSidebarIndicatorStyle.rawValue },
|
||||||
|
set: { sidebarActiveTabIndicatorStyle = $0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
|
@ -1898,6 +1945,17 @@ private struct SidebarDebugView: View {
|
||||||
.padding(.top, 2)
|
.padding(.top, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GroupBox("Active Workspace Indicator") {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Picker("Style", selection: sidebarIndicatorStyleSelection) {
|
||||||
|
ForEach(SidebarActiveTabIndicatorStyle.allCases) { style in
|
||||||
|
Text(style.displayName).tag(style.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 2)
|
||||||
|
}
|
||||||
|
|
||||||
GroupBox("Workspace Metadata") {
|
GroupBox("Workspace Metadata") {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Toggle("Render branch list vertically", isOn: $sidebarBranchVerticalLayout)
|
Toggle("Render branch list vertically", isOn: $sidebarBranchVerticalLayout)
|
||||||
|
|
@ -1925,6 +1983,9 @@ private struct SidebarDebugView: View {
|
||||||
Button("Reset Hints") {
|
Button("Reset Hints") {
|
||||||
resetShortcutHintOffsets()
|
resetShortcutHintOffsets()
|
||||||
}
|
}
|
||||||
|
Button("Reset Active Indicator") {
|
||||||
|
sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Copy Config") {
|
Button("Copy Config") {
|
||||||
|
|
@ -1992,6 +2053,7 @@ private struct SidebarDebugView: View {
|
||||||
sidebarTintOpacity=\(String(format: "%.2f", sidebarTintOpacity))
|
sidebarTintOpacity=\(String(format: "%.2f", sidebarTintOpacity))
|
||||||
sidebarCornerRadius=\(String(format: "%.1f", sidebarCornerRadius))
|
sidebarCornerRadius=\(String(format: "%.1f", sidebarCornerRadius))
|
||||||
sidebarBranchVerticalLayout=\(sidebarBranchVerticalLayout)
|
sidebarBranchVerticalLayout=\(sidebarBranchVerticalLayout)
|
||||||
|
sidebarActiveTabIndicatorStyle=\(sidebarActiveTabIndicatorStyle)
|
||||||
shortcutHintSidebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintXOffset)))
|
shortcutHintSidebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintXOffset)))
|
||||||
shortcutHintSidebarYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintYOffset)))
|
shortcutHintSidebarYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintYOffset)))
|
||||||
shortcutHintTitlebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset)))
|
shortcutHintTitlebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset)))
|
||||||
|
|
@ -2505,6 +2567,8 @@ struct SettingsView: View {
|
||||||
@AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
@AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||||
@AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
@AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
||||||
@AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
|
@AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
|
||||||
|
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
|
private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
||||||
@State private var shortcutResetToken = UUID()
|
@State private var shortcutResetToken = UUID()
|
||||||
@State private var topBlurOpacity: Double = 0
|
@State private var topBlurOpacity: Double = 0
|
||||||
@State private var topBlurBaselineOffset: CGFloat?
|
@State private var topBlurBaselineOffset: CGFloat?
|
||||||
|
|
@ -2517,11 +2581,24 @@ struct SettingsView: View {
|
||||||
@State private var socketPasswordDraft = ""
|
@State private var socketPasswordDraft = ""
|
||||||
@State private var socketPasswordStatusMessage: String?
|
@State private var socketPasswordStatusMessage: String?
|
||||||
@State private var socketPasswordStatusIsError = false
|
@State private var socketPasswordStatusIsError = false
|
||||||
|
@State private var workspaceTabDefaultEntries = WorkspaceTabColorSettings.defaultPaletteWithOverrides()
|
||||||
|
@State private var workspaceTabCustomColors = WorkspaceTabColorSettings.customColors()
|
||||||
|
|
||||||
private var selectedWorkspacePlacement: NewWorkspacePlacement {
|
private var selectedWorkspacePlacement: NewWorkspacePlacement {
|
||||||
NewWorkspacePlacement(rawValue: newWorkspacePlacement) ?? WorkspacePlacementSettings.defaultPlacement
|
NewWorkspacePlacement(rawValue: newWorkspacePlacement) ?? WorkspacePlacementSettings.defaultPlacement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var selectedSidebarActiveTabIndicatorStyle: SidebarActiveTabIndicatorStyle {
|
||||||
|
SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sidebarIndicatorStyleSelection: Binding<String> {
|
||||||
|
Binding(
|
||||||
|
get: { selectedSidebarActiveTabIndicatorStyle.rawValue },
|
||||||
|
set: { sidebarActiveTabIndicatorStyle = $0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private var selectedSocketControlMode: SocketControlMode {
|
private var selectedSocketControlMode: SocketControlMode {
|
||||||
SocketControlSettings.migrateMode(socketControlMode)
|
SocketControlSettings.migrateMode(socketControlMode)
|
||||||
}
|
}
|
||||||
|
|
@ -2683,6 +2760,97 @@ struct SettingsView: View {
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCardDivider()
|
||||||
|
|
||||||
|
SettingsCardRow(
|
||||||
|
"Active Workspace Indicator",
|
||||||
|
controlWidth: pickerColumnWidth
|
||||||
|
) {
|
||||||
|
Picker("", selection: sidebarIndicatorStyleSelection) {
|
||||||
|
ForEach(SidebarActiveTabIndicatorStyle.allCases) { style in
|
||||||
|
Text(style.displayName).tag(style.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSectionHeader(title: "Workspace Colors")
|
||||||
|
SettingsCard {
|
||||||
|
SettingsCardNote("Customize the workspace color palette used by Sidebar > Tab Color. \"Choose Custom Color...\" entries are persisted below.")
|
||||||
|
|
||||||
|
ForEach(Array(workspaceTabDefaultEntries.enumerated()), id: \.element.name) { index, entry in
|
||||||
|
if index > 0 {
|
||||||
|
SettingsCardDivider()
|
||||||
|
}
|
||||||
|
SettingsCardRow(
|
||||||
|
entry.name,
|
||||||
|
subtitle: "Base: \(baseTabColorHex(for: entry.name))"
|
||||||
|
) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
ColorPicker(
|
||||||
|
"",
|
||||||
|
selection: defaultTabColorBinding(for: entry.name),
|
||||||
|
supportsOpacity: false
|
||||||
|
)
|
||||||
|
.labelsHidden()
|
||||||
|
.frame(width: 38)
|
||||||
|
|
||||||
|
Text(entry.hex)
|
||||||
|
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(width: 76, alignment: .trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCardDivider()
|
||||||
|
|
||||||
|
if workspaceTabCustomColors.isEmpty {
|
||||||
|
SettingsCardNote("Custom colors: none yet. Use \"Choose Custom Color...\" from a workspace context menu.")
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Custom Colors")
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
|
||||||
|
ForEach(workspaceTabCustomColors, id: \.self) { hex in
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(Color(nsColor: NSColor(hex: hex) ?? .gray))
|
||||||
|
.frame(width: 11, height: 11)
|
||||||
|
|
||||||
|
Text(hex)
|
||||||
|
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
|
Button("Remove") {
|
||||||
|
removeWorkspaceCustomColor(hex)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCardDivider()
|
||||||
|
|
||||||
|
SettingsCardRow(
|
||||||
|
"Reset Palette",
|
||||||
|
subtitle: "Restore built-in defaults and clear all custom colors."
|
||||||
|
) {
|
||||||
|
Button("Reset") {
|
||||||
|
resetWorkspaceTabColors()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSectionHeader(title: "Automation")
|
SettingsSectionHeader(title: "Automation")
|
||||||
|
|
@ -3081,6 +3249,7 @@ struct SettingsView: View {
|
||||||
browserForcedDarkModeOpacity = BrowserForcedDarkModeSettings.normalizedOpacity(browserForcedDarkModeOpacity)
|
browserForcedDarkModeOpacity = BrowserForcedDarkModeSettings.normalizedOpacity(browserForcedDarkModeOpacity)
|
||||||
browserHistoryEntryCount = BrowserHistoryStore.shared.entries.count
|
browserHistoryEntryCount = BrowserHistoryStore.shared.entries.count
|
||||||
browserInsecureHTTPAllowlistDraft = browserInsecureHTTPAllowlist
|
browserInsecureHTTPAllowlistDraft = browserInsecureHTTPAllowlist
|
||||||
|
reloadWorkspaceTabColorSettings()
|
||||||
}
|
}
|
||||||
.onChange(of: browserInsecureHTTPAllowlist) { oldValue, newValue in
|
.onChange(of: browserInsecureHTTPAllowlist) { oldValue, newValue in
|
||||||
// Keep draft in sync with external changes unless the user has local unsaved edits.
|
// Keep draft in sync with external changes unless the user has local unsaved edits.
|
||||||
|
|
@ -3091,6 +3260,9 @@ struct SettingsView: View {
|
||||||
.onReceive(BrowserHistoryStore.shared.$entries) { entries in
|
.onReceive(BrowserHistoryStore.shared.$entries) { entries in
|
||||||
browserHistoryEntryCount = entries.count
|
browserHistoryEntryCount = entries.count
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
|
||||||
|
reloadWorkspaceTabColorSettings()
|
||||||
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
"Clear browser history?",
|
"Clear browser history?",
|
||||||
isPresented: $showClearBrowserHistoryConfirmation,
|
isPresented: $showClearBrowserHistoryConfirmation,
|
||||||
|
|
@ -3137,15 +3309,53 @@ struct SettingsView: View {
|
||||||
newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||||
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
||||||
sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
|
sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
|
||||||
|
sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
||||||
showOpenAccessConfirmation = false
|
showOpenAccessConfirmation = false
|
||||||
pendingOpenAccessMode = nil
|
pendingOpenAccessMode = nil
|
||||||
socketPasswordDraft = ""
|
socketPasswordDraft = ""
|
||||||
socketPasswordStatusMessage = nil
|
socketPasswordStatusMessage = nil
|
||||||
socketPasswordStatusIsError = false
|
socketPasswordStatusIsError = false
|
||||||
KeyboardShortcutSettings.resetAll()
|
KeyboardShortcutSettings.resetAll()
|
||||||
|
WorkspaceTabColorSettings.reset()
|
||||||
|
reloadWorkspaceTabColorSettings()
|
||||||
shortcutResetToken = UUID()
|
shortcutResetToken = UUID()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func defaultTabColorBinding(for name: String) -> Binding<Color> {
|
||||||
|
Binding(
|
||||||
|
get: {
|
||||||
|
let hex = WorkspaceTabColorSettings.defaultColorHex(named: name)
|
||||||
|
return Color(nsColor: NSColor(hex: hex) ?? .systemBlue)
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
let hex = NSColor(newValue).hexString()
|
||||||
|
WorkspaceTabColorSettings.setDefaultColor(named: name, hex: hex)
|
||||||
|
reloadWorkspaceTabColorSettings()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func baseTabColorHex(for name: String) -> String {
|
||||||
|
WorkspaceTabColorSettings.defaultPalette
|
||||||
|
.first(where: { $0.name == name })?
|
||||||
|
.hex ?? "#1565C0"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeWorkspaceCustomColor(_ hex: String) {
|
||||||
|
WorkspaceTabColorSettings.removeCustomColor(hex)
|
||||||
|
reloadWorkspaceTabColorSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetWorkspaceTabColors() {
|
||||||
|
WorkspaceTabColorSettings.reset()
|
||||||
|
reloadWorkspaceTabColorSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadWorkspaceTabColorSettings() {
|
||||||
|
workspaceTabDefaultEntries = WorkspaceTabColorSettings.defaultPaletteWithOverrides()
|
||||||
|
workspaceTabCustomColors = WorkspaceTabColorSettings.customColors()
|
||||||
|
}
|
||||||
|
|
||||||
private func saveBrowserInsecureHTTPAllowlist() {
|
private func saveBrowserInsecureHTTPAllowlist() {
|
||||||
browserInsecureHTTPAllowlist = browserInsecureHTTPAllowlistDraft
|
browserInsecureHTTPAllowlist = browserInsecureHTTPAllowlistDraft
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import XCTest
|
import XCTest
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
import WebKit
|
import WebKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ObjectiveC.runtime
|
import ObjectiveC.runtime
|
||||||
|
|
@ -1066,6 +1067,171 @@ final class WorkspacePlacementSettingsTests: XCTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class WorkspaceTabColorSettingsTests: XCTestCase {
|
||||||
|
func testNormalizedHexAcceptsAndNormalizesValidInput() {
|
||||||
|
XCTAssertEqual(WorkspaceTabColorSettings.normalizedHex("#abc123"), "#ABC123")
|
||||||
|
XCTAssertEqual(WorkspaceTabColorSettings.normalizedHex(" aBcDeF "), "#ABCDEF")
|
||||||
|
XCTAssertNil(WorkspaceTabColorSettings.normalizedHex("#1234"))
|
||||||
|
XCTAssertNil(WorkspaceTabColorSettings.normalizedHex("#GG1234"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBuiltInPaletteMatchesOriginalPRPalette() {
|
||||||
|
let suiteName = "WorkspaceTabColorSettingsTests.BuiltInPalette.\(UUID().uuidString)"
|
||||||
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
|
XCTFail("Failed to create isolated UserDefaults suite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
|
let palette = WorkspaceTabColorSettings.defaultPaletteWithOverrides(defaults: defaults)
|
||||||
|
XCTAssertEqual(palette.count, 16)
|
||||||
|
XCTAssertEqual(palette.first?.name, "Red")
|
||||||
|
XCTAssertEqual(palette.first?.hex, "#C0392B")
|
||||||
|
XCTAssertEqual(palette.last?.name, "Charcoal")
|
||||||
|
XCTAssertFalse(palette.contains(where: { $0.name == "Gold" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDefaultOverrideRoundTripFallsBackWhenResetToBase() {
|
||||||
|
let suiteName = "WorkspaceTabColorSettingsTests.DefaultOverride.\(UUID().uuidString)"
|
||||||
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
|
XCTFail("Failed to create isolated UserDefaults suite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
|
let first = WorkspaceTabColorSettings.defaultPalette[0]
|
||||||
|
XCTAssertEqual(
|
||||||
|
WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults),
|
||||||
|
first.hex
|
||||||
|
)
|
||||||
|
|
||||||
|
WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: "#00aa33", defaults: defaults)
|
||||||
|
XCTAssertEqual(
|
||||||
|
WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults),
|
||||||
|
"#00AA33"
|
||||||
|
)
|
||||||
|
|
||||||
|
WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: first.hex, defaults: defaults)
|
||||||
|
XCTAssertEqual(
|
||||||
|
WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults),
|
||||||
|
first.hex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddCustomColorPersistsAndDeduplicatesByMostRecent() {
|
||||||
|
let suiteName = "WorkspaceTabColorSettingsTests.CustomColors.\(UUID().uuidString)"
|
||||||
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
|
XCTFail("Failed to create isolated UserDefaults suite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
WorkspaceTabColorSettings.addCustomColor(" #00aa33 ", defaults: defaults),
|
||||||
|
"#00AA33"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
WorkspaceTabColorSettings.addCustomColor("#112233", defaults: defaults),
|
||||||
|
"#112233"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
WorkspaceTabColorSettings.addCustomColor("#00AA33", defaults: defaults),
|
||||||
|
"#00AA33"
|
||||||
|
)
|
||||||
|
XCTAssertNil(WorkspaceTabColorSettings.addCustomColor("nope", defaults: defaults))
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
WorkspaceTabColorSettings.customColors(defaults: defaults),
|
||||||
|
["#00AA33", "#112233"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPaletteIncludesCustomEntriesAndResetClearsAll() {
|
||||||
|
let suiteName = "WorkspaceTabColorSettingsTests.Reset.\(UUID().uuidString)"
|
||||||
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
|
XCTFail("Failed to create isolated UserDefaults suite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
|
let first = WorkspaceTabColorSettings.defaultPalette[0]
|
||||||
|
WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: "#334455", defaults: defaults)
|
||||||
|
_ = WorkspaceTabColorSettings.addCustomColor("#778899", defaults: defaults)
|
||||||
|
|
||||||
|
let paletteBeforeReset = WorkspaceTabColorSettings.palette(defaults: defaults)
|
||||||
|
XCTAssertEqual(paletteBeforeReset.count, WorkspaceTabColorSettings.defaultPalette.count + 1)
|
||||||
|
XCTAssertEqual(paletteBeforeReset[0].hex, "#334455")
|
||||||
|
XCTAssertEqual(paletteBeforeReset.last?.name, "Custom 1")
|
||||||
|
XCTAssertEqual(paletteBeforeReset.last?.hex, "#778899")
|
||||||
|
|
||||||
|
WorkspaceTabColorSettings.reset(defaults: defaults)
|
||||||
|
|
||||||
|
XCTAssertEqual(WorkspaceTabColorSettings.customColors(defaults: defaults), [])
|
||||||
|
XCTAssertEqual(
|
||||||
|
WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults),
|
||||||
|
first.hex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisplayColorLightModeKeepsOriginalHex() {
|
||||||
|
let originalHex = "#1A5276"
|
||||||
|
let rendered = WorkspaceTabColorSettings.displayNSColor(
|
||||||
|
hex: originalHex,
|
||||||
|
colorScheme: .light
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(rendered?.hexString(), originalHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisplayColorDarkModeBrightensColor() {
|
||||||
|
let originalHex = "#1A5276"
|
||||||
|
guard let base = NSColor(hex: originalHex),
|
||||||
|
let rendered = WorkspaceTabColorSettings.displayNSColor(
|
||||||
|
hex: originalHex,
|
||||||
|
colorScheme: .dark
|
||||||
|
) else {
|
||||||
|
XCTFail("Expected valid color conversion")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertNotEqual(rendered.hexString(), originalHex)
|
||||||
|
XCTAssertGreaterThan(rendered.luminance, base.luminance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisplayColorDarkModeKeepsGrayscaleNeutral() {
|
||||||
|
let originalHex = "#808080"
|
||||||
|
guard let base = NSColor(hex: originalHex),
|
||||||
|
let rendered = WorkspaceTabColorSettings.displayNSColor(
|
||||||
|
hex: originalHex,
|
||||||
|
colorScheme: .dark
|
||||||
|
),
|
||||||
|
let renderedSRGB = rendered.usingColorSpace(.sRGB) else {
|
||||||
|
XCTFail("Expected valid color conversion")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertGreaterThan(rendered.luminance, base.luminance)
|
||||||
|
XCTAssertLessThan(abs(renderedSRGB.redComponent - renderedSRGB.greenComponent), 0.003)
|
||||||
|
XCTAssertLessThan(abs(renderedSRGB.greenComponent - renderedSRGB.blueComponent), 0.003)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisplayColorForceBrightensInLightMode() {
|
||||||
|
let originalHex = "#1A5276"
|
||||||
|
guard let base = NSColor(hex: originalHex),
|
||||||
|
let rendered = WorkspaceTabColorSettings.displayNSColor(
|
||||||
|
hex: originalHex,
|
||||||
|
colorScheme: .light,
|
||||||
|
forceBright: true
|
||||||
|
) else {
|
||||||
|
XCTFail("Expected valid color conversion")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertNotEqual(rendered.hexString(), originalHex)
|
||||||
|
XCTAssertGreaterThan(rendered.luminance, base.luminance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class WorkspaceAutoReorderSettingsTests: XCTestCase {
|
final class WorkspaceAutoReorderSettingsTests: XCTestCase {
|
||||||
func testDefaultIsEnabled() {
|
func testDefaultIsEnabled() {
|
||||||
let suiteName = "WorkspaceAutoReorderSettingsTests.Default.\(UUID().uuidString)"
|
let suiteName = "WorkspaceAutoReorderSettingsTests.Default.\(UUID().uuidString)"
|
||||||
|
|
@ -1131,6 +1297,44 @@ final class SidebarBranchLayoutSettingsTests: XCTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class SidebarActiveTabIndicatorSettingsTests: XCTestCase {
|
||||||
|
func testDefaultStyleWhenUnset() {
|
||||||
|
let suiteName = "SidebarActiveTabIndicatorSettingsTests.Default.\(UUID().uuidString)"
|
||||||
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
|
XCTFail("Failed to create isolated UserDefaults suite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
|
defaults.removeObject(forKey: SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
|
XCTAssertEqual(
|
||||||
|
SidebarActiveTabIndicatorSettings.current(defaults: defaults),
|
||||||
|
SidebarActiveTabIndicatorSettings.defaultStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStoredStyleParsesAndInvalidFallsBack() {
|
||||||
|
let suiteName = "SidebarActiveTabIndicatorSettingsTests.Stored.\(UUID().uuidString)"
|
||||||
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
|
XCTFail("Failed to create isolated UserDefaults suite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
|
defaults.set(SidebarActiveTabIndicatorStyle.leftRail.rawValue, forKey: SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
|
XCTAssertEqual(SidebarActiveTabIndicatorSettings.current(defaults: defaults), .leftRail)
|
||||||
|
|
||||||
|
defaults.set("rail", forKey: SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
|
XCTAssertEqual(SidebarActiveTabIndicatorSettings.current(defaults: defaults), .leftRail)
|
||||||
|
|
||||||
|
defaults.set("not-a-style", forKey: SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
|
XCTAssertEqual(
|
||||||
|
SidebarActiveTabIndicatorSettings.current(defaults: defaults),
|
||||||
|
SidebarActiveTabIndicatorSettings.defaultStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class AppearanceSettingsTests: XCTestCase {
|
final class AppearanceSettingsTests: XCTestCase {
|
||||||
func testResolvedModeDefaultsToSystemWhenUnset() {
|
func testResolvedModeDefaultsToSystemWhenUnset() {
|
||||||
let suiteName = "AppearanceSettingsTests.Default.\(UUID().uuidString)"
|
let suiteName = "AppearanceSettingsTests.Default.\(UUID().uuidString)"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,24 @@
|
||||||
import XCTest
|
import XCTest
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
// UI runners can adjust wall clock time mid-test; use monotonic uptime for polling deadlines.
|
||||||
|
private func pollUntil(
|
||||||
|
timeout: TimeInterval,
|
||||||
|
pollInterval: TimeInterval = 0.05,
|
||||||
|
condition: () -> Bool
|
||||||
|
) -> Bool {
|
||||||
|
let start = ProcessInfo.processInfo.systemUptime
|
||||||
|
while true {
|
||||||
|
if condition() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (ProcessInfo.processInfo.systemUptime - start) >= timeout {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(pollInterval))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class UpdatePillUITests: XCTestCase {
|
final class UpdatePillUITests: XCTestCase {
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
|
|
@ -131,25 +149,28 @@ final class UpdatePillUITests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func waitForWindowCount(atLeast count: Int, app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
private func waitForWindowCount(atLeast count: Int, app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
pollUntil(timeout: timeout) {
|
||||||
while Date() < deadline {
|
app.windows.count >= count
|
||||||
if app.windows.count >= count { return true }
|
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
|
||||||
}
|
}
|
||||||
return app.windows.count >= count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func assertVisibleSize(_ element: XCUIElement, timeout: TimeInterval = 2.0) {
|
private func assertVisibleSize(_ element: XCUIElement, timeout: TimeInterval = 2.0) {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let pollInterval: TimeInterval = 0.05
|
||||||
var size = element.frame.size
|
var size = element.frame.size
|
||||||
while Date() < deadline {
|
var exists = element.exists
|
||||||
|
var hittable = element.isHittable
|
||||||
|
|
||||||
|
let visible = pollUntil(timeout: timeout, pollInterval: pollInterval) {
|
||||||
size = element.frame.size
|
size = element.frame.size
|
||||||
if size.width > 20 && size.height > 10 {
|
exists = element.exists
|
||||||
return
|
hittable = element.isHittable
|
||||||
}
|
return size.width > 20 && size.height > 10
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
}
|
||||||
|
if !visible {
|
||||||
|
XCTFail(
|
||||||
|
"Expected UpdatePill to have visible size, got \(size), exists=\(exists), hittable=\(hittable)"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
XCTFail("Expected UpdatePill to have visible size, got \(size)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attachScreenshot(name: String, screenshot: XCUIScreenshot = XCUIScreen.main.screenshot()) {
|
private func attachScreenshot(name: String, screenshot: XCUIScreenshot = XCUIScreen.main.screenshot()) {
|
||||||
|
|
@ -197,12 +218,14 @@ final class UpdatePillUITests: XCTestCase {
|
||||||
|
|
||||||
private func launchAndActivate(_ app: XCUIApplication, activateTimeout: TimeInterval = 2.0) {
|
private func launchAndActivate(_ app: XCUIApplication, activateTimeout: TimeInterval = 2.0) {
|
||||||
app.launch()
|
app.launch()
|
||||||
let deadline = Date().addingTimeInterval(activateTimeout)
|
let activated = pollUntil(timeout: activateTimeout) {
|
||||||
while Date() < deadline, app.state != .runningForeground {
|
guard app.state != .runningForeground else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
app.activate()
|
app.activate()
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
return app.state == .runningForeground
|
||||||
}
|
}
|
||||||
if app.state != .runningForeground {
|
if !activated {
|
||||||
app.activate()
|
app.activate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -293,40 +316,32 @@ final class TitlebarShortcutHintsUITests: XCTestCase {
|
||||||
app.launchArguments += ["-shortcutHintTitlebarYOffset", "0"]
|
app.launchArguments += ["-shortcutHintTitlebarYOffset", "0"]
|
||||||
app.launch()
|
app.launch()
|
||||||
|
|
||||||
let deadline = Date().addingTimeInterval(2.0)
|
_ = pollUntil(timeout: 2.0) {
|
||||||
while Date() < deadline, app.state != .runningForeground {
|
guard app.state != .runningForeground else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
app.activate()
|
app.activate()
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
return app.state == .runningForeground
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
private func waitForWindowCount(atLeast count: Int, app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
private func waitForWindowCount(atLeast count: Int, app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
pollUntil(timeout: timeout) {
|
||||||
while Date() < deadline {
|
app.windows.count >= count
|
||||||
if app.windows.count >= count { return true }
|
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
|
||||||
}
|
}
|
||||||
return app.windows.count >= count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func waitForElementVisible(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
|
private func waitForElementVisible(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
pollUntil(timeout: timeout) {
|
||||||
while Date() < deadline {
|
|
||||||
if element.exists {
|
if element.exists {
|
||||||
let frame = element.frame
|
let frame = element.frame
|
||||||
if frame.width > 1, frame.height > 1 {
|
if frame.width > 1, frame.height > 1 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if element.exists {
|
|
||||||
let frame = element.frame
|
|
||||||
return frame.width > 1 && frame.height > 1
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ sidebarBlurOpacity="$(format_number "$(read_value sidebarBlurOpacity 0.79)" 2)"
|
||||||
sidebarTintHex="$(read_value sidebarTintHex '#101010')"
|
sidebarTintHex="$(read_value sidebarTintHex '#101010')"
|
||||||
sidebarTintOpacity="$(format_number "$(read_value sidebarTintOpacity 0.54)" 2)"
|
sidebarTintOpacity="$(format_number "$(read_value sidebarTintOpacity 0.54)" 2)"
|
||||||
sidebarCornerRadius="$(format_number "$(read_value sidebarCornerRadius 0.0)" 1)"
|
sidebarCornerRadius="$(format_number "$(read_value sidebarCornerRadius 0.0)" 1)"
|
||||||
|
sidebarActiveTabIndicatorStyle="$(read_value sidebarActiveTabIndicatorStyle solidFill)"
|
||||||
shortcutHintSidebarXOffset="$(format_number "$(read_value shortcutHintSidebarXOffset 0.0)" 1)"
|
shortcutHintSidebarXOffset="$(format_number "$(read_value shortcutHintSidebarXOffset 0.0)" 1)"
|
||||||
shortcutHintSidebarYOffset="$(format_number "$(read_value shortcutHintSidebarYOffset 0.0)" 1)"
|
shortcutHintSidebarYOffset="$(format_number "$(read_value shortcutHintSidebarYOffset 0.0)" 1)"
|
||||||
shortcutHintTitlebarXOffset="$(format_number "$(read_value shortcutHintTitlebarXOffset 4.0)" 1)"
|
shortcutHintTitlebarXOffset="$(format_number "$(read_value shortcutHintTitlebarXOffset 4.0)" 1)"
|
||||||
|
|
@ -141,6 +142,7 @@ sidebarBlurOpacity=$sidebarBlurOpacity
|
||||||
sidebarTintHex=$sidebarTintHex
|
sidebarTintHex=$sidebarTintHex
|
||||||
sidebarTintOpacity=$sidebarTintOpacity
|
sidebarTintOpacity=$sidebarTintOpacity
|
||||||
sidebarCornerRadius=$sidebarCornerRadius
|
sidebarCornerRadius=$sidebarCornerRadius
|
||||||
|
sidebarActiveTabIndicatorStyle=$sidebarActiveTabIndicatorStyle
|
||||||
shortcutHintSidebarXOffset=$shortcutHintSidebarXOffset
|
shortcutHintSidebarXOffset=$shortcutHintSidebarXOffset
|
||||||
shortcutHintSidebarYOffset=$shortcutHintSidebarYOffset
|
shortcutHintSidebarYOffset=$shortcutHintSidebarYOffset
|
||||||
shortcutHintTitlebarXOffset=$shortcutHintTitlebarXOffset
|
shortcutHintTitlebarXOffset=$shortcutHintTitlebarXOffset
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue