diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 0d697ab9fe..28123a3f00 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -618,6 +618,7 @@ DE513F52281B672D002260B9 /* EditorTabBarAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE513F51281B672D002260B9 /* EditorTabBarAccessory.swift */; }; DE6F77872813625500D00A76 /* EditorTabBarDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F77862813625500D00A76 /* EditorTabBarDivider.swift */; }; EC0870F72A455F6400EB8692 /* ProjectNavigatorViewController+NSMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0870F62A455F6400EB8692 /* ProjectNavigatorViewController+NSMenuDelegate.swift */; }; + F3176C932CC2177100670B85 /* codeedit_shell_integration.fish in Resources */ = {isa = PBXBuildFile; fileRef = F3176C922CC2177100670B85 /* codeedit_shell_integration.fish */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1282,6 +1283,7 @@ DE513F51281B672D002260B9 /* EditorTabBarAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarAccessory.swift; sourceTree = ""; }; DE6F77862813625500D00A76 /* EditorTabBarDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarDivider.swift; sourceTree = ""; }; EC0870F62A455F6400EB8692 /* ProjectNavigatorViewController+NSMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSMenuDelegate.swift"; sourceTree = ""; }; + F3176C922CC2177100670B85 /* codeedit_shell_integration.fish */ = {isa = PBXFileReference; lastKnownFileType = text; path = codeedit_shell_integration.fish; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1598,6 +1600,7 @@ isa = PBXGroup; children = ( 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */, + F3176C922CC2177100670B85 /* codeedit_shell_integration.fish */, 3E0196722A3921AC002648D8 /* codeedit_shell_integration_rc.zsh */, 6C48B5D52C0D08C5001E9955 /* codeedit_shell_integration_profile.zsh */, 6C48B5D72C0D5DB5001E9955 /* codeedit_shell_integration_login.zsh */, @@ -3825,6 +3828,7 @@ 3E0196732A3921AC002648D8 /* codeedit_shell_integration_rc.zsh in Resources */, 58A5DFA529339F6400D1BD5D /* default_keybindings.json in Resources */, 6C48B5D42C0D0743001E9955 /* codeedit_shell_integration_env.zsh in Resources */, + F3176C932CC2177100670B85 /* codeedit_shell_integration.fish in Resources */, 3E01967A2A392B45002648D8 /* codeedit_shell_integration.bash in Resources */, D7211D4727E06BFE008F2ED7 /* Localizable.strings in Resources */, 6C48B5D62C0D08C5001E9955 /* codeedit_shell_integration_profile.zsh in Resources */, diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift index 38f18bb316..f49b16b7e7 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift @@ -83,10 +83,12 @@ extension SettingsData { /// The shell options. /// - **bash**: uses the default bash shell + /// - **fish**: uses the default fish shell /// - **zsh**: uses the ZSH shell /// - **system**: uses the system default shell (most likely ZSH) enum TerminalShell: String, Codable, Hashable { case bash + case fish case zsh case system } diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift index 99c90fdd74..5ad711bde3 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift @@ -47,6 +47,8 @@ private extension TerminalSettingsView { .tag(SettingsData.TerminalShell.zsh) Text("Bash") .tag(SettingsData.TerminalShell.bash) + Text("Fish") + .tag(SettingsData.TerminalShell.fish) } } @@ -95,7 +97,7 @@ private extension TerminalSettingsView { VStack { Toggle("Shell Integration", isOn: $settings.useShellIntegration) // swiftlint:disable:next line_length - .help("CodeEdit supports integrating with common shells such as Bash and Zsh. This enables features like terminal title detection.") + .help("CodeEdit supports integrating with common shells such as Bash, Fish and Zsh. This enables features like terminal title detection.") if !settings.useShellIntegration { HStack { Image(systemName: "exclamationmark.triangle.fill") diff --git a/CodeEdit/Features/TerminalEmulator/Model/Shell.swift b/CodeEdit/Features/TerminalEmulator/Model/Shell.swift index ba556d46b6..2282e600d6 100644 --- a/CodeEdit/Features/TerminalEmulator/Model/Shell.swift +++ b/CodeEdit/Features/TerminalEmulator/Model/Shell.swift @@ -11,6 +11,7 @@ import Foundation enum Shell: String, CaseIterable { case bash case zsh + case fish var url: String { switch self { @@ -18,12 +19,14 @@ enum Shell: String, CaseIterable { "/bin/bash" case .zsh: "/bin/zsh" + case .fish: + "/opt/homebrew/bin/fish" } } var isSh: Bool { switch self { - case .bash, .zsh: + case .bash, .zsh, .fish: return true } } @@ -84,6 +87,8 @@ enum Shell: String, CaseIterable { "/bin/bash" case .zsh: "/bin/zsh" + case .fish: + Shell.getFishShellPath() } } @@ -96,4 +101,31 @@ enum Shell: String, CaseIterable { } return currentUser.shell } + + static func getFishShellPath() -> String { + let command = "which fish" + let process = Process() + let outputPipe = Pipe() + + process.executableURL = URL(fileURLWithPath: "/bin/zsh") + process.arguments = ["--login", "-c", command] + process.standardOutput = outputPipe + process.standardError = outputPipe + + do { + try process.run() + process.waitUntilExit() + + let data = outputPipe.fileHandleForReading.readDataToEndOfFile() + guard let shellPath = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !shellPath.isEmpty + else { + print("Fish shell not found.") + return "" + } + return shellPath + } catch { + print("Error running command: \(error.localizedDescription)") + return "" + } + } } diff --git a/CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift b/CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift index ee3e9a1002..0e7d2ac8d2 100644 --- a/CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift +++ b/CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift @@ -26,6 +26,7 @@ enum ShellIntegration { enum Error: Swift.Error, LocalizedError { case bashShellFileNotFound case zshShellFileNotFound + case fishShellFileNotFound var localizedDescription: String { switch self { @@ -33,6 +34,8 @@ enum ShellIntegration { return "Failed to find bash injection file." case .zshShellFileNotFound: return "Failed to find zsh injection file." + case .fishShellFileNotFound: + return "Failed to find fish injection file." } } } @@ -64,6 +67,8 @@ enum ShellIntegration { try bash(&args) case .zsh: try zsh(&args, &environment, useLogin) + case .fish: + try fish(&args, &environment) } if useLogin { @@ -155,6 +160,35 @@ enum ShellIntegration { try copyFile(rcScriptURL, toDir: tempDir.appending(path: ".zshrc")) } + /// Sets up the `fish` shell integration. + /// + /// Sets the fish init directory to a temporary directory containing CE setup scripts. Each script corresponds to an + /// available zsh init script, and will source the user's real init script. + /// Also sets up an interactive session using the `-i` parameter. + /// + /// - Parameters: + /// - shellExecArgs: The args to use for shell exec, will be modified by this function. + /// - environment: Environment variables in an array. Formatted as `EnvVar=Value`. Will be modified by this + /// function. + private static func fish(_ args: inout [String], _ environment: inout [String]) throws { + // Set the args for executing Fish shell + args.append("-i") + + // Locate the Fish integration script + guard let fishScriptURL = Bundle.main.url( + forResource: "codeedit_shell_integration", + withExtension: "fish" + ) else { + throw Error.fishShellFileNotFound + } + + // Make a temporary directory for storing the integration script + let tempDir = try makeTempDir(forShell: .fish) + + // Copy the Fish integration script to the temporary directory + try copyFile(fishScriptURL, toDir: tempDir.appending(path: "config.fish")) + } + /// Helper function for safely copying files, removing existing ones if needed. /// - Parameters: /// - origin: The path of the file to copy from diff --git a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift index 871445be5c..353aeb71f0 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -66,6 +66,8 @@ struct TerminalEmulatorView: NSViewRepresentable { return "/bin/bash" case .zsh: return "/bin/zsh" + case .fish: + return Shell.getFishShellPath() } } diff --git a/CodeEdit/ShellIntegration/codeedit_shell_integration.fish b/CodeEdit/ShellIntegration/codeedit_shell_integration.fish new file mode 100644 index 0000000000..a7f40c7c03 --- /dev/null +++ b/CodeEdit/ShellIntegration/codeedit_shell_integration.fish @@ -0,0 +1,14 @@ + +# Check if the FISH_CONFIG_DIR is set and if the config.fish exists +if test -n "$USER_CONFIG_DIR" -a -f "$USER_CONFIG_DIR/config.fish" + set CE_CONFIG_DIR $FISH_CONFIG_DIR + set FISH_CONFIG_DIR $USER_CONFIG_DIR + + # Source the user's config.fish + . "$USER_CONFIG_DIR/config.fish" + + # Restore the original FISH_CONFIG_DIR + set FISH_CONFIG_DIR $CE_CONFIG_DIR +end + +# Additional integration functions can go here