Skip to content

Commit 451e807

Browse files
committed
Feature flag: KVO observation for live toggle
1 parent 03b354f commit 451e807

6 files changed

Lines changed: 115 additions & 24 deletions

File tree

Sources/AppDelegate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11069,7 +11069,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
1106911069
return true
1107011070
}
1107111071

11072-
if matchConfiguredShortcut(event: event, action: .toggleFileExplorer) {
11072+
if FileExplorerFeatureSettings.isEnabled(),
11073+
matchConfiguredShortcut(event: event, action: .toggleFileExplorer) {
1107311074
// Dispatch async to escape AppKit's performKeyEquivalent animation context.
1107411075
// Without this, NSAnimationContext implicitly animates the layout change.
1107511076
DispatchQueue.main.async { [weak self] in

Sources/ContentView.swift

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2764,7 +2764,7 @@ struct ContentView: View {
27642764
// File explorer is always in the view tree. Visibility is controlled by
27652765
// frame width (0 when hidden), avoiding SwiftUI view insertion/removal
27662766
// and all associated transition animations.
2767-
let explorerVisible = fileExplorerState.isVisible
2767+
let explorerVisible = fileExplorerState.isVisible && fileExplorerState.isFeatureEnabled
27682768
return HStack(spacing: 0) {
27692769
terminalContent
27702770
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -11235,20 +11235,22 @@ private struct SidebarFooterButtons: View {
1123511235
HStack(spacing: 4) {
1123611236
SidebarHelpMenuButton(onSendFeedback: onSendFeedback)
1123711237

11238-
Button(action: { fileExplorerState.toggle() }) {
11239-
Image(systemName: fileExplorerState.isVisible ? "folder.fill" : "folder")
11240-
.font(.system(size: 12))
11241-
.foregroundStyle(fileExplorerState.isVisible ? Color.accentColor : Color(nsColor: .secondaryLabelColor))
11242-
}
11243-
.buttonStyle(SidebarFooterIconButtonStyle())
11244-
.frame(width: 22, height: 22, alignment: .center)
11245-
.help(fileExplorerState.isVisible
11246-
? String(localized: "sidebar.fileExplorer.hide", defaultValue: "Hide File Explorer")
11247-
: String(localized: "sidebar.fileExplorer.show", defaultValue: "Show File Explorer"))
11248-
.accessibilityLabel(fileExplorerState.isVisible
11249-
? String(localized: "sidebar.fileExplorer.hide", defaultValue: "Hide File Explorer")
11250-
: String(localized: "sidebar.fileExplorer.show", defaultValue: "Show File Explorer"))
11251-
.accessibilityIdentifier("sidebarFooter.toggleFileExplorer")
11238+
if fileExplorerState.isFeatureEnabled {
11239+
Button(action: { fileExplorerState.toggle() }) {
11240+
Image(systemName: fileExplorerState.isVisible ? "folder.fill" : "folder")
11241+
.font(.system(size: 12))
11242+
.foregroundStyle(fileExplorerState.isVisible ? Color.accentColor : Color(nsColor: .secondaryLabelColor))
11243+
}
11244+
.buttonStyle(SidebarFooterIconButtonStyle())
11245+
.frame(width: 22, height: 22, alignment: .center)
11246+
.help(fileExplorerState.isVisible
11247+
? String(localized: "sidebar.fileExplorer.hide", defaultValue: "Hide File Explorer")
11248+
: String(localized: "sidebar.fileExplorer.show", defaultValue: "Show File Explorer"))
11249+
.accessibilityLabel(fileExplorerState.isVisible
11250+
? String(localized: "sidebar.fileExplorer.hide", defaultValue: "Hide File Explorer")
11251+
: String(localized: "sidebar.fileExplorer.show", defaultValue: "Show File Explorer"))
11252+
.accessibilityIdentifier("sidebarFooter.toggleFileExplorer")
11253+
}
1125211254

1125311255
UpdatePill(model: updateViewModel)
1125411256
}

Sources/FileExplorerStore.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import Foundation
44
import QuartzCore
55
import SwiftUI
66

7+
extension UserDefaults {
8+
@objc dynamic var fileExplorerFeatureEnabled: Bool {
9+
bool(forKey: FileExplorerFeatureSettings.enabledKey)
10+
}
11+
}
12+
713
// MARK: - Explorer Visual Style
814

915
enum FileExplorerStyle: Int, CaseIterable {
@@ -425,6 +431,9 @@ final class FileExplorerState: ObservableObject {
425431
@Published var isVisible: Bool {
426432
didSet { UserDefaults.standard.set(isVisible, forKey: "fileExplorer.isVisible") }
427433
}
434+
@Published var isFeatureEnabled: Bool {
435+
didSet { UserDefaults.standard.set(isFeatureEnabled, forKey: FileExplorerFeatureSettings.enabledKey) }
436+
}
428437
@Published var width: CGFloat {
429438
didSet { UserDefaults.standard.set(Double(width), forKey: "fileExplorer.width") }
430439
}
@@ -440,15 +449,36 @@ final class FileExplorerState: ObservableObject {
440449
didSet { UserDefaults.standard.set(showHiddenFiles, forKey: "fileExplorer.showHidden") }
441450
}
442451

452+
private var defaultsObserver: NSKeyValueObservation?
453+
443454
init() {
444455
let defaults = UserDefaults.standard
445456
self.isVisible = defaults.bool(forKey: "fileExplorer.isVisible")
457+
self.isFeatureEnabled = FileExplorerFeatureSettings.isEnabled(defaults: defaults)
446458
let storedWidth = defaults.double(forKey: "fileExplorer.width")
447459
self.width = storedWidth > 0 ? CGFloat(storedWidth) : 220
448460
let storedPosition = defaults.double(forKey: "fileExplorer.dividerPosition")
449461
self.dividerPosition = storedPosition > 0 ? CGFloat(storedPosition) : 0.6
450462
let storedShowHidden = defaults.object(forKey: "fileExplorer.showHidden")
451463
self.showHiddenFiles = storedShowHidden == nil ? true : defaults.bool(forKey: "fileExplorer.showHidden")
464+
465+
// KVO on the specific UserDefaults key for reliable cross-window observation
466+
defaultsObserver = UserDefaults.standard.observe(
467+
\.fileExplorerFeatureEnabled,
468+
options: [.new]
469+
) { [weak self] _, change in
470+
DispatchQueue.main.async {
471+
guard let self else { return }
472+
let enabled = FileExplorerFeatureSettings.isEnabled()
473+
if self.isFeatureEnabled != enabled {
474+
self.isFeatureEnabled = enabled
475+
}
476+
}
477+
}
478+
}
479+
480+
deinit {
481+
defaultsObserver?.invalidate()
452482
}
453483

454484
func toggle() {

Sources/Update/UpdateTitlebarAccessory.swift

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,9 +1210,19 @@ final class UpdateTitlebarAccessoryController {
12101210
private let fileExplorerIdentifier = NSUserInterfaceItemIdentifier("cmux.fileExplorerToggle")
12111211
private let controlsControllers = NSHashTable<TitlebarControlsAccessoryViewController>.weakObjects()
12121212
private var lastKnownPresentationMode: WorkspacePresentationModeSettings.Mode = WorkspacePresentationModeSettings.mode()
1213+
private var lastKnownFileExplorerEnabled: Bool = FileExplorerFeatureSettings.isEnabled()
1214+
private var fileExplorerKVO: NSKeyValueObservation?
12131215

12141216
init(viewModel: UpdateViewModel) {
12151217
self.updateViewModel = viewModel
1218+
fileExplorerKVO = UserDefaults.standard.observe(
1219+
\.fileExplorerFeatureEnabled,
1220+
options: [.new]
1221+
) { [weak self] _, _ in
1222+
DispatchQueue.main.async {
1223+
self?.reattachIfFileExplorerFlagChanged()
1224+
}
1225+
}
12161226
}
12171227

12181228
deinit {
@@ -1273,7 +1283,16 @@ final class UpdateTitlebarAccessoryController {
12731283
// AppKit does not provide a stable cross-SDK API for this. Startup scans handle this case.
12741284
}
12751285

1286+
private func reattachIfFileExplorerFlagChanged() {
1287+
let enabled = FileExplorerFeatureSettings.isEnabled()
1288+
guard enabled != lastKnownFileExplorerEnabled else { return }
1289+
lastKnownFileExplorerEnabled = enabled
1290+
attachToExistingWindows()
1291+
}
1292+
12761293
private func reattachIfPresentationModeChanged() {
1294+
reattachIfFileExplorerFlagChanged()
1295+
12771296
let currentMode = WorkspacePresentationModeSettings.mode()
12781297
guard currentMode != lastKnownPresentationMode else { return }
12791298
lastKnownPresentationMode = currentMode
@@ -1367,13 +1386,20 @@ final class UpdateTitlebarAccessoryController {
13671386
controlsControllers.add(controls)
13681387
}
13691388

1370-
if !window.titlebarAccessoryViewControllers.contains(where: { $0.view.identifier == fileExplorerIdentifier }) {
1371-
let toggle = FileExplorerTitlebarAccessoryViewController(onToggle: {
1372-
AppDelegate.shared?.fileExplorerState?.toggle()
1373-
})
1374-
toggle.layoutAttribute = .trailing
1375-
toggle.view.identifier = fileExplorerIdentifier
1376-
window.addTitlebarAccessoryViewController(toggle)
1389+
if FileExplorerFeatureSettings.isEnabled() {
1390+
if !window.titlebarAccessoryViewControllers.contains(where: { $0.view.identifier == fileExplorerIdentifier }) {
1391+
let toggle = FileExplorerTitlebarAccessoryViewController(onToggle: {
1392+
AppDelegate.shared?.fileExplorerState?.toggle()
1393+
})
1394+
toggle.layoutAttribute = .trailing
1395+
toggle.view.identifier = fileExplorerIdentifier
1396+
window.addTitlebarAccessoryViewController(toggle)
1397+
}
1398+
} else {
1399+
// Remove the button if the feature was disabled
1400+
if let index = window.titlebarAccessoryViewControllers.firstIndex(where: { $0.view.identifier == fileExplorerIdentifier }) {
1401+
window.removeTitlebarAccessoryViewController(at: index)
1402+
}
13771403
}
13781404

13791405
attachedWindows.add(window)

Sources/cmuxApp.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ enum WorkspaceButtonFadeSettings {
8787
}
8888
}
8989

90+
enum FileExplorerFeatureSettings {
91+
static let enabledKey = "fileExplorer.featureEnabled"
92+
static let defaultEnabled = false
93+
94+
static func isEnabled(defaults: UserDefaults = .standard) -> Bool {
95+
defaults.object(forKey: enabledKey) as? Bool ?? defaultEnabled
96+
}
97+
}
98+
9099
enum PaneFirstClickFocusSettings {
91100
static let enabledKey = "paneFirstClickFocus.enabled"
92101
static let defaultEnabled = false
@@ -4247,6 +4256,8 @@ struct SettingsView: View {
42474256
private var closeWorkspaceOnLastSurfaceShortcut = LastSurfaceCloseShortcutSettings.defaultValue
42484257
@AppStorage(PaneFirstClickFocusSettings.enabledKey)
42494258
private var paneFirstClickFocusEnabled = PaneFirstClickFocusSettings.defaultEnabled
4259+
@AppStorage(FileExplorerFeatureSettings.enabledKey)
4260+
private var fileExplorerFeatureEnabled = FileExplorerFeatureSettings.defaultEnabled
42504261
@AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
42514262
@AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey)
42524263
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
@@ -4346,6 +4357,22 @@ struct SettingsView: View {
43464357
)
43474358
}
43484359

4360+
@ViewBuilder
4361+
private var fileExplorerSettingsRow: some View {
4362+
SettingsCardRow(
4363+
configurationReview: .settingsOnly,
4364+
String(localized: "settings.app.fileExplorer", defaultValue: "File Explorer"),
4365+
subtitle: String(localized: "settings.app.fileExplorer.subtitle", defaultValue: "Show a file explorer panel on the right side of the terminal (Cmd+Option+B).")
4366+
) {
4367+
Toggle("", isOn: $fileExplorerFeatureEnabled)
4368+
.labelsHidden()
4369+
.controlSize(.small)
4370+
.accessibilityLabel(
4371+
String(localized: "settings.app.fileExplorer", defaultValue: "File Explorer")
4372+
)
4373+
}
4374+
}
4375+
43494376
private var paneFirstClickFocusSubtitle: String {
43504377
if paneFirstClickFocusEnabled {
43514378
return String(
@@ -4889,6 +4916,10 @@ struct SettingsView: View {
48894916

48904917
SettingsCardDivider()
48914918

4919+
fileExplorerSettingsRow
4920+
4921+
SettingsCardDivider()
4922+
48924923
SettingsCardRow(
48934924
configurationReview: .json("app.preferredEditor"),
48944925
String(localized: "settings.app.preferredEditor", defaultValue: "Open Files With"),
@@ -6333,6 +6364,7 @@ struct SettingsView: View {
63336364
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
63346365
closeWorkspaceOnLastSurfaceShortcut = LastSurfaceCloseShortcutSettings.defaultValue
63356366
paneFirstClickFocusEnabled = PaneFirstClickFocusSettings.defaultEnabled
6367+
fileExplorerFeatureEnabled = FileExplorerFeatureSettings.defaultEnabled
63366368
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
63376369
sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
63386370
sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage

0 commit comments

Comments
 (0)