From a112b9f6769a468d92a2e63848605c00740bca80 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:07:36 -0500 Subject: [PATCH 1/2] Stop Modifying User's RC Files --- CodeEdit.xcodeproj/project.pbxproj | 66 ++++++- .../Models/TerminalSettings.swift | 8 + .../TerminalSettingsView.swift | 32 ++- .../TerminalEmulator/Model/CurrentUser.swift | 46 +++++ .../TerminalEmulator/Model/Shell.swift | 21 ++ .../Model/ShellIntegration.swift | 187 ++++++++++++++++++ .../TerminalEmulatorView+Coordinator.swift | 2 +- .../{ => Views}/TerminalEmulatorView.swift | 128 +++--------- .../codeedit_shell_integration_login.zsh | 8 + .../codeedit_shell_integration.bash | 43 +++- .../codeedit_shell_integration.zsh | 24 --- .../codeedit_shell_integration_env.zsh | 16 ++ .../codeedit_shell_integration_profile.zsh | 10 + .../codeedit_shell_integration_rc.zsh | 64 ++++++ .../ShellIntegrationTests.swift | 101 ++++++++++ 15 files changed, 623 insertions(+), 133 deletions(-) create mode 100644 CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift create mode 100644 CodeEdit/Features/TerminalEmulator/Model/Shell.swift create mode 100644 CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift rename CodeEdit/Features/TerminalEmulator/{ => Views}/TerminalEmulatorView+Coordinator.swift (92%) rename CodeEdit/Features/TerminalEmulator/{ => Views}/TerminalEmulatorView.swift (66%) create mode 100644 CodeEdit/Features/TerminalEmulator/codeedit_shell_integration_login.zsh delete mode 100644 CodeEdit/ShellIntegration/codeedit_shell_integration.zsh create mode 100644 CodeEdit/ShellIntegration/codeedit_shell_integration_env.zsh create mode 100644 CodeEdit/ShellIntegration/codeedit_shell_integration_profile.zsh create mode 100644 CodeEdit/ShellIntegration/codeedit_shell_integration_rc.zsh create mode 100644 CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 9830229f9..c89dd6436 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -68,7 +68,7 @@ 30AB4EBD2BF71CA800ED4431 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */; }; 30AB4EC22BF7253200ED4431 /* KeyValueTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */; }; 30E6D0012A6E505200A58B20 /* NavigatorSidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */; }; - 3E0196732A3921AC002648D8 /* codeedit_shell_integration.zsh in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196722A3921AC002648D8 /* codeedit_shell_integration.zsh */; }; + 3E0196732A3921AC002648D8 /* codeedit_shell_integration_rc.zsh in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196722A3921AC002648D8 /* codeedit_shell_integration_rc.zsh */; }; 3E01967A2A392B45002648D8 /* codeedit_shell_integration.bash in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */; }; 4E7F066629602E7B00BB3C12 /* CodeEditSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7F066529602E7B00BB3C12 /* CodeEditSplitViewController.swift */; }; 4EE96ECB2960565E00FFBEA8 /* DocumentsUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE96ECA2960565E00FFBEA8 /* DocumentsUnitTests.swift */; }; @@ -314,6 +314,7 @@ 6C18620A298BF5A800C663EA /* RecentProjectsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C186209298BF5A800C663EA /* RecentProjectsListView.swift */; }; 6C1CC9982B1E770B0002349B /* AsyncFileIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CC9972B1E770B0002349B /* AsyncFileIterator.swift */; }; 6C1CC99B2B1E7CBC0002349B /* FindNavigatorIndexBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */; }; + 6C1F3DA22C18C55800F6DEF6 /* ShellIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */; }; 6C2C155829B4F49100EA60A5 /* SplitViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155729B4F49100EA60A5 /* SplitViewItem.swift */; }; 6C2C155A29B4F4CC00EA60A5 /* Variadic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */; }; 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */; }; @@ -324,6 +325,12 @@ 6C4104E9297C970F00F472BA /* AboutDefaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4104E8297C970F00F472BA /* AboutDefaultView.swift */; }; 6C48B5C52C0A2835001E9955 /* FileEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48B5C42C0A2835001E9955 /* FileEncoding.swift */; }; 6C48B5C92C0B5F7A001E9955 /* NSTextStorage+isEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48B5C72C0B5F7A001E9955 /* NSTextStorage+isEmpty.swift */; }; + 6C48B5CE2C0C1BE4001E9955 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48B5CD2C0C1BE4001E9955 /* Shell.swift */; }; + 6C48B5D12C0D0519001E9955 /* ShellIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48B5D02C0D0519001E9955 /* ShellIntegration.swift */; }; + 6C48B5D42C0D0743001E9955 /* codeedit_shell_integration_env.zsh in Resources */ = {isa = PBXBuildFile; fileRef = 6C48B5D32C0D0743001E9955 /* codeedit_shell_integration_env.zsh */; }; + 6C48B5D62C0D08C5001E9955 /* codeedit_shell_integration_profile.zsh in Resources */ = {isa = PBXBuildFile; fileRef = 6C48B5D52C0D08C5001E9955 /* codeedit_shell_integration_profile.zsh */; }; + 6C48B5D82C0D5DB5001E9955 /* codeedit_shell_integration_login.zsh in Resources */ = {isa = PBXBuildFile; fileRef = 6C48B5D72C0D5DB5001E9955 /* codeedit_shell_integration_login.zsh */; }; + 6C48B5DA2C0D5FC5001E9955 /* CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48B5D92C0D5FC5001E9955 /* CurrentUser.swift */; }; 6C48D8F22972DAFC00D6D205 /* Env+IsFullscreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F12972DAFC00D6D205 /* Env+IsFullscreen.swift */; }; 6C48D8F42972DB1A00D6D205 /* Env+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F32972DB1A00D6D205 /* Env+Window.swift */; }; 6C48D8F72972E5F300D6D205 /* WindowObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */; }; @@ -642,7 +649,7 @@ 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = ""; }; 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueTable.swift; sourceTree = ""; }; 30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarViewModel.swift; sourceTree = ""; }; - 3E0196722A3921AC002648D8 /* codeedit_shell_integration.zsh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = codeedit_shell_integration.zsh; sourceTree = ""; }; + 3E0196722A3921AC002648D8 /* codeedit_shell_integration_rc.zsh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = codeedit_shell_integration_rc.zsh; sourceTree = ""; }; 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = codeedit_shell_integration.bash; sourceTree = ""; }; 4E7F066529602E7B00BB3C12 /* CodeEditSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditSplitViewController.swift; sourceTree = ""; }; 4EE96ECA2960565E00FFBEA8 /* DocumentsUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsUnitTests.swift; sourceTree = ""; }; @@ -885,6 +892,7 @@ 6C186209298BF5A800C663EA /* RecentProjectsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentProjectsListView.swift; sourceTree = ""; }; 6C1CC9972B1E770B0002349B /* AsyncFileIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFileIterator.swift; sourceTree = ""; }; 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorIndexBar.swift; sourceTree = ""; }; + 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellIntegrationTests.swift; sourceTree = ""; }; 6C2C155729B4F49100EA60A5 /* SplitViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewItem.swift; sourceTree = ""; }; 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Variadic.swift; sourceTree = ""; }; 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewReader.swift; sourceTree = ""; }; @@ -894,6 +902,12 @@ 6C4104E8297C970F00F472BA /* AboutDefaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutDefaultView.swift; sourceTree = ""; }; 6C48B5C42C0A2835001E9955 /* FileEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileEncoding.swift; sourceTree = ""; }; 6C48B5C72C0B5F7A001E9955 /* NSTextStorage+isEmpty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextStorage+isEmpty.swift"; sourceTree = ""; }; + 6C48B5CD2C0C1BE4001E9955 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = ""; }; + 6C48B5D02C0D0519001E9955 /* ShellIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellIntegration.swift; sourceTree = ""; }; + 6C48B5D32C0D0743001E9955 /* codeedit_shell_integration_env.zsh */ = {isa = PBXFileReference; lastKnownFileType = text; path = codeedit_shell_integration_env.zsh; sourceTree = ""; }; + 6C48B5D52C0D08C5001E9955 /* codeedit_shell_integration_profile.zsh */ = {isa = PBXFileReference; lastKnownFileType = text; path = codeedit_shell_integration_profile.zsh; sourceTree = ""; }; + 6C48B5D72C0D5DB5001E9955 /* codeedit_shell_integration_login.zsh */ = {isa = PBXFileReference; lastKnownFileType = text; name = codeedit_shell_integration_login.zsh; path = ../Features/TerminalEmulator/codeedit_shell_integration_login.zsh; sourceTree = ""; }; + 6C48B5D92C0D5FC5001E9955 /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = ""; }; 6C48D8F12972DAFC00D6D205 /* Env+IsFullscreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Env+IsFullscreen.swift"; sourceTree = ""; }; 6C48D8F32972DB1A00D6D205 /* Env+Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Env+Window.swift"; sourceTree = ""; }; 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowObserver.swift; sourceTree = ""; }; @@ -1359,7 +1373,10 @@ isa = PBXGroup; children = ( 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */, - 3E0196722A3921AC002648D8 /* codeedit_shell_integration.zsh */, + 3E0196722A3921AC002648D8 /* codeedit_shell_integration_rc.zsh */, + 6C48B5D52C0D08C5001E9955 /* codeedit_shell_integration_profile.zsh */, + 6C48B5D72C0D5DB5001E9955 /* codeedit_shell_integration_login.zsh */, + 6C48B5D32C0D0743001E9955 /* codeedit_shell_integration_env.zsh */, ); path = ShellIntegration; sourceTree = ""; @@ -1732,8 +1749,8 @@ 5879827E292ED0FB0085B254 /* TerminalEmulator */ = { isa = PBXGroup; children = ( - 58798280292ED0FB0085B254 /* TerminalEmulatorView.swift */, - 58798281292ED0FB0085B254 /* TerminalEmulatorView+Coordinator.swift */, + 6C48B5DB2C0D664A001E9955 /* Model */, + 6C48B5DC2C0D6654001E9955 /* Views */, ); path = TerminalEmulator; sourceTree = ""; @@ -1767,11 +1784,12 @@ 587B60FE293416C900D5CD8F /* Features */ = { isa = PBXGroup; children = ( - 613899BD2B6E70E200A5CAF6 /* Search */, 283BDCC22972F211002AFF81 /* Acknowledgements */, 4EE96EC82960562000FFBEA8 /* Documents */, 583E527429361B39001AB554 /* CodeEditUI */, 587B612C2934199800D5CD8F /* CodeFile */, + 613899BD2B6E70E200A5CAF6 /* Search */, + 6C1F3DA02C18C53C00F6DEF6 /* TerminalEmulator */, ); path = Features; sourceTree = ""; @@ -2442,6 +2460,14 @@ path = FindNavigatorResultList; sourceTree = ""; }; + 6C1F3DA02C18C53C00F6DEF6 /* TerminalEmulator */ = { + isa = PBXGroup; + children = ( + 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */, + ); + path = TerminalEmulator; + sourceTree = ""; + }; 6C48B5C82C0B5F7A001E9955 /* NSTextStorage */ = { isa = PBXGroup; children = ( @@ -2451,6 +2477,25 @@ path = CodeEdit/Utils/Extensions/NSTextStorage; sourceTree = SOURCE_ROOT; }; + 6C48B5DB2C0D664A001E9955 /* Model */ = { + isa = PBXGroup; + children = ( + 6C48B5D92C0D5FC5001E9955 /* CurrentUser.swift */, + 6C48B5CD2C0C1BE4001E9955 /* Shell.swift */, + 6C48B5D02C0D0519001E9955 /* ShellIntegration.swift */, + ); + path = Model; + sourceTree = ""; + }; + 6C48B5DC2C0D6654001E9955 /* Views */ = { + isa = PBXGroup; + children = ( + 58798280292ED0FB0085B254 /* TerminalEmulatorView.swift */, + 58798281292ED0FB0085B254 /* TerminalEmulatorView+Coordinator.swift */, + ); + path = Views; + sourceTree = ""; + }; 6C48D8EF2972DAC300D6D205 /* Environment */ = { isa = PBXGroup; children = ( @@ -3231,12 +3276,15 @@ B6FF04782B6C08AC002C2C78 /* DefaultThemes in Resources */, 283BDCBD2972EEBD002AFF81 /* Package.resolved in Resources */, B658FB3727DA9E1000EA4DBD /* Preview Assets.xcassets in Resources */, - 3E0196732A3921AC002648D8 /* codeedit_shell_integration.zsh in Resources */, + 3E0196732A3921AC002648D8 /* codeedit_shell_integration_rc.zsh in Resources */, 58A5DFA529339F6400D1BD5D /* default_keybindings.json in Resources */, + 6C48B5D42C0D0743001E9955 /* codeedit_shell_integration_env.zsh in Resources */, 3E01967A2A392B45002648D8 /* codeedit_shell_integration.bash in Resources */, D7211D4727E06BFE008F2ED7 /* Localizable.strings in Resources */, + 6C48B5D62C0D08C5001E9955 /* codeedit_shell_integration_profile.zsh in Resources */, 284DC8512978BA2600BF2770 /* .all-contributorsrc in Resources */, B658FB3427DA9E1000EA4DBD /* Assets.xcassets in Resources */, + 6C48B5D82C0D5DB5001E9955 /* codeedit_shell_integration_login.zsh in Resources */, 6C6BD6FC29CD152400235D17 /* codeedit.extension.appextensionpoint in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3400,6 +3448,7 @@ 611192022B08CCDC00D4459B /* SearchIndexer+Search.swift in Sources */, 04BA7C272AE2E9F100584E1C /* GitClient+Push.swift in Sources */, B664C3B32B96634F00816B4E /* NavigationSettingsView.swift in Sources */, + 6C48B5D12C0D0519001E9955 /* ShellIntegration.swift in Sources */, 2B7A583527E4BA0100D25D4E /* AppDelegate.swift in Sources */, D7012EE827E757850001E1EF /* FindNavigatorView.swift in Sources */, 58A5DF8029325B5A00D1BD5D /* GitClient.swift in Sources */, @@ -3648,6 +3697,7 @@ B65B11042B09DB1C002852CF /* GitClient+Fetch.swift in Sources */, 5878DA872918642F00DD95A3 /* AcknowledgementsViewModel.swift in Sources */, B6E41C7929DE02800088F9F4 /* AccountSelectionView.swift in Sources */, + 6C48B5CE2C0C1BE4001E9955 /* Shell.swift in Sources */, 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */, 30AB4EBB2BF718A100ED4431 /* DeveloperSettings.swift in Sources */, B6C4F2A92B3CB00100B2B140 /* CommitDetailsHeaderView.swift in Sources */, @@ -3661,6 +3711,7 @@ 61A53A7E2B4449870093BF8A /* WorkspaceDocument+Find.swift in Sources */, 6CABB19E29C5591D00340467 /* NSTableViewWrapper.swift in Sources */, 5879821B292D92370085B254 /* SearchResultMatchModel.swift in Sources */, + 6C48B5DA2C0D5FC5001E9955 /* CurrentUser.swift in Sources */, 04BA7C1E2AE2D8A000584E1C /* GitClient+Clone.swift in Sources */, 58F2EB09292FB2B0004A9BDE /* TerminalSettings.swift in Sources */, 6C578D8429CD343800DC73B2 /* ExtensionDetailView.swift in Sources */, @@ -3785,6 +3836,7 @@ 587B60F82934124200D5CD8F /* CEWorkspaceFileManagerTests.swift in Sources */, 6130535F2B23A31300D767E3 /* MemorySearchTests.swift in Sources */, 587B61012934170A00D5CD8F /* UnitTests_Extensions.swift in Sources */, + 6C1F3DA22C18C55800F6DEF6 /* ShellIntegrationTests.swift in Sources */, 283BDCC52972F236002AFF81 /* AcknowledgementsTests.swift in Sources */, 4EE96ECB2960565E00FFBEA8 /* DocumentsUnitTests.swift in Sources */, 4EE96ECE296059E000FFBEA8 /* NSHapticFeedbackPerformerMock.swift in Sources */, diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift index b3201179a..9ffa41513 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift @@ -54,6 +54,12 @@ extension SettingsData { // Use font settings from Text Editing var useTextEditorFont: Bool = true + /// If `true`, use injection scripts for terminal features like automatic tab title. + var useShellIntegration: Bool = true + + /// If `true`, use a login shell. + var useLoginShell: Bool = true + /// Default initializer init() {} @@ -70,6 +76,8 @@ extension SettingsData { ) ?? .block self.cursorBlink = try container.decodeIfPresent(Bool.self, forKey: .cursorBlink) ?? false self.useTextEditorFont = try container.decodeIfPresent(Bool.self, forKey: .useTextEditorFont) ?? true + self.useShellIntegration = try container.decodeIfPresent(Bool.self, forKey: .useShellIntegration) ?? true + self.useLoginShell = try container.decodeIfPresent(Bool.self, forKey: .useLoginShell) ?? true } } diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift index ad0e5087a..79d793c94 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift @@ -28,6 +28,10 @@ struct TerminalSettingsView: View { cursorStyle cursorBlink } + Section { + injectionOptions + useLoginShell + } } } } @@ -38,7 +42,7 @@ private extension TerminalSettingsView { Text("System Default") .tag(SettingsData.TerminalShell.system) Divider() - Text("ZSH") + Text("Zsh") .tag(SettingsData.TerminalShell.zsh) Text("Bash") .tag(SettingsData.TerminalShell.bash) @@ -81,4 +85,30 @@ private extension TerminalSettingsView { format: .number ) } + + @ViewBuilder private var injectionOptions: some View { + 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.") + if !settings.useShellIntegration { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(Color(NSColor.systemYellow)) + Text("Warning: Disabling integration disables features such as terminal title detection.") + Spacer() + } + } + } + } + + @ViewBuilder private var useLoginShell: some View { + if settings.useShellIntegration { + Toggle("Use Login Shell", isOn: $settings.useLoginShell) + // swiftlint:disable:next line_length + .help("Whether or not to use a login shell when starting a terminal session. By default, a login shell is used used similar to Terminal.app.") + } else { + EmptyView() + } + } } diff --git a/CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift b/CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift new file mode 100644 index 000000000..dccd1e3d1 --- /dev/null +++ b/CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift @@ -0,0 +1,46 @@ +// +// CurrentUser.swift +// CodeEdit +// +// Created by Khan Winter on 6/2/24. +// + +import Foundation + +struct CurrentUser { + let name: String + let shell: String + let homeDir: String + let uid: uid_t + let gid: gid_t + + private init(name: String, shell: String, homeDir: String, uid: uid_t, gid: gid_t) { + self.name = name + self.shell = shell + self.homeDir = homeDir + self.uid = uid + self.gid = gid + } + + static func getCurrentUser() -> CurrentUser? { + let bufsize = sysconf(_SC_GETPW_R_SIZE_MAX) + guard bufsize != -1 else { return nil } + let buffer = UnsafeMutablePointer.allocate(capacity: bufsize) + defer { + buffer.deallocate() + } + var pwd = passwd() + // points to `pwd` + var result: UnsafeMutablePointer? = UnsafeMutablePointer.allocate(capacity: 1) + + if getpwuid_r(getuid(), &pwd, buffer, bufsize, &result) != 0 { return nil } + + return CurrentUser( + name: String(cString: pwd.pw_name), + shell: String(cString: pwd.pw_shell), + homeDir: String(cString: pwd.pw_dir), + uid: pwd.pw_uid, + gid: pwd.pw_gid + ) + } +} diff --git a/CodeEdit/Features/TerminalEmulator/Model/Shell.swift b/CodeEdit/Features/TerminalEmulator/Model/Shell.swift new file mode 100644 index 000000000..b7a0359d9 --- /dev/null +++ b/CodeEdit/Features/TerminalEmulator/Model/Shell.swift @@ -0,0 +1,21 @@ +// +// ShellIntegration.swift +// CodeEdit +// +// Created by Khan Winter on 6/1/24. +// + +import Foundation + +/// Shells supported by CodeEdit +enum Shell: String, CaseIterable { + case bash + case zsh + + var isSh: Bool { + switch self { + case .bash, .zsh: + return true + } + } +} diff --git a/CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift b/CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift new file mode 100644 index 000000000..ee3e9a100 --- /dev/null +++ b/CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift @@ -0,0 +1,187 @@ +// +// ShellIntegration.swift +// CodeEdit +// +// Created by Khan Winter on 6/2/24. +// + +import Foundation +import os + +/// Provides a single function for setting up shell integrations. +/// See ``ShellIntegration/setUpIntegration(for:environment:)`` +enum ShellIntegration { + private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "ShellIntegration") + + /// Variable constants used by setup scripts. + enum Variables { + static let shellLogin = "CE_SHELL_LOGIN" + static let ceZDotDir = "CE_ZDOTDIR" + static let userZDotDir = "USER_ZDOTDIR" + static let zDotDir = "ZDOTDIR" + static let ceInjection = "CE_INJECTION" + } + + /// Errors for shell integration setup. + enum Error: Swift.Error, LocalizedError { + case bashShellFileNotFound + case zshShellFileNotFound + + var localizedDescription: String { + switch self { + case .bashShellFileNotFound: + return "Failed to find bash injection file." + case .zshShellFileNotFound: + return "Failed to find zsh injection file." + } + } + } + + /// Setup shell integration. + /// + /// Injects necessary init files for whatever shell is being used for CodeEdit to receive notifications about + /// running processes for display in the UI. + /// Any other setup/configuration should also be done here. + /// + /// - Parameters: + /// - shell: The shell being set up. + /// - environment: The existing environment variables. Passed as an `inout` parameter because this function will + /// modify this array. + /// - useLogin: Whether or not to use a login shell. + /// - Returns: An array of args to pass to the shell executable. + /// - Throws: Errors involving filesystem operations. This function requires copying various files, which can + /// throw. Can also throw ``ShellIntegration/Error`` errors if required files are not found in the bundle. + static func setUpIntegration(for shell: Shell, environment: inout [String], useLogin: Bool) throws -> [String] { + do { + logger.debug("Setting up shell: \(shell.rawValue)") + var args: [String] = [] + + // Enable injection in our scripts. + environment.append("\(Variables.ceInjection)=1") + + switch shell { + case .bash: + try bash(&args) + case .zsh: + try zsh(&args, &environment, useLogin) + } + + if useLogin { + environment.append("\(Variables.shellLogin)=1") + } + + return args + } catch { + // catch so we can log this here + logger.error("Failed to setup shell integration: \(error.localizedDescription)") + throw error + } + } + + // MARK: - Shell Specific Setup + + /// Sets up the `bash` shell integration. + /// + /// Sets the bash `--init-file` option to point to CE's shell integration script. This script will source the + /// user's "real" init file and then install our required functions. + /// Also sets the `-i` option to initialize an interactive session. + /// + /// - Parameter args: The args to use for shell exec, will be modified by this function. + private static func bash(_ args: inout [String]) throws { + // Inject our own bash script that will execute the user's init files, then install our pre/post exec functions. + guard let scriptURL = Bundle.main.url( + forResource: "codeedit_shell_integration", + withExtension: "bash" + ) else { + throw Error.bashShellFileNotFound + } + args += ["--init-file", scriptURL.path(), "-i"] + } + + /// Sets up the `zsh` shell integration. + /// + /// Sets the zsh 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. To inject our `preexec/precmd` functions + /// we first source the user's zsh init files, then install our functions. Transparently installing our functions + /// and still using the user's init files w/o modifying anyone's rc files. + /// 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. + /// - useLogin: Whether to use a login shell. + private static func zsh(_ args: inout [String], _ environment: inout [String], _ useLogin: Bool) throws { + // Interactive, login shell. + if useLogin { + args.append("-il") + } else { + args.append("-i") + } + + // All injection script URLs + guard let profileScriptURL = Bundle.main.url( + forResource: "codeedit_shell_integration_profile", + withExtension: "zsh" + ), let envScriptURL = Bundle.main.url( + forResource: "codeedit_shell_integration_env", + withExtension: "zsh" + ), let loginScriptURL = Bundle.main.url( + forResource: "codeedit_shell_integration_login", + withExtension: "zsh" + ), let rcScriptURL = Bundle.main.url( + forResource: "codeedit_shell_integration_rc", + withExtension: "zsh" + ) else { + throw Error.zshShellFileNotFound + } + + // Make the current user here to avoid a duplicate fetch. + let currentUser = CurrentUser.getCurrentUser() + let tempDir = try makeTempDir(forShell: .zsh, user: currentUser) + + // Save any existing home dir. First getting a value from the environment. + // Falling back to the user's home dir, then ~ + let envZDotDir = environment.first(where: { $0.starts(with: "ZDOTDIR=") })?.trimmingPrefix("ZDOTDIR=") + let userZDotDir = (envZDotDir?.isEmpty ?? true) ? currentUser?.homeDir ?? "~" : String(envZDotDir ?? "") + + environment.append("\(Variables.zDotDir)=\(tempDir.path())") + environment.append("\(Variables.userZDotDir)=\(userZDotDir)") + + // Move all shell files to new temp dir + try copyFile(profileScriptURL, toDir: tempDir.appending(path: ".zprofile")) + try copyFile(envScriptURL, toDir: tempDir.appending(path: ".zshenv")) + try copyFile(loginScriptURL, toDir: tempDir.appending(path: ".zlogin")) + try copyFile(rcScriptURL, toDir: tempDir.appending(path: ".zshrc")) + } + + /// Helper function for safely copying files, removing existing ones if needed. + /// - Parameters: + /// - origin: The path of the file to copy from + /// - destination: The destination URL to copy the file to. + /// - Throws: Errors from `FileManager` operations. + private static func copyFile(_ origin: URL, toDir destination: URL) throws { + if FileManager.default.fileExists(atPath: destination.path()) { + try FileManager.default.removeItem(at: destination) + } + try FileManager.default.copyItem(at: origin, to: destination) + } + + /// Creates a temporary directory for a user/shell combination. + /// - Parameters: + /// - shell: The shell to create the directory for. + /// - user: The current user, will attempt to get the current user if none are supplied. + /// - Returns: The URL of the temporary directory. + /// - Throws: Errors from `FileManager` operations. + private static func makeTempDir(forShell shell: Shell, user: CurrentUser? = .getCurrentUser()) throws -> URL { + let username = user?.name ?? "unknown" // doesn't really matter but this is used later so might as well + + // Create a temp directory to store our init files in. + // The name of the directory is user-specific and shell-specific to avoid overlap. + let tempDir = FileManager.default.temporaryDirectory.appending( + path: "\(username)-codeedit-\(shell.rawValue)" + ) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + return tempDir + } +} diff --git a/CodeEdit/Features/TerminalEmulator/TerminalEmulatorView+Coordinator.swift b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift similarity index 92% rename from CodeEdit/Features/TerminalEmulator/TerminalEmulatorView+Coordinator.swift rename to CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift index cf88289fa..1c51eb422 100644 --- a/CodeEdit/Features/TerminalEmulator/TerminalEmulatorView+Coordinator.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift @@ -34,7 +34,7 @@ extension TerminalEmulatorView { return } source.feed(text: "Exit code: \(exitCode)\n\r\n") - source.feed(text: "To open a new session close and reopen the terminal drawer") + source.feed(text: "To open a new session, create a new terminal tab.") TerminalEmulatorView.lastTerminal[url.path] = nil } } diff --git a/CodeEdit/Features/TerminalEmulator/TerminalEmulatorView.swift b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift similarity index 66% rename from CodeEdit/Features/TerminalEmulator/TerminalEmulatorView.swift rename to CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift index 1e0fd9ded..4918aebd1 100644 --- a/CodeEdit/Features/TerminalEmulator/TerminalEmulatorView.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -92,95 +92,10 @@ struct TerminalEmulatorView: NSViewRepresentable { /// Gets the default shell from the current user and returns the string of the shell path. private func autoDetectDefaultShell() -> String { - let bufsize = sysconf(_SC_GETPW_R_SIZE_MAX) - guard bufsize != -1 else { return "/bin/bash" } - let buffer = UnsafeMutablePointer.allocate(capacity: bufsize) - defer { - buffer.deallocate() - } - var pwd = passwd() - var result: UnsafeMutablePointer? = UnsafeMutablePointer.allocate(capacity: 1) - - if getpwuid_r(getuid(), &pwd, buffer, bufsize, &result) != 0 { return "/bin/bash" } - return String(cString: pwd.pw_shell) - } - - /// Check if the source command for shell integration already exists - /// Returns true if it already exists or encountered an error, no new commands will be added to user's source file - /// Returns false if it's not there, new commands will be added to user's source file - private func shellIntegrationInstalled(sourceScriptPath: String, command: String) -> Bool { - do { - // Get user's shell's source file - let sourceScript = try String(contentsOfFile: sourceScriptPath) - let sourceScriptSeperatedByLines = sourceScript.components(separatedBy: .newlines) - // Check line by line - for line in sourceScriptSeperatedByLines where line == command { - // If one line matches the command, no new commands are needed - return true - } - // If no line matches the command, new command is needed - return false - } catch { - if let error = error as NSError? { - switch error._code { - case 260: - // If error 260 is thrown, it's just the source file is missing - // Create a new file and add new command - FileManager.default.createFile(atPath: sourceScriptPath, contents: nil, attributes: nil) - return false - default: - // Otherwise just abort the shell integration setup - print("Cannot setup shell integration, error: \(error)") - return true - } - } - } - } - - /// Configure shell integration script - private func setupShellIntegration(shell: String, environment: [String]) { - // Get user's home dir - var homePath: String = "" - environment.forEach { value in - if value.starts(with: "HOME=") { - homePath = value - } - } - homePath.removeSubrange(homePath.startIndex.. LocalProcessTerminalView { terminal.processDelegate = context.coordinator - setupSession() + do { + try setupSession() + } catch { + terminal.feed(text: "Failed to start a terminal session: \(error.localizedDescription)") + } return terminal } - func setupSession() { + func setupSession() throws { terminal.getTerminal().silentLog = true if TerminalEmulatorView.lastTerminal[url.path] == nil { - let shell = getShell() - let shellName = NSString(string: shell).lastPathComponent - onTitleChange(shellName) - let shellIdiom = "-" + shellName - // changes working directory to project root // TODO: Get rid of FileManager shared instance to prevent problems // using shared instance of FileManager might lead to problems when using @@ -273,9 +187,29 @@ struct TerminalEmulatorView: NSViewRepresentable { var terminalEnvironment: [String] = Terminal.getEnvironmentVariables() terminalEnvironment.append("TERM_PROGRAM=CodeEditApp_Terminal") - setupShellIntegration(shell: shellName, environment: terminalEnvironment) + let shellPath = getShell() + guard let shell = Shell(rawValue: NSString(string: shellPath).lastPathComponent) else { + return + } + onTitleChange(shell.rawValue) + + let shellArgs: [String] + if terminalSettings.useShellIntegration { + shellArgs = try ShellIntegration.setUpIntegration( + for: shell, + environment: &terminalEnvironment, + useLogin: terminalSettings.useLoginShell + ) + } else { + shellArgs = [] + } - terminal.startProcess(executable: shell, environment: terminalEnvironment, execName: shellIdiom) + terminal.startProcess( + executable: shellPath, + args: shellArgs, + environment: terminalEnvironment, + execName: shell.rawValue + ) terminal.font = font terminal.configureNativeColors() terminal.installColors(self.colors) diff --git a/CodeEdit/Features/TerminalEmulator/codeedit_shell_integration_login.zsh b/CodeEdit/Features/TerminalEmulator/codeedit_shell_integration_login.zsh new file mode 100644 index 000000000..ae1cc77b8 --- /dev/null +++ b/CodeEdit/Features/TerminalEmulator/codeedit_shell_integration_login.zsh @@ -0,0 +1,8 @@ +# Modified from Microsoft's VSCode. MIT License. +# Permalink to original file: +# https://github.com/microsoft/vscode/blob/60d7343892f10e0c5f09cb55a6a3f268eb0dd4fb/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-login.zsh + +ZDOTDIR=$USER_ZDOTDIR +if [[ -o "login" && -f $ZDOTDIR/.zlogin ]]; then + . $ZDOTDIR/.zlogin +fi diff --git a/CodeEdit/ShellIntegration/codeedit_shell_integration.bash b/CodeEdit/ShellIntegration/codeedit_shell_integration.bash index 236547b1c..c4cd43b9c 100644 --- a/CodeEdit/ShellIntegration/codeedit_shell_integration.bash +++ b/CodeEdit/ShellIntegration/codeedit_shell_integration.bash @@ -1,17 +1,54 @@ -#!/bin/sh - # codeedit_shell_intergration.bash # CodeEdit # # Created by Qian Qian "Cubik" (@Cubik65536) on 2023-06-13. # # This script is used to configure bash shells -# so the terminal title would be setted properly +# so the terminal title can be set properly # with shell name or program's command name # # bash-preexec.sh (https://github.com/rcaloras/bash-preexec, licensed under MIT) # is used so we can use ZSH-like 'preexec' and 'precmd' functions # +# Parts of this file also use modified versions of a source file from Microsoft's VSCode. MIT License. +# Permalink to original, licensed file: +# https://github.com/microsoft/vscode/blob/60d7343892f10e0c5f09cb55a6a3f268eb0dd4fb/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh + +# BEGIN: Modified Microsoft code + +# Prevent the script recursing when setting up +if [[ -n "${CE_SHELL_INTEGRATION}" ]]; then + builtin return +fi + +CE_SHELL_INTEGRATION=1 + +# Run relevant rc/profile only if shell integration has been injected, not when run manually +if [ "$CE_INJECTION" == "1" ]; then + if [ -z "$CE_SHELL_LOGIN" ]; then + if [ -r ~/.bashrc ]; then + . ~/.bashrc + fi + else + # Imitate -l because --init-file doesn't support it: + # run the first of these files that exists + if [ -r /etc/profile ]; then + . /etc/profile + fi + # execute the first that exists + if [ -r ~/.bash_profile ]; then + . ~/.bash_profile + elif [ -r ~/.bash_login ]; then + . ~/.bash_login + elif [ -r ~/.profile ]; then + . ~/.profile + fi + builtin unset CE_SHELL_LOGIN + fi + builtin unset CE_INJECTION +fi + +# END: Modified Microsoft code # Wrap bash-preexec.sh in a function so that, if it exits early due to having # been sourced elsewhere, it doesn't exit our entire script. diff --git a/CodeEdit/ShellIntegration/codeedit_shell_integration.zsh b/CodeEdit/ShellIntegration/codeedit_shell_integration.zsh deleted file mode 100644 index 7e5834a3c..000000000 --- a/CodeEdit/ShellIntegration/codeedit_shell_integration.zsh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/zsh - -# codeedit-shell_Integration.zsh -# CodeEdit -# -# Created by Qian Qian "Cubik" (@Cubik65536) on 2023-06-13. -# -# This script is used to configure zsh/OhMyZsh shells -# so the terminal title would be setted properly -# with shell name or program's command name -# - -autoload -Uz add-zsh-hook - -__codeedit_preexec() { - echo -n "\033]0;${1}\007" -} - -__codeedit_precmd() { - echo -n "\033]0;zsh\007" -} - -add-zsh-hook preexec __codeedit_preexec -add-zsh-hook precmd __codeedit_precmd diff --git a/CodeEdit/ShellIntegration/codeedit_shell_integration_env.zsh b/CodeEdit/ShellIntegration/codeedit_shell_integration_env.zsh new file mode 100644 index 000000000..80465c2b3 --- /dev/null +++ b/CodeEdit/ShellIntegration/codeedit_shell_integration_env.zsh @@ -0,0 +1,16 @@ +# Modified from Microsoft's VSCode. MIT License. +# Permalink to original file: +# https://github.com/microsoft/vscode/blob/60d7343892f10e0c5f09cb55a6a3f268eb0dd4fb/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh + +if [[ -f $USER_ZDOTDIR/.zshenv ]]; then + CE_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$USER_ZDOTDIR + + # prevent recursion + if [[ $USER_ZDOTDIR != $CE_ZDOTDIR ]]; then + . $USER_ZDOTDIR/.zshenv + fi + + USER_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$CE_ZDOTDIR +fi diff --git a/CodeEdit/ShellIntegration/codeedit_shell_integration_profile.zsh b/CodeEdit/ShellIntegration/codeedit_shell_integration_profile.zsh new file mode 100644 index 000000000..22eb97b2c --- /dev/null +++ b/CodeEdit/ShellIntegration/codeedit_shell_integration_profile.zsh @@ -0,0 +1,10 @@ +# Modified from Microsoft's VSCode. MIT License. +# Permalink to original file: +# https://github.com/microsoft/vscode/blob/60d7343892f10e0c5f09cb55a6a3f268eb0dd4fb/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh + +if [[ -o "login" && -f $USER_ZDOTDIR/.zprofile ]]; then + CE_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$USER_ZDOTDIR + . $USER_ZDOTDIR/.zprofile + ZDOTDIR=$CE_ZDOTDIR +fi diff --git a/CodeEdit/ShellIntegration/codeedit_shell_integration_rc.zsh b/CodeEdit/ShellIntegration/codeedit_shell_integration_rc.zsh new file mode 100644 index 000000000..127b346d0 --- /dev/null +++ b/CodeEdit/ShellIntegration/codeedit_shell_integration_rc.zsh @@ -0,0 +1,64 @@ +# codeedit-shell_Integration_rc.zsh +# CodeEdit +# +# Created by Qian Qian "Cubik" (@Cubik65536) on 2023-06-13. +# +# This script is used to configure zsh/OhMyZsh shells +# so the terminal title would be set properly +# with shell name or program's command name +# + +# Parts of this file contain modified versions of a source file from Microsoft's VSCode. MIT License. +# Permalink to original file: +# https://github.com/microsoft/vscode/blob/60d7343892f10e0c5f09cb55a6a3f268eb0dd4fb/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh + +# BEGIN: Modified Microsoft code + +# Prevent the script recursing when setting up +if [ -n "$CE_SHELL_INTEGRATION" ]; then + ZDOTDIR=$USER_ZDOTDIR + builtin return +fi + +# This variable allows the shell to both detect that VS Code's shell integration is enabled as well +# as disable it by unsetting the variable. +CE_SHELL_INTEGRATION=1 + +# By default, zsh will set the $HISTFILE to the $ZDOTDIR location automatically. In the case of the +# shell integration being injected, this means that the terminal will use a different history file +# to other terminals. To fix this issue, set $HISTFILE back to the default location before ~/.zshrc +# is called as that may depend upon the value. +if [[ "$CE_INJECTION" == "1" ]]; then + HISTFILE=$USER_ZDOTDIR/.zsh_history +fi + +# Only fix up ZDOTDIR if shell integration was injected (not manually installed) and has not been called yet +if [[ "$CE_INJECTION" == "1" ]]; then + if [[ -f $USER_ZDOTDIR/.zshrc ]]; then + CE_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$USER_ZDOTDIR + # A user's custom HISTFILE location might be set when their .zshrc file is sourced below + . $USER_ZDOTDIR/.zshrc + fi +fi + +# END: Microsoft code + +builtin autoload -Uz add-zsh-hook + +__codeedit_preexec() { + echo -n "\033]0;${1}\007" +} + +__codeedit_precmd() { + echo -n "\033]0;zsh\007" +} + +add-zsh-hook preexec __codeedit_preexec +add-zsh-hook precmd __codeedit_precmd + +# Fix ZDOTDIR + +if [[ $USER_ZDOTDIR != $CE_ZDOTDIR ]]; then + ZDOTDIR=$USER_ZDOTDIR +fi diff --git a/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift b/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift new file mode 100644 index 000000000..ba4383c5b --- /dev/null +++ b/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift @@ -0,0 +1,101 @@ +// +// ShellIntegrationTests.swift +// CodeEditTests +// +// Created by Khan Winter on 6/11/24. +// + +import Foundation +import SwiftUI +import XCTest +@testable import CodeEdit + +final class ShellIntegrationTests: XCTestCase { + func testBash() throws { + var environment: [String] = [] + let args = try ShellIntegration.setUpIntegration(for: .bash, environment: &environment, useLogin: false) + XCTAssertTrue( + environment.contains("\(ShellIntegration.Variables.ceInjection)=1"), "Does not contain injection flag" + ) + XCTAssertTrue( + !environment.contains("\(ShellIntegration.Variables.shellLogin)=1"), "Should not contain login flag" + ) + XCTAssertTrue(args.contains("--init-file"), "No init file flag") + XCTAssertTrue( + args.contains(where: { $0.hasSuffix("/codeedit_shell_integration.bash") }), "No setup file provided in args" + ) + XCTAssertTrue(args.contains("-i"), "No interactive flag found") + } + + func testBashLogin() throws { + var environment: [String] = [] + let args = try ShellIntegration.setUpIntegration(for: .bash, environment: &environment, useLogin: true) + XCTAssertTrue( + environment.contains("\(ShellIntegration.Variables.ceInjection)=1"), "Does not contain injection flag" + ) + XCTAssertTrue(environment.contains("\(ShellIntegration.Variables.shellLogin)=1"), "Does not contain login flag") + XCTAssertTrue(args.contains("--init-file"), "No init file flag") + XCTAssertTrue( + args.contains(where: { $0.hasSuffix("/codeedit_shell_integration.bash") }), "No setup file provided in args" + ) + XCTAssertTrue(args.contains("-i"), "No interactive flag found") + } + + func testZsh() throws { + var environment: [String] = [] + let args = try ShellIntegration.setUpIntegration(for: .zsh, environment: &environment, useLogin: false) + XCTAssertTrue(args.contains("-i"), "Interactive flag") + XCTAssertTrue(!args.contains("-il"), "No Interactive/Login flag") + + XCTAssertTrue( + environment.contains("\(ShellIntegration.Variables.ceInjection)=1"), "Does not contain injection flag" + ) + XCTAssertTrue( + !environment.contains("\(ShellIntegration.Variables.shellLogin)=1"), "Should not contain login flag" + ) + XCTAssertTrue(environment.contains(where: { $0.hasPrefix(ShellIntegration.Variables.zDotDir) })) + XCTAssertTrue(environment.contains(where: { $0.hasPrefix(ShellIntegration.Variables.userZDotDir) })) + // Should not use this one + XCTAssertTrue(!environment.contains(where: { $0.hasPrefix(ShellIntegration.Variables.ceZDotDir) })) + + guard var tempDir = environment.first(where: { $0.hasPrefix(ShellIntegration.Variables.zDotDir) }) else { + XCTFail("No temp dir") + return + } + // trim "ZDOTDIR=" from var + tempDir = String(tempDir.dropFirst(ShellIntegration.Variables.zDotDir.count + 1)) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appending("/.zshrc"))) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appending("/.zprofile"))) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appending("/.zlogin"))) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appending("/.zshenv"))) + } + + func testZshLogin() throws { + var environment: [String] = [] + let args = try ShellIntegration.setUpIntegration(for: .zsh, environment: &environment, useLogin: true) + XCTAssertTrue(!args.contains("-i"), "No Interactive flag") + XCTAssertTrue(args.contains("-il"), "Interactive/Login flag") + + XCTAssertTrue( + environment.contains("\(ShellIntegration.Variables.ceInjection)=1"), "Does not contain injection flag" + ) + XCTAssertTrue( + environment.contains("\(ShellIntegration.Variables.shellLogin)=1"), "Does not contain login flag" + ) + XCTAssertTrue(environment.contains(where: { $0.hasPrefix(ShellIntegration.Variables.zDotDir) })) + XCTAssertTrue(environment.contains(where: { $0.hasPrefix(ShellIntegration.Variables.userZDotDir) })) + // Should not use this one + XCTAssertTrue(!environment.contains(where: { $0.hasPrefix(ShellIntegration.Variables.ceZDotDir) })) + + guard var tempDir = environment.first(where: { $0.hasPrefix(ShellIntegration.Variables.zDotDir) }) else { + XCTFail("No temp dir") + return + } + // trim "ZDOTDIR=" from var + tempDir = String(tempDir.dropFirst(ShellIntegration.Variables.zDotDir.count + 1)) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appending("/.zshrc"))) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appending("/.zprofile"))) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appending("/.zlogin"))) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appending("/.zshenv"))) + } +} From e4a02ef5e59f1fe9d623bf4772413a2966786705 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:33:41 -0500 Subject: [PATCH 2/2] Document `CurrentUser` struct --- .../TerminalEmulator/Model/CurrentUser.swift | 9 +++++++++ .../Views/TerminalEmulatorView.swift | 15 --------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift b/CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift index dccd1e3d1..33d513859 100644 --- a/CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift +++ b/CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift @@ -7,11 +7,19 @@ import Foundation +/// Represents the currently logged in user. +/// +/// Do not initialize this struct, instead use ``CurrentUser/getCurrentUser()`` to create and fill in the information. struct CurrentUser { + /// The user's username. let name: String + /// The path to the user's shell executable. let shell: String + /// The user's home directory path. let homeDir: String + /// The users id. let uid: uid_t + /// The user's group id. let gid: gid_t private init(name: String, shell: String, homeDir: String, uid: uid_t, gid: gid_t) { @@ -22,6 +30,7 @@ struct CurrentUser { self.gid = gid } + /// Gets the current user using the `getpwuid_r` syscall. static func getCurrentUser() -> CurrentUser? { let bufsize = sysconf(_SC_GETPW_R_SIZE_MAX) guard bufsize != -1 else { return nil } diff --git a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift index 4918aebd1..8c4aa4507 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -49,21 +49,6 @@ struct TerminalEmulatorView: NSViewRepresentable { } /// Returns a string of a shell path to use - /// - /// Default implementation pulled from Example app from "SwiftTerm": - /// ```swift - /// let bufsize = sysconf(_SC_GETPW_R_SIZE_MAX) - /// guard bufsize != -1 else { return "/bin/bash" } - /// let buffer = UnsafeMutablePointer.allocate(capacity: bufsize) - /// defer { - /// buffer.deallocate() - /// } - /// var pwd = passwd() - /// var result: UnsafeMutablePointer? = UnsafeMutablePointer.allocate(capacity: 1) - /// - /// if getpwuid_r(getuid(), &pwd, buffer, bufsize, &result) != 0 { return "/bin/bash" } - /// return String(cString: pwd.pw_shell) - /// ``` private func getShell() -> String { if shellType != ""{ return shellType