Skip to content
Merged
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ public struct AppCatalogSection: SettingCatalogSection {
userDefaultsKey: "newWorkspacePlacement"
)

/// Folder the Open Folder panel starts in. Empty keeps the active
/// workspace's directory. Supports a leading `~`.
public let defaultWorkspacePath = DefaultsKey<String>(
id: "app.defaultWorkspacePath",
defaultValue: "",
userDefaultsKey: "defaultWorkspacePath"
)

public let workspaceInheritWorkingDirectory = DefaultsKey<Bool>(
id: "app.workspaceInheritWorkingDirectory",
defaultValue: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation
import os

nonisolated private let openFolderPanelStartDirectoryLogger = Logger(
subsystem: "com.cmuxterm.app",
category: "OpenFolderPanel"
)

/// Picks the folder the Open Folder panel starts in.
///
/// `app.defaultWorkspacePath` pins it (#3156). When that is empty or does
/// not name an existing directory, the panel starts in the active
/// workspace's directory, as before.
public struct OpenFolderPanelStartDirectory {
public var homeDirectory: String
public var isDirectory: (String) -> Bool

public init(
homeDirectory: String = NSHomeDirectory(),
isDirectory: @escaping (String) -> Bool = { path in
var isDirectory: ObjCBool = false
return FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && isDirectory.boolValue
}
) {
self.homeDirectory = homeDirectory
self.isDirectory = isDirectory
}

public func resolve(configuredPath: String, workspaceDirectory: String?) -> URL? {
let trimmed = configuredPath.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
if let path = expandedPath(trimmed), isDirectory(path) {
return URL(fileURLWithPath: path, isDirectory: true)
}
openFolderPanelStartDirectoryLogger.warning(
"app.defaultWorkspacePath is not an existing folder; using the active workspace directory"
)
}
if let workspaceDirectory, !workspaceDirectory.isEmpty {
return URL(fileURLWithPath: workspaceDirectory, isDirectory: true)
}
return nil
}

/// Expands a leading `~`. Returns nil for a path that is not absolute
/// after expansion.
func expandedPath(_ path: String) -> String? {
var expanded = path
if expanded == "~" {
expanded = homeDirectory
} else if expanded.hasPrefix("~/") {
expanded = homeDirectory + String(expanded.dropFirst())
}
return expanded.hasPrefix("/") ? expanded : nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation
import Testing
@testable import CmuxSettings

@Suite("Open Folder panel start directory")
struct OpenFolderPanelStartDirectoryTests {
private let resolver = OpenFolderPanelStartDirectory(
homeDirectory: "/Users/me",
isDirectory: { ["/Users/me/workspace", "/Users/me", "/srv/code"].contains($0) }
)

private func resolve(_ configuredPath: String, workspaceDirectory: String? = "/active") -> String? {
resolver.resolve(configuredPath: configuredPath, workspaceDirectory: workspaceDirectory)?.path
}

@Test func configuredPathWinsWhenItIsAnExistingFolder() {
#expect(resolve("~/workspace") == "/Users/me/workspace")
#expect(resolve(" ~/workspace\n") == "/Users/me/workspace")
#expect(resolve("~") == "/Users/me")
#expect(resolve("/srv/code") == "/srv/code")
}

@Test func fallsBackToTheActiveWorkspaceDirectory() {
#expect(resolve("") == "/active")
#expect(resolve("~/missing") == "/active")
#expect(resolve("relative/path") == "/active")
#expect(resolve("~other/code") == "/active")
}

@Test func returnsNilWithNoUsableDirectory() {
#expect(resolve("", workspaceDirectory: nil) == nil)
#expect(resolve("~/missing", workspaceDirectory: "") == nil)
}

@Test func defaultsToEmpty() {
#expect(AppCatalogSection().defaultWorkspacePath.defaultValue.isEmpty)
}
}
11 changes: 7 additions & 4 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9267,10 +9267,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
panel.prompt = String(localized: "menu.file.openFolder.panelPrompt", defaultValue: "Open")
// Seed the panel with the active workspace's directory. Use the shared
// main-window resolver so this works even when an auxiliary window is key.
if let context = preferredMainWindowContextForWorkspaceCreation(debugSource: "openFolderPanel.seed"),
let cwd = context.tabManager.selectedWorkspace?.currentDirectory,
!cwd.isEmpty {
panel.directoryURL = URL(fileURLWithPath: cwd)
// app.defaultWorkspacePath, when set to an existing folder, wins.
let context = preferredMainWindowContextForWorkspaceCreation(debugSource: "openFolderPanel.seed")
if let startDirectory = OpenFolderPanelStartDirectory().resolve(
configuredPath: AppCatalogSection().defaultWorkspacePath.value(in: .standard),
workspaceDirectory: context?.tabManager.selectedWorkspace?.currentDirectory
) {
panel.directoryURL = startDirectory
}
if panel.runModal() == .OK, let url = panel.url {
openWorkspaceForExternalDirectory(
Expand Down
1 change: 1 addition & 0 deletions Sources/CmuxSettingsFileStore+SupportedPaths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extension CmuxSettingsFileStore {
"app.paneResizeStepPixels",
"app.focusHistoryIncludesPanesAndTabs",
"app.preferredEditor",
"app.defaultWorkspacePath",
"app.openSupportedFilesInCmux",
"app.openMarkdownInCmuxViewer",
"app.iMessageMode",
Expand Down
1 change: 1 addition & 0 deletions Sources/CmuxSettingsJSONPathSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ enum AppSettingsFileMapping {

static let stringSettings: [SettingsFileStringMapping] = [
.init(jsonKey: "preferredEditor", defaultsKey: app.preferredEditor.userDefaultsKey),
.init(jsonKey: "defaultWorkspacePath", defaultsKey: app.defaultWorkspacePath.userDefaultsKey),
]
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8659,6 +8659,12 @@ struct ContentView: View {
panel.allowsMultipleSelection = false
panel.title = String(localized: "panel.openFolder.title", defaultValue: "Open Folder")
panel.prompt = String(localized: "panel.openFolder.prompt", defaultValue: "Open")
if let startDirectory = OpenFolderPanelStartDirectory().resolve(
configuredPath: AppCatalogSection().defaultWorkspacePath.value(in: .standard),
workspaceDirectory: tabManager.selectedWorkspace?.currentDirectory
) {
panel.directoryURL = startDirectory
}
if panel.runModal() == .OK, let url = panel.url {
_ = tabManager.acquireOptionalWorkspaceIfActive {
tabManager.addWorkspaceIfActive(workingDirectory: url.path)
Expand Down
1 change: 1 addition & 0 deletions Sources/KeyboardShortcutSettingsFileStore+Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ extension CmuxSettingsFileStore {
"paneResizeStepPixels": SettingCatalog().app.paneResizeStepPixels.defaultValue,
"focusHistoryIncludesPanesAndTabs": SettingCatalog().app.focusHistoryIncludesPanesAndTabs.defaultValue,
"preferredEditor": "",
"defaultWorkspacePath": AppCatalogSection().defaultWorkspacePath.defaultValue,
"openSupportedFilesInCmux": AppCatalogSection().openSupportedFilesInCmux.defaultValue,
"openMarkdownInCmuxViewer": AppCatalogSection().openMarkdownInCmuxViewer.defaultValue,
"reorderOnNotification": SettingCatalog().app.reorderOnNotification.defaultValue,
Expand Down
1 change: 1 addition & 0 deletions skills/cmux-settings/references/all-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ General app preferences from Settings > App.
| `app.keepWorkspaceOpenWhenClosingLastSurface` | boolean | `false` | When true, closing the last surface keeps the workspace open. |
| `app.focusPaneOnFirstClick` | boolean | `true` | When cmux is inactive, the first click can activate and focus the clicked pane. |
| `app.preferredEditor` | string | `""` | Custom editor command used when Cmd-click file previews are disabled or a file is unsupported. Leave empty to use the default. |
| `app.defaultWorkspacePath` | string | `""` | Folder the Open Folder panel starts in, for example ~/code. Supports a leading ~. Leave empty to start in the active workspace's directory. |
| `app.openSupportedFilesInCmux` | boolean | `true` | When enabled, Cmd-clicking readable local files opens supported previews in cmux, including text, code, PDFs, images, audio, video, and Quick Look files. Preview headers include an Open With menu based on the user's default and compatible macOS apps for that file. |
| `app.openMarkdownInCmuxViewer` | boolean | `true` | When enabled, Cmd-clicking .md/.markdown/.mkd/.mdx files opens the rendered cmux markdown viewer panel (with live reload) instead of the generic file preview. |
| `app.devWindowDisplay` | string | `""` | DEBUG-only display name used when opening new windows. An empty value uses the system default. |
Expand Down
5 changes: 5 additions & 0 deletions web/data/cmux.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,11 @@
"default": "",
"description": "Custom editor command used when Cmd-click file previews are disabled or a file is unsupported. Leave empty to use the default."
},
"defaultWorkspacePath": {
"type": "string",
"default": "",
"description": "Folder the Open Folder panel starts in, for example ~/code. Supports a leading ~. Leave empty to start in the active workspace's directory."
},
"openSupportedFilesInCmux": {
"type": "boolean",
"default": true,
Expand Down
Loading