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
6 changes: 5 additions & 1 deletion Sources/HostSettingsActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ final class HostSettingsActions: SettingsHostActions {
}

func openConfigInExternalEditor() {
NSWorkspace.shared.open(configFileURL)
// Honor the user's configured editor (`preferredEditorCommand`),
// falling back to the OS default. Opening the config file directly
// through `NSWorkspace.shared.open` would route to the default
// `.json` handler and ignore the cmux setting.
PreferredEditorSettings.open(configFileURL)
}

func sendFeedback() {
Expand Down
19 changes: 17 additions & 2 deletions Sources/SidebarWorkspaceGroupConfigOpener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,23 @@ import Foundation

/// Opens workspace-group configuration and documentation surfaces.
enum SidebarWorkspaceGroupConfigOpener {
/// Opens the cmux config file (`~/.config/cmux/cmux.json`) in the user's
/// configured editor, materializing an empty config first if none exists.
static func openCmuxConfigInEditor() {
let home = FileManager.default.homeDirectoryForCurrentUser
openCmuxConfigInEditor(
home: FileManager.default.homeDirectoryForCurrentUser,
open: { PreferredEditorSettings.open($0) }
)
}

/// Testable seam: resolves the cmux config path under `home`, materializes
/// an empty config if absent, then hands the file to `open`.
///
/// The public ``openCmuxConfigInEditor()`` entry point passes
/// `PreferredEditorSettings.open` so the config file honors
/// `preferredEditorCommand` (with an OS-default fallback). Tests inject a
/// capturing closure to assert the config file is routed through `open`.
static func openCmuxConfigInEditor(home: URL, open: (URL) -> Void) {
Comment thread
cursor[bot] marked this conversation as resolved.
let configURL = home
.appendingPathComponent(".config", isDirectory: true)
.appendingPathComponent("cmux", isDirectory: true)
Expand All @@ -17,7 +32,7 @@ enum SidebarWorkspaceGroupConfigOpener {
)
try? Data("{}\n".utf8).write(to: configURL, options: .atomic)
}
NSWorkspace.shared.open(configURL)
open(configURL)
}

static func openWorkspaceGroupsDocs() {
Expand Down
8 changes: 8 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@
F4100000A1B2C3D4E5F60718 /* PortScannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4100001A1B2C3D4E5F60718 /* PortScannerTests.swift */; };
A5001270 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = A5001271 /* PostHog */; };
A5001521 /* PostHogAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001520 /* PostHogAnalytics.swift */; };
C135190000000000000000A1 /* PreferredEditorSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C135190000000000000000A2 /* PreferredEditorSettingsTests.swift */; };
C47110010000000000000001 /* ProcessPipeReadCrashRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47110010000000000000002 /* ProcessPipeReadCrashRegressionTests.swift */; };
C47110020000000000000001 /* ProcessPipeReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47110020000000000000002 /* ProcessPipeReader.swift */; };
C47110020000000000000003 /* ProcessPipeReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47110020000000000000002 /* ProcessPipeReader.swift */; };
Expand Down Expand Up @@ -453,6 +454,7 @@
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */; };
D7AB34300000000000000005 /* SidebarWorkspaceDropPlannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34300000000000000006 /* SidebarWorkspaceDropPlannerTests.swift */; };
C9A57103C9A57103C9A57103 /* SidebarWorkspaceGroupConfigOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57104C9A57104C9A57104 /* SidebarWorkspaceGroupConfigOpener.swift */; };
C135190000000000000000B1 /* SidebarWorkspaceGroupConfigOpenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C135190000000000000000B2 /* SidebarWorkspaceGroupConfigOpenerTests.swift */; };
C9A57105C9A57105C9A57105 /* SidebarWorkspaceGroupContextMenuRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57106C9A57106C9A57106 /* SidebarWorkspaceGroupContextMenuRunner.swift */; };
C9A57107C9A57107C9A57107 /* SidebarWorkspaceGroupDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57108C9A57108C9A57108 /* SidebarWorkspaceGroupDialogs.swift */; };
C9A57101C9A57101C9A57101 /* SidebarWorkspaceGroupHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */; };
Expand Down Expand Up @@ -978,6 +980,7 @@
A5001541 /* PortScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortScanner.swift; sourceTree = "<group>"; };
F4100001A1B2C3D4E5F60718 /* PortScannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortScannerTests.swift; sourceTree = "<group>"; };
A5001520 /* PostHogAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalytics.swift; sourceTree = "<group>"; };
C135190000000000000000A2 /* PreferredEditorSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferredEditorSettingsTests.swift; sourceTree = "<group>"; };
C47110010000000000000002 /* ProcessPipeReadCrashRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessPipeReadCrashRegressionTests.swift; sourceTree = "<group>"; };
C47110020000000000000002 /* ProcessPipeReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessPipeReader.swift; sourceTree = "<group>"; };
A5C0DE0000000000000000B9 /* ProjectBuildSettingsTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/ProjectBuildSettingsTabView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1069,6 +1072,7 @@
EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWidthPolicyTests.swift; sourceTree = "<group>"; };
D7AB34300000000000000006 /* SidebarWorkspaceDropPlannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceDropPlannerTests.swift; sourceTree = "<group>"; };
C9A57104C9A57104C9A57104 /* SidebarWorkspaceGroupConfigOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupConfigOpener.swift; sourceTree = "<group>"; };
C135190000000000000000B2 /* SidebarWorkspaceGroupConfigOpenerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupConfigOpenerTests.swift; sourceTree = "<group>"; };
C9A57106C9A57106C9A57106 /* SidebarWorkspaceGroupContextMenuRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupContextMenuRunner.swift; sourceTree = "<group>"; };
C9A57108C9A57108C9A57108 /* SidebarWorkspaceGroupDialogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupDialogs.swift; sourceTree = "<group>"; };
C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupHeaderView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1748,6 +1752,8 @@
A11EB0000000000000000001 /* GhosttyConfigPathResolverTests.swift */,
C13519000000000000000004 /* GhosttyTerminalStartupEnvironmentTests.swift */,
C13519000000000000000008 /* ClaudeConfigDirectoryPathTests.swift */,
C135190000000000000000A2 /* PreferredEditorSettingsTests.swift */,
C135190000000000000000B2 /* SidebarWorkspaceGroupConfigOpenerTests.swift */,
A11EAE000000000000000001 /* WorkspaceAppearanceConfigResolutionTests.swift */,
A11EAF000000000000000001 /* GhosttyNotificationDispatcherTests.swift */,
F4200001A1B2C3D4E5F60718 /* WindowAppearanceSnapshotTests.swift */,
Expand Down Expand Up @@ -2720,6 +2726,7 @@
B3575000000000000000000A /* PiVaultAgentPersistenceTests.swift in Sources */,
D0B10008A1B2C3D4E5F60001 /* PortalTabDragRoutingTests.swift in Sources */,
F4100000A1B2C3D4E5F60718 /* PortScannerTests.swift in Sources */,
C135190000000000000000A1 /* PreferredEditorSettingsTests.swift in Sources */,
C47110010000000000000001 /* ProcessPipeReadCrashRegressionTests.swift in Sources */,
F5410004A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift in Sources */,
F5410000A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift in Sources */,
Expand All @@ -2743,6 +2750,7 @@
D7AB34300000000000000105 /* SidebarTabDropIndicatorPredicateTests.swift in Sources */,
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */,
D7AB34300000000000000005 /* SidebarWorkspaceDropPlannerTests.swift in Sources */,
C135190000000000000000B1 /* SidebarWorkspaceGroupConfigOpenerTests.swift in Sources */,
62270F3DCECB4787D789CCE3 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift in Sources */,
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */,
F6355600A1B2C3D4E5F60718 /* SSHStartupSignalLifecycleTests.swift in Sources */,
Expand Down
50 changes: 50 additions & 0 deletions cmuxTests/PreferredEditorSettingsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Foundation
import Testing

