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
12 changes: 12 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
2F0C05000000000000000002 /* MainWindowFocusController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0C05000000000000000001 /* MainWindowFocusController.swift */; };
A500D010A1B2C3D4E5F60718 /* DebugLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500D011A1B2C3D4E5F60718 /* DebugLogging.swift */; };
A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; };
E30750000000000000000002 /* WorkspaceTabColorResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30750000000000000000001 /* WorkspaceTabColorResolution.swift */; };
A5001004 /* GhosttyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001014 /* GhosttyConfig.swift */; };
A5001005 /* GhosttyTerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001015 /* GhosttyTerminalView.swift */; };
A5001006 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5001016 /* GhosttyKit.xcframework */; };
Expand Down Expand Up @@ -107,6 +108,7 @@
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; };
A5001660 /* RestorableAgentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001661 /* RestorableAgentSession.swift */; };
A5001650 /* CmuxConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001651 /* CmuxConfig.swift */; };
E30750000000000000000004 /* CmuxWorkspaceDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30750000000000000000003 /* CmuxWorkspaceDefinition.swift */; };
A5001652 /* CmuxConfigExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001653 /* CmuxConfigExecutor.swift */; };
A5001654 /* CmuxActionTrust.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001655 /* CmuxActionTrust.swift */; };
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */ = {isa = PBXBuildFile; fileRef = A5002001 /* THIRD_PARTY_LICENSES.md */; };
Expand Down Expand Up @@ -142,6 +144,7 @@
B9000027A1B2C3D4E5F60719 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; };
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */; };
C1A2B3C4D5E6F70800000001 /* CmuxConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */; };
E30750000000000000000006 /* CmuxConfigNamedColorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30750000000000000000005 /* CmuxConfigNamedColorTests.swift */; };
C1ADE00002A1B2C3D4E5F719 /* claude in Copy CLI */ = {isa = PBXBuildFile; fileRef = C1ADE00001A1B2C3D4E5F719 /* claude */; };
C2577000A1B2C3D4E5F60718 /* TerminalCmdClickUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2577001A1B2C3D4E5F60718 /* TerminalCmdClickUITests.swift */; };
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */; };
Expand Down Expand Up @@ -299,6 +302,7 @@
C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/MenuBarExtraController.swift; sourceTree = "<group>"; };
A500D011A1B2C3D4E5F60718 /* DebugLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/DebugLogging.swift; sourceTree = "<group>"; };
A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = "<group>"; };
E30750000000000000000001 /* WorkspaceTabColorResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceTabColorResolution.swift; sourceTree = "<group>"; };
A5001014 /* GhosttyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfig.swift; sourceTree = "<group>"; };
A5001015 /* GhosttyTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTerminalView.swift; sourceTree = "<group>"; };
A5001016 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
Expand Down Expand Up @@ -355,6 +359,7 @@
A5001641 /* RemoteRelayZshBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRelayZshBootstrap.swift; sourceTree = "<group>"; };
A5001661 /* RestorableAgentSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentSession.swift; sourceTree = "<group>"; };
A5001651 /* CmuxConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfig.swift; sourceTree = "<group>"; };
E30750000000000000000003 /* CmuxWorkspaceDefinition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxWorkspaceDefinition.swift; sourceTree = "<group>"; };
A5001653 /* CmuxConfigExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigExecutor.swift; sourceTree = "<group>"; };
A5001655 /* CmuxActionTrust.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxActionTrust.swift; sourceTree = "<group>"; };
A5002001 /* THIRD_PARTY_LICENSES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = THIRD_PARTY_LICENSES.md; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -389,6 +394,7 @@
BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAndDragTests.swift; sourceTree = "<group>"; };
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = "<group>"; };
C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigTests.swift; sourceTree = "<group>"; };
E30750000000000000000005 /* CmuxConfigNamedColorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigNamedColorTests.swift; sourceTree = "<group>"; };
C1ADE00001A1B2C3D4E5F719 /* claude */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Resources/bin/claude; sourceTree = SOURCE_ROOT; };
C2577001A1B2C3D4E5F60718 /* TerminalCmdClickUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCmdClickUITests.swift; sourceTree = "<group>"; };
D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPaneNavigationKeybindUITests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -595,6 +601,7 @@
A5F10010A1B2C3D4E5F60719 /* KeyboardShortcutSettingsFileStore.swift */,
A50012F4 /* KeyboardLayout.swift */,
A5001013 /* TabManager.swift */,
E30750000000000000000001 /* WorkspaceTabColorResolution.swift */,
A5001511 /* UITestRecorder.swift */,
A5001520 /* PostHogAnalytics.swift */,
A5001416 /* Workspace.swift */,
Expand Down Expand Up @@ -650,6 +657,7 @@
A5001661 /* RestorableAgentSession.swift */,
A5001641 /* RemoteRelayZshBootstrap.swift */,
A5001651 /* CmuxConfig.swift */,
E30750000000000000000003 /* CmuxWorkspaceDefinition.swift */,
A5001653 /* CmuxConfigExecutor.swift */,
A5001655 /* CmuxActionTrust.swift */,
FE001001 /* FileExplorerStore.swift */,
Expand Down Expand Up @@ -721,6 +729,7 @@
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */,
10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */,
C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */,
E30750000000000000000005 /* CmuxConfigNamedColorTests.swift */,
FE002001 /* FileExplorerRootResolverTests.swift */,
FE002002 /* FileExplorerStoreTests.swift */,
);
Expand Down Expand Up @@ -984,6 +993,7 @@
A5F10011A1B2C3D4E5F60719 /* KeyboardShortcutSettingsFileStore.swift in Sources */,
A50012F5 /* KeyboardLayout.swift in Sources */,
A5001003 /* TabManager.swift in Sources */,
E30750000000000000000002 /* WorkspaceTabColorResolution.swift in Sources */,
A5001501 /* UITestRecorder.swift in Sources */,
A5001521 /* PostHogAnalytics.swift in Sources */,
A5001406 /* Workspace.swift in Sources */,
Expand Down Expand Up @@ -1038,6 +1048,7 @@
A5001660 /* RestorableAgentSession.swift in Sources */,
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */,
A5001650 /* CmuxConfig.swift in Sources */,
E30750000000000000000004 /* CmuxWorkspaceDefinition.swift in Sources */,
A5001652 /* CmuxConfigExecutor.swift in Sources */,
A5001654 /* CmuxActionTrust.swift in Sources */,
FE001101 /* FileExplorerStore.swift in Sources */,
Expand Down Expand Up @@ -1140,6 +1151,7 @@
8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */,
2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */,
C1A2B3C4D5E6F70800000001 /* CmuxConfigTests.swift in Sources */,
E30750000000000000000006 /* CmuxConfigNamedColorTests.swift in Sources */,
FE002101 /* FileExplorerRootResolverTests.swift in Sources */,
FE002102 /* FileExplorerStoreTests.swift in Sources */,
);
Expand Down
46 changes: 11 additions & 35 deletions Sources/CmuxConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import Combine
import CryptoKit
import Foundation

