diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index fa65d6dcf094..0be3ec518ff2 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -5,9 +5,9 @@ 17954 Sources/AppDelegate.swift 16427 Sources/ContentView.swift 14270 Sources/TerminalController.swift -13172 Sources/Workspace.swift +13349 Sources/Workspace.swift 12348 cmuxTests/AppDelegateShortcutRoutingTests.swift -12296 Sources/GhosttyTerminalView.swift +12324 Sources/GhosttyTerminalView.swift 11669 Sources/Panels/BrowserPanel.swift 9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift 8032 Sources/Panels/BrowserPanelView.swift @@ -24,8 +24,8 @@ 4483 Sources/cmuxApp.swift 4482 Sources/Panels/FilePreviewPanel.swift 4367 cmuxTests/BrowserPanelTests.swift -4283 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift 4121 Sources/BrowserWindowPortal.swift +3964 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift 3934 Sources/Feed/FeedPanelView.swift 3926 cmuxTests/TabManagerUnitTests.swift 3896 cmuxTests/WindowAndDragTests.swift @@ -45,7 +45,7 @@ 2229 Sources/TerminalWindowPortal.swift 2126 cmuxTests/CmuxConfigTests.swift 2085 cmuxTests/ShortcutAndCommandPaletteTests.swift -2078 Sources/SessionPersistence.swift +2081 Sources/SessionPersistence.swift 2011 Sources/KeyboardShortcutSettingsFileStore.swift 1944 Sources/RestorableAgentSession.swift 1900 cmuxTests/NotificationAndMenuBarTests.swift @@ -214,6 +214,7 @@ 549 Sources/Panels/BrowserAutomation.swift 547 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift 546 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift +546 Sources/App/WorkspaceRuntimeSettings.swift 544 cmuxUITests/DisplayResolutionRegressionUITests.swift 540 Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift 539 CLI/CMUXCLI+Themes.swift @@ -221,7 +222,6 @@ 538 Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridTests.swift 538 Packages/macOS/CmuxRemoteWorkspace/Sources/CmuxRemoteWorkspace/PTYBridge/RemotePTYBridgeSession.swift 536 cmuxTests/CmuxConfigContextMenuTests.swift -535 Sources/App/WorkspaceRuntimeSettings.swift 533 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift 530 Sources/BrowserPaneDropTargetView.swift 530 Sources/TerminalController+ControlSurfaceContext3.swift @@ -250,8 +250,8 @@ 506 Sources/App/MainWindowVisibilityController.swift 504 Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/UserDefaultsSettingsStoreTests.swift 504 cmuxTests/TerminalNotificationSocketActionTests.swift +503 Sources/RemoteTmuxSessionMirror.swift 503 Sources/Settings/ConfigSource.swift 502 Sources/CmuxEventPublishing.swift -502 Sources/RemoteTmuxSessionMirror.swift 500 Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift 500 Sources/KeyboardShortcutRecorder.swift diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift index e67f2dfb0368..6d05935f9a1e 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift @@ -27,6 +27,14 @@ public struct TerminalCatalogSection: SettingCatalogSection { userDefaultsKey: "terminal.autoResumeAgentSessions" ) + /// Whether new terminal split panes automatically receive subtle, distinct + /// background tints so adjacent panes stay visually distinguishable. On by default. + public let autoTintSplitPanes = DefaultsKey( + id: "terminal.autoTintSplitPanes", + defaultValue: true, + userDefaultsKey: "terminal.autoTintSplitPanes" + ) + public let agentHibernationEnabled = DefaultsKey( id: "terminal.agentHibernation.enabled", defaultValue: false, diff --git a/Sources/App/WorkspaceRuntimeSettings.swift b/Sources/App/WorkspaceRuntimeSettings.swift index e1de9c9a0d80..c0a807aae04c 100644 --- a/Sources/App/WorkspaceRuntimeSettings.swift +++ b/Sources/App/WorkspaceRuntimeSettings.swift @@ -1,4 +1,5 @@ import Darwin +import CmuxSettings import Foundation enum WorkspaceTitlebarSettings { @@ -203,6 +204,16 @@ enum TerminalCopyOnSelectSettings { } } +struct TerminalSplitPaneTintSettings { + private static let autoTintSplitPanes = TerminalCatalogSection().autoTintSplitPanes + static var autoTintSplitPanesKey: String { autoTintSplitPanes.userDefaultsKey } + static var defaultAutoTintSplitPanes: Bool { autoTintSplitPanes.defaultValue } + + func isEnabled(defaults: UserDefaults = .standard) -> Bool { + Self.autoTintSplitPanes.value(in: defaults) + } +} + enum TerminalManagedGhosttySettings { static func ghosttyConfigContents(defaults: UserDefaults = .standard, emitsCopyOnSelectFalse: Bool = true) -> String? { let lines = [ diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index 037bb90c18dd..a2b033fb39c4 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -185,6 +185,11 @@ enum TerminalSettingsFileMapping { defaultsKey: AgentSessionAutoResumeSettings.autoResumeAgentSessionsKey, invalidPath: "terminal.autoResumeAgentSessions" ), + .init( + jsonKey: "autoTintSplitPanes", + defaultsKey: TerminalSplitPaneTintSettings.autoTintSplitPanesKey, + invalidPath: "terminal.autoTintSplitPanes" + ), ] } @@ -369,6 +374,7 @@ extension CmuxSettingsFileStore { "terminal.scrollSpeed", "terminal.copyOnSelect", "terminal.autoResumeAgentSessions", + "terminal.autoTintSplitPanes", "terminal.showTextBoxOnNewTerminals", "terminal.focusTextBoxOnNewTerminals", "terminal.textBoxDefaultSubmitAction", diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index b46893ecb0d5..0c58c3ed26c7 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -308,6 +308,16 @@ extension TerminalSurface { } return hosted } + + var paneBackgroundOverrideColor: NSColor? { + get { hostedView.paneBackgroundOverrideColor } + set { hostedView.paneBackgroundOverrideColor = newValue } + } + + var autoAssignedSplitTintHex: String? { + get { hostedView.autoAssignedSplitTintHex } + set { hostedView.autoAssignedSplitTintHex = newValue } + } } // The engine's Metal layer reports vended drawables through this seam @@ -8036,6 +8046,24 @@ final class GhosttySurfaceScrollView: NSView { return window } + var paneBackgroundOverrideColor: NSColor? { + get { surfaceView.backgroundColor } + set { + surfaceView.backgroundColor = newValue + surfaceView.applySurfaceBackground() + surfaceView.applyWindowBackgroundIfActive() + } + } + + /// Hex of the split-pane tint cmux most recently assigned to this surface, + /// or nil when cmux never auto-tinted it. This is provenance for session + /// persistence only: `paneBackgroundOverrideColor` is a shared slot that + /// terminal OSC background changes (`GHOSTTY_ACTION_COLOR_CHANGE`) and + /// Ghostty config reloads (`GHOSTTY_ACTION_CONFIG_CHANGE`) also write, so a + /// live override is a cmux-owned tint worth persisting only while it still + /// equals this value. See `TerminalSplitPaneTintPlanner.persistableTintHex`. + var autoAssignedSplitTintHex: String? + func forwardKeyDownToSurface(_ event: NSEvent) { surfaceView.keyDown(with: event) } diff --git a/Sources/KeyboardShortcutSettingsFileStore+Template.swift b/Sources/KeyboardShortcutSettingsFileStore+Template.swift index 8a176038fb9e..7c7b00a7d151 100644 --- a/Sources/KeyboardShortcutSettingsFileStore+Template.swift +++ b/Sources/KeyboardShortcutSettingsFileStore+Template.swift @@ -93,6 +93,7 @@ extension CmuxSettingsFileStore { "scrollSpeed": TerminalScrollSpeedSettings.defaultMultiplier, "copyOnSelect": TerminalCopyOnSelectSettings.defaultCopyOnSelect, "autoResumeAgentSessions": AgentSessionAutoResumeSettings.defaultAutoResumeAgentSessions, + "autoTintSplitPanes": TerminalSplitPaneTintSettings.defaultAutoTintSplitPanes, "showTextBoxOnNewTerminals": TerminalTextBoxInputSettings.defaultShowOnNewTerminals, "focusTextBoxOnNewTerminals": TerminalTextBoxInputSettings.defaultFocusOnNewTerminals, "textBoxDefaultSubmitAction": TerminalTextBoxInputSettings.defaultSubmitActionID, diff --git a/Sources/RemoteTmuxSessionMirror.swift b/Sources/RemoteTmuxSessionMirror.swift index b37ab47aa446..af348624e0b2 100644 --- a/Sources/RemoteTmuxSessionMirror.swift +++ b/Sources/RemoteTmuxSessionMirror.swift @@ -330,6 +330,7 @@ final class RemoteTmuxSessionMirror { panelId: panelId, connection: connection, layout: window.layout, + paneTintDefaults: workspace.terminalSplitPaneTintDefaults, makePanel: { [weak workspace, weak connection] tmuxPaneId in workspace?.makeRemoteTmuxPanePanel(onInput: { data in Task { @MainActor in connection?.sendKeys(paneId: tmuxPaneId, data: data) } diff --git a/Sources/RemoteTmuxWindowMirror.swift b/Sources/RemoteTmuxWindowMirror.swift index 3475f879f1c8..e8c7d9079e28 100644 --- a/Sources/RemoteTmuxWindowMirror.swift +++ b/Sources/RemoteTmuxWindowMirror.swift @@ -1,5 +1,6 @@ import AppKit import Bonsplit +import CmuxFoundation import CmuxTerminal import Foundation import Observation @@ -25,6 +26,7 @@ final class RemoteTmuxWindowMirror { @ObservationIgnored private weak var connection: RemoteTmuxControlConnection? /// Creates a configured manual-I/O pane panel whose input goes to `tmuxPaneId`. @ObservationIgnored private let makePanel: (_ tmuxPaneId: Int) -> TerminalPanel? + @ObservationIgnored private let paneTintDefaults: UserDefaults /// The window's current pane layout — drives the SwiftUI split container. private(set) var layout: RemoteTmuxLayoutNode @@ -44,12 +46,14 @@ final class RemoteTmuxWindowMirror { panelId: UUID, connection: RemoteTmuxControlConnection, layout: RemoteTmuxLayoutNode, + paneTintDefaults: UserDefaults = .standard, makePanel: @escaping (_ tmuxPaneId: Int) -> TerminalPanel? ) { self.windowId = windowId self.panelId = panelId self.connection = connection self.makePanel = makePanel + self.paneTintDefaults = paneTintDefaults self.layout = layout reconcile(layout: layout) } @@ -95,6 +99,34 @@ final class RemoteTmuxWindowMirror { if activePaneId == paneId { activePaneId = nil } } if layout != newLayout { layout = newLayout } + applyAutomaticPaneTints() + } + + private func applyAutomaticPaneTints() { + guard TerminalSplitPaneTintSettings().isEnabled(defaults: paneTintDefaults) else { return } + let baseColor = GhosttyApp.shared.defaultBackgroundColor + var usedHexes = Set( + panelsByPaneId.values.compactMap { panel in + panel.surface.paneBackgroundOverrideColor?.hexString() + } + ) + var selectedHexes = Set() + for paneId in paneIDsInOrder { + guard let panel = panelsByPaneId[paneId], + panel.surface.paneBackgroundOverrideColor == nil, + let color = TerminalSplitPaneTintPlanner.nextColor( + baseColor: baseColor, + usedHexes: usedHexes, + excludedHexes: selectedHexes + ) else { + continue + } + panel.surface.paneBackgroundOverrideColor = color + let hex = color.hexString() + panel.surface.autoAssignedSplitTintHex = hex + usedHexes.insert(hex) + selectedHexes.insert(hex) + } } /// Routes a tmux `%output` to the surface for `paneId` (no-op if unknown). diff --git a/Sources/SessionPersistence.swift b/Sources/SessionPersistence.swift index 4557c220d663..644a27b44a1a 100644 --- a/Sources/SessionPersistence.swift +++ b/Sources/SessionPersistence.swift @@ -1395,6 +1395,7 @@ struct SessionTerminalPanelSnapshot: Codable, Sendable { var textBoxDraft: SessionTextBoxInputDraftSnapshot? var isRemoteTerminal: Bool? var remotePTYSessionID: String? + var backgroundColorHex: String? /// Whether the agent process was actively running when this snapshot was captured. /// Nil means unknown (legacy snapshots); treated as true for backwards compatibility. var wasAgentRunning: Bool? @@ -1409,6 +1410,7 @@ struct SessionTerminalPanelSnapshot: Codable, Sendable { textBoxDraft: SessionTextBoxInputDraftSnapshot? = nil, isRemoteTerminal: Bool? = nil, remotePTYSessionID: String? = nil, + backgroundColorHex: String? = nil, wasAgentRunning: Bool? = nil ) { self.workingDirectory = workingDirectory @@ -1420,6 +1422,7 @@ struct SessionTerminalPanelSnapshot: Codable, Sendable { self.textBoxDraft = textBoxDraft self.isRemoteTerminal = isRemoteTerminal self.remotePTYSessionID = remotePTYSessionID + self.backgroundColorHex = backgroundColorHex self.wasAgentRunning = wasAgentRunning } } diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 4a2dbb9781e1..acf2f6821fbc 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -48,6 +48,112 @@ private struct SessionPaneRestoreEntry { let snapshot: SessionPaneLayoutSnapshot } +struct TerminalSplitPaneTintAssignment { + var source: NSColor? + var newPane: NSColor? +} + +enum TerminalSplitPaneTintPlanner { + private static let paletteHexes = [ + "#6EA8FE", + "#F2CC60", + "#7BD88F", + "#FF9F7A", + "#C89CFF", + "#6DD6D6", + "#FF8FB3", + "#B7D66A", + ] + private static let opacityOnDarkBackground: CGFloat = 0.18 + private static let opacityOnLightBackground: CGFloat = 0.14 + + static func nextColor( + baseColor: NSColor, + usedHexes: Set, + excludedHexes: Set = [] + ) -> NSColor? { + let palette = paletteHexes.compactMap { hex -> NSColor? in + guard let accentColor = NSColor(hex: hex) else { return nil } + return tintColor(baseColor: baseColor, accentColor: accentColor) + } + guard !palette.isEmpty else { return nil } + if let unusedColor = palette.first(where: { + let hex = $0.hexString() + return !usedHexes.contains(hex) && !excludedHexes.contains(hex) + }) { + return unusedColor + } + let wrappedPalette = palette.filter { !excludedHexes.contains($0.hexString()) } + guard !wrappedPalette.isEmpty else { + return palette[usedHexes.count % palette.count] + } + return wrappedPalette[usedHexes.count % wrappedPalette.count] + } + + static func assignmentForTerminalSplit( + baseColor: NSColor, + usedHexes: Set, + sourceNeedsTint: Bool, + newPaneNeedsTint: Bool + ) -> TerminalSplitPaneTintAssignment { + var usedHexes = usedHexes + var selectedHexes = Set() + var sourceColor: NSColor? + if sourceNeedsTint, let color = nextColor(baseColor: baseColor, usedHexes: usedHexes) { + sourceColor = color + let hex = color.hexString() + usedHexes.insert(hex) + selectedHexes.insert(hex) + } + let newPaneColor = newPaneNeedsTint + ? nextColor(baseColor: baseColor, usedHexes: usedHexes, excludedHexes: selectedHexes) + : nil + return TerminalSplitPaneTintAssignment(source: sourceColor, newPane: newPaneColor) + } + + /// The hex to persist for a pane's auto-assigned split tint, or nil when the + /// live background override is not a cmux-owned tint and must not be made + /// sticky across restarts. + /// + /// `paneBackgroundOverrideColor` is a shared surface slot: terminal OSC + /// background changes (`GHOSTTY_ACTION_COLOR_CHANGE`) and Ghostty config + /// reloads (`GHOSTTY_ACTION_CONFIG_CHANGE`) also write it. Persisting it + /// unconditionally would resurrect a transient terminal-controlled + /// background as a cmux pane tint on the next session restore. Only a live + /// override that still equals the color cmux assigned (`autoAssignedHex`) is + /// a cmux-owned tint; a terminal overwrite or a config-reload clear makes the + /// two diverge, so the value drops out of persistence. + static func persistableTintHex(liveOverrideHex: String?, autoAssignedHex: String?) -> String? { + guard let liveOverrideHex, liveOverrideHex == autoAssignedHex else { return nil } + return liveOverrideHex + } + + private static func tintColor(baseColor: NSColor, accentColor: NSColor) -> NSColor? { + guard let base = baseColor.usingColorSpace(.sRGB), + let accent = accentColor.usingColorSpace(.sRGB) else { + return nil + } + let opacity = base.isLightColor ? opacityOnLightBackground : opacityOnDarkBackground + var baseRed: CGFloat = 0 + var baseGreen: CGFloat = 0 + var baseBlue: CGFloat = 0 + var baseAlpha: CGFloat = 0 + var accentRed: CGFloat = 0 + var accentGreen: CGFloat = 0 + var accentBlue: CGFloat = 0 + var accentAlpha: CGFloat = 0 + base.getRed(&baseRed, green: &baseGreen, blue: &baseBlue, alpha: &baseAlpha) + accent.getRed(&accentRed, green: &accentGreen, blue: &accentBlue, alpha: &accentAlpha) + let tintOpacity = opacity * accentAlpha + return NSColor( + srgbRed: baseRed * (1 - tintOpacity) + accentRed * tintOpacity, + green: baseGreen * (1 - tintOpacity) + accentGreen * tintOpacity, + blue: baseBlue * (1 - tintOpacity) + accentBlue * tintOpacity, + alpha: baseAlpha + ) + } +} + extension Workspace { func sessionSnapshot( includeScrollback: Bool, @@ -525,6 +631,10 @@ extension Workspace { textBoxDraft: terminalPanel.sessionTextBoxDraftSnapshot(), isRemoteTerminal: activeRemoteTerminalSurfaceIds.contains(panelId), remotePTYSessionID: remotePTYSessionIDForSnapshot(panelId: panelId), + backgroundColorHex: TerminalSplitPaneTintPlanner.persistableTintHex( + liveOverrideHex: terminalPanel.surface.paneBackgroundOverrideColor?.hexString(), + autoAssignedHex: terminalPanel.surface.autoAssignedSplitTintHex + ), wasAgentRunning: agentWasRunning ) browserSnapshot = nil @@ -1397,6 +1507,18 @@ extension Workspace { ) else { return nil } + // Only cmux-owned split tints are persisted (see + // `TerminalSplitPaneTintPlanner.persistableTintHex`), and they are + // resurrected only while the feature is enabled: disabling + // `terminal.autoTintSplitPanes` must not bring back an auto-assigned + // tint across a restart. Re-record provenance so a later snapshot + // recognises the restored color as a cmux tint. + if TerminalSplitPaneTintSettings().isEnabled(defaults: terminalSplitPaneTintDefaults), + let backgroundColorHex = snapshot.terminal?.backgroundColorHex, + let backgroundColor = NSColor(hex: backgroundColorHex) { + terminalPanel.surface.paneBackgroundOverrideColor = backgroundColor + terminalPanel.surface.autoAssignedSplitTintHex = backgroundColorHex + } // Re-bind the resumed agent session from cmux's own authority, keyed // on the surface that was actually created. `terminalPanel.id` equals // `snapshot.id` on the normal path, but on a surface-id collision @@ -2237,7 +2359,6 @@ final class Workspace: Identifiable, ObservableObject { static let terminalScrollBarHiddenDidChangeNotification = Notification.Name( "cmux.workspaceTerminalScrollBarHiddenDidChange" ) - let id: UUID /// When this workspace instance came into existence in this app session /// (creation, or restore at launch). The mobile list's last-activity @@ -2293,7 +2414,7 @@ final class Workspace: Identifiable, ObservableObject { private var extensionSidebarProjectRootRefreshID: UInt64 = 0 @Published private(set) var surfaceTabBarDirectory: String? private(set) var preferredBrowserProfileID: UUID? - let closeTabWarningDefaults, agentSessionAutoResumeDefaults: UserDefaults + let closeTabWarningDefaults, agentSessionAutoResumeDefaults, terminalSplitPaneTintDefaults: UserDefaults /// Ordinal for CMUX_PORT range assignment (monotonically increasing per app session) var portOrdinal: Int = 0 @@ -3130,6 +3251,7 @@ final class Workspace: Identifiable, ObservableObject { allowTextBoxFocusDefault: Bool = true, closeTabWarningDefaults: UserDefaults = .standard, agentSessionAutoResumeDefaults: UserDefaults = .standard, + terminalSplitPaneTintDefaults: UserDefaults = .standard, initialDetachedSurface: DetachedSurfaceTransfer? = nil, sessionRestorePolicy: WorkspaceSessionRestorePolicyService? = nil ) { @@ -3137,6 +3259,7 @@ final class Workspace: Identifiable, ObservableObject { self.sessionRestorePolicy = sessionRestorePolicy ?? Self.makeSessionRestorePolicyService() self.closeTabWarningDefaults = closeTabWarningDefaults self.agentSessionAutoResumeDefaults = agentSessionAutoResumeDefaults + self.terminalSplitPaneTintDefaults = terminalSplitPaneTintDefaults let sanitizedWorkspaceEnvironment = Self.sanitizedWorkspaceEnvironment(workspaceEnvironment) self.workspaceEnvironment = sanitizedWorkspaceEnvironment self.portOrdinal = portOrdinal @@ -7273,6 +7396,59 @@ final class Workspace: Identifiable, ObservableObject { return nil } + static func automaticSplitPaneTintColor( + baseColor: NSColor, + usedHexes: Set + ) -> NSColor? { + TerminalSplitPaneTintPlanner.nextColor(baseColor: baseColor, usedHexes: usedHexes) + } + + /// Assigns distinct background tints to a terminal split's two panels. + /// + /// Each assignment also records its hex on `surface.autoAssignedSplitTintHex` + /// so session persistence can tell a cmux-owned tint apart from a + /// terminal-controlled background sharing the same + /// `paneBackgroundOverrideColor` slot (see + /// `TerminalSplitPaneTintPlanner.persistableTintHex`). + /// + /// Known v1 limitation (PR #6981): tints live in `paneBackgroundOverrideColor`, + /// the surface's OSC/config background slot, so a Ghostty config/theme reload + /// (`GHOSTTY_ACTION_CONFIG_CHANGE`) clears them until the next split re-assigns. + private func applyAutomaticSplitPaneTints(sourcePanelId: UUID, newPanel: TerminalPanel) { + guard TerminalSplitPaneTintSettings().isEnabled(defaults: terminalSplitPaneTintDefaults) else { return } + let sourcePanel = terminalPanel(for: sourcePanelId) + // Always plan tints against the real terminal background, never a pane's + // existing override. `TerminalSplitPaneTintPlanner` blends its palette + // *into* `baseColor`, so seeding it with an already-tinted source color + // would generate off-palette tint-of-a-tint colors and make repeated + // splits from a tinted pane drift/compound instead of cycling the distinct + // palette. Existing source/manual overrides stay represented in `usedHexes` + // (dedup) and `sourceNeedsTint` below, matching the remote-tmux mirror path + // (`RemoteTmuxWindowMirror.applyAutomaticPaneTints`). + let baseColor = WorkspaceContentView.resolveGhosttyAppearanceConfig(reason: "splitPaneTint").backgroundColor + let usedHexes = Set( + panels.values.compactMap { panel -> String? in + guard let terminalPanel = panel as? TerminalPanel else { return nil } + return terminalPanel.surface.paneBackgroundOverrideColor?.hexString() + } + ) + let assignment = TerminalSplitPaneTintPlanner.assignmentForTerminalSplit( + baseColor: baseColor, + usedHexes: usedHexes, + sourceNeedsTint: sourcePanel.map { $0.surface.paneBackgroundOverrideColor == nil } ?? false, + newPaneNeedsTint: newPanel.surface.paneBackgroundOverrideColor == nil + ) + + if let sourceColor = assignment.source, let sourcePanel { + sourcePanel.surface.paneBackgroundOverrideColor = sourceColor + sourcePanel.surface.autoAssignedSplitTintHex = sourceColor.hexString() + } + if let newPaneColor = assignment.newPane { + newPanel.surface.paneBackgroundOverrideColor = newPaneColor + newPanel.surface.autoAssignedSplitTintHex = newPaneColor.hexString() + } + } + /// Create a new split with a terminal panel @discardableResult func newTerminalSplit( @@ -7494,6 +7670,7 @@ final class Workspace: Identifiable, ObservableObject { return nil } applyInitialSplitDividerPosition(initialDividerPosition, sourcePaneId: paneId, newPaneId: newPaneId) + applyAutomaticSplitPaneTints(sourcePanelId: panelId, newPanel: newPanel) publishCmuxSplitCreated(newPaneId, sourcePaneId: paneId, orientation: orientation, surfaceId: newPanel.id, kind: "terminal", origin: "terminal_split", focused: focus) #if DEBUG diff --git a/cmuxTests/RemoteTmuxMirrorSplitRoutingTests.swift b/cmuxTests/RemoteTmuxMirrorSplitRoutingTests.swift index aad099518c9e..6e70c5591da8 100644 --- a/cmuxTests/RemoteTmuxMirrorSplitRoutingTests.swift +++ b/cmuxTests/RemoteTmuxMirrorSplitRoutingTests.swift @@ -1,4 +1,5 @@ import AppKit +import CmuxFoundation import Testing #if canImport(cmux_DEV) @@ -88,3 +89,186 @@ import Testing } } } + +@MainActor +@Suite(.serialized) struct WorkspaceSplitPaneTintTests { + @Test func splitTintPlannerAssignsDistinctSourceAndNewPaneColors() throws { + let assignment = TerminalSplitPaneTintPlanner.assignmentForTerminalSplit( + baseColor: try #require(NSColor(hex: "#101010")), + usedHexes: [], + sourceNeedsTint: true, + newPaneNeedsTint: true + ) + let sourceTint = try #require(assignment.source?.hexString()) + let newPaneTint = try #require(assignment.newPane?.hexString()) + + #expect(sourceTint != newPaneTint) + } + + @Test func splitTintPlannerPreservesManualSourceTintAndTintsNewPane() throws { + let manualTint = "#123456" + let assignment = TerminalSplitPaneTintPlanner.assignmentForTerminalSplit( + baseColor: try #require(NSColor(hex: "#101010")), + usedHexes: [manualTint], + sourceNeedsTint: false, + newPaneNeedsTint: true + ) + let newPaneTint = try #require(assignment.newPane?.hexString()) + + #expect(assignment.source == nil) + #expect(newPaneTint != manualTint) + } + + @Test func splitTintPlannerReusesFirstPaletteColorAfterExhaustion() throws { + let baseColor = try #require(NSColor(hex: "#101010")) + var usedHexes = Set() + var tints: [String] = [] + for _ in 0..<10 { + let tint = try #require(TerminalSplitPaneTintPlanner.nextColor(baseColor: baseColor, usedHexes: usedHexes)) + let hex = tint.hexString() + tints.append(hex) + usedHexes.insert(hex) + } + + // The first eight requests exhaust the eight-color palette with distinct tints. + #expect(Set(tints.prefix(8)).count == 8) + // After that every palette color is in `usedHexes`, so the Set saturates at + // eight entries and `nextColor` deterministically falls back to the first + // palette color for every subsequent request: a saturated Set cannot advance + // the `usedHexes.count % palette.count` wrap past index 0, which mirrors the + // real call site where `usedHexes` is the set of distinct live pane colors. + #expect(tints[8] == tints[0]) + #expect(tints[9] == tints[0]) + } + + @Test func splitTintPlannerKeepsCurrentSplitDistinctAfterPaletteWrap() throws { + let baseColor = try #require(NSColor(hex: "#101010")) + var usedHexes = Set() + for _ in 0..<8 { + let tint = try #require(TerminalSplitPaneTintPlanner.nextColor(baseColor: baseColor, usedHexes: usedHexes)) + usedHexes.insert(tint.hexString()) + } + + let assignment = TerminalSplitPaneTintPlanner.assignmentForTerminalSplit( + baseColor: baseColor, + usedHexes: usedHexes, + sourceNeedsTint: true, + newPaneNeedsTint: true + ) + let sourceTint = try #require(assignment.source?.hexString()) + let newPaneTint = try #require(assignment.newPane?.hexString()) + + #expect(sourceTint != newPaneTint) + } + + @Test func terminalSplitTintingCanBeDisabled() throws { + try withIsolatedDefaults { defaults in + defaults.set(false, forKey: TerminalSplitPaneTintSettings.autoTintSplitPanesKey) + #expect(!TerminalSplitPaneTintSettings().isEnabled(defaults: defaults)) + } + } + + @Test func terminalSnapshotPersistsPaneBackgroundHex() throws { + let snapshot = SessionTerminalPanelSnapshot(backgroundColorHex: "#123456") + let data = try JSONEncoder().encode(snapshot) + let restored = try JSONDecoder().decode(SessionTerminalPanelSnapshot.self, from: data) + + #expect(restored.backgroundColorHex == "#123456") + } + + @Test func persistableTintHexKeepsLiveCmuxAssignedTint() { + // A live override that still equals the color cmux assigned is a + // cmux-owned split tint and must survive session persistence. + #expect( + TerminalSplitPaneTintPlanner.persistableTintHex( + liveOverrideHex: "#6EA8FE", + autoAssignedHex: "#6EA8FE" + ) == "#6EA8FE" + ) + } + + @Test func persistableTintHexDropsTerminalControlledOverride() { + // No cmux provenance: the override is a terminal OSC / theme background, + // not a split tint, so it must not be persisted as a sticky pane tint. + #expect( + TerminalSplitPaneTintPlanner.persistableTintHex( + liveOverrideHex: "#001122", + autoAssignedHex: nil + ) == nil + ) + } + + @Test func persistableTintHexDropsTintOverwrittenByTerminal() { + // cmux assigned #6EA8FE, but the terminal later OSC-set #001122; the live + // override no longer matches provenance, so it is terminal-controlled now. + #expect( + TerminalSplitPaneTintPlanner.persistableTintHex( + liveOverrideHex: "#001122", + autoAssignedHex: "#6EA8FE" + ) == nil + ) + } + + @Test func persistableTintHexDropsClearedTint() { + // A Ghostty config reload cleared the override; there is nothing to + // persist even though provenance still remembers the old assignment. + #expect( + TerminalSplitPaneTintPlanner.persistableTintHex( + liveOverrideHex: nil, + autoAssignedHex: "#6EA8FE" + ) == nil + ) + } + + @Test func splitTintPlanningUsesTrueBackgroundNotAlreadyTintedSource() throws { + // Regression for PR #6981 autoreview: the local split path must plan tints + // against the real terminal background, never a source pane's already-applied + // tint. `nextColor` blends its palette *into* `baseColor`, so the base choice + // changes every generated hex; feeding an already-tinted color drifts into + // tint-of-a-tint colors instead of cycling the intended true-base palette. + let trueBase = try #require(NSColor(hex: "#101010")) + + // A first split tints the source with the first true-base palette color. + let sourceTint = try #require( + TerminalSplitPaneTintPlanner.nextColor(baseColor: trueBase, usedHexes: []) + ) + let sourceHex = sourceTint.hexString() + + // Correct (post-fix): the next split is planned against the TRUE base with the + // source tint only recorded in `usedHexes`, yielding a distinct, on-palette color. + let againstTrueBase = try #require( + TerminalSplitPaneTintPlanner.nextColor(baseColor: trueBase, usedHexes: [sourceHex]) + ) + // Wrong (pre-fix): planning against the already-tinted source color compounds + // the tint into an off-palette color. + let againstTintedSource = try #require( + TerminalSplitPaneTintPlanner.nextColor(baseColor: sourceTint, usedHexes: [sourceHex]) + ) + + #expect(againstTrueBase.hexString() != sourceHex) + #expect(againstTintedSource.hexString() != sourceHex) + #expect(againstTrueBase.hexString() != againstTintedSource.hexString()) + + // The true-base result stays within the palette generated against the real + // background; the compounded tint-of-a-tint color does not. + var paletteUsedHexes = Set() + var truePalette = Set() + for _ in 0..<8 { + let color = try #require( + TerminalSplitPaneTintPlanner.nextColor(baseColor: trueBase, usedHexes: paletteUsedHexes) + ) + truePalette.insert(color.hexString()) + paletteUsedHexes.insert(color.hexString()) + } + #expect(truePalette.contains(againstTrueBase.hexString())) + #expect(!truePalette.contains(againstTintedSource.hexString())) + } + + private func withIsolatedDefaults(_ body: (UserDefaults) throws -> Void) throws { + let suiteName = "cmux.split-pane-tint.\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defaults.removePersistentDomain(forName: suiteName) + defer { defaults.removePersistentDomain(forName: suiteName) } + try body(defaults) + } +} diff --git a/cmuxTests/TerminalScrollSpeedSettingsFileStoreTests.swift b/cmuxTests/TerminalScrollSpeedSettingsFileStoreTests.swift index 1f6bd96430a4..57ea6f94a1b5 100644 --- a/cmuxTests/TerminalScrollSpeedSettingsFileStoreTests.swift +++ b/cmuxTests/TerminalScrollSpeedSettingsFileStoreTests.swift @@ -42,6 +42,14 @@ struct TerminalScrollSpeedSettingsFileStoreTests { } } + @Test + func settingsFileStoreAppliesAutoTintSplitPanesSetting() throws { + try loadAutoTintSplitPanesSetting(false) { defaults in + #expect(defaults.object(forKey: TerminalSplitPaneTintSettings.autoTintSplitPanesKey) as? Bool == false) + #expect(!TerminalSplitPaneTintSettings().isEnabled(defaults: defaults)) + } + } + private func loadScrollSpeedSetting(_ value: Double, verify: (UserDefaults) throws -> Void) throws { let defaults = UserDefaults.standard try preservingDefaults(keys: [ @@ -76,6 +84,40 @@ struct TerminalScrollSpeedSettingsFileStoreTests { } } + private func loadAutoTintSplitPanesSetting(_ value: Bool, verify: (UserDefaults) throws -> Void) throws { + let defaults = UserDefaults.standard + try preservingDefaults(keys: [ + TerminalSplitPaneTintSettings.autoTintSplitPanesKey, + settingsFileBackupsDefaultsKey, + importedManagedDefaultsKey, + ]) { + defaults.removeObject(forKey: TerminalSplitPaneTintSettings.autoTintSplitPanesKey) + defaults.removeObject(forKey: settingsFileBackupsDefaultsKey) + defaults.removeObject(forKey: importedManagedDefaultsKey) + + let directoryURL = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: directoryURL) } + + let settingsFileURL = directoryURL.appendingPathComponent("cmux.json", isDirectory: false) + try """ + { + "terminal": { + "autoTintSplitPanes": \(value) + } + } + """.write(to: settingsFileURL, atomically: true, encoding: .utf8) + + _ = KeyboardShortcutSettingsFileStore( + primaryPath: settingsFileURL.path, + fallbackPath: nil, + additionalFallbackPaths: [], + startWatching: false + ) + + try verify(defaults) + } + } + private func preservingDefaults(keys: [String], _ body: () throws -> Void) throws { let defaults = UserDefaults.standard let saved = keys.map { ($0, defaults.object(forKey: $0)) } diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index cea4b5a67860..2fdd373d8970 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -77,6 +77,7 @@ function buildSettingsFileExample(t: ConfigurationTranslation) { // "showScrollBar": false, // "copyOnSelect": true, // "autoResumeAgentSessions": true, + // "autoTintSplitPanes": true, // "showTextBoxOnNewTerminals": false, // "focusTextBoxOnNewTerminals": false, // "agentHibernation": { diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 145b8c003027..f1e69d542c9b 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -441,6 +441,12 @@ "default": true, "description": "Automatically run agent resume commands for restored terminal sessions when cmux reopens after quit. Set false to restore panes while keeping Claude Code, Codex, OpenCode, and other saved agent sessions idle until you resume them manually." }, + "autoTintSplitPanes": { + "type": "boolean", + "default": true, + "descriptionKey": "schemaDescriptions.terminal.autoTintSplitPanes", + "description": "Automatically give new terminal split panes subtle distinct background tints." + }, "showTextBoxOnNewTerminals": { "type": "boolean", "default": false, diff --git a/web/messages/ar.json b/web/messages/ar.json index 49fd5ba4e416..6e3b2d1705c8 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "عند true، ينسخ النص المحدد في الطرفية إلى حافظة النظام عند تثبيت التحديد. عند false، لا يصدر cmux تجاوز Ghostty copy-on-select؛ وتتحكم إعدادات Ghostty وقيمه الافتراضية في سلوك حافظة التحديد.", "scrollSpeed": "مُضاعِف يُطبَّق على مقدار التمرير بعجلة الفأرة ولوحة اللمس في الطرفية. القيم الأعلى تُمرِّر أسرع، والقيم الأدنى تُمرِّر أبطأ.", + "autoTintSplitPanes": "يمنح أجزاء التقسيم الطرفية الجديدة تلقائياً تدرجات خلفية خفيفة ومميزة.", "showTextBoxOnNewTerminals": "يعرض إدخال TextBox التجريبي افتراضيًا لمساحات العمل وتبويبات الطرفية وتقسيماتها المنشأة حديثًا.", "focusTextBoxOnNewTerminals": "يركز إدخال TextBox التجريبي افتراضيًا لمساحات العمل وتبويبات الطرفية وتقسيماتها المنشأة حديثًا. التركيز يعرض TextBox أيضًا." }, diff --git a/web/messages/bs.json b/web/messages/bs.json index 42854cfbceb0..8f2958aec167 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Kada je true, odabrani tekst terminala kopira se u sistemski međuspremnik kada se odabir potvrdi. Kada je false, cmux ne emitira Ghostty copy-on-select nadjačavanje; Ghostty konfiguracija i zadane vrijednosti kontrolišu ponašanje međuspremnika odabira.", "scrollSpeed": "Množitelj koji se primjenjuje na pomak točkića miša i dodirne plohe u terminalu. Veće vrijednosti pomiču brže, a manje sporije.", + "autoTintSplitPanes": "Automatski daje novim podjelama terminala suptilne, različite nijanse pozadine.", "showTextBoxOnNewTerminals": "Podrazumijevano prikazuje beta TextBox unos za novokreirane radne prostore, terminalske tabove i podjele.", "focusTextBoxOnNewTerminals": "Podrazumijevano fokusira beta TextBox unos za novokreirane radne prostore, terminalske tabove i podjele. Fokusiranje također prikazuje TextBox." }, diff --git a/web/messages/da.json b/web/messages/da.json index 7099fcecd776..1282db4f1955 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Når værdien er true, kopieres markeret terminaltekst til systemets udklipsholder, når markeringen bekræftes. Når værdien er false, udsender cmux ikke en Ghostty copy-on-select-tilsidesættelse; Ghostty-konfiguration og standarder styrer markeringsudklipsholderens adfærd.", "scrollSpeed": "Multiplikator anvendt på rulleværdier fra musehjul og pegefelt i terminalen. Højere værdier ruller hurtigere, lavere værdier langsommere.", + "autoTintSplitPanes": "Giver automatisk nye terminalsplits diskrete, forskellige baggrundstoner.", "showTextBoxOnNewTerminals": "Viser beta-TextBox-input som standard for nyoprettede arbejdsområder, terminalfaner og terminalsplits.", "focusTextBoxOnNewTerminals": "Fokuserer beta-TextBox-input som standard for nyoprettede arbejdsområder, terminalfaner og terminalsplits. Fokus viser også TextBox." }, diff --git a/web/messages/de.json b/web/messages/de.json index 378ca2d07ec1..d7cf6573dd39 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Bei true wird ausgewählter Terminaltext in die Systemzwischenablage kopiert, sobald die Auswahl bestätigt wird. Bei false gibt cmux keine Ghostty-copy-on-select-Überschreibung aus; Ghostty-Konfiguration und Standardwerte steuern das Verhalten der Auswahlzwischenablage.", "scrollSpeed": "Multiplikator für die Scroll-Werte von Mausrad und Trackpad im Terminal. Höhere Werte scrollen schneller, niedrigere Werte langsamer.", + "autoTintSplitPanes": "Gibt neuen Terminal-Splits automatisch dezente, unterschiedliche Hintergrundtönungen.", "showTextBoxOnNewTerminals": "Zeigt die Beta-TextBox-Eingabe standardmaessig fuer neu erstellte Workspaces, Terminal-Tabs und Terminal-Splits an.", "focusTextBoxOnNewTerminals": "Fokussiert die Beta-TextBox-Eingabe standardmaessig fuer neu erstellte Workspaces, Terminal-Tabs und Terminal-Splits. Fokussieren zeigt die TextBox ebenfalls an." }, diff --git a/web/messages/en.json b/web/messages/en.json index b19ef75f8723..26b4c97a908a 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -1093,6 +1093,7 @@ "terminal": { "copyOnSelect": "When true, copy selected terminal text to the system clipboard when the selection is committed. When false, cmux does not emit a Ghostty copy-on-select override; Ghostty config and defaults control selection-clipboard behavior.", "scrollSpeed": "Multiplier applied to terminal scroll wheel and trackpad deltas. Higher values scroll faster; lower values scroll slower.", + "autoTintSplitPanes": "Automatically give new terminal split panes subtle distinct background tints.", "showTextBoxOnNewTerminals": "Show the beta TextBox input by default for newly created workspaces, terminal tabs, and terminal splits.", "focusTextBoxOnNewTerminals": "Focus the beta TextBox input by default for newly created workspaces, terminal tabs, and terminal splits. Focusing also shows the TextBox.", "textBoxDefaultSubmitAction": "Default TextBox submit action ID for new terminal sessions. Use text-entry for plain input or one of the configured action IDs.", diff --git a/web/messages/es.json b/web/messages/es.json index 5dbcfba2e4c2..b1adad8cfb7e 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Cuando es true, copia el texto seleccionado del terminal al portapapeles del sistema cuando se confirma la selección. Cuando es false, cmux no emite una anulación de copy-on-select de Ghostty; la configuración y los valores predeterminados de Ghostty controlan el comportamiento del portapapeles de selección.", "scrollSpeed": "Multiplicador aplicado a los desplazamientos de la rueda de desplazamiento y del panel táctil en el terminal. Los valores más altos desplazan más rápido y los más bajos, más lento.", + "autoTintSplitPanes": "Asigna automáticamente a las nuevas divisiones de terminal tintes de fondo sutiles y distintos.", "showTextBoxOnNewTerminals": "Muestra la entrada TextBox beta de forma predeterminada en espacios de trabajo, pestañas y divisiones de terminal recién creados.", "focusTextBoxOnNewTerminals": "Enfoca la entrada TextBox beta de forma predeterminada en espacios de trabajo, pestañas y divisiones de terminal recién creados. Enfocar también muestra TextBox." }, diff --git a/web/messages/fr.json b/web/messages/fr.json index 8181f3a87644..cd1b268de40f 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Lorsque la valeur est true, le texte sélectionné dans le terminal est copié dans le presse-papiers système quand la sélection est validée. Lorsque la valeur est false, cmux n’émet pas de remplacement Ghostty copy-on-select; la configuration et les valeurs par défaut de Ghostty contrôlent le comportement du presse-papiers de sélection.", "scrollSpeed": "Multiplicateur appliqué aux déplacements de la molette de défilement et du pavé tactile dans le terminal. Les valeurs plus élevées font défiler plus vite, les plus basses plus lentement.", + "autoTintSplitPanes": "Attribue automatiquement aux nouveaux volets de terminal divisés de subtiles teintes d’arrière-plan distinctes.", "showTextBoxOnNewTerminals": "Affiche l'entrée TextBox bêta par défaut pour les espaces de travail, onglets et divisions de terminal nouvellement créés.", "focusTextBoxOnNewTerminals": "Place le focus sur l'entrée TextBox bêta par défaut pour les espaces de travail, onglets et divisions de terminal nouvellement créés. Le focus affiche aussi TextBox." }, diff --git a/web/messages/it.json b/web/messages/it.json index bc8230ce127f..ebb1511e2812 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Quando è true, il testo selezionato nel terminale viene copiato negli appunti di sistema quando la selezione viene confermata. Quando è false, cmux non emette un override Ghostty copy-on-select; configurazione e valori predefiniti di Ghostty controllano il comportamento degli appunti di selezione.", "scrollSpeed": "Moltiplicatore applicato agli spostamenti della rotellina di scorrimento e del trackpad nel terminale. Valori più alti scorrono più velocemente, valori più bassi più lentamente.", + "autoTintSplitPanes": "Assegna automaticamente alle nuove divisioni del terminale tinte di sfondo discrete e distinte.", "showTextBoxOnNewTerminals": "Mostra l'input TextBox beta per impostazione predefinita nei workspace, nelle schede e nelle divisioni del terminale appena creati.", "focusTextBoxOnNewTerminals": "Mette a fuoco l'input TextBox beta per impostazione predefinita nei workspace, nelle schede e nelle divisioni del terminale appena creati. Il focus mostra anche TextBox." }, diff --git a/web/messages/ja.json b/web/messages/ja.json index 261185592d09..7dfb662076a8 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -1063,6 +1063,7 @@ "terminal": { "copyOnSelect": "true の場合、選択が確定したときにターミナルで選択したテキストをシステムクリップボードへコピーします。false の場合、cmux は Ghostty の copy-on-select 上書きを出力せず、選択クリップボードの動作は Ghostty の設定と既定値に従います。", "scrollSpeed": "ターミナルのスクロールホイールとトラックパッドの移動量に適用される倍率です。値を大きくするとスクロールが速くなり、小さくすると遅くなります。", + "autoTintSplitPanes": "新しいターミナル分割ペインに、控えめで区別しやすい背景の色合いを自動的に付けます。", "showTextBoxOnNewTerminals": "新しいワークスペース、ターミナルタブ、ターミナル分割でベータ版の TextBox 入力を既定で表示します。", "focusTextBoxOnNewTerminals": "新しいワークスペース、ターミナルタブ、ターミナル分割でベータ版の TextBox 入力に既定でフォーカスします。フォーカスする場合は TextBox も表示されます。", "textBoxDefaultSubmitAction": "新しいターミナルセッションで使う既定の TextBox 送信アクション ID です。通常の入力には text-entry、または設定済みアクション ID のいずれかを使います。", diff --git a/web/messages/km.json b/web/messages/km.json index d9fb77e7d6b6..173105409a4b 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "នៅពេល true អត្ថបទ terminal ដែលបានជ្រើសនឹងត្រូវចម្លងទៅ clipboard របស់ប្រព័ន្ធ ពេលការជ្រើសត្រូវបានបញ្ជាក់។ នៅពេល false cmux មិនបញ្ចេញការកំណត់ជាន់លើ Ghostty copy-on-select ទេ; ការកំណត់ និងលំនាំដើមរបស់ Ghostty គ្រប់គ្រងអាកប្បកិរិយា clipboard សម្រាប់ការជ្រើស។", "scrollSpeed": "មេគុណដែលអនុវត្តចំពោះចលនារមូររបស់កង់រមូរ និងផ្ទាំងប៉ះក្នុង terminal។ តម្លៃកាន់តែខ្ពស់ រមូរកាន់តែលឿន ហើយតម្លៃកាន់តែទាប រមូរកាន់តែយឺត។", + "autoTintSplitPanes": "ផ្តល់ពណ៌ផ្ទៃខាងក្រោយស្រាលៗផ្សេងគ្នាដោយស្វ័យប្រវត្តិដល់ផ្ទាំងបំបែកស្ថានីយថ្មី។", "showTextBoxOnNewTerminals": "បង្ហាញការបញ្ចូល TextBox បេតាជាលំនាំដើមសម្រាប់ workspace ផ្ទាំងស្ថានីយ និងស្ប្លីតស្ថានីយដែលទើបបង្កើតថ្មី។", "focusTextBoxOnNewTerminals": "ផ្តោតការបញ្ចូល TextBox បេតាជាលំនាំដើមសម្រាប់ workspace ផ្ទាំងស្ថានីយ និងស្ប្លីតស្ថានីយដែលទើបបង្កើតថ្មី។ ការផ្តោតក៏បង្ហាញ TextBox ផងដែរ។" }, diff --git a/web/messages/ko.json b/web/messages/ko.json index ea3ed14f8744..4293f1fea728 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "true이면 선택이 확정될 때 선택한 터미널 텍스트를 시스템 클립보드에 복사합니다. false이면 cmux는 Ghostty copy-on-select 재정의를 내보내지 않으며, 선택 클립보드 동작은 Ghostty 설정과 기본값이 제어합니다.", "scrollSpeed": "터미널 스크롤 휠과 트랙패드 이동량에 적용되는 배율입니다. 값이 클수록 빠르게 스크롤되고, 작을수록 느리게 스크롤됩니다.", + "autoTintSplitPanes": "새 터미널 분할 창에 은은하고 서로 다른 배경 색조를 자동으로 지정합니다.", "showTextBoxOnNewTerminals": "새로 생성된 작업공간, 터미널 탭, 터미널 분할에서 베타 TextBox 입력을 기본으로 표시합니다.", "focusTextBoxOnNewTerminals": "새로 생성된 작업공간, 터미널 탭, 터미널 분할에서 베타 TextBox 입력에 기본으로 포커스합니다. 포커스하면 TextBox도 표시됩니다." }, diff --git a/web/messages/no.json b/web/messages/no.json index d9bd759aece1..b7356ccb7eae 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Når verdien er true, kopieres markert terminaltekst til systemutklippstavlen når markeringen bekreftes. Når verdien er false, sender ikke cmux ut en Ghostty copy-on-select-overstyring; Ghostty-konfigurasjon og standardverdier styrer oppførselen til markeringsutklippstavlen.", "scrollSpeed": "Multiplikator brukt på rulleverdier fra musehjul og styreflate i terminalen. Høyere verdier ruller raskere, lavere verdier saktere.", + "autoTintSplitPanes": "Gir automatisk nye terminalsplitt diskrete, ulike bakgrunnstoner.", "showTextBoxOnNewTerminals": "Viser beta-TextBox-inndata som standard for nyopprettede arbeidsområder, terminalfaner og terminalsplitt.", "focusTextBoxOnNewTerminals": "Fokuserer beta-TextBox-inndata som standard for nyopprettede arbeidsområder, terminalfaner og terminalsplitt. Fokus viser også TextBox." }, diff --git a/web/messages/pl.json b/web/messages/pl.json index cda279f25e64..72a538fb1ef8 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Gdy wartość to true, zaznaczony tekst terminala jest kopiowany do schowka systemowego po zatwierdzeniu zaznaczenia. Gdy wartość to false, cmux nie emituje nadpisania Ghostty copy-on-select; konfiguracja i wartości domyślne Ghostty sterują zachowaniem schowka zaznaczenia.", "scrollSpeed": "Mnożnik stosowany do przewijania kółkiem myszy i gładzikiem w terminalu. Wyższe wartości przewijają szybciej, niższe wolniej.", + "autoTintSplitPanes": "Automatycznie nadaje nowym podziałom terminala subtelne, różne odcienie tła.", "showTextBoxOnNewTerminals": "Domyślnie pokazuje wejście TextBox beta dla nowo utworzonych obszarów roboczych, kart terminala i podziałów.", "focusTextBoxOnNewTerminals": "Domyślnie ustawia fokus na wejściu TextBox beta dla nowo utworzonych obszarów roboczych, kart terminala i podziałów. Fokus pokazuje też TextBox." }, diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 91aa528928f1..3dda850fbe7e 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Quando true, copia o texto selecionado no terminal para a área de transferência do sistema quando a seleção é confirmada. Quando false, o cmux não emite uma substituição copy-on-select do Ghostty; a configuração e os padrões do Ghostty controlam o comportamento da área de transferência de seleção.", "scrollSpeed": "Multiplicador aplicado aos deslocamentos da roda de rolagem e do trackpad no terminal. Valores mais altos rolam mais rápido e os mais baixos, mais devagar.", + "autoTintSplitPanes": "Aplica automaticamente às novas divisões de terminal tons de fundo sutis e distintos.", "showTextBoxOnNewTerminals": "Mostra a entrada TextBox beta por padrão em áreas de trabalho, abas e divisões de terminal recém-criadas.", "focusTextBoxOnNewTerminals": "Foca a entrada TextBox beta por padrão em áreas de trabalho, abas e divisões de terminal recém-criadas. Focar também mostra o TextBox." }, diff --git a/web/messages/ru.json b/web/messages/ru.json index 5e87385515cd..932aaa426e94 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Если значение true, выбранный текст терминала копируется в системный буфер обмена после подтверждения выделения. Если значение false, cmux не выводит переопределение Ghostty copy-on-select; конфигурация и значения по умолчанию Ghostty управляют поведением буфера выделения.", "scrollSpeed": "Множитель, применяемый к величине прокрутки колесом мыши и трекпадом в терминале. Чем выше значение, тем быстрее прокрутка, чем ниже — тем медленнее.", + "autoTintSplitPanes": "Автоматически назначает новым разделённым панелям терминала мягкие разные оттенки фона.", "showTextBoxOnNewTerminals": "По умолчанию показывает бета-ввод TextBox для новых рабочих пространств, вкладок терминала и разделений.", "focusTextBoxOnNewTerminals": "По умолчанию фокусирует бета-ввод TextBox для новых рабочих пространств, вкладок терминала и разделений. Фокус также показывает TextBox." }, diff --git a/web/messages/th.json b/web/messages/th.json index 3e365973e924..3750462abab3 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "เมื่อเป็น true ข้อความเทอร์มินัลที่เลือกจะถูกคัดลอกไปยังคลิปบอร์ดของระบบเมื่อยืนยันการเลือก เมื่อเป็น false cmux จะไม่ส่งค่าแทนที่ Ghostty copy-on-select; การกำหนดค่าและค่าเริ่มต้นของ Ghostty จะควบคุมพฤติกรรมของคลิปบอร์ดการเลือก", "scrollSpeed": "ตัวคูณที่ใช้กับระยะการเลื่อนของล้อเลื่อนเมาส์และแทร็กแพดในเทอร์มินัล ค่ายิ่งสูงยิ่งเลื่อนเร็ว ค่ายิ่งต่ำยิ่งเลื่อนช้า", + "autoTintSplitPanes": "กำหนดเฉดพื้นหลังที่แตกต่างและละเอียดอ่อนให้กับบานหน้าต่างเทอร์มินัลที่แบ่งใหม่โดยอัตโนมัติ", "showTextBoxOnNewTerminals": "แสดงอินพุต TextBox รุ่นเบต้าเป็นค่าเริ่มต้นสำหรับเวิร์กสเปซ แท็บเทอร์มินัล และการแบ่งเทอร์มินัลที่สร้างใหม่", "focusTextBoxOnNewTerminals": "โฟกัสอินพุต TextBox รุ่นเบต้าเป็นค่าเริ่มต้นสำหรับเวิร์กสเปซ แท็บเทอร์มินัล และการแบ่งเทอร์มินัลที่สร้างใหม่ การโฟกัสจะแสดง TextBox ด้วย" }, diff --git a/web/messages/tr.json b/web/messages/tr.json index 7f0dd773d5e0..4e92836f571c 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "true olduğunda, seçim kesinleştiğinde seçilen terminal metni sistem panosuna kopyalanır. false olduğunda cmux bir Ghostty copy-on-select geçersiz kılması yaymaz; seçim panosu davranışını Ghostty yapılandırması ve varsayılanları kontrol eder.", "scrollSpeed": "Terminalde fare tekerleği ve dokunmatik yüzey kaydırma değerlerine uygulanan çarpan. Yüksek değerler daha hızlı, düşük değerler daha yavaş kaydırır.", + "autoTintSplitPanes": "Yeni terminal bölmelerine otomatik olarak ince ve farklı arka plan tonları verir.", "showTextBoxOnNewTerminals": "Yeni oluşturulan çalışma alanları, terminal sekmeleri ve terminal bölmeleri için beta TextBox girişini varsayılan olarak gösterir.", "focusTextBoxOnNewTerminals": "Yeni oluşturulan çalışma alanları, terminal sekmeleri ve terminal bölmeleri için beta TextBox girişini varsayılan olarak odaklar. Odaklamak TextBox'ı da gösterir." }, diff --git a/web/messages/uk.json b/web/messages/uk.json index 1b232c14a8b6..a0a0552e7be3 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "Якщо значення true, вибраний текст термінала копіюється до системного буфера обміну після підтвердження виділення. Якщо значення false, cmux не виводить перевизначення Ghostty copy-on-select; конфігурація та стандартні значення Ghostty керують поведінкою буфера виділення.", "scrollSpeed": "Множник, що застосовується до величини прокручування коліщатком миші та трекпадом у терміналі. Вищі значення прокручують швидше, нижчі — повільніше.", + "autoTintSplitPanes": "Автоматично надає новим розділеним панелям термінала ледь помітні різні відтінки фону.", "showTextBoxOnNewTerminals": "Типово показує бета-ввід TextBox для новостворених робочих просторів, вкладок термінала й розділень.", "focusTextBoxOnNewTerminals": "Типово фокусує бета-ввід TextBox для новостворених робочих просторів, вкладок термінала й розділень. Фокусування також показує TextBox." }, diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index 63d7e76eba50..d77aa90a372d 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "为 true 时,在确认选择后将所选终端文本复制到系统剪贴板。为 false 时,cmux 不会输出 Ghostty copy-on-select 覆盖项;选择剪贴板行为由 Ghostty 配置和默认值控制。", "scrollSpeed": "应用于终端滚轮和触控板滚动量的倍数。值越大滚动越快,值越小滚动越慢。", + "autoTintSplitPanes": "自动为新建的终端分屏窗格添加细微且不同的背景色调。", "showTextBoxOnNewTerminals": "默认在新建的工作区、终端标签页和终端分屏中显示测试版 TextBox 输入。", "focusTextBoxOnNewTerminals": "默认在新建的工作区、终端标签页和终端分屏中聚焦测试版 TextBox 输入。聚焦也会显示 TextBox。" }, diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index 23a22483d1d8..c3319a501845 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -819,6 +819,7 @@ "terminal": { "copyOnSelect": "為 true 時,選取確認後會將所選終端機文字複製到系統剪貼簿。為 false 時,cmux 不會輸出 Ghostty copy-on-select 覆寫;選取剪貼簿行為由 Ghostty 設定與預設值控制。", "scrollSpeed": "套用於終端機滾輪與觸控板捲動量的倍率。數值越大捲動越快,數值越小捲動越慢。", + "autoTintSplitPanes": "自動為新建的終端機分割窗格加入細微且不同的背景色調。", "showTextBoxOnNewTerminals": "預設在新建的工作區、終端機分頁和終端機分割中顯示測試版 TextBox 輸入。", "focusTextBoxOnNewTerminals": "預設在新建的工作區、終端機分頁和終端機分割中聚焦測試版 TextBox 輸入。聚焦也會顯示 TextBox。" },