Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@ public final class NativeTitlebarBackdropCoordinator {
private static var unifiedTitlebarTransparencyKey: UInt8 = 0

private let fullscreenAuxiliaryWindows: @MainActor () -> [NSWindow]

/// Creates a coordinator with an injected provider for fullscreen auxiliary windows.
public init(fullscreenAuxiliaryWindows: @escaping @MainActor () -> [NSWindow]) {
private let titlebarControlsIdentifiers: Set<NSUserInterfaceItemIdentifier>

/// Creates a coordinator with the app's titlebar controls and fullscreen window provider.
///
/// - Parameters:
/// - titlebarControlsIdentifiers: Accessories hidden by fullscreen or minimal presentation.
/// - fullscreenAuxiliaryWindows: Provides the windows belonging to fullscreen chrome.
public init(
titlebarControlsIdentifiers: Set<NSUserInterfaceItemIdentifier>,
fullscreenAuxiliaryWindows: @escaping @MainActor () -> [NSWindow]
) {
self.titlebarControlsIdentifiers = titlebarControlsIdentifiers
self.fullscreenAuxiliaryWindows = fullscreenAuxiliaryWindows
}

Expand Down Expand Up @@ -78,13 +87,13 @@ public final class NativeTitlebarBackdropCoordinator {
in window: NSWindow,
isMinimalMode: Bool
) {
let controlsId = NSUserInterfaceItemIdentifier("cmux.titlebarControls")
let shouldHide = hidden || isMinimalMode
for accessory in window.titlebarAccessoryViewControllers {
if accessory.view.identifier == controlsId {
accessory.isHidden = shouldHide
accessory.view.alphaValue = shouldHide ? 0 : 1
}
guard let identifier = accessory.view.identifier,
titlebarControlsIdentifiers.contains(identifier) else { continue }
accessory.isHidden = shouldHide
accessory.view.isHidden = shouldHide
accessory.view.alphaValue = shouldHide ? 0 : 1
}
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

/// Settings under the dotted-id prefix `rightSidebar.*`.
public struct RightSidebarCatalogSection: SettingCatalogSection {
/// Whether the persistent right-sidebar toggle is present in the titlebar.
public let showTitlebarToggle = DefaultsKey<Bool>(
id: "rightSidebar.showTitlebarToggle",
defaultValue: RightSidebarChromeSettings.defaultShowTitlebarToggle,
userDefaultsKey: RightSidebarChromeSettings.showTitlebarToggleKey
)

/// Whether the right-sidebar mode bar offers Open as Pane.
public let showOpenAsPaneButton = DefaultsKey<Bool>(
id: "rightSidebar.showOpenAsPaneButton",
defaultValue: RightSidebarChromeSettings.defaultShowOpenAsPaneButton,
userDefaultsKey: RightSidebarChromeSettings.showOpenAsPaneButtonKey
)

/// Creates the right-sidebar settings section.
public init() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public struct SettingCatalog: SettingCatalogSection {
public let terminal = TerminalCatalogSection()
public let notifications = NotificationsCatalogSection()
public let sidebar = SidebarCatalogSection()
/// Settings for the right-sidebar titlebar and mode-bar chrome.
public let rightSidebar = RightSidebarCatalogSection()
public let sidebarAppearance = SidebarAppearanceCatalogSection()
/// Settings for cmux pane divider and focused-pane chrome.
public let paneChrome = PaneChromeCatalogSection()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

/// Shared storage keys and defaults for right-sidebar chrome preferences.
public struct RightSidebarChromeSettings: Sendable {
/// Creates the stateless right-sidebar chrome policy.
public init() {}

/// The `UserDefaults` key controlling the persistent titlebar toggle.
public static let showTitlebarToggleKey = "rightSidebar.showTitlebarToggle"

/// The `UserDefaults` key controlling the mode-bar Open as Pane button.
public static let showOpenAsPaneButtonKey = "rightSidebar.showOpenAsPaneButton"

/// The default visibility for the persistent titlebar toggle.
public static let defaultShowTitlebarToggle = true

/// The default visibility for the mode-bar Open as Pane button.
public static let defaultShowOpenAsPaneButton = true
}
2 changes: 2 additions & 0 deletions Sources/CmuxSettingsFileStore+SupportedPaths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ extension CmuxSettingsFileStore {
"fileEditor.currentLineHighlight",
"fileEditor.tabWidth",
"fileExplorer.doubleClickAction",
"rightSidebar.showTitlebarToggle",
"rightSidebar.showOpenAsPaneButton",
"shortcuts.bindings",
"shortcuts.showModifierHoldHints",
]
Expand Down
12 changes: 0 additions & 12 deletions Sources/CmuxSettingsJSONPathSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,6 @@ enum AutomationSettings {
static let defaultPortRange = 10
}

struct SettingsFileBooleanMapping {
let jsonKey: String
let defaultsKey: String
let invalidPath: String?

init(jsonKey: String, defaultsKey: String, invalidPath: String? = nil) {
self.jsonKey = jsonKey
self.defaultsKey = defaultsKey
self.invalidPath = invalidPath
}
}

struct SettingsFileStringMapping {
let jsonKey: String
let defaultsKey: String
Expand Down
61 changes: 61 additions & 0 deletions Sources/ContentView+TitlebarLayout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import CoreGraphics

extension ContentView {
static func effectiveTitlebarPadding(
isMinimalMode: Bool,
isFullScreen: Bool,
titlebarPadding: CGFloat,
hostingSafeAreaTop: CGFloat
) -> CGFloat {
guard isMinimalMode else { return WindowChromeMetrics.appTitlebarHeight }
guard !isFullScreen else { return 0 }
return -max(0, min(titlebarPadding, hostingSafeAreaTop))
}

nonisolated static func customTitlebarLeadingPadding(
isFullScreen: Bool,
isSidebarVisible: Bool,
sidebarWidth: CGFloat,
minimumSidebarWidth: CGFloat,
titlebarLeadingInset: CGFloat
) -> CGFloat {
if isFullScreen && !isSidebarVisible {
return 8
}

let minimumSidebarTitleInset = max(titlebarLeadingInset, minimumSidebarWidth + 12)
guard isSidebarVisible else {
return minimumSidebarTitleInset
}

let visibleSidebarTitleInset = sidebarWidth + 12
// Absorb floating-point drift around the minimum-width clamp.
guard sidebarWidth > minimumSidebarWidth + 0.5 else {
return minimumSidebarTitleInset
}
return max(titlebarLeadingInset, visibleSidebarTitleInset)
}

/// Where the always-visible fullscreen titlebar controls (sidebar toggle,
/// history, new tab, notifications) are anchored inside the titlebar band.
struct FullscreenControlsPlacement: Equatable {
var leadingPadding: CGFloat
var topPadding: CGFloat
}

/// Resolves the placement for the fullscreen titlebar controls, or `nil` when
/// they should not be shown. The controls are mounted in a single overlay
/// anchor driven by this function so their on-screen position never depends on
/// sidebar visibility; toggling the sidebar must not shift the accessory bar.
nonisolated static func fullscreenControlsPlacement(
isFullScreen: Bool,
isSidebarVisible: Bool
) -> FullscreenControlsPlacement? {
guard isFullScreen else { return nil }
// Placement is intentionally independent of sidebar visibility so toggling
// the sidebar in fullscreen never shifts the accessory bar. `topPadding`
// mirrors the title row's top inset (see `customTitlebar`) so the controls'
// center lines up with the folder icon / title.
return FullscreenControlsPlacement(leadingPadding: 10, topPadding: 2)
}
}
61 changes: 2 additions & 59 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1785,64 +1785,6 @@ struct ContentView: View {
workspacePresentationModeRuntimeCache.isMinimalMode
}

static func effectiveTitlebarPadding(
isMinimalMode: Bool,
isFullScreen: Bool,
titlebarPadding: CGFloat,
hostingSafeAreaTop: CGFloat
) -> CGFloat {
guard isMinimalMode else { return WindowChromeMetrics.appTitlebarHeight }
guard !isFullScreen else { return 0 }
return -max(0, min(titlebarPadding, hostingSafeAreaTop))
}

nonisolated static func customTitlebarLeadingPadding(
isFullScreen: Bool,
isSidebarVisible: Bool,
sidebarWidth: CGFloat,
minimumSidebarWidth: CGFloat,
titlebarLeadingInset: CGFloat
) -> CGFloat {
if isFullScreen && !isSidebarVisible {
return 8
}

let minimumSidebarTitleInset = max(titlebarLeadingInset, minimumSidebarWidth + 12)
guard isSidebarVisible else {
return minimumSidebarTitleInset
}

let visibleSidebarTitleInset = sidebarWidth + 12
// Absorb floating-point drift around the minimum-width clamp.
guard sidebarWidth > minimumSidebarWidth + 0.5 else {
return minimumSidebarTitleInset
}
return max(titlebarLeadingInset, visibleSidebarTitleInset)
}

/// Where the always-visible fullscreen titlebar controls (sidebar toggle,
/// history, new tab, notifications) are anchored inside the titlebar band.
struct FullscreenControlsPlacement: Equatable {
var leadingPadding: CGFloat
var topPadding: CGFloat
}

/// Resolves the placement for the fullscreen titlebar controls, or `nil` when
/// they should not be shown. The controls are mounted in a single overlay
/// anchor driven by this function so their on-screen position never depends on
/// sidebar visibility; toggling the sidebar must not shift the accessory bar.
nonisolated static func fullscreenControlsPlacement(
isFullScreen: Bool,
isSidebarVisible: Bool
) -> FullscreenControlsPlacement? {
guard isFullScreen else { return nil }
// Placement is intentionally independent of sidebar visibility so toggling
// the sidebar in fullscreen never shifts the accessory bar. `topPadding`
// mirrors the title row's top inset (see `customTitlebar`) so the controls'
// center lines up with the folder icon / title.
return FullscreenControlsPlacement(leadingPadding: 10, topPadding: 2)
}

private func terminalContent(appearance: WindowAppearanceSnapshot) -> some View {
let selectedWorkspaceId = tabManager.selectedTabId
// Selection reaches body before onChange reconciles the mount cache.
Expand Down Expand Up @@ -1982,13 +1924,14 @@ struct ContentView: View {
}

private func rightSidebarPanel(appearance: WindowAppearanceSnapshot) -> some View {
return RightSidebarPanelView(
RightSidebarPanelView(
devicesModel: devicesModel,
tabManager: tabManager,
fileExplorerStore: fileExplorerStore,
fileExplorerState: fileExplorerState,
sessionIndexStore: sessionIndexStore,
titlebarHeight: RightSidebarChromeMetrics.titlebarHeight,
isFullScreen: isFullScreen,
windowAppearance: appearance,
workspaceId: tabManager.selectedTabId,
onResumeSession: { entry in
Expand Down
18 changes: 18 additions & 0 deletions Sources/KeyboardShortcutSettingsFileStore+SectionParsers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ extension CmuxSettingsFileStore {
}
}

func parseRightSidebarSection(
_ section: [String: Any],
sourcePath: String,
snapshot: inout ResolvedSettingsSnapshot
) {
let catalog = SettingCatalog().rightSidebar
if let value = jsonBool(section["showTitlebarToggle"]) {
snapshot.managedUserDefaults[catalog.showTitlebarToggle.userDefaultsKey] = .bool(value)
} else if section.keys.contains("showTitlebarToggle") {
logInvalid("rightSidebar.showTitlebarToggle", sourcePath: sourcePath)
}
if let value = jsonBool(section["showOpenAsPaneButton"]) {
snapshot.managedUserDefaults[catalog.showOpenAsPaneButton.userDefaultsKey] = .bool(value)
} else if section.keys.contains("showOpenAsPaneButton") {
logInvalid("rightSidebar.showOpenAsPaneButton", sourcePath: sourcePath)
}
}

func parseSidebarWorkspaceTodosBeta(
_ beta: [String: Any],
sourcePath: String,
Expand Down
6 changes: 6 additions & 0 deletions Sources/KeyboardShortcutSettingsFileStore+Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ extension CmuxSettingsFileStore {
"doubleClickAction": FileExplorerDoubleClickActionSettings.defaultValue.rawValue,
],
],
[
"rightSidebar": [
"showTitlebarToggle": SettingCatalog().rightSidebar.showTitlebarToggle.defaultValue,
"showOpenAsPaneButton": SettingCatalog().rightSidebar.showOpenAsPaneButton.defaultValue,
],
],
[
"diffViewer": [
"defaultLayout": "unified",
Expand Down
41 changes: 3 additions & 38 deletions Sources/KeyboardShortcutSettingsFileStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ final class CmuxSettingsFileStore {
if let fileExplorerSection = root["fileExplorer"] as? [String: Any] {
parseFileExplorerSection(fileExplorerSection, sourcePath: sourcePath, snapshot: &snapshot)
}
if let rightSidebarSection = root["rightSidebar"] as? [String: Any] {
parseRightSidebarSection(rightSidebarSection, sourcePath: sourcePath, snapshot: &snapshot)
}
if let workspaceGroupsSection = root["workspaceGroups"] as? [String: Any] {
parseWorkspaceGroupsSection(workspaceGroupsSection, sourcePath: sourcePath, snapshot: &snapshot)
}
Expand Down Expand Up @@ -1807,44 +1810,6 @@ final class CmuxSettingsFileStore {

typealias KeyboardShortcutSettingsFileStore = CmuxSettingsFileStore

struct ResolvedSettingsSnapshot {
var path: String?
var shortcuts: [KeyboardShortcutSettings.Action: StoredShortcut] = [:]
var managedShortcutActions: Set<KeyboardShortcutSettings.Action> = []
/// Per-action `when`-clause overrides parsed from `shortcuts.when` — gate a
/// binding to a focus context (see ``ShortcutWhenClause``).
var whenClauses: [KeyboardShortcutSettings.Action: ShortcutWhenClause] = [:]
var managedUserDefaults: [String: ManagedSettingsValue] = [:]
var legacyDerivedManagedUserDefaultKeys: Set<String> = []
var managedCustomSettings = ManagedCustomSettings()

mutating func fillMissingSettings(from fallback: ResolvedSettingsSnapshot) {
if path == nil && (!fallback.managedShortcutActions.isEmpty ||
!fallback.managedUserDefaults.isEmpty ||
!fallback.managedCustomSettings.isEmpty) {
path = fallback.path
}
let missingShortcutActions = fallback.managedShortcutActions
.subtracting(managedShortcutActions)
for action in missingShortcutActions {
managedShortcutActions.insert(action)
if let shortcut = fallback.shortcuts[action] {
shortcuts[action] = shortcut
}
}
for (action, clause) in fallback.whenClauses where whenClauses[action] == nil {
whenClauses[action] = clause
}
for (key, value) in fallback.managedUserDefaults where managedUserDefaults[key] == nil {
managedUserDefaults[key] = value
if fallback.legacyDerivedManagedUserDefaultKeys.contains(key) {
legacyDerivedManagedUserDefaultKeys.insert(key)
}
}
managedCustomSettings.fillMissingSettings(from: fallback.managedCustomSettings)
}
}

private struct ManagedDefaultSideEffect {
let defaultsKey: String
}
Expand Down
39 changes: 39 additions & 0 deletions Sources/ResolvedSettingsSnapshot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import CmuxSettings

struct ResolvedSettingsSnapshot {
var path: String?
var shortcuts: [KeyboardShortcutSettings.Action: StoredShortcut] = [:]
var managedShortcutActions: Set<KeyboardShortcutSettings.Action> = []
/// Per-action `when`-clause overrides parsed from `shortcuts.when` — gate a
/// binding to a focus context (see ``ShortcutWhenClause``).
var whenClauses: [KeyboardShortcutSettings.Action: ShortcutWhenClause] = [:]
var managedUserDefaults: [String: ManagedSettingsValue] = [:]
var legacyDerivedManagedUserDefaultKeys: Set<String> = []
var managedCustomSettings = ManagedCustomSettings()

mutating func fillMissingSettings(from fallback: ResolvedSettingsSnapshot) {
if path == nil && (!fallback.managedShortcutActions.isEmpty ||
!fallback.managedUserDefaults.isEmpty ||
!fallback.managedCustomSettings.isEmpty) {
path = fallback.path
}
let missingShortcutActions = fallback.managedShortcutActions
.subtracting(managedShortcutActions)
for action in missingShortcutActions {
managedShortcutActions.insert(action)
if let shortcut = fallback.shortcuts[action] {
shortcuts[action] = shortcut
}
}
for (action, clause) in fallback.whenClauses where whenClauses[action] == nil {
whenClauses[action] = clause
}
for (key, value) in fallback.managedUserDefaults where managedUserDefaults[key] == nil {
managedUserDefaults[key] = value
if fallback.legacyDerivedManagedUserDefaultKeys.contains(key) {
legacyDerivedManagedUserDefaultKeys.insert(key)
}
}
managedCustomSettings.fillMissingSettings(from: fallback.managedCustomSettings)
}
}
Loading
Loading