extension CodingUserInfoKey {
static let cmuxWorkspaceColorDefaults = CodingUserInfoKey(rawValue: "cmuxWorkspaceColorDefaults")!
}

struct CmuxConfigFile: Codable, Sendable {
var actions: [String: CmuxConfigActionDefinition]
var ui: CmuxConfigUIDefinition?
Expand Down Expand Up @@ -1586,40 +1590,6 @@ enum CmuxRestartBehavior: String, Codable, Sendable {
case confirm
}

struct CmuxWorkspaceDefinition: Codable, Sendable {
var name: String?
var cwd: String?
var color: String?
var layout: CmuxLayoutNode?

init(name: String? = nil, cwd: String? = nil, color: String? = nil, layout: CmuxLayoutNode? = nil) {
self.name = name
self.cwd = cwd
self.color = color
self.layout = layout
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: .name)
cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
layout = try container.decodeIfPresent(CmuxLayoutNode.self, forKey: .layout)

if let rawColor = try container.decodeIfPresent(String.self, forKey: .color) {
guard let normalized = WorkspaceTabColorSettings.normalizedHex(rawColor) else {
throw DecodingError.dataCorruptedError(
forKey: .color,
in: container,
debugDescription: "Invalid color \"\(rawColor)\". Expected 6-digit hex format: #RRGGBB"
)
}
color = normalized
} else {
color = nil
}
}
}

indirect enum CmuxLayoutNode: Codable, Sendable {
case pane(CmuxPaneDefinition)
case split(CmuxSplitDefinition)
Expand Down Expand Up @@ -1858,6 +1828,7 @@ final class CmuxConfigStore: ObservableObject {
private struct ParsedConfigCacheEntry {
let fileSize: UInt64
let modificationDate: Date?
let workspaceColorPaletteFingerprint: String
let config: CmuxConfigFile?
let issue: CmuxConfigIssue?
}
Expand Down Expand Up @@ -2512,10 +2483,12 @@ final class CmuxConfigStore: ObservableObject {
let attributes = try? fileManager.attributesOfItem(atPath: path)
let fileSize = (attributes?[.size] as? NSNumber)?.uint64Value ?? 0
let modificationDate = attributes?[.modificationDate] as? Date
let paletteFingerprint = WorkspaceTabColorSettings.paletteCacheFingerprint()

if let cached = parsedConfigCache[path],
cached.fileSize == fileSize,
cached.modificationDate == modificationDate {
cached.modificationDate == modificationDate,
cached.workspaceColorPaletteFingerprint == paletteFingerprint {
return ParsedConfigResult(config: cached.config, issue: cached.issue)
}

Expand All @@ -2525,6 +2498,7 @@ final class CmuxConfigStore: ObservableObject {
parsedConfigCache[path] = ParsedConfigCacheEntry(
fileSize: fileSize,
modificationDate: modificationDate,
workspaceColorPaletteFingerprint: paletteFingerprint,
config: nil,
issue: issue
)
Expand All @@ -2535,6 +2509,7 @@ final class CmuxConfigStore: ObservableObject {
parsedConfigCache[path] = ParsedConfigCacheEntry(
fileSize: fileSize,
modificationDate: modificationDate,
workspaceColorPaletteFingerprint: paletteFingerprint,
config: config,
issue: nil
)
Expand All @@ -2544,6 +2519,7 @@ final class CmuxConfigStore: ObservableObject {
parsedConfigCache[path] = ParsedConfigCacheEntry(
fileSize: fileSize,
modificationDate: modificationDate,
workspaceColorPaletteFingerprint: paletteFingerprint,
config: nil,
issue: issue
)
Expand Down
36 changes: 36 additions & 0 deletions Sources/CmuxWorkspaceDefinition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

struct CmuxWorkspaceDefinition: Codable, Sendable {
var name: String?
var cwd: String?
var color: String?
var layout: CmuxLayoutNode?

init(name: String? = nil, cwd: String? = nil, color: String? = nil, layout: CmuxLayoutNode? = nil) {
self.name = name
self.cwd = cwd
self.color = color
self.layout = layout
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: .name)
cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
layout = try container.decodeIfPresent(CmuxLayoutNode.self, forKey: .layout)

if let rawColor = try container.decodeIfPresent(String.self, forKey: .color) {
let defaults = decoder.userInfo[.cmuxWorkspaceColorDefaults] as? UserDefaults ?? .standard
guard let normalized = WorkspaceTabColorSettings.resolvedColorHex(rawColor, defaults: defaults) else {
throw DecodingError.dataCorruptedError(
forKey: .color,
in: container,
debugDescription: "Invalid color \"\(rawColor)\". Expected 6-digit hex format (#RRGGBB) or a workspace color name"
)
}
color = normalized
} else {
color = nil
}
}
}
22 changes: 22 additions & 0 deletions Sources/WorkspaceTabColorResolution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

extension WorkspaceTabColorSettings {
static func resolvedColorHex(_ raw: String, defaults: UserDefaults = .standard) -> String? {
if let normalized = normalizedHex(raw) {
return normalized
}

let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
return resolvedPaletteMap(defaults: defaults)
.first { name, _ in name.caseInsensitiveCompare(trimmed) == .orderedSame }?
.value
}

static func paletteCacheFingerprint(defaults: UserDefaults = .standard) -> String {
resolvedPaletteMap(defaults: defaults)
.sorted { lhs, rhs in lhs.key.localizedStandardCompare(rhs.key) == .orderedAscending }
.map { "\($0.key)=\($0.value)" }
.joined(separator: "\n")
}
}
100 changes: 100 additions & 0 deletions cmuxTests/CmuxConfigNamedColorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import XCTest

#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif

final class CmuxConfigNamedColorTests: XCTestCase {
private func decode(_ json: String, colorDefaults: UserDefaults? = nil) throws -> CmuxConfigFile {
let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
if let colorDefaults {
decoder.userInfo[.cmuxWorkspaceColorDefaults] = colorDefaults
}
return try decoder.decode(CmuxConfigFile.self, from: data)
}

func testDecodeWorkspaceCommandAcceptsNamedColor() throws {
let suiteName = "cmux-config-named-color-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer { defaults.removePersistentDomain(forName: suiteName) }
WorkspaceTabColorSettings.persistPaletteMap(["Indigo": "#283593"], defaults: defaults)

let json = """
{
"commands": [{
"name": "Dev env",
"workspace": {
"name": "Development",
"color": "Indigo"
}
}]
}
"""
let config = try decode(json, colorDefaults: defaults)
XCTAssertEqual(config.commands[0].workspace?.color, "#283593")
}

func testDecodeWorkspaceCommandRejectsUnknownNamedColor() {
let suiteName = "cmux-config-unknown-color-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer { defaults.removePersistentDomain(forName: suiteName) }

let json = """
{
"commands": [{
"name": "Dev env",
"workspace": {
"name": "Development",
"color": "Definitely Not A Palette Color"
}
}]
}
"""
XCTAssertThrowsError(try decode(json, colorDefaults: defaults))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@MainActor
func testConfigParseCacheInvalidatesWhenWorkspaceColorPaletteChanges() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(
"cmux-config-store-\(UUID().uuidString)",
isDirectory: true
)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }

let previousPalette = UserDefaults.standard.dictionary(forKey: WorkspaceTabColorSettings.paletteKey)
defer {
if let previousPalette {
UserDefaults.standard.set(previousPalette, forKey: WorkspaceTabColorSettings.paletteKey)
} else {
UserDefaults.standard.removeObject(forKey: WorkspaceTabColorSettings.paletteKey)
}
}

let configURL = root.appendingPathComponent("cmux.json")
let json = """
{
"commands": [{
"name": "Dev env",
"workspace": {
"name": "Development",
"color": "Codex Test"
}
}]
}
"""
try json.write(to: configURL, atomically: true, encoding: .utf8)

let store = CmuxConfigStore(globalConfigPath: configURL.path, startFileWatchers: false)
WorkspaceTabColorSettings.persistPaletteMap(["Codex Test": "#111111"])
store.loadAll()
XCTAssertEqual(store.loadedCommands.first?.workspace?.color, "#111111")

WorkspaceTabColorSettings.persistPaletteMap(["Codex Test": "#222222"])
store.loadAll()
XCTAssertEqual(store.loadedCommands.first?.workspace?.color, "#222222")
}
}