Persist workspace tab colors across session restore

This commit is contained in:
Lawrence Chen 2026-02-23 19:27:32 -08:00
parent ea33e3adbd
commit 78d1a43733
4 changed files with 45 additions and 5 deletions

View file

@ -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 = "<group>"; };
A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = "<group>"; };
A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = "<group>"; };
A5001601 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = "<group>"; };
A5001611 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = "<group>"; };
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = "<group>"; };
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = "<group>"; };
A5001101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -357,7 +357,7 @@
A5001219 /* WindowToolbarController.swift */,
A5001241 /* WindowDecorationsController.swift */,
A5001222 /* WindowAccessor.swift */,
A5001601 /* SessionPersistence.swift */,
A5001611 /* SessionPersistence.swift */,
);
path = Sources;
sourceTree = "<group>";
@ -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;
};

View file

@ -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?

View file

@ -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?) {

View file

@ -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,