diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 0b100e14..7a798706 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ A5001208 /* UpdateTitlebarAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001218 /* UpdateTitlebarAccessory.swift */; }; A5001209 /* WindowToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001219 /* WindowToolbarController.swift */; }; A5001240 /* WindowDecorationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001241 /* WindowDecorationsController.swift */; }; - A5001600 /* SessionPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001601 /* SessionPersistence.swift */; }; + A5001610 /* SessionPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001611 /* SessionPersistence.swift */; }; A5001100 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5001101 /* Assets.xcassets */; }; A5001230 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A5001231 /* Sparkle */; }; B9000002A1B2C3D4E5F60719 /* cmux.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000001A1B2C3D4E5F60719 /* cmux.swift */; }; @@ -184,7 +184,7 @@ A5001222 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = ""; }; A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = ""; }; - A5001601 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = ""; }; + A5001611 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = ""; }; 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = ""; }; C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = ""; }; A5001101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -357,7 +357,7 @@ A5001219 /* WindowToolbarController.swift */, A5001241 /* WindowDecorationsController.swift */, A5001222 /* WindowAccessor.swift */, - A5001601 /* SessionPersistence.swift */, + A5001611 /* SessionPersistence.swift */, ); path = Sources; sourceTree = ""; @@ -590,7 +590,7 @@ A5001209 /* WindowToolbarController.swift in Sources */, A5001240 /* WindowDecorationsController.swift in Sources */, A500120C /* WindowAccessor.swift in Sources */, - A5001600 /* SessionPersistence.swift in Sources */, + A5001610 /* SessionPersistence.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/SessionPersistence.swift b/Sources/SessionPersistence.swift index f4459a68..d660d467 100644 --- a/Sources/SessionPersistence.swift +++ b/Sources/SessionPersistence.swift @@ -322,6 +322,7 @@ indirect enum SessionWorkspaceLayoutSnapshot: Codable, Sendable { struct SessionWorkspaceSnapshot: Codable, Sendable { var processTitle: String var customTitle: String? + var customColor: String? var isPinned: Bool var currentDirectory: String var focusedPanelId: UUID? diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index a9b8021c..1fda66ce 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -118,6 +118,7 @@ extension Workspace { return SessionWorkspaceSnapshot( processTitle: processTitle, customTitle: customTitle, + customColor: customColor, isPinned: isPinned, currentDirectory: currentDirectory, focusedPanelId: focusedPanelId, @@ -156,6 +157,7 @@ extension Workspace { applyProcessTitle(snapshot.processTitle) setCustomTitle(snapshot.customTitle) + setCustomColor(snapshot.customColor) isPinned = snapshot.isPinned statusEntries = Dictionary( @@ -1323,7 +1325,11 @@ final class Workspace: Identifiable, ObservableObject { } func setCustomColor(_ hex: String?) { - customColor = hex + if let hex { + customColor = WorkspaceTabColorSettings.normalizedHex(hex) + } else { + customColor = nil + } } func setCustomTitle(_ title: String?) { diff --git a/cmuxTests/SessionPersistenceTests.swift b/cmuxTests/SessionPersistenceTests.swift index 1fb01fa0..638e8794 100644 --- a/cmuxTests/SessionPersistenceTests.swift +++ b/cmuxTests/SessionPersistenceTests.swift @@ -25,6 +25,38 @@ final class SessionPersistenceTests: XCTestCase { XCTAssertEqual(loaded?.windows.first?.sidebar.selection, .tabs) } + func testSaveAndLoadRoundTripPreservesWorkspaceCustomColor() { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-session-tests-\(UUID().uuidString)", isDirectory: true) + try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let snapshotURL = tempDir.appendingPathComponent("session.json", isDirectory: false) + var snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion) + snapshot.windows[0].tabManager.workspaces[0].customColor = "#C0392B" + + XCTAssertTrue(SessionPersistenceStore.save(snapshot, fileURL: snapshotURL)) + + let loaded = SessionPersistenceStore.load(fileURL: snapshotURL) + XCTAssertEqual( + loaded?.windows.first?.tabManager.workspaces.first?.customColor, + "#C0392B" + ) + } + + func testWorkspaceCustomColorDecodeSupportsMissingLegacyField() throws { + var snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion) + snapshot.windows[0].tabManager.workspaces[0].customColor = nil + + let encoder = JSONEncoder() + let data = try encoder.encode(snapshot) + let json = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertFalse(json.contains("\"customColor\"")) + + let decoded = try JSONDecoder().decode(AppSessionSnapshot.self, from: data) + XCTAssertNil(decoded.windows.first?.tabManager.workspaces.first?.customColor) + } + func testLoadRejectsSchemaVersionMismatch() { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent("cmux-session-tests-\(UUID().uuidString)", isDirectory: true) @@ -442,6 +474,7 @@ final class SessionPersistenceTests: XCTestCase { let workspace = SessionWorkspaceSnapshot( processTitle: "Terminal", customTitle: "Restored", + customColor: nil, isPinned: true, currentDirectory: "/tmp", focusedPanelId: nil,