Skip to content
Closed
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 @@ -19,6 +19,11 @@ extension ShortcutAction {
first: ShortcutStroke(key: "["),
second: ShortcutStroke(key: "f")
)
case .toggleZenMode:
return StoredShortcut(
first: ShortcutStroke(key: "k", command: true),
second: ShortcutStroke(key: "z")
)
default:
return defaultStroke.map { StoredShortcut(first: $0) }
}
Expand All @@ -41,6 +46,7 @@ extension ShortcutAction {
case .newWindow: return ShortcutStroke(key: "n", command: true, shift: true)
case .closeWindow: return ShortcutStroke(key: "w", command: true, control: true)
case .toggleFullScreen: return ShortcutStroke(key: "f", command: true, control: true)
case .toggleZenMode: return nil
case .quit: return ShortcutStroke(key: "q", command: true)
case .toggleSidebar: return ShortcutStroke(key: "b", command: true)
case .newTab: return ShortcutStroke(key: "n", command: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum ShortcutAction: String, CaseIterable, Sendable, Hashable, SettingCod
case newWindow
case closeWindow
case toggleFullScreen
case toggleZenMode
case quit

// MARK: Workspace
Expand Down Expand Up @@ -177,7 +178,7 @@ extension ShortcutAction {
public var group: Group {
switch self {
case .openSettings, .reloadConfiguration, .showHideAllWindows, .globalSearch,
.newWindow, .closeWindow, .toggleFullScreen, .quit:
.newWindow, .closeWindow, .toggleFullScreen, .toggleZenMode, .quit:
return .app
case .toggleSidebar, .newTab, .newBrowserWorkspace, .saveLayoutTemplate, .openFolder, .reopenPreviousSession, .goToWorkspace,
.commandPalette, .commandPaletteNext, .commandPalettePrevious, .sendFeedback,
Expand Down Expand Up @@ -333,6 +334,8 @@ extension ShortcutAction {
case .newWindow: return "New Window"
case .closeWindow: return "Close Window"
case .toggleFullScreen: return "Toggle Full Screen"
case .toggleZenMode:
return String(localized: "command.toggleZenMode.title", defaultValue: "Toggle Zen Mode")
case .quit: return "Quit cmux"
case .toggleSidebar: return "Toggle Left Sidebar"
case .newTab: return "New Workspace"
Expand Down
17 changes: 17 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -59635,6 +59635,23 @@
}
}
},
"command.toggleZenMode.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Toggle Zen Mode"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Zenモードを切り替え"
}
}
}
},
Comment thread
lawrencecchen marked this conversation as resolved.
"command.toggleLeftSidebar.title": {
"localizations": {
"ar": {
Expand Down
176 changes: 139 additions & 37 deletions Sources/AppDelegate.swift

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6401,6 +6401,14 @@ struct ContentView: View {
keywords: ["fullscreen", "full", "screen", "window", "toggle"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.toggleZenMode",
title: constant(String(localized: "command.toggleZenMode.title", defaultValue: "Toggle Zen Mode")),
subtitle: constant(String(localized: "command.toggleSidebar.subtitle", defaultValue: "Layout")),
keywords: ["zen", "focus", "minimal", "fullscreen", "sidebar", "width"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.reopenClosedBrowserTab",
Expand Down Expand Up @@ -7590,6 +7598,11 @@ struct ContentView: View {
}
window.toggleFullScreen(nil)
}
registry.register(commandId: "palette.toggleZenMode") {
if AppDelegate.shared?.toggleZenMode(preferredWindow: observedWindow ?? NSApp.keyWindow ?? NSApp.mainWindow) != true {
NSSound.beep()
}
}
registry.register(commandId: "palette.reopenClosedBrowserTab") {
if let appDelegate = AppDelegate.shared {
_ = appDelegate.reopenMostRecentlyClosedItem(preferredTabManager: tabManager)
Expand Down
7 changes: 7 additions & 0 deletions Sources/KeyboardShortcutSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ enum KeyboardShortcutSettings {
case newWindow
case closeWindow
case toggleFullScreen
case toggleZenMode
Comment thread
lawrencecchen marked this conversation as resolved.
case quit

// Titlebar / primary UI
Expand Down Expand Up @@ -204,6 +205,7 @@ enum KeyboardShortcutSettings {
case .newWindow: return String(localized: "shortcut.newWindow.label", defaultValue: "New Window")
case .closeWindow: return String(localized: "shortcut.closeWindow.label", defaultValue: "Close Window")
case .toggleFullScreen: return String(localized: "command.toggleFullScreen.title", defaultValue: "Toggle Full Screen")
case .toggleZenMode: return String(localized: "command.toggleZenMode.title", defaultValue: "Toggle Zen Mode")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
case .quit: return String(localized: "menu.quitCmux", defaultValue: "Quit cmux")
case .toggleSidebar: return String(localized: "shortcut.toggleLeftSidebar.label", defaultValue: "Toggle Left Sidebar")
case .newTab: return String(localized: "shortcut.newWorkspace.label", defaultValue: "New Workspace")
Expand Down Expand Up @@ -347,6 +349,11 @@ enum KeyboardShortcutSettings {
return StoredShortcut(key: "w", command: true, shift: false, option: false, control: true)
case .toggleFullScreen:
return StoredShortcut(key: "f", command: true, shift: false, option: false, control: true)
case .toggleZenMode:
return StoredShortcut(
first: ShortcutStroke(key: "k", command: true, shift: false, option: false, control: false),
second: ShortcutStroke(key: "z", command: false, shift: false, option: false, control: false)
Comment thread
lawrencecchen marked this conversation as resolved.
)
case .quit:
return StoredShortcut(key: "q", command: true, shift: false, option: false, control: false)
case .toggleSidebar:
Expand Down
214 changes: 214 additions & 0 deletions Sources/ZenModeController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import CmuxSettings
import Foundation

/// Owns the reversible settings ledger for the app-wide Zen Mode session.
@MainActor
final class ZenModeController {
struct Session: Equatable {
let windowID: UUID
let restoresSidebarVisibility: Bool
let exitsFullScreen: Bool
}

private static let recoveryActiveKey = "zenMode.recovery.active"
private static let recoveryPresentationModeChangedKey = "zenMode.recovery.presentationModeChanged"
private static let recoveryPresentationModeWasPresentKey = "zenMode.recovery.presentationModeWasPresent"
private static let recoveryPresentationModeValueKey = "zenMode.recovery.presentationModeValue"
private static let recoveryContentWidthChangedKey = "zenMode.recovery.contentWidthChanged"
private static let recoveryContentWidthWasPresentKey = "zenMode.recovery.contentWidthWasPresent"
private static let recoveryContentWidthValueKey = "zenMode.recovery.contentWidthValue"
private static let recoveryAppliedContentWidthKey = "zenMode.recovery.appliedContentWidth"
private static let recoverySidebarVisibilityChangedKey = "zenMode.recovery.sidebarVisibilityChanged"
private static let recoveryWindowIDKey = "zenMode.recovery.windowID"

private let defaults: UserDefaults
private let contentWidthSettings: SessionContentWidthSettings
private(set) var activeSession: Session?
private var interruptedSidebarRecoveryWindowID: UUID?

var isActive: Bool { activeSession != nil }

init(
defaults: UserDefaults = .standard,
contentWidthSettings: SessionContentWidthSettings = SessionContentWidthSettings()
) {
self.defaults = defaults
self.contentWidthSettings = contentWidthSettings
restoreInterruptedSettingsIfNeeded(captureSidebarRecovery: true)
}

/// Begins Zen Mode and records only state that must be restored later.
func begin(
windowID: UUID,
isSidebarVisible: Bool,
isFullScreen: Bool
) -> Session? {
guard activeSession == nil else { return nil }

let presentationModeChanged = !WorkspacePresentationModeSettings.isMinimal(defaults: defaults)
let storedContentWidth = defaults.object(forKey: SessionContentWidthSettings.maxWidthKey)
.flatMap { ($0 as? NSNumber)?.doubleValue }
?? SessionContentWidthSettings.noMaximumWidth
let contentWidthChanged = contentWidthSettings.configuredMaximumWidth(from: storedContentWidth) == nil

recordGlobalSettingsRecovery(
windowID: windowID,
presentationModeChanged: presentationModeChanged,
contentWidthChanged: contentWidthChanged,
sidebarVisibilityChanged: isSidebarVisible
)

if presentationModeChanged {
defaults.set(
WorkspacePresentationModeSettings.Mode.minimal.rawValue,
forKey: WorkspacePresentationModeSettings.modeKey
)
}

if contentWidthChanged {
let rememberedWidth = defaults.object(forKey: SessionContentWidthSettings.rememberedMaxWidthKey)
.flatMap { ($0 as? NSNumber)?.doubleValue }
?? SessionContentWidthSettings.defaultConfiguredMaximumWidth
let zenWidth = contentWidthSettings.editorMaximumWidth(
activeStoredValue: storedContentWidth,
rememberedStoredValue: rememberedWidth
)
defaults.set(zenWidth, forKey: SessionContentWidthSettings.maxWidthKey)
defaults.set(zenWidth, forKey: Self.recoveryAppliedContentWidthKey)
}

let session = Session(
windowID: windowID,
restoresSidebarVisibility: isSidebarVisible,
exitsFullScreen: !isFullScreen
)
activeSession = session
return session
}

/// Ends Zen Mode and restores global settings that still have Zen's values.
func end() -> Session? {
guard let activeSession else { return nil }
restoreInterruptedSettingsIfNeeded(captureSidebarRecovery: false)
self.activeSession = nil
return activeSession
}

/// Ends Zen Mode when its target window closes.
func endIfTargeting(windowID: UUID) -> Session? {
guard activeSession?.windowID == windowID else { return nil }
return end()
}
Comment thread
lawrencecchen marked this conversation as resolved.

/// Restores temporary global settings before normal application termination.
func restoreForTermination() -> Session? {
end()
}

/// Consumes crash recovery after session restoration has recreated the target window.
func consumeInterruptedSidebarVisibilityRecovery(windowID: UUID) -> Bool {
guard interruptedSidebarRecoveryWindowID == windowID else { return false }
interruptedSidebarRecoveryWindowID = nil
defaults.removeObject(forKey: Self.recoverySidebarVisibilityChangedKey)
defaults.removeObject(forKey: Self.recoveryWindowIDKey)
return true
}

private func recordGlobalSettingsRecovery(
windowID: UUID,
presentationModeChanged: Bool,
contentWidthChanged: Bool,
sidebarVisibilityChanged: Bool
) {
guard presentationModeChanged || contentWidthChanged || sidebarVisibilityChanged else { return }

defaults.set(true, forKey: Self.recoveryActiveKey)
defaults.set(presentationModeChanged, forKey: Self.recoveryPresentationModeChangedKey)
defaults.set(contentWidthChanged, forKey: Self.recoveryContentWidthChangedKey)
defaults.set(sidebarVisibilityChanged, forKey: Self.recoverySidebarVisibilityChangedKey)
if sidebarVisibilityChanged {
defaults.set(windowID.uuidString, forKey: Self.recoveryWindowIDKey)
}

if presentationModeChanged {
let previousValue = defaults.string(forKey: WorkspacePresentationModeSettings.modeKey)
defaults.set(previousValue != nil, forKey: Self.recoveryPresentationModeWasPresentKey)
if let previousValue {
defaults.set(previousValue, forKey: Self.recoveryPresentationModeValueKey)
}
}

if contentWidthChanged {
let previousValue = defaults.object(forKey: SessionContentWidthSettings.maxWidthKey)
.flatMap { ($0 as? NSNumber)?.doubleValue }
defaults.set(previousValue != nil, forKey: Self.recoveryContentWidthWasPresentKey)
if let previousValue {
defaults.set(previousValue, forKey: Self.recoveryContentWidthValueKey)
}
}
}

private func restoreInterruptedSettingsIfNeeded(captureSidebarRecovery: Bool) {
var capturedSidebarRecovery = false
if captureSidebarRecovery,
defaults.bool(forKey: Self.recoverySidebarVisibilityChangedKey),
let rawWindowID = defaults.string(forKey: Self.recoveryWindowIDKey),
let windowID = UUID(uuidString: rawWindowID) {
interruptedSidebarRecoveryWindowID = windowID
capturedSidebarRecovery = true
}

guard defaults.bool(forKey: Self.recoveryActiveKey) else { return }

if defaults.bool(forKey: Self.recoveryPresentationModeChangedKey),
WorkspacePresentationModeSettings.isMinimal(defaults: defaults) {
restorePreviousValue(
key: WorkspacePresentationModeSettings.modeKey,
wasPresentKey: Self.recoveryPresentationModeWasPresentKey,
valueKey: Self.recoveryPresentationModeValueKey
)
}

if defaults.bool(forKey: Self.recoveryContentWidthChangedKey),
let currentWidth = defaults.object(forKey: SessionContentWidthSettings.maxWidthKey)
.flatMap({ ($0 as? NSNumber)?.doubleValue }),
currentWidth == defaults.double(forKey: Self.recoveryAppliedContentWidthKey) {
if defaults.bool(forKey: Self.recoveryContentWidthWasPresentKey) {
defaults.set(
defaults.double(forKey: Self.recoveryContentWidthValueKey),
forKey: SessionContentWidthSettings.maxWidthKey
)
} else {
defaults.removeObject(forKey: SessionContentWidthSettings.maxWidthKey)
}
}

clearRecoveryState(preservingSidebarRecovery: capturedSidebarRecovery)
}

private func restorePreviousValue(key: String, wasPresentKey: String, valueKey: String) {
if defaults.bool(forKey: wasPresentKey), let previousValue = defaults.string(forKey: valueKey) {
defaults.set(previousValue, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
}

private func clearRecoveryState(preservingSidebarRecovery: Bool = false) {
var keys = [
Self.recoveryActiveKey,
Self.recoveryPresentationModeChangedKey,
Self.recoveryPresentationModeWasPresentKey,
Self.recoveryPresentationModeValueKey,
Self.recoveryContentWidthChangedKey,
Self.recoveryContentWidthWasPresentKey,
Self.recoveryContentWidthValueKey,
Self.recoveryAppliedContentWidthKey,
]
if !preservingSidebarRecovery {
keys.append(Self.recoverySidebarVisibilityChangedKey)
keys.append(Self.recoveryWindowIDKey)
}
keys.forEach(defaults.removeObject(forKey:))
}
}
6 changes: 6 additions & 0 deletions Sources/cmuxApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,12 @@ struct cmuxApp: App {
targetWindow.toggleFullScreen(nil)
}

splitCommandButton(title: String(localized: "command.toggleZenMode.title", defaultValue: "Toggle Zen Mode"), shortcut: menuShortcut(for: .toggleZenMode)) {
if AppDelegate.shared?.toggleZenMode(preferredWindow: NSApp.keyWindow ?? NSApp.mainWindow) != true {
NSSound.beep()
}
}

Divider()

splitCommandButton(title: String(localized: "menu.view.splitRight", defaultValue: "Split Right"), shortcut: menuShortcut(for: .splitRight)) {
Expand Down
Loading
Loading