Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions Sources/Panels/PanelContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,25 @@ struct PanelContentView: View {
}
}

private let panelFilePathHeaderIconSlotSize: CGFloat = 14
private let panelFilePathHeaderHorizontalPadding: CGFloat = 6
private let panelFilePathHeaderContentSpacing: CGFloat = 6

struct PanelFilePathHeader<TrailingContent: View>: View {
let iconSystemName: String
let filePath: String
let foregroundColor: NSColor
@ViewBuilder let trailingContent: () -> TrailingContent

var body: some View {
HStack(spacing: 8) {
HStack(spacing: panelFilePathHeaderContentSpacing) {
Image(systemName: iconSystemName)
.font(.system(size: panelFilePathHeaderIconSlotSize))
.foregroundStyle(.secondary)
.frame(width: 16)
.frame(
width: panelFilePathHeaderIconSlotSize,
height: panelFilePathHeaderIconSlotSize
)
Text(filePath)
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(Color(nsColor: foregroundColor).opacity(0.68))
Expand All @@ -146,7 +154,7 @@ struct PanelFilePathHeader<TrailingContent: View>: View {
Spacer(minLength: 8)
trailingContent()
}
.padding(.horizontal, 12)
.padding(.horizontal, panelFilePathHeaderHorizontalPadding)
.frame(height: 30)
.background(Color.clear)
}
Expand Down
58 changes: 58 additions & 0 deletions Sources/RenderableSystemSymbol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import AppKit

enum RenderableSystemSymbol {
static let defaultWorkspaceGroupIcon = "folder.fill"
static let defaultSurfaceTabIcon = "doc.text"
@MainActor
private static var renderabilityCache: [String: Bool] = [:]

static func trimmed(_ raw: String?) -> String? {
guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty else {
return nil
}
return trimmed
}

@MainActor
static func normalized(_ raw: String?) -> String? {
guard let trimmed = trimmed(raw),
isRenderable(trimmed) else {
return nil
}
return trimmed
}

@MainActor
static func resolvedWorkspaceGroupIcon(explicit: String?, configured: String?) -> String {
for candidate in [explicit, configured] {
guard let normalized = normalized(candidate) else { continue }
return normalized
}
return defaultWorkspaceGroupIcon
}

@MainActor
static func resolvedSurfaceTabIcon(_ raw: String?, fallback: String = defaultSurfaceTabIcon) -> String {
normalized(raw)
?? normalized(fallback)
?? defaultSurfaceTabIcon
}

@MainActor
static func isRenderable(_ symbol: String) -> Bool {
if let cached = renderabilityCache[symbol] {
return cached
}
let resolved = NSImage(systemSymbolName: symbol, accessibilityDescription: nil) != nil
renderabilityCache[symbol] = resolved
return resolved
}

#if DEBUG
@MainActor
static func resetRenderabilityCacheForTesting() {
renderabilityCache.removeAll()
}
#endif
}
10 changes: 8 additions & 2 deletions Sources/SidebarWorkspaceGroupHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
return .secondary
}

private var displayedIconSymbol: String {
RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: iconSymbol, configured: nil)
}

private var shortcutHintPillText: String? {
guard showsShortcutHint,
let shortcutDigit,
Expand Down Expand Up @@ -111,9 +115,11 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
)

HStack(spacing: 6) {
Image(systemName: iconSymbol)
.font(.system(size: 11))
Image(systemName: displayedIconSymbol)
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(iconColor)
.frame(width: 14, height: 14)
.accessibilityHidden(true)
Text(name)
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(isAnchorActive ? Color.primary : Color.primary.opacity(0.9))
Expand Down
11 changes: 7 additions & 4 deletions Sources/TabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6584,10 +6584,13 @@ class TabManager: ObservableObject {
workspaceGroups[index].customColor = hex
}

func setWorkspaceGroupIcon(groupId: UUID, symbol: String?) {
guard let index = workspaceGroups.firstIndex(where: { $0.id == groupId }) else { return }
guard workspaceGroups[index].iconSymbol != symbol else { return }
workspaceGroups[index].iconSymbol = symbol
@discardableResult
func setWorkspaceGroupIcon(groupId: UUID, symbol: String?) -> String? {
let normalized = RenderableSystemSymbol.normalized(symbol)
guard let index = workspaceGroups.firstIndex(where: { $0.id == groupId }) else { return nil }
guard workspaceGroups[index].iconSymbol != normalized else { return normalized }
workspaceGroups[index].iconSymbol = normalized
return normalized
}

/// Reassign which member workspace serves as the group's anchor.
Expand Down
7 changes: 5 additions & 2 deletions Sources/TerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6560,12 +6560,15 @@ class TerminalController {
let symbol: String? = (params["symbol"] as? String).map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
let normalized: String? = (symbol?.isEmpty == false) ? symbol : nil
var ok = false
var storedIconSymbol: String?
v2MainSync {
ok = tabManager.workspaceGroups.contains(where: { $0.id == gid })
if ok { tabManager.setWorkspaceGroupIcon(groupId: gid, symbol: normalized) }
if ok {
storedIconSymbol = tabManager.setWorkspaceGroupIcon(groupId: gid, symbol: normalized)
}
}
return ok
? .ok(["group_id": gid.uuidString, "icon_symbol": v2OrNull(normalized)])
? .ok(["group_id": gid.uuidString, "icon_symbol": v2OrNull(storedIconSymbol)])
: .err(code: "not_found", message: "Group not found", data: ["group_id": gid.uuidString])
}

Expand Down
5 changes: 4 additions & 1 deletion Sources/VerticalTabsSidebar+WorkspaceGroups.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ extension VerticalTabsSidebar {
let anchorCwd = renderContext.workspaceById[group.anchorWorkspaceId]?.currentDirectory
let resolvedConfig = cmuxConfigStore.resolveWorkspaceGroupConfig(forCwd: anchorCwd)
let effectiveColor = group.customColor ?? resolvedConfig?.color
let effectiveIcon = group.iconSymbol ?? resolvedConfig?.iconSymbol ?? "folder.fill"
let effectiveIcon = RenderableSystemSymbol.resolvedWorkspaceGroupIcon(
explicit: group.iconSymbol,
configured: resolvedConfig?.iconSymbol
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let cwdContextMenuItems = resolvedConfig?.contextMenuItems ?? []
let newWorkspacePlacement = resolvedConfig?.newWorkspacePlacement
let anchorUnreadCount: Int = {
Expand Down
7 changes: 4 additions & 3 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11048,8 +11048,9 @@ final class Workspace: Identifiable, ObservableObject {
self.panelTitles[filePreviewPanel.id] = newTitle
}
let resolvedTitle = self.resolvedPanelTitle(panelId: filePreviewPanel.id, fallback: newTitle)
let resolvedIcon = RenderableSystemSymbol.resolvedSurfaceTabIcon(displayIcon)
let titleUpdate: String? = existing.title == resolvedTitle ? nil : resolvedTitle
let iconUpdate: String?? = existing.icon == displayIcon ? nil : .some(displayIcon)
let iconUpdate: String?? = existing.icon == resolvedIcon ? nil : .some(resolvedIcon)
let dirtyUpdate: Bool? = existing.isDirty == isDirty ? nil : isDirty
guard titleUpdate != nil || iconUpdate != nil || dirtyUpdate != nil else { return }
self.bonsplitController.updateTab(
Expand Down Expand Up @@ -14178,7 +14179,7 @@ final class Workspace: Identifiable, ObservableObject {

guard let newTabId = bonsplitController.createTab(
title: filePreviewPanel.displayTitle,
icon: filePreviewPanel.displayIcon,
icon: RenderableSystemSymbol.resolvedSurfaceTabIcon(filePreviewPanel.displayIcon),
kind: SurfaceKind.filePreview,
isDirty: filePreviewPanel.isDirty,
isLoading: false,
Expand Down Expand Up @@ -14294,7 +14295,7 @@ final class Workspace: Identifiable, ObservableObject {

let newTab = Bonsplit.Tab(
title: filePreviewPanel.displayTitle,
icon: filePreviewPanel.displayIcon,
icon: RenderableSystemSymbol.resolvedSurfaceTabIcon(filePreviewPanel.displayIcon),
kind: SurfaceKind.filePreview,
isDirty: filePreviewPanel.isDirty,
isLoading: false,
Expand Down
4 changes: 4 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; };
B9000027A1B2C3D4E5F60719 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; };
E4321000E4321000E4321001 /* RemoteShellSessionParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4321000E4321000E4321002 /* RemoteShellSessionParsing.swift */; };
D5037010000000000000003 /* RenderableSystemSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037010000000000000002 /* RenderableSystemSymbol.swift */; };
F5410004A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410005A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift */; };
F5410000A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410001A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift */; };
F5410002A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410003A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift */; };
Expand Down Expand Up @@ -981,6 +982,7 @@
D0C0D0C0D0C0D0C0D0C00004 /* RemoteLoopbackRuntimeBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteLoopbackRuntimeBridge.swift; sourceTree = "<group>"; };
A5001641 /* RemoteRelayZshBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRelayZshBootstrap.swift; sourceTree = "<group>"; };
E4321000E4321000E4321002 /* RemoteShellSessionParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShellSessionParsing.swift; sourceTree = "<group>"; };
D5037010000000000000002 /* RenderableSystemSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderableSystemSymbol.swift; sourceTree = "<group>"; };
F5410005A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentHookProviderHermesTests.swift; sourceTree = "<group>"; };
F5410001A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentHookProviderResumeTests.swift; sourceTree = "<group>"; };
F5410003A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentNonInteractiveTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1421,6 +1423,7 @@
C9A57108C9A57108C9A57108 /* SidebarWorkspaceGroupDialogs.swift */,
C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */,
C9A5710AC9A5710AC9A5710A /* SidebarWorkspaceGroupingMetrics.swift */,
D5037010000000000000002 /* RenderableSystemSymbol.swift */,
C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */,
C9A5720CC9A5720CC9A5720C /* TabItemView+WorkspaceGroups.swift */,
C9A5720AC9A5720AC9A5720A /* VerticalTabsSidebar+WorkspaceGroups.swift */,
Expand Down Expand Up @@ -2366,6 +2369,7 @@
D0C0D0C0D0C0D0C0D0C00003 /* RemoteLoopbackRuntimeBridge.swift in Sources */,
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */,
E4321000E4321000E4321001 /* RemoteShellSessionParsing.swift in Sources */,
D5037010000000000000003 /* RenderableSystemSymbol.swift in Sources */,
A5001660 /* RestorableAgentSession.swift in Sources */,
C13519000000000000000005 /* RestorableAgentTypes.swift in Sources */,
B7F9A600B7F9A600B7F9A600 /* RightSidebarChromeGeometryReporting.swift in Sources */,
Expand Down
29 changes: 29 additions & 0 deletions cmuxTests/WorkspaceGroupTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -679,4 +679,33 @@ struct WorkspaceGroupTests {
#expect(restoredGroup.customColor == "#123456")
#expect(restoredGroup.iconSymbol == "leaf.fill")
}

@Test func workspaceGroupIconSymbolResolutionFallsBackToRenderableIcon() {
#expect(RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: nil, configured: nil) == "folder.fill")
#expect(RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: " ", configured: "leaf.fill") == "leaf.fill")
#expect(RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: "not.an.sf.symbol", configured: nil) == "folder.fill")
}

@Test func setWorkspaceGroupIconDropsInvalidSymbols() {
let manager = makeTabManager()
let groupId = manager.createWorkspaceGroup(
name: "G",
childWorkspaceIds: [manager.tabs[0].id]
)!

let invalidStoredIcon = manager.setWorkspaceGroupIcon(groupId: groupId, symbol: "not.an.sf.symbol")
#expect(invalidStoredIcon == nil)
#expect(manager.workspaceGroups.first { $0.id == groupId }?.iconSymbol == nil)

let validStoredIcon = manager.setWorkspaceGroupIcon(groupId: groupId, symbol: " leaf.fill ")
#expect(validStoredIcon == "leaf.fill")
#expect(manager.workspaceGroups.first { $0.id == groupId }?.iconSymbol == "leaf.fill")
}

@Test func surfaceTabIconSymbolResolutionFallsBackForInvalidInput() {
#expect(RenderableSystemSymbol.resolvedSurfaceTabIcon("doc.text") == "doc.text")
#expect(RenderableSystemSymbol.resolvedSurfaceTabIcon(" doc.text ") == "doc.text")
#expect(RenderableSystemSymbol.resolvedSurfaceTabIcon("not.an.sf.symbol") == "doc.text")
#expect(RenderableSystemSymbol.resolvedSurfaceTabIcon(" ") == "doc.text")
}
}
Loading