#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif

/// Behavioral coverage for the editor-command resolver shared by every
/// cmux config-file opener (the workspace-group "Edit Group Configuration"
/// action, the "Open Cmux Settings File" action, and the Settings config
/// window). Verifies that a configured `preferredEditorCommand` is honored
/// and that an unset/blank value falls back to `nil` (the OS-default path).
@Suite struct PreferredEditorSettingsTests {
private func makeDefaults() -> UserDefaults {
let suiteName = "cmux-preferred-editor-tests-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return defaults
}

@Test func returnsNilWhenUnset() {
let defaults = makeDefaults()
#expect(PreferredEditorSettings.resolvedCommand(defaults: defaults) == nil)
}

@Test func returnsNilForEmptyOrWhitespaceCommand() {
for raw in ["", " ", "\n\t "] {
let defaults = makeDefaults()
defaults.set(raw, forKey: PreferredEditorSettings.key)
#expect(
PreferredEditorSettings.resolvedCommand(defaults: defaults) == nil,
"blank command \(raw.debugDescription) should fall back to OS default"
)
}
}

@Test func returnsConfiguredCommandWhenSet() {
let defaults = makeDefaults()
defaults.set("code", forKey: PreferredEditorSettings.key)
#expect(PreferredEditorSettings.resolvedCommand(defaults: defaults) == "code")
}

@Test func trimsSurroundingWhitespaceFromConfiguredCommand() {
let defaults = makeDefaults()
defaults.set(" code -w \n", forKey: PreferredEditorSettings.key)
#expect(PreferredEditorSettings.resolvedCommand(defaults: defaults) == "code -w")
}
}
38 changes: 38 additions & 0 deletions cmuxTests/SidebarWorkspaceGroupConfigOpenerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation
import Testing

#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif

/// Regression coverage for the workspace-group "Edit Group Configuration…"
/// action. The opener must route the cmux config file through the same
/// editor-resolving path the rest of the app uses (so `preferredEditorCommand`
/// is honored) rather than handing it to `NSWorkspace.shared.open`, which
/// routes to the OS default `.json` handler and ignores the cmux setting.
///
/// Before the fix the opener called `NSWorkspace.shared.open(configURL)`
/// directly, so the injected opener was never invoked and this test fails.
@Suite struct SidebarWorkspaceGroupConfigOpenerTests {
@Test func routesConfigFileThroughInjectedOpener() throws {
let home = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-group-config-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager.default.removeItem(at: home) }

var opened: [URL] = []
SidebarWorkspaceGroupConfigOpener.openCmuxConfigInEditor(home: home) { opened.append($0) }

#expect(opened.count == 1)
let url = try #require(opened.first)
#expect(url.lastPathComponent == "cmux.json")
// Resolved beneath the injected home, under .config/cmux.
#expect(url.path.hasPrefix(home.path))
#expect(url.deletingLastPathComponent().lastPathComponent == "cmux")

// The opener materializes an empty config when none exists, scoped to
// the injected home (no real-filesystem side effects).
#expect(FileManager.default.fileExists(atPath: url.path))
}
}
Loading