diff --git a/Applite.xcodeproj/project.pbxproj b/Applite.xcodeproj/project.pbxproj index 4cceab3..ce7eee1 100644 --- a/Applite.xcodeproj/project.pbxproj +++ b/Applite.xcodeproj/project.pbxproj @@ -12,23 +12,17 @@ 41062C972A3A20F900FD48EA /* UninstallSelf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41062C962A3A20F900FD48EA /* UninstallSelf.swift */; }; 41062C992A3A263F00FD48EA /* UninstallSelfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41062C982A3A263F00FD48EA /* UninstallSelfView.swift */; }; 41062C9B2A3E4AFA00FD48EA /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41062C9A2A3E4AFA00FD48EA /* BundleExtension.swift */; }; - 411EDDD52A9F56020051E07B /* pinentry.ksh in Resources */ = {isa = PBXBuildFile; fileRef = 411EDDD22A9DD5F40051E07B /* pinentry.ksh */; }; 411EDDD72A9F58180051E07B /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411EDDD62A9F58180051E07B /* URLExtension.swift */; }; - 411EDDD92A9F7E220051E07B /* PinentryScriptHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411EDDD82A9F7E220051E07B /* PinentryScriptHash.swift */; }; - 411EDDDB2AA4A0D80051E07B /* PinentryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411EDDDA2AA4A0D80051E07B /* PinentryError.swift */; }; 4120AB652A754B1700F68EFE /* AppliteAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4120AB642A754B1700F68EFE /* AppliteAppView.swift */; }; 4120AB682A755B5A00F68EFE /* CheckForUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4120AB672A755B5A00F68EFE /* CheckForUpdatesView.swift */; }; 4125BB8A29539907000FBD25 /* PlaceholderAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4125BB8929539907000FBD25 /* PlaceholderAppView.swift */; }; 4126353E2A77C6EF00155034 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4126353D2A77C6EF00155034 /* ArrayExtension.swift */; }; 412635442A77FB1600155034 /* BrewInstallationProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412635432A77FB1600155034 /* BrewInstallationProgress.swift */; }; - 412635492A7804E700155034 /* CaskDataLoadError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412635482A7804E700155034 /* CaskDataLoadError.swift */; }; - 4126354B2A79075900155034 /* ShellResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4126354A2A79075900155034 /* ShellResult.swift */; }; 4129FFD92A7A613E00CFE392 /* Fuse in Frameworks */ = {isa = PBXBuildFile; productRef = 4129FFD82A7A613E00CFE392 /* Fuse */; }; 413E60B72BBAE5E000978F6A /* NetworkProxyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413E60B62BBAE5E000978F6A /* NetworkProxyManager.swift */; }; 413E60C02BBF0E5C00978F6A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 413E60BF2BBF0E5C00978F6A /* Kingfisher */; }; 413E60C22BBFF98A00978F6A /* AppIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413E60C12BBFF98A00978F6A /* AppIconView.swift */; }; 413F77A52972B2E70053349A /* DependencyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413F77A42972B2E70053349A /* DependencyManager.swift */; }; - 413F77A72972C8000053349A /* ShellOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413F77A62972C8000053349A /* ShellOutputStream.swift */; }; 414074F528DF53E80073EB22 /* AppliteApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 414074F428DF53E80073EB22 /* AppliteApp.swift */; }; 414074F728DF53E80073EB22 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 414074F628DF53E80073EB22 /* ContentView.swift */; }; 414074F928DF53EB0073EB22 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 414074F828DF53EB0073EB22 /* Assets.xcassets */; }; @@ -56,15 +50,17 @@ 418F332428EC8BA10023D76F /* Cask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418F332328EC8BA10023D76F /* Cask.swift */; }; 418F332628EC921D0023D76F /* CaskData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418F332528EC921D0023D76F /* CaskData.swift */; }; 4191392C29159B5C00F1D75D /* CaskDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4191392B29159B5C00F1D75D /* CaskDTO.swift */; }; + 419256062D1C546D00D9EF10 /* askpass.js in Resources */ = {isa = PBXBuildFile; fileRef = 419256052D1C546D00D9EF10 /* askpass.js */; }; + 419256082D1C734600D9EF10 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256072D1C734600D9EF10 /* Shell.swift */; }; + 4192560A2D1C9FF800D9EF10 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256092D1C9FF800D9EF10 /* StringExtension.swift */; }; + 4192560D2D1CA02C00D9EF10 /* ShellError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192560C2D1CA02C00D9EF10 /* ShellError.swift */; }; + 4192560F2D1CC09500D9EF10 /* DependencyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192560E2D1CC09500D9EF10 /* DependencyError.swift */; }; 419506A42964A27F00FE5802 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A32964A27F00FE5802 /* SetupView.swift */; }; 419506A62964A5EF00FE5802 /* BrewPathSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */; }; 4196C8F528F9CB2600EADDDA /* DiscoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C8F428F9CB2600EADDDA /* DiscoverView.swift */; }; 4196C8F928F9CDF700EADDDA /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C8F828F9CDF700EADDDA /* DownloadView.swift */; }; 4196C8FE28F9E13600EADDDA /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C8FD28F9E13600EADDDA /* UpdateView.swift */; }; 4196C90028F9E1F400EADDDA /* InstalledView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C8FF28F9E1F400EADDDA /* InstalledView.swift */; }; - 4196C90428FC03A900EADDDA /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C90328FC03A900EADDDA /* Shell.swift */; }; - 41A261E92ABCB2C3007B65D3 /* DependencyInstallationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A261E82ABCB2C3007B65D3 /* DependencyInstallationError.swift */; }; - 41B731372A8789D4008BF6B9 /* ImportCasks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41B731362A8789D4008BF6B9 /* ImportCasks.swift */; }; 41B731392A879353008BF6B9 /* ActiveTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41B731382A879353008BF6B9 /* ActiveTasksView.swift */; }; 41C8FA292A7A598B000BB9A2 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 41C8FA282A7A598B000BB9A2 /* Sparkle */; }; 41DF006429EAA094004EB7AE /* SendNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41DF006329EAA094004EB7AE /* SendNotification.swift */; }; @@ -77,21 +73,15 @@ 41062C962A3A20F900FD48EA /* UninstallSelf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UninstallSelf.swift; sourceTree = ""; }; 41062C982A3A263F00FD48EA /* UninstallSelfView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UninstallSelfView.swift; sourceTree = ""; }; 41062C9A2A3E4AFA00FD48EA /* BundleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtension.swift; sourceTree = ""; }; - 411EDDD22A9DD5F40051E07B /* pinentry.ksh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = pinentry.ksh; sourceTree = ""; }; 411EDDD62A9F58180051E07B /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; - 411EDDD82A9F7E220051E07B /* PinentryScriptHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinentryScriptHash.swift; sourceTree = ""; }; - 411EDDDA2AA4A0D80051E07B /* PinentryError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinentryError.swift; sourceTree = ""; }; 4120AB642A754B1700F68EFE /* AppliteAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppliteAppView.swift; sourceTree = ""; }; 4120AB672A755B5A00F68EFE /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = ""; }; 4125BB8929539907000FBD25 /* PlaceholderAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAppView.swift; sourceTree = ""; }; 4126353D2A77C6EF00155034 /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = ""; }; 412635432A77FB1600155034 /* BrewInstallationProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewInstallationProgress.swift; sourceTree = ""; }; - 412635482A7804E700155034 /* CaskDataLoadError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskDataLoadError.swift; sourceTree = ""; }; - 4126354A2A79075900155034 /* ShellResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellResult.swift; sourceTree = ""; }; 413E60B62BBAE5E000978F6A /* NetworkProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProxyManager.swift; sourceTree = ""; }; 413E60C12BBFF98A00978F6A /* AppIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconView.swift; sourceTree = ""; }; 413F77A42972B2E70053349A /* DependencyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyManager.swift; sourceTree = ""; }; - 413F77A62972C8000053349A /* ShellOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellOutputStream.swift; sourceTree = ""; }; 414074F128DF53E80073EB22 /* Applite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Applite.app; sourceTree = BUILT_PRODUCTS_DIR; }; 414074F428DF53E80073EB22 /* AppliteApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppliteApp.swift; sourceTree = ""; }; 414074F628DF53E80073EB22 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -119,6 +109,11 @@ 418F332328EC8BA10023D76F /* Cask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cask.swift; sourceTree = ""; }; 418F332528EC921D0023D76F /* CaskData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskData.swift; sourceTree = ""; }; 4191392B29159B5C00F1D75D /* CaskDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskDTO.swift; sourceTree = ""; }; + 419256052D1C546D00D9EF10 /* askpass.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = askpass.js; sourceTree = ""; }; + 419256072D1C734600D9EF10 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = ""; }; + 419256092D1C9FF800D9EF10 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; + 4192560C2D1CA02C00D9EF10 /* ShellError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellError.swift; sourceTree = ""; }; + 4192560E2D1CC09500D9EF10 /* DependencyError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyError.swift; sourceTree = ""; }; 419506A32964A27F00FE5802 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPathSelectorView.swift; sourceTree = ""; }; 419506A729696A5300FE5802 /* Applite-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Applite-Info.plist"; sourceTree = SOURCE_ROOT; }; @@ -127,9 +122,6 @@ 4196C8FD28F9E13600EADDDA /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; 4196C8FF28F9E1F400EADDDA /* InstalledView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledView.swift; sourceTree = ""; }; 4196C90128FAF57A00EADDDA /* AppliteDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AppliteDebug.entitlements; sourceTree = ""; }; - 4196C90328FC03A900EADDDA /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = ""; }; - 41A261E82ABCB2C3007B65D3 /* DependencyInstallationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyInstallationError.swift; sourceTree = ""; }; - 41B731362A8789D4008BF6B9 /* ImportCasks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportCasks.swift; sourceTree = ""; }; 41B731382A879353008BF6B9 /* ActiveTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveTasksView.swift; sourceTree = ""; }; 41DF006329EAA094004EB7AE /* SendNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendNotification.swift; sourceTree = ""; }; BD7546562A868DA30083996B /* Localizable.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -170,10 +162,8 @@ 4126353F2A77C71F00155034 /* Shell */ = { isa = PBXGroup; children = ( - 4196C90328FC03A900EADDDA /* Shell.swift */, - 413F77A62972C8000053349A /* ShellOutputStream.swift */, - 4126354A2A79075900155034 /* ShellResult.swift */, - 411EDDD82A9F7E220051E07B /* PinentryScriptHash.swift */, + 419256072D1C734600D9EF10 /* Shell.swift */, + 4192560C2D1CA02C00D9EF10 /* ShellError.swift */, ); path = Shell; sourceTree = ""; @@ -184,6 +174,7 @@ 41062C9A2A3E4AFA00FD48EA /* BundleExtension.swift */, 4126353D2A77C6EF00155034 /* ArrayExtension.swift */, 411EDDD62A9F58180051E07B /* URLExtension.swift */, + 419256092D1C9FF800D9EF10 /* StringExtension.swift */, ); path = Extensions; sourceTree = ""; @@ -204,6 +195,7 @@ children = ( 413F77A42972B2E70053349A /* DependencyManager.swift */, 412635432A77FB1600155034 /* BrewInstallationProgress.swift */, + 4192560E2D1CC09500D9EF10 /* DependencyError.swift */, ); path = "Brew Installation"; sourceTree = ""; @@ -288,8 +280,8 @@ 41483CC9290F047A00BB10C2 /* Resources */ = { isa = PBXGroup; children = ( + 419256052D1C546D00D9EF10 /* askpass.js */, 41857B722911A2F2004A1894 /* categories.json */, - 411EDDD22A9DD5F40051E07B /* pinentry.ksh */, ); path = Resources; sourceTree = ""; @@ -301,9 +293,6 @@ 418F332328EC8BA10023D76F /* Cask.swift */, 4166EE7C28F73B2300CE305A /* BrewAnalytics.swift */, 4191392B29159B5C00F1D75D /* CaskDTO.swift */, - 412635482A7804E700155034 /* CaskDataLoadError.swift */, - 411EDDDA2AA4A0D80051E07B /* PinentryError.swift */, - 41A261E82ABCB2C3007B65D3 /* DependencyInstallationError.swift */, ); path = "Cask Data"; sourceTree = ""; @@ -388,7 +377,6 @@ isa = PBXGroup; children = ( 4178CF912A8689AF0037F270 /* ExportCasks.swift */, - 41B731362A8789D4008BF6B9 /* ImportCasks.swift */, 4155639F2A9265CE00AE2F2E /* CaskExportType.swift */, ); path = "Import Export"; @@ -429,7 +417,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1530; + LastUpgradeCheck = 1620; TargetAttributes = { 414074F028DF53E80073EB22 = { CreatedOnToolsVersion = 14.0; @@ -470,11 +458,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 411EDDD52A9F56020051E07B /* pinentry.ksh in Resources */, 414074FC28DF53EB0073EB22 /* Preview Assets.xcassets in Resources */, 41857B732911A2F2004A1894 /* categories.json in Resources */, BD7546572A868DA30083996B /* Localizable.xcstrings in Resources */, 414074F928DF53EB0073EB22 /* Assets.xcassets in Resources */, + 419256062D1C546D00D9EF10 /* askpass.js in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -489,7 +477,6 @@ 415563A42A98C54300AE2F2E /* AppdirSelectorView.swift in Sources */, 4140750528DF5FA60073EB22 /* AppView.swift in Sources */, 418F332428EC8BA10023D76F /* Cask.swift in Sources */, - 41A261E92ABCB2C3007B65D3 /* DependencyInstallationError.swift in Sources */, 4126353E2A77C6EF00155034 /* ArrayExtension.swift in Sources */, 4166EE7028F5D4C900CE305A /* Commands.swift in Sources */, 4166EE7D28F73B2300CE305A /* BrewAnalytics.swift in Sources */, @@ -498,11 +485,11 @@ 4189CE41293C980E009C836D /* BigButtonStyle.swift in Sources */, 41DF006429EAA094004EB7AE /* SendNotification.swift in Sources */, 41857B752912D94A004A1894 /* CategoryView.swift in Sources */, - 411EDDDB2AA4A0D80051E07B /* PinentryError.swift in Sources */, 4196C8F528F9CB2600EADDDA /* DiscoverView.swift in Sources */, 4125BB8A29539907000FBD25 /* PlaceholderAppView.swift in Sources */, 413F77A52972B2E70053349A /* DependencyManager.swift in Sources */, 418989B42A35D67C004AC23B /* isCommandLineToolsInstalled.swift in Sources */, + 4192560F2D1CC09500D9EF10 /* DependencyError.swift in Sources */, 419506A42964A27F00FE5802 /* SetupView.swift in Sources */, 41524B99295E352200D0046A /* SettingsView.swift in Sources */, 415563A02A9265CE00AE2F2E /* CaskExportType.swift in Sources */, @@ -511,25 +498,21 @@ 4196C8FE28F9E13600EADDDA /* UpdateView.swift in Sources */, 41062C992A3A263F00FD48EA /* UninstallSelfView.swift in Sources */, 414074F528DF53E80073EB22 /* AppliteApp.swift in Sources */, - 412635492A7804E700155034 /* CaskDataLoadError.swift in Sources */, 41062C9B2A3E4AFA00FD48EA /* BundleExtension.swift in Sources */, 418989AF2A33B65A004AC23B /* SmallProgressView.swift in Sources */, 41B731392A879353008BF6B9 /* ActiveTasksView.swift in Sources */, 411EDDD72A9F58180051E07B /* URLExtension.swift in Sources */, 419506A62964A5EF00FE5802 /* BrewPathSelectorView.swift in Sources */, + 4192560A2D1C9FF800D9EF10 /* StringExtension.swift in Sources */, 4191392C29159B5C00F1D75D /* CaskDTO.swift in Sources */, 413E60C22BBFF98A00978F6A /* AppIconView.swift in Sources */, - 4196C90428FC03A900EADDDA /* Shell.swift in Sources */, - 413F77A72972C8000053349A /* ShellOutputStream.swift in Sources */, 418989AD2A33A5C4004AC23B /* BrewManagementView.swift in Sources */, 418989B22A35D651004AC23B /* isBrewPathValid.swift in Sources */, + 4192560D2D1CA02C00D9EF10 /* ShellError.swift in Sources */, 41062C952A3794EA00FD48EA /* BrewPaths.swift in Sources */, 41483CCD29101C9900BB10C2 /* Category.swift in Sources */, 418F331C28EB3D540023D76F /* AppGridView.swift in Sources */, 41062C972A3A20F900FD48EA /* UninstallSelf.swift in Sources */, - 41B731372A8789D4008BF6B9 /* ImportCasks.swift in Sources */, - 4126354B2A79075900155034 /* ShellResult.swift in Sources */, - 411EDDD92A9F7E220051E07B /* PinentryScriptHash.swift in Sources */, 418F332628EC921D0023D76F /* CaskData.swift in Sources */, 4178CF922A8689AF0037F270 /* ExportCasks.swift in Sources */, 413E60B72BBAE5E000978F6A /* NetworkProxyManager.swift in Sources */, @@ -537,6 +520,7 @@ 4120AB682A755B5A00F68EFE /* CheckForUpdatesView.swift in Sources */, 4120AB652A754B1700F68EFE /* AppliteAppView.swift in Sources */, 412635442A77FB1600155034 /* BrewInstallationProgress.swift in Sources */, + 419256082D1C734600D9EF10 /* Shell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Applite.xcodeproj/xcshareddata/xcschemes/Applite.xcscheme b/Applite.xcodeproj/xcshareddata/xcschemes/Applite.xcscheme index e4ff707..5760b65 100755 --- a/Applite.xcodeproj/xcshareddata/xcschemes/Applite.xcscheme +++ b/Applite.xcodeproj/xcshareddata/xcschemes/Applite.xcscheme @@ -1,6 +1,6 @@ String { + replacingOccurrences(of: "\\\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression) + } +} diff --git a/Applite/Model/Cask Data/Cask.swift b/Applite/Model/Cask Data/Cask.swift index 65af6a0..9f27a1d 100755 --- a/Applite/Model/Cask Data/Cask.swift +++ b/Applite/Model/Cask Data/Cask.swift @@ -18,8 +18,6 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { /// Short description let description: String let homepageURL: URL? - @Published var isInstalled: Bool = false - @Published var isOutdated: Bool = false /// Number of downloads in the last 365 days var downloadsIn365days: Int = 0 /// Description of any caveats with the app @@ -35,7 +33,13 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { case success case failed(output: String) } - + + @MainActor + @Published var isInstalled: Bool = false + + @MainActor + @Published var isOutdated: Bool = false + /// Progress state of the cask when installing, updating or uninstalling @MainActor @Published public var progressState: ProgressState = .idle @@ -45,7 +49,7 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { category: String(describing: Cask.self) ) - required init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let rawData = try? CaskDTO(from: decoder) let homepage: String = rawData?.homepage ?? "https://brew.sh/" @@ -58,7 +62,7 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { self.pkgInstaller = rawData?.url.hasSuffix("pkg") ?? false } - required init() { + init() { self.id = "test" self.name = "Test app" self.description = "An application to test this application" @@ -72,64 +76,63 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { /// - Parameters: /// - force: If `true` install will be run with the `--force` flag /// - Returns: `Void` - @discardableResult - func install(caskData: CaskData, force: Bool = false) async -> ShellResult { + func install(caskData: CaskData, force: Bool = false) async { defer { resetProgressState(caskData: caskData) } Self.logger.info("Cask \"\(self.id)\" installation started") - - // Check if pinentry is installed - guard ((try? await checkPinentry()) != nil) else { - return ShellResult(output: "Pinentry check error", didFail: true) - } - var cancellables = Set() - let shellOutputStream = ShellOutputStream() + // Appdir argument let appdirOn = UserDefaults.standard.bool(forKey: Preferences.appdirOn.rawValue) let appdirPath = UserDefaults.standard.string(forKey: Preferences.appdirPath.rawValue) let appdirArgument = "--appdir=\"\(appdirPath ?? "/Applications")\"" - + + // Install command + let command = "\(BrewPaths.currentBrewExecutable) install --cask \(force ? "--force" : "") \(self.id) \(appdirOn ? appdirArgument : "")" + + // Setup progress await MainActor.run { self.progressState = .busy(withTask: "") caskData.busyCasks.insert(self) } - - shellOutputStream.outputPublisher - .sink { output in - Task { - await MainActor.run { self.progressState = self.parseBrewInstall(output: output) } + + var completeOutput = "" + + // Run install command and stream output + do { + for try await line in Shell.stream(command) { + completeOutput += line + + await MainActor.run { + self.progressState = self.parseBrewInstall(output: line) } } - .store(in: &cancellables) - - let result = await shellOutputStream.run("\(BrewPaths.currentBrewExecutable) install --cask \(force ? "--force" : "") \(self.id) \(appdirOn ? appdirArgument : "")") - - if result.didFail { - Self.logger.error("Failed to install cask \(self.id). Output: \(result.output)") - + } catch { + Self.logger.error("Failed to install cask \(self.id).") + + // Capture output + let output = completeOutput + await MainActor.run { - progressState = .failed(output: result.output) + progressState = .failed(output: output) caskData.busyCasks.remove(self) } - + sendNotification(title: String(localized: "Failed to download \(self.name)"), reason: .failure) - } else { - Self.logger.info("Successfully installed cask \(self.id)") - - sendNotification(title: String(localized: "\(self.name) successfully installed!"), reason: .success) - - await MainActor.run { - progressState = .success - self.isInstalled = true - } - - // Show success for 2 seconds - try? await Task.sleep(for: .seconds(2)) } - - return result + + Self.logger.info("Successfully installed cask \(self.id)") + + sendNotification(title: String(localized: "\(self.name) successfully installed!"), reason: .success) + + await MainActor.run { + progressState = .success + self.isInstalled = true + } + + // Show success for 2 seconds + try? await Task.sleep(for: .seconds(2)) } /// Parses the shell output when installing a cask @@ -157,9 +160,7 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { /// - Parameters: /// - caskData: ``CaskData`` object /// - zap: If true the app will be uninstalled completely using the brew --zap flag - /// - Returns: Bool - Whether the task has failed or not - @discardableResult - func uninstall(caskData: CaskData, zap: Bool = false) async -> Bool { + func uninstall(caskData: CaskData, zap: Bool = false) async { defer { resetProgressState(caskData: caskData) } @@ -170,18 +171,19 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { let arguments: [String] = if zap { ["--zap", self.id] } else { [self.id] } - return await runBrewCommand(command: "uninstall", - arguments: arguments, - taskDescription: "Uninstalling", - notificationSuccess: String(localized:"\(self.name) successfully uninstalled"), - notificationFailure: "Failed to uninstall \(self.name)", - onSuccess: { self.isInstalled = false }) + await runBrewCommand( + command: "uninstall", + arguments: arguments, + taskDescription: "Uninstalling", + notificationSuccess: String(localized:"\(self.name) successfully uninstalled"), + notificationFailure: "Failed to uninstall \(self.name)", + onSuccess: { self.isInstalled = false } + ) } /// Updates the cask /// - Returns: Bool - Whether the task has failed or not - @discardableResult - func update(caskData: CaskData) async -> Bool { + func update(caskData: CaskData) async { defer { resetProgressState(caskData: caskData) } @@ -190,25 +192,21 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { caskData.busyCasks.insert(self) } - return await runBrewCommand(command: "upgrade", - arguments: [self.id], - taskDescription: "Updating", - notificationSuccess: String(localized: "\(self.name) successfully updated"), - notificationFailure: String(localized: "Failed to update \(self.name)"), - onSuccess: { - Task { - await MainActor.run { - self.isOutdated = false - caskData.outdatedCasks.remove(self) - } - } + await runBrewCommand( + command: "upgrade", + arguments: [self.id], + taskDescription: "Updating", + notificationSuccess: String(localized: "\(self.name) successfully updated"), + notificationFailure: String(localized: "Failed to update \(self.name)"), + onSuccess: { + self.isOutdated = false + caskData.outdatedCasks.remove(self) }) } /// Updates the cask /// - Returns: Bool - Whether the task has failed or not - @discardableResult - func reinstall(caskData: CaskData) async -> Bool { + func reinstall(caskData: CaskData) async { defer { resetProgressState(caskData: caskData) } @@ -217,19 +215,16 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { caskData.busyCasks.insert(self) } - return await runBrewCommand(command: "reinstall", - arguments: [self.id], - taskDescription: "Reinstalling", - notificationSuccess: String(localized: "\(self.name) successfully reinstalled"), - notificationFailure: String(localized:"Failed to reinstall \(self.name)"), - onSuccess: { - - Task { - await MainActor.run { - caskData.busyCasks.remove(self) - } + await runBrewCommand( + command: "reinstall", + arguments: [self.id], + taskDescription: "Reinstalling", + notificationSuccess: String(localized: "\(self.name) successfully reinstalled"), + notificationFailure: String(localized:"Failed to reinstall \(self.name)"), + onSuccess: { + caskData.busyCasks.remove(self) } - }) + ) } /// Runs a shell command with the currently selected brew path @@ -242,46 +237,50 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { /// - notificationFailure: Notification message if fails /// - onSuccess: Closure run if task succeeds /// - Returns: Bool - Whether the the task has failed or not - private func runBrewCommand(command: String, arguments: [String], taskDescription: String, - notificationSuccess: String, notificationFailure: String, onSuccess: (() -> Void)? = nil) async -> Bool { - - // Check if pinentry is installed - guard ((try? await checkPinentry()) != nil) else { - return true - } - + private func runBrewCommand( + command: String, + arguments: [String], + taskDescription: String, + notificationSuccess: String, + notificationFailure: String, + onSuccess: (@MainActor () -> Void)? = nil + ) async { await MainActor.run { let localizedTaskDescription = String.LocalizationValue(stringLiteral: taskDescription) self.progressState = .busy(withTask: String(localized: localizedTaskDescription)) } - - let result = await shell("HOMEBREW_NO_AUTO_UPDATE=1 \(BrewPaths.currentBrewExecutable) \(command) --cask \(arguments.joined(separator: " "))") - - if !result.didFail && onSuccess != nil { + + let command = "HOMEBREW_NO_AUTO_UPDATE=1 \(BrewPaths.currentBrewExecutable) \(command) --cask \(arguments.joined(separator: " "))" + + var output: String = "" + + do { + output = try await Shell.runAsync(command) + } catch { + Self.logger.error("Failed to run brew command: \(error.localizedDescription)") + + sendNotification(title: notificationFailure, reason: .failure) + + await MainActor.run { self.progressState = .failed(output: error.localizedDescription) } + } + + if let onSuccess { await MainActor.run { - onSuccess?() + onSuccess() } } // Log and Notify - if result.didFail { - Self.logger.error("Failed to run brew command \"\(command)\" with arguments \"\(arguments)\", output: \(result.output)") - - sendNotification(title: notificationFailure, reason: .failure) - await MainActor.run { self.progressState = .failed(output: result.output) } - } else { - Self.logger.notice("Successfully run brew command \"\(command)\" with arguments \"\(arguments)\", output: \(result.output)") - - sendNotification(title: notificationSuccess, reason: .success) - await MainActor.run { self.progressState = .success } - try? await Task.sleep(for: .seconds(2)) - } - - return result.didFail + Self.logger.notice("Successfully run brew command \"\(command)\" with arguments \"\(arguments)\", output: \(output)") + + sendNotification(title: notificationSuccess, reason: .success) + + // Show success for 2 seconds + await MainActor.run { self.progressState = .success } + try? await Task.sleep(for: .seconds(2)) } - - @discardableResult - public func launchApp() -> ShellResult { + + public func launchApp() throws { let appPath: String if self.pkgInstaller { @@ -305,39 +304,8 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject { appPath = "\(brewDirectory.replacingOccurrences(of: " ", with: "\\ ") )/Caskroom/\(self.id)/*/*.app" } - - let result = shell("open \(appPath)") - - if result.didFail { - Self.logger.error("Couldn't launch app at path: \(appPath). Output: \(result.output)") - } - - return result - } - - /// Checks if pinentry-mac is installed, if not it tries it install it - private func checkPinentry() async throws { - if self.pkgInstaller { - do { - await MainActor.run { - progressState = .busy(withTask: "Preparing") - } - - if await BrewPaths.isPinentryInstalled() { return } - - Self.logger.notice("pinentry-mac is not installed. Installing now...") - - try await DependencyManager.installPinentry(forceInstall: true) - } catch { - Self.logger.error("Cask: Application has PKG installer. Pinentry not installed. Installation attempt failed.") - - await MainActor.run { - progressState = .failed(output: "Application has a PKG installer that requires an admin password. Pinentry was not installed and the installation attempt failed.") - } - - throw PinentryError.installError - } - } + + try Shell.run("open \(appPath)") } /// Resets progress state and removes self from ``CaskData.busyCasks`` diff --git a/Applite/Model/Cask Data/CaskData.swift b/Applite/Model/Cask Data/CaskData.swift index 2f6ee53..98ebd88 100755 --- a/Applite/Model/Cask Data/CaskData.swift +++ b/Applite/Model/Cask Data/CaskData.swift @@ -69,13 +69,7 @@ final class CaskData: ObservableObject { await cacheData(data: caskData, to: Self.caskCacheURL) // Decode data - do { - return try JSONDecoder().decode([Cask].self, from: caskData) - } - catch { - await Self.logger.error("Failed to parse cask data, error: \(error.localizedDescription)") - throw CaskDataLoadError.decodeError - } + return try JSONDecoder().decode([Cask].self, from: caskData) } /// Gets cask analytics information from the Homebrew API and decodes it into a dictionary @@ -106,13 +100,7 @@ final class CaskData: ObservableObject { let analyticsDecoded: BrewAnalytics // Decode data - do { - analyticsDecoded = try JSONDecoder().decode(BrewAnalytics.self, from: analyticsData) - } - catch { - await Self.logger.error("Failed to parse cask data, error: \(error.localizedDescription)") - throw CaskDataLoadError.decodeError - } + analyticsDecoded = try JSONDecoder().decode(BrewAnalytics.self, from: analyticsData) // Convert analytics to a cask ID to download count dictionary let analyticsDict: BrewAnalyticsDictionary = Dictionary(uniqueKeysWithValues: analyticsDecoded.items.map { @@ -126,32 +114,26 @@ final class CaskData: ObservableObject { /// - Returns: A list of Cask ID's @Sendable func getInstalledCasks() async throws -> [String] { - let result = await shell("\(BrewPaths.currentBrewExecutable) list --cask") - - if result.didFail { - await Self.logger.error("Couldn't get installed apps. Shell output: \(result.output)") - throw CaskDataLoadError.shellError - } - - if result.output.isEmpty { + let output = try await Shell.runAsync("\(BrewPaths.currentBrewExecutable) list --cask") + + if output.isEmpty { await Self.logger.notice("No installed casks were found") } - return result.output.components(separatedBy: "\n") + return output + .trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: "\n") } /// Gets the list of outdated casks /// - Returns: A list of Cask ID's @Sendable func getOutdatedCasks() async throws -> [String] { - let result = await shell("\(BrewPaths.currentBrewExecutable) outdated --cask -q") - - if result.didFail { - await Self.logger.error("Couldn't get outdated apps. Shell output: \(result.output)") - throw CaskDataLoadError.shellError - } - - return result.output.components(separatedBy: "\n") + let output = try await Shell.runAsync("\(BrewPaths.currentBrewExecutable) outdated --cask -q") + + return output + .trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: "\n") } /// Saves ``Data`` objects to cache @@ -172,6 +154,7 @@ final class CaskData: ObservableObject { } } catch { await Self.logger.error("Cound't create cache directory") + return } // Save data to cache @@ -186,13 +169,9 @@ final class CaskData: ObservableObject { /// - Returns: A ``Data`` object @Sendable func loadDataFromCache(dataURL: URL) async throws -> Data { - do { - let data = try Data(contentsOf: dataURL) - - return data - } catch { - throw CaskDataLoadError.cacheError - } + let data = try Data(contentsOf: dataURL) + + return data } /// Filters casks into a category to casks dictionary @@ -253,10 +232,13 @@ final class CaskData: ObservableObject { (casksByCategory, casksByCategoryCoupled) = fillCategoryDicts() } - func refreshOutdatedApps(greedy: Bool = false) async -> Void { - let outdatedCaskIDs = await shell("\(BrewPaths.currentBrewExecutable) outdated --cask \(greedy ? "-g" : "") -q").output + func refreshOutdatedApps(greedy: Bool = false) async throws -> Void { + let output = try await Shell.runAsync("\(BrewPaths.currentBrewExecutable) outdated --cask \(greedy ? "-g" : "") -q") + + let outdatedCaskIDs = output + .trimmingCharacters(in: .whitespacesAndNewlines) .components(separatedBy: "\n") - .filter({ $0.count > 0 }) // Remove empty strings + .filter({ !$0.isEmpty }) // Remove empty strings .map({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) // Trim whitespace for i in self.casks.indices { diff --git a/Applite/Model/Cask Data/CaskDataLoadError.swift b/Applite/Model/Cask Data/CaskDataLoadError.swift deleted file mode 100755 index 5d879b0..0000000 --- a/Applite/Model/Cask Data/CaskDataLoadError.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CaskDataLoadError.swift -// Applite -// -// Created by Milán Várady on 2023. 07. 31.. -// - -import Foundation -import SwiftUI - -enum CaskDataLoadError: Error { - case loadError - case cacheError - case decodeError - case shellError - case concurrencyError -} - -extension CaskDataLoadError: LocalizedError { - var errorDescription: String? { - switch self { - case .loadError: return String(localized: "Load error") - case .cacheError: return String(localized: "Failed to load cask data from cache") - case .decodeError: return String(localized: "Failed to decode json data from the Homebrew API") - case .shellError: return String(localized: "Faild to retrieve cask information from selected brew executable") - case .concurrencyError: return String(localized: "Concurrency error") - } - } -} - diff --git a/Applite/Model/Cask Data/DependencyInstallationError.swift b/Applite/Model/Cask Data/DependencyInstallationError.swift deleted file mode 100644 index d4f8158..0000000 --- a/Applite/Model/Cask Data/DependencyInstallationError.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// DependencyInstallationError.swift -// Applite -// -// Created by Milán Várady on 2023. 09. 21.. -// - -import Foundation - -/// Installation errors -enum DependencyInstallationError: Error { - case CommandLineToolsError - case DirectoryError - case BrewFetchError - case PinentryError -} diff --git a/Applite/Model/Cask Data/PinentryError.swift b/Applite/Model/Cask Data/PinentryError.swift deleted file mode 100644 index db7c7c9..0000000 --- a/Applite/Model/Cask Data/PinentryError.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// PinentryError.swift -// Applite -// -// Created by Milán Várady on 2023. 09. 03.. -// - -import Foundation - -enum PinentryError: Error { - case installError -} diff --git a/Applite/Resources/askpass.js b/Applite/Resources/askpass.js new file mode 100755 index 0000000..7f4f14c --- /dev/null +++ b/Applite/Resources/askpass.js @@ -0,0 +1,20 @@ +#!/usr/bin/env osascript -l JavaScript + +ObjC.import('stdlib') + +const app = Application.currentApplication() +app.includeStandardAdditions = true + +const result = app.displayDialog('Applite needs privileged access to complete the current task.\n\nPlease enter your password to allow this:', { + defaultAnswer: '', + withIcon: 'caution', + buttons: ['Cancel', 'Ok'], + defaultButton: 'Ok', + hiddenAnswer: true, +}) + +if (result.buttonReturned === 'Ok') { + result.textReturned +} else { + $.exit(255) +} diff --git a/Applite/Resources/pinentry.ksh b/Applite/Resources/pinentry.ksh deleted file mode 100755 index b8e15b0..0000000 --- a/Applite/Resources/pinentry.ksh +++ /dev/null @@ -1,7 +0,0 @@ -#! /bin/ksh - -# Add homebrew bin directories to path -typeset PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/Library/Application Support/Applite/homebrew/bin:${PATH}" - -# Prompt user for password and return it -printf "%s\n" "SETOK OK" "SETCANCEL Cancel" "SETDESC Applite needs your admin password to complete the task" "SETPROMPT Enter Password:" "SETTITLE Applite Password Request" "GETPIN" | /usr/bin/env pinentry-mac --no-global-grab --timeout 60 | /usr/bin/awk '/^D / {print substr($0, index($0, $2))}' diff --git a/Applite/Utilities/Brew Installation/BrewInstallationProgress.swift b/Applite/Utilities/Brew Installation/BrewInstallationProgress.swift index f108ff1..cb8f566 100755 --- a/Applite/Utilities/Brew Installation/BrewInstallationProgress.swift +++ b/Applite/Utilities/Brew Installation/BrewInstallationProgress.swift @@ -10,13 +10,13 @@ import Foundation public enum InstallPhase: Int { case waitingForXcodeCommandLineTools = 0 case fetchingHomebrew = 1 - case installingPinentry = 2 - case done = 3 + case done = 2 } /// Keeps track of current brew installation progress /// /// Used by the ``DependencyManager`` struct -public final class BrewInstallationProgress: ObservableObject { +@MainActor +final class BrewInstallationProgress: ObservableObject { @Published var phase: InstallPhase = .waitingForXcodeCommandLineTools } diff --git a/Applite/Utilities/Brew Installation/DependencyError.swift b/Applite/Utilities/Brew Installation/DependencyError.swift new file mode 100644 index 0000000..58a0398 --- /dev/null +++ b/Applite/Utilities/Brew Installation/DependencyError.swift @@ -0,0 +1,21 @@ +// +// DependencyError.swift +// Applite +// +// Created by Milán Várady on 2024.12.25. +// + +import Foundation + +enum DependencyError: Error { + case xcodeCommandLineToolsTimeout +} + +extension DependencyError: LocalizedError { + public var errorDescription: String? { + switch self { + case .xcodeCommandLineToolsTimeout: + return "Xcode Command Line Tools install timeout" + } + } +} diff --git a/Applite/Utilities/Brew Installation/DependencyManager.swift b/Applite/Utilities/Brew Installation/DependencyManager.swift index 0acac4f..8e92149 100755 --- a/Applite/Utilities/Brew Installation/DependencyManager.swift +++ b/Applite/Utilities/Brew Installation/DependencyManager.swift @@ -8,17 +8,17 @@ import Foundation import os -/// Installs app dependecies: Homebrew, Xcode Command Line Tools and PINEntry +/// Installs app dependecies: Homebrew and Xcode Command Line Tools /// /// Reports the current progress of the installation through a ``BrewInstallationProgress`` observable object -public struct DependencyManager { +struct DependencyManager { private static let logger = Logger( subsystem: Bundle.main.bundleIdentifier!, category: String(describing: DependencyManager.self) ) /// Message shown when brew path is broken - static public var brokenPathOrIstallMessage = "Error. Broken brew path, or damaged installation. Check brew path in settings, or try reinstalling Homebrew (Manage Homebrew->Reinstall)" + static var brokenPathOrIstallMessage = "Error. Broken brew path, or damaged installation. Check brew path in settings, or try reinstalling Homebrew (Manage Homebrew->Reinstall)" /// Installs dependencies to `~/Library/Application Support/Applite/homebrew/` /// @@ -27,21 +27,14 @@ public struct DependencyManager { /// - keepCurrentInstall: (default: `false`) If `true`, then if a brew installation already exists it won't be deleted and reinstalled /// /// - Returns: `Void` - @MainActor - static func install(progressObject: BrewInstallationProgress, keepCurrentInstall: Bool = false) async throws -> Void { + static func install(progressObject: BrewInstallationProgress, keepCurrentInstall: Bool = false) async throws { Self.logger.info("Brew installation started") // Install command line tools if !isCommandLineToolsInstalled() { Self.logger.info("Prompting user to install Xcode Command Line Tools") - - let result = await shell("xcode-select --install") - - if result.didFail { - Self.logger.error("Failed to request Xcode Command Line Tools install") - Self.logger.error("\(result.output)") - throw DependencyInstallationError.CommandLineToolsError - } + + try await Shell.runAsync("xcode-select --install") // Wait for command line tools installation with a 30 minute timeout var didBreak = false @@ -57,99 +50,51 @@ public struct DependencyManager { if !didBreak { Self.logger.error("Command Line Tools Install timeout") - throw DependencyInstallationError.CommandLineToolsError + throw DependencyError.xcodeCommandLineToolsTimeout } } else { Self.logger.info("Xcode Command Line Tool are already installed. Skipping...") } // Skip Homebrew installation if keepCurrentInstall is set to true - if isBrewPathValid(path: BrewPaths.appBrewExetutable.path) && keepCurrentInstall { + guard keepCurrentInstall && !isBrewPathValid(path: BrewPaths.appBrewExetutable.path) else { Self.logger.notice("Brew is already installed, skipping installation") - progressObject.phase = .done + await MainActor.run { progressObject.phase = .done } return } - - progressObject.phase = .fetchingHomebrew - + + await MainActor.run { progressObject.phase = .fetchingHomebrew } + // Install brew try await Self.installHomebrew() - // Install Pinentry - progressObject.phase = .installingPinentry - try await Self.installPinentry() - - progressObject.phase = .done - Self.logger.notice("Dependencies installed successfully!") + await MainActor.run { progressObject.phase = .done } + Self.logger.notice("Brew installed successfully!") } /// Installs Homebrew static func installHomebrew() async throws -> Void { Self.logger.info("Brew installation started") - - do { - var isDirectory: ObjCBool = true - - // Delete Homebrew directory (~/Library/Application Support/Applite/homebrew) if exists so we have a clean install - if FileManager.default.fileExists(atPath: BrewPaths.appBrewDirectory.path, isDirectory: &isDirectory) { - Self.logger.info("Homebrew directory already exists, attempting to delete it") - try FileManager.default.removeItem(at: BrewPaths.appBrewDirectory) - } - - // Create Homebrew directory - if !FileManager.default.fileExists(atPath: BrewPaths.appBrewDirectory.path, isDirectory: &isDirectory) { - Self.logger.info("Attempting to create Homebrew directory") - try FileManager.default.createDirectory(at: BrewPaths.appBrewDirectory, withIntermediateDirectories: true) - } - } catch { - Self.logger.error("Couldn't create or remove Homebrew directory in Application Support") - throw DependencyInstallationError.DirectoryError + + var isDirectory: ObjCBool = true + + // Delete Homebrew directory (~/Library/Application Support/Applite/homebrew) if exists so we have a clean install + if FileManager.default.fileExists(atPath: BrewPaths.appBrewDirectory.path, isDirectory: &isDirectory) { + Self.logger.info("Homebrew directory already exists, attempting to delete it") + try FileManager.default.removeItem(at: BrewPaths.appBrewDirectory) + } + + // Create Homebrew directory + if !FileManager.default.fileExists(atPath: BrewPaths.appBrewDirectory.path, isDirectory: &isDirectory) { + Self.logger.info("Attempting to create Homebrew directory") + try FileManager.default.createDirectory(at: BrewPaths.appBrewDirectory, withIntermediateDirectories: true) } // Fetch Homebrew tarball Self.logger.info("Fetching tarball and unpacking") - let brewFetchResult = await shell("curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C \"\(BrewPaths.appBrewDirectory.path)\"") - - if brewFetchResult.didFail { - Self.logger.error("Failed to fetch and unpack tarball") - Self.logger.error("\(brewFetchResult.output)") - throw DependencyInstallationError.BrewFetchError - } else { - Self.logger.info("Brew install done") - } - } - - /// Installs the `pinentry-mac` package for sudo askpass - static func installPinentry(forceInstall: Bool = false) async throws { - Self.logger.info("Installing pinentry-mac\(forceInstall ? " with --force flag" : "")") - - if await BrewPaths.isPinentryInstalled() { - Self.logger.notice("pinentry-mac already installed. Skipping...") - return - } - - // Install gettext and libgpg-error first with the --force-bottle flag - // gettext and libgpg-error are dependencies for pinentry-mac but if we do a normal install - // it will fail when clang tries to find the cellar folder, because the path may have spaces in it. (e.g. Application Support) - // So we can bypass bulding from source with the --force-bottle to download only the binraies - Self.logger.info("Installing gettext and libgpg-error with --force-bottle flag") - let dependencyResult = await shell("\(BrewPaths.currentBrewExecutable) install --force-bottle \(forceInstall ? "--force" : "") gettext libgpg-error") - - if dependencyResult.didFail { - Self.logger.error("Failed to install gettext and libgpg-error with --force-bottle flag. Output: \(dependencyResult.output)") - throw DependencyInstallationError.PinentryError - } - - // Install pinentry-mac - Self.logger.info("Installing pinentry-mac") - let pinentryResult = await shell("\(BrewPaths.currentBrewExecutable) install \(forceInstall ? "--force" : "") pinentry-mac") - - if pinentryResult.didFail { - Self.logger.error("Failed to install pinentry-mac. Output: \(pinentryResult.output)") - throw DependencyInstallationError.PinentryError - } - - Self.logger.info("pinentry-mac installation successfull") + try await Shell.runAsync("curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C \"\(BrewPaths.appBrewDirectory.path)\"") + + Self.logger.info("Brew install done") } } diff --git a/Applite/Utilities/BrewPaths.swift b/Applite/Utilities/BrewPaths.swift index ba819bd..48eadb4 100755 --- a/Applite/Utilities/BrewPaths.swift +++ b/Applite/Utilities/BrewPaths.swift @@ -106,8 +106,4 @@ struct BrewPaths { static public func isSelectedBrewPathValid() -> Bool { return isBrewPathValid(path: Self.currentBrewExecutable) } - - static public func isPinentryInstalled() async -> Bool { - return await shell("\(Self.currentBrewExecutable) list --formula").output.contains("pinentry-mac") - } } diff --git a/Applite/Utilities/Import Export/ExportCasks.swift b/Applite/Utilities/Import Export/ExportCasks.swift index 8f02a3b..7dc5395 100644 --- a/Applite/Utilities/Import Export/ExportCasks.swift +++ b/Applite/Utilities/Import Export/ExportCasks.swift @@ -8,45 +8,72 @@ import Foundation import OSLog -enum CaskExportError: Error { - case ExportError +enum CaskImportError: Error { + case EmptyFile } -func exportCasks(url: URL, exportType: CaskExportType) throws { - let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "CaskExport") - - let today = Date.now - - let formatter = DateFormatter() - formatter.dateFormat = "y_MM_dd_HH:mm" - let currentDateString = formatter.string(from: today) - - if exportType == .brewfile { - let brewfileURL = url.appendingPathComponent("Brewfile_\(currentDateString)") - - let result = shell("\(BrewPaths.currentBrewExecutable) bundle dump --file=\"\(brewfileURL.path)\"") - - if result.didFail { - logger.error("Failed to export brewfile. Shell output: \(result.output, privacy: .public)") - throw CaskExportError.ExportError +enum CaskToFileManager { + static func export(url: URL, exportType: CaskExportType) throws { + let today = Date.now + + let formatter = DateFormatter() + formatter.dateFormat = "y_MM_dd_HH:mm" + let currentDateString = formatter.string(from: today) + + switch exportType { + case .txtFile: + let output = try Shell.run("\(BrewPaths.currentBrewExecutable) list --cask") + + let exportedCasks = output.trimmingCharacters(in: .whitespacesAndNewlines) + + let fileURL = url.appendingPathComponent("applite_export_\(currentDateString).txt", conformingTo: .plainText) + + let data = exportedCasks.data(using: .utf8) + try data?.write(to: fileURL) + case .brewfile: + let brewfileURL = url.appendingPathComponent("Brewfile_\(currentDateString)") + + try Shell.run("\(BrewPaths.currentBrewExecutable) bundle dump --file=\"\(brewfileURL.path)\"") } - } else { - let result = shell("\(BrewPaths.currentBrewExecutable) list --cask") - - if result.didFail { - throw CaskExportError.ExportError + } + + static func readCaskFile(url: URL) throws -> [String] { + let content = try String(contentsOf: url) + var casks: [String] = [] + let brewfileRegex = /cask "([\w-]+)"/ + + if content.contains("cask \"") { + // Brewfile + let matches = content.matches(of: brewfileRegex) + casks = matches.map({ String($0.1) }) + } else { + // Txt file + casks = content.components(separatedBy: .newlines) + + // Trim whitespace + casks = casks.map({ $0.trimmingCharacters(in: .whitespaces) }) + } + + // Remove empty elements + casks = casks.filter({ !$0.isEmpty }) + + if casks.isEmpty { + throw CaskImportError.EmptyFile } - - let exportedCasks = result.output - - let fileURL = url.appendingPathComponent("applite_export_\(currentDateString).txt", conformingTo: .plainText) - - if let data = exportedCasks.data(using: .utf8) { - do { - try data.write(to: fileURL) - } catch { - logger.error("Failed to export cask list (txt). Reason: \(error.localizedDescription)") - throw CaskExportError.ExportError + + return casks + } + + static func installImportedCasks(casks: [String], caskData: CaskData) async { + let casksToInstall: [Cask] = await caskData.casks.filter({ casks.contains($0.id) }) + + await withTaskGroup(of: Void.self) { group in + for cask in casksToInstall { + group.addTask { + if await !cask.isInstalled { + await cask.install(caskData: caskData) + } + } } } } diff --git a/Applite/Utilities/Import Export/ImportCasks.swift b/Applite/Utilities/Import Export/ImportCasks.swift deleted file mode 100644 index 470b3a5..0000000 --- a/Applite/Utilities/Import Export/ImportCasks.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ImportCasks.swift -// Applite -// -// Created by Milán Várady on 2023. 08. 12.. -// - -import Foundation - -enum CaskImportError: Error { - case FileReadError - case ParseError -} - -func readCaskFile(url: URL) throws -> [String] { - do { - let content = try String(contentsOf: url) - var casks: [String] = [] - let brewfileRegex = /cask "([\w-]+)"/ - - if content.contains("cask \"") { - // Brewfile - let matches = content.matches(of: brewfileRegex) - casks = matches.map({ String($0.1) }) - } else { - // Txt file - casks = content.components(separatedBy: .newlines) - - // Trim whitespace - casks = casks.map({ $0.trimmingCharacters(in: .whitespaces) }) - } - - // Remove empty elements - casks = casks.filter({ !$0.isEmpty }) - - if casks.isEmpty { - throw CaskImportError.ParseError - } - - return casks - } catch { - throw CaskImportError.FileReadError - } -} - -func installImportedCasks(casks: [String], caskData: CaskData) async { - let casksToInstall: [Cask] = await caskData.casks.filter({ casks.contains($0.id) }) - - await withTaskGroup(of: Void.self) { group in - for cask in casksToInstall { - group.addTask { - if !cask.isInstalled { - await cask.install(caskData: caskData) - } - } - } - } -} diff --git a/Applite/Utilities/Shell/PinentryScriptHash.swift b/Applite/Utilities/Shell/PinentryScriptHash.swift deleted file mode 100644 index 4059fd1..0000000 --- a/Applite/Utilities/Shell/PinentryScriptHash.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// PinentryScriptHash.swift -// Applite -// -// Created by Milán Várady on 2023. 08. 30.. -// - -import Foundation - -/// MD5 hash of the pinentry.ksh file -let pinentryScriptHash = "AnfcWA+MzXeqGOc3oQHu0Q==" diff --git a/Applite/Utilities/Shell/Shell.swift b/Applite/Utilities/Shell/Shell.swift old mode 100755 new mode 100644 index c6c9ca6..2d4b05f --- a/Applite/Utilities/Shell/Shell.swift +++ b/Applite/Utilities/Shell/Shell.swift @@ -2,80 +2,118 @@ // Shell.swift // Applite // -// Created by Milán Várady on 2022. 10. 16.. +// Created by Milán Várady on 2024.12.25. // import Foundation import OSLog -fileprivate let shellPath = "/bin/zsh" - -/// Runs a shell commands -/// -/// - Parameters: -/// - command: Command to run -/// -/// - Returns: A ``ShellResult`` containing the output and exit status of command -@discardableResult -func shell(_ command: String) -> ShellResult { - let task = Process() - let pipe = Pipe() - let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "shell") - - // Get pinentry script for sudo askpass - guard let pinentryScript = Bundle.main.path(forResource: "pinentry", ofType: "ksh") else { - return ShellResult(output: "pinentry.ksh not found", didFail: true) +/// Namespace for shell command execution utilities +public enum Shell { + private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Shell") + private static let askpassChecksum = "fAl63ShrMp8Sp9HIj/FYYA==" + + /// Executes a shell command synchronously + @discardableResult + static func run(_ command: String) throws -> String { + let (task, pipe) = try createProcess(command: command) + + try task.run() + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + + guard let output = String(data: data, encoding: .utf8) else { + throw ShellError.outputDecodingFailed + } + + let cleanOutput = output.cleanANSIEscapeCodes() + + guard task.terminationStatus == 0 else { + throw ShellError.nonZeroExit( + command: command, + exitCode: task.terminationStatus, + output: cleanOutput + ) + } + + return cleanOutput } - - // Verify pinentry script checksum - if URL(string: pinentryScript)?.checksumInBase64() != pinentryScriptHash { - return ShellResult(output: "pinentry.ksh checksum mismatch. The file has been modified.", didFail: true) + + /// Executes a shell command asynchronously + @discardableResult + static func runAsync(_ command: String) async throws -> String { + // Simply mark it as async and use the same implementation + try run(command) } - // Set up environment - var environment: [String: String] = [ - "SUDO_ASKPASS": pinentryScript - ] + /// Executes a shell command and streams the output + static func stream(_ command: String) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + do { + let (task, pipe) = try createProcess(command: command) + let fileHandle = pipe.fileHandleForReading - if let proxySettings = try? NetworkProxyManager.getSystemProxySettings() { - logger.info("Network proxy is enabled. Type: \(proxySettings.type.rawValue)") - environment["ALL_PROXY"] = proxySettings.fullString - } + try task.run() - task.standardOutput = pipe - task.standardError = pipe - task.environment = environment - task.arguments = ["-l", "-c", command] - task.executableURL = URL(fileURLWithPath: shellPath) - task.standardInput = nil + for try await line in fileHandle.bytes.lines { + let cleanOutput = line.cleanANSIEscapeCodes() + continuation.yield(cleanOutput) + } - do { - try task.run() - } catch { - logger.error("Shell run error. Failed to run shell(\(command)).") - return ShellResult(output: "", didFail: true) - } - - task.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - - if let output = String(data: data, encoding: .utf8) { - let cleanOutput = output.replacingOccurrences(of: "\\\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression) - return ShellResult(output: cleanOutput, didFail: task.terminationStatus != 0) - } else { - logger.error("Shell data error. Failed to get shell(\(command)) output. Most likely due to a UTF-8 decoding failure.") - return ShellResult(output: "Error: Invalid UTF-8 data", didFail: true) + task.waitUntilExit() + + if task.terminationStatus != 0 { + continuation.finish( + throwing: ShellError.nonZeroExit( + command: command, + exitCode: task.terminationStatus, + output: "n/a (streamed output)" + ) + ) + } else { + continuation.finish() + } + } catch { + logger.error("Stream error: \(error.localizedDescription)") + continuation.finish(throwing: error) + } + } + } } -} -/// Async version of shell command -@discardableResult -func shell(_ command: String) async -> ShellResult { - return dummyShell(command) -} + /// Creates a shell process with a given command + private static func createProcess(command: String) throws -> (Process, Pipe) { + // Verify askpass script + guard let scriptPath = Bundle.main.path(forResource: "askpass", ofType: "js") else { + throw ShellError.scriptNotFound + } + + if URL(string: scriptPath)?.checksumInBase64() != askpassChecksum { + throw ShellError.checksumMismatch + } -// This is needed so we can overload the shell function with an async version -fileprivate func dummyShell(_ command: String) -> ShellResult { - return shell(command) + let task = Process() + let pipe = Pipe() + + // Set up environment + var environment: [String: String] = [ + "SUDO_ASKPASS": scriptPath + ] + + if let proxySettings = try? NetworkProxyManager.getSystemProxySettings() { + logger.info("Network proxy is enabled. Type: \(proxySettings.type.rawValue)") + environment["ALL_PROXY"] = proxySettings.fullString + } + + task.standardOutput = pipe + task.standardError = pipe + task.environment = environment + task.arguments = ["-l", "-c", command] + task.executableURL = URL(fileURLWithPath: "/bin/zsh") + task.standardInput = nil + + return (task, pipe) + } } diff --git a/Applite/Utilities/Shell/ShellError.swift b/Applite/Utilities/Shell/ShellError.swift new file mode 100644 index 0000000..d693896 --- /dev/null +++ b/Applite/Utilities/Shell/ShellError.swift @@ -0,0 +1,28 @@ +// +// ShellError.swift +// Applite +// +// Created by Milán Várady on 2024.12.25. +// + +import Foundation + +enum ShellError: LocalizedError { + case scriptNotFound + case checksumMismatch + case outputDecodingFailed + case nonZeroExit(command: String, exitCode: Int32, output: String) + + var errorDescription: String? { + switch self { + case .scriptNotFound: + return "Required script file not found" + case .checksumMismatch: + return "Script checksum mismatch. The file has been modified." + case .outputDecodingFailed: + return "Failed to decode command output as UTF-8" + case .nonZeroExit(let command, let exitCode, let output): + return "Failed to run shell command.\nCommand: \(command) (exit code: \(exitCode))\nOutput: \(output)" + } + } +} diff --git a/Applite/Utilities/Shell/ShellOutputStream.swift b/Applite/Utilities/Shell/ShellOutputStream.swift deleted file mode 100755 index 7a5b0fd..0000000 --- a/Applite/Utilities/Shell/ShellOutputStream.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ShellOutputStream.swift -// Applite -// -// Created by Milán Várady on 2023. 01. 14.. -// - -import Foundation -import Combine -import OSLog - -/// Streams the output of a shell command in real time -public class ShellOutputStream { - public let outputPublisher = PassthroughSubject() - - private var output: String = "" - private var task: Process? - private var fileHandle: FileHandle? - - static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ShellOutputStream") - - /// Runs shell command - /// - /// - Parameters: - /// - command: Shell command to run - /// - environmentVariables: (optional) Environment varables to include in the command - /// - /// - Returns: A ``ShellResult`` containing the output and exit status of command - func run(_ command: String) async -> ShellResult { - self.task = Process() - - // Get pinentry script for sudo askpass - guard let pinentryScript = Bundle.main.path(forResource: "pinentry", ofType: "ksh") else { - return ShellResult(output: "pinentry.ksh not found", didFail: true) - } - - // Verify pinentry script checksum - if URL(string: pinentryScript)?.checksumInBase64() != pinentryScriptHash { - return ShellResult(output: "pinentry.ksh checksum mismatch. The file has been modified.", didFail: true) - } - - // Set up environment - var environment: [String: String] = [ - "SUDO_ASKPASS": pinentryScript - ] - - if let proxySettings = try? NetworkProxyManager.getSystemProxySettings() { - Self.logger.info("Network proxy is enabled. Type: \(proxySettings.type.rawValue)") - environment["ALL_PROXY"] = proxySettings.fullString - } - - self.task?.launchPath = "/bin/zsh" - self.task?.environment = environment - self.task?.arguments = ["-l", "-c", "script -q /dev/null \(command)"] - - let pipe = Pipe() - self.task?.standardOutput = pipe - self.fileHandle = pipe.fileHandleForReading - - // Read in output changes - self.fileHandle?.readabilityHandler = { [weak self] handle in - guard let self = self else { return } - let data = handle.availableData - - if data.count > 0 { - let text = String(data: data, encoding: .utf8) ?? "" - let cleanOutput = text.replacingOccurrences(of: "\\\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression) - - // Send new changes - Task { @MainActor in - self.outputPublisher.send(cleanOutput) - } - - self.output += cleanOutput - } else if !(self.task?.isRunning ?? false) { - self.fileHandle?.readabilityHandler = nil - } - } - - self.task?.launch() - - self.task?.waitUntilExit() - - return ShellResult(output: self.output, didFail: self.task?.terminationStatus ?? -1 != 0) - } -} diff --git a/Applite/Utilities/Shell/ShellResult.swift b/Applite/Utilities/Shell/ShellResult.swift deleted file mode 100755 index 7081697..0000000 --- a/Applite/Utilities/Shell/ShellResult.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// ShellResult.swift -// Applite -// -// Created by Milán Várady on 2023. 08. 01.. -// - -import Foundation - -/// Returned by functions that run shell commands, ``shell(_:)-51uzj`` and ``ShellOutputStream`` -public struct ShellResult { - let output: String - let didFail: Bool -} diff --git a/Applite/Utilities/UninstallSelf.swift b/Applite/Utilities/UninstallSelf.swift index 02427dc..44f0045 100755 --- a/Applite/Utilities/UninstallSelf.swift +++ b/Applite/Utilities/UninstallSelf.swift @@ -14,7 +14,7 @@ enum UninstallError: Error { } /// This function will uninstall Applite and all it's related files -func uninstallSelf(deleteBrewCache: Bool) { +func uninstallSelf(deleteBrewCache: Bool) throws { let logger = Logger() logger.notice("Applite uninstallation stated. deleteBrewCache: \(deleteBrewCache)") @@ -46,13 +46,13 @@ func uninstallSelf(deleteBrewCache: Bool) { logger.notice("Running command: \(command)") - let result = shell(command) + let output = try Shell.run(command) - logger.notice("Uninstall result: \(result.output)") + logger.notice("Uninstall result: \(output)") // Homebrew cache if deleteBrewCache { - shell("rm -rf $HOME/Library/Caches/Homebrew") + try Shell.run("rm -rf $HOME/Library/Caches/Homebrew") } logger.notice("Self destructing. Goodbye world! o7") diff --git a/Applite/Utilities/Verify Brew Installation/isBrewPathValid.swift b/Applite/Utilities/Verify Brew Installation/isBrewPathValid.swift index 0a85e30..39e4e58 100755 --- a/Applite/Utilities/Verify Brew Installation/isBrewPathValid.swift +++ b/Applite/Utilities/Verify Brew Installation/isBrewPathValid.swift @@ -27,7 +27,9 @@ public func isBrewPathValid(path: String) -> Bool { } // Check if Homebrew is returned when checking version - let result = shell("\(path) --version") - - return !result.didFail && result.output.contains("Homebrew") + guard let output = try? Shell.run("\(path) --version") else { + return false + } + + return output.contains("Homebrew") } diff --git a/Applite/Utilities/Verify Brew Installation/isCommandLineToolsInstalled.swift b/Applite/Utilities/Verify Brew Installation/isCommandLineToolsInstalled.swift index 4efe5fc..5a3e321 100755 --- a/Applite/Utilities/Verify Brew Installation/isCommandLineToolsInstalled.swift +++ b/Applite/Utilities/Verify Brew Installation/isCommandLineToolsInstalled.swift @@ -11,5 +11,11 @@ import Foundation /// /// - Returns: Whether it is installed or not public func isCommandLineToolsInstalled() -> Bool { - return !shell("xcode-select -p").didFail + do { + try Shell.run("xcode-select -p") + } catch { + return false + } + + return true } diff --git a/Applite/Views/App Views/AppView.swift b/Applite/Views/App Views/AppView.swift index aa12477..b30e854 100755 --- a/Applite/Views/App Views/AppView.swift +++ b/Applite/Views/App Views/AppView.swift @@ -178,9 +178,6 @@ struct AppView: View { } else if output.contains("Could not resolve host") { failureAlertMessage = String(localized: "Couldn't download app. No internet connection, or host is unreachable.") showingFailureAlert = true - } else if output.lowercased().contains("pinentry") { - failureAlertMessage = output - showingFailureAlert = true } } .alert("Error", isPresented: $showingFailureAlert) { @@ -320,9 +317,9 @@ struct AppView: View { var body: some View { // Lauch app Button("Open") { - let result = cask.launchApp() - - if result.didFail { + do { + try cask.launchApp() + } catch { appNotFoundShowing = true } } diff --git a/Applite/Views/ContentView.swift b/Applite/Views/ContentView.swift index 2a7d861..65be6d8 100755 --- a/Applite/Views/ContentView.swift +++ b/Applite/Views/ContentView.swift @@ -22,8 +22,6 @@ struct ContentView: View { @State var loadAlertShowing = false @State var errorMessage = "" - @State var pinentryErrorShowing = false - @State var brokenInstall = false /// If true the sidebar is disabled @@ -109,7 +107,6 @@ struct ContentView: View { } .task { await loadCasks() - await checkPinentry() } .searchable(text: $searchText, placement: .sidebar) .onSubmit(of: .search) { @@ -141,11 +138,6 @@ struct ContentView: View { } message: { Text(errorMessage) } - .alert("PINEntry not installed correctly", isPresented: $pinentryErrorShowing) { - Button("I Understand", role: .cancel) { } - } message: { - Text("Applications that require an admin password to install will fail to install.") - } } private func loadCasks() async { @@ -153,15 +145,15 @@ struct ContentView: View { errorMessage = DependencyManager.brokenPathOrIstallMessage loadAlertShowing = true brokenInstall = true - - let output = await shell("\(BrewPaths.currentBrewExecutable) --version").output - + + let output = (try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) --version")) ?? "n/a" + logger.error(""" Initial cask load failure. Reason: selected brew path seems invalid. Brew executable path path: \(BrewPaths.currentBrewExecutable) brew --version output: \(output) """) - + return } @@ -176,20 +168,6 @@ brew --version output: \(output) logger.error("Initial cask load failure. Reason: \(error.localizedDescription)") } } - - /// Checks if pinentry-mac is correctly installed, if not it installes it - private func checkPinentry() async { - // Return if installed - if await BrewPaths.isPinentryInstalled() { return } - - logger.notice("pinentry-mac is not installed. Installing now...") - - do { - try await DependencyManager.installPinentry(forceInstall: true) - } catch { - pinentryErrorShowing = true - } - } } struct ContentView_Previews: PreviewProvider { diff --git a/Applite/Views/Detail Views/BrewManagementView.swift b/Applite/Views/Detail Views/BrewManagementView.swift index f2443fd..9c6c9d5 100755 --- a/Applite/Views/Detail Views/BrewManagementView.swift +++ b/Applite/Views/Detail Views/BrewManagementView.swift @@ -102,20 +102,16 @@ struct BrewManagementView: View { } .task { // Get version - let versionOutput = await shell("\(BrewPaths.currentBrewExecutable) --version").output - - if let version = versionOutput.firstMatch(of: /Homebrew ([\d\.]+)/) { - homebrewVersion = String(version.1) - } else { + guard let versionOutput = try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) --version"), + let version = versionOutput.firstMatch(of: /Homebrew ([\d\.]+)/), + let casksInstalled = try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) list --cask | wc -w") else { homebrewVersion = "N/a" numberOfCasks = "N/a" return } - - // Get number of installed casks - let countOutput = await shell("\(BrewPaths.currentBrewExecutable) list --cask | wc -w").output - - numberOfCasks = countOutput.trimmingCharacters(in: .whitespacesAndNewlines) + + homebrewVersion = String(version.1) + numberOfCasks = casksInstalled.trimmingCharacters(in: .whitespacesAndNewlines) } } } @@ -189,20 +185,21 @@ struct BrewManagementView: View { Task { logger.info("Updating brew started") - - let result = await shell("\(BrewPaths.currentBrewExecutable) update") - - logger.info("Brew update output: \(result.output)") - - await MainActor.run { - if result.didFail { + + do { + try await Shell.runAsync("\(BrewPaths.currentBrewExecutable) update") + } catch { + await MainActor.run { + logger.error("Brew update failed. Error: \(error.localizedDescription)") updateFailed = true - logger.error("Brew update failed") - } else { - logger.info("Brew update successful") - updateDone = true } - + } + + logger.info("Brew update successful") + + await MainActor.run { + updateDone = true + withAnimation { modifyingBrew = false } @@ -291,9 +288,9 @@ struct BrewManagementView: View { switch result { case .success(let url): do { - try exportCasks(url: url[0], exportType: selectedExportFileType) + try CaskToFileManager.export(url: url[0], exportType: selectedExportFileType) } catch { - logger.error("Failed to export casks") + logger.error("Failed to export casks. Error: \(error.localizedDescription)") showingExportError = true } case .failure(let error): @@ -325,8 +322,8 @@ struct BrewManagementView: View { switch result { case .success(let url): do { - let casks = try readCaskFile(url: url[0]) - + let casks = try CaskToFileManager.readCaskFile(url: url[0]) + installImported(casks: casks) } catch { logger.error("Failed to import cask. Reason: \(error.localizedDescription)") @@ -345,7 +342,7 @@ struct BrewManagementView: View { func installImported(casks: [String]) { Task { - await installImportedCasks(casks: casks, caskData: caskData) + await CaskToFileManager.installImportedCasks(casks: casks, caskData: caskData) } } } diff --git a/Applite/Views/Detail Views/UpdateView.swift b/Applite/Views/Detail Views/UpdateView.swift index 426181e..374612c 100755 --- a/Applite/Views/Detail Views/UpdateView.swift +++ b/Applite/Views/Detail Views/UpdateView.swift @@ -19,7 +19,8 @@ struct UpdateView: View { @State var updateAllButtonRotation = 0.0 @State var showingGreedyUpdateConfirm = false - + @State var showOutdatedFailAlert = false + // Filter outdated casks var casks: [Cask] { var filteredCasks = caskData.casks.filter { $0.isOutdated } @@ -103,7 +104,11 @@ struct UpdateView: View { .alert("Notice", isPresented: $showingGreedyUpdateConfirm) { Button("Show All") { Task { - await caskData.refreshOutdatedApps(greedy: true) + do { + try await caskData.refreshOutdatedApps(greedy: true) + } catch { + showOutdatedFailAlert = true + } } } @@ -120,9 +125,15 @@ struct UpdateView: View { } else { Button { - Task.init { + Task { refreshing = true - await caskData.refreshOutdatedApps() + + do { + try await caskData.refreshOutdatedApps(greedy: true) + } catch { + showOutdatedFailAlert = true + } + refreshing = false } } label: { @@ -130,6 +141,7 @@ struct UpdateView: View { } } } + .alert("Failed to load outdated apps", isPresented: $showOutdatedFailAlert) {} } } diff --git a/Applite/Views/SettingsView.swift b/Applite/Views/SettingsView.swift index 67cb8d0..e0023bf 100755 --- a/Applite/Views/SettingsView.swift +++ b/Applite/Views/SettingsView.swift @@ -147,8 +147,8 @@ fileprivate struct BrewPathView: View { .fixedSize(horizontal: false, vertical: true) Button("Relaunch", role: .destructive) { - Task { - await shell("/usr/bin/osascript -e 'tell application \"\(Bundle.main.appName)\" to quit' && sleep 2 && open \"\(Bundle.main.bundlePath)\"") + Task.detached { + try? await Shell.runAsync("/usr/bin/osascript -e 'tell application \"\(Bundle.main.appName)\" to quit' && sleep 2 && open \"\(Bundle.main.bundlePath)\"") } } } diff --git a/Applite/Views/SetupView.swift b/Applite/Views/SetupView.swift index 148ff63..1c42748 100755 --- a/Applite/Views/SetupView.swift +++ b/Applite/Views/SetupView.swift @@ -261,15 +261,9 @@ struct SetupView: View { // Alerts @State var showingAlert = false @State var showingCommandLineToolsAlert = false - @State var showingPinentryAlert = false @StateObject var installationProgress = BrewInstallationProgress() - enum SetupInstallError: Error { - case homebrew - case pinentry - } - var body: some View { VStack { Text("Installing dependencies") @@ -289,11 +283,6 @@ struct SetupView: View { description: "[Homebrew](https://brew.sh) is a free and open source package manager tool that makes installing third party applications really easy. \(Bundle.main.appName) uses Homebrew under the hood to download and manage applications.", progressOrder: .fetchingHomebrew) - // Pinentry - dependencyView(title: "Pinentry", - description: "Pinentry is used to securely prompt for the admin password when it is required during the installation of an application.", - progressOrder: .installingPinentry) - // Retry button if failed { Button { @@ -303,7 +292,7 @@ struct SetupView: View { } label: { Label("Retry Install", systemImage: "arrow.clockwise.circle") } - .bigButton(backgroundColor: .accentColor) + .bigButton(backgroundColor: .secondary) } } .frame(width: 440) @@ -335,17 +324,6 @@ struct SetupView: View { }, message: { Text("Retry the installation or visit the troubleshooting page.") }) - .alert("Failed to install pinentry", isPresented: $showingPinentryAlert, actions: { - Button("OK") { } - - Button("Troubleshooting") { - if let url = URL(string: "https://aerolite.dev/applite/troubleshooting.html") { - NSWorkspace.shared.open(url) - } - } - }, message: { - Text("You can safely continue, but you won't be able to install apps with .pkg installers.") - }) } } @@ -382,9 +360,6 @@ struct SetupView: View { do { try await DependencyManager.install(progressObject: installationProgress) - } catch DependencyInstallationError.PinentryError { - showingPinentryAlert = true - installationProgress.phase = .done } catch { failed = true } diff --git a/Applite/Views/UninstallSelfView.swift b/Applite/Views/UninstallSelfView.swift index 4e8a3ef..f4fed1f 100755 --- a/Applite/Views/UninstallSelfView.swift +++ b/Applite/Views/UninstallSelfView.swift @@ -11,7 +11,9 @@ import SwiftUI struct UninstallSelfView: View { @State var deleteBrewCache = false @State var showConfirmation = false - + + @State var showUninstallFailedAlert = false + var body: some View { VStack(alignment: .leading) { Text("Uninstall \(Bundle.main.appName)") @@ -39,12 +41,16 @@ struct UninstallSelfView: View { .frame(width: 400, height: 250) .confirmationDialog("Are you sure you want to permanently uninstall \(Bundle.main.appName)?", isPresented: $showConfirmation) { Button("Uninstall", role: .destructive) { - uninstallSelf(deleteBrewCache: deleteBrewCache) - + do { + try uninstallSelf(deleteBrewCache: deleteBrewCache) + } catch { + showUninstallFailedAlert = true + } } Button("Cancel", role: .cancel) { } } + .alert("Failed to uninstall", isPresented: $showUninstallFailedAlert) {} } } diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 2dfc46d..ef9ad12 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -847,34 +847,6 @@ } } }, - "Applications that require an admin password to install will fail to install." : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Les applications qui nécessitent un mot de passe administrateur pour s'installer ne pourront pas s'installer." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A telepítéshez admin jelszót igénylő alkalmazások telepítése sikertelen lesz." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要管理员密码才能安装的应用程序将无法安装。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要管理員密碼才能安裝的應用程式將無法安裝。" - } - } - } - }, "Applite" : { "localizations" : { "fr" : { @@ -1467,35 +1439,6 @@ } } }, - "Concurrency error" : { - "comment" : "CaskDataLoadError", - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erreur de simultanéité" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Concurrency error" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "并发错误" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "並發錯誤" - } - } - } - }, "Continue" : { "localizations" : { "fr" : { @@ -2123,64 +2066,6 @@ } } }, - "Faild to retrieve cask information from selected brew executable" : { - "comment" : "CaskDataLoadError", - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Échec de la récupération des informations de cask à partir de l'exécutable de Homebrew sélectionné" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nem sikerült cask információt szerezni a kiválasztott brew executable révén" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无法从所选的 brew 可执行文件中检索 cask 信息" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法從選定的brew可執行檔中檢索桶資訊" - } - } - } - }, - "Failed to decode json data from the Homebrew API" : { - "comment" : "CaskDataLoadError", - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Impossible de décoder les données json de l'API Homebrew" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nem sikerült dekódolni a Homebrew API-ról érkezett információt" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无法从 Homebrew API 解码 json 数据" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法從 Homebrew API 解碼 json 數據" - } - } - } - }, "Failed to download %@" : { "localizations" : { "fr" : { @@ -2209,62 +2094,8 @@ } } }, - "Failed to install pinentry" : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Échec de l'installation de pinentry" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nem sikerült a Pinentry telepítése" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无法安装 pinentry" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "pinentry安裝失敗" - } - } - } - }, - "Failed to load cask data from cache" : { - "comment" : "CaskDataLoadError", - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Échec du chargement des données de cask à partir du cache" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nem sikerült betölteni a cask adatait a cache-ből" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无法从缓存加载 cask 数据" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法從快取加載桶數據" - } - } - } + "Failed to load outdated apps" : { + }, "Failed to reinstall %@" : { "localizations" : { @@ -2293,6 +2124,9 @@ } } } + }, + "Failed to uninstall" : { + }, "Failed to update %@" : { "localizations" : { @@ -2518,34 +2352,6 @@ } } }, - "I Understand" : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "J'ai compris" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Értem" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "我明白" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "我明白" - } - } - } - }, "IDEs" : { "extractionState" : "manual", "localizations" : { @@ -2885,35 +2691,6 @@ } } }, - "Load error" : { - "comment" : "CaskDataLoadError", - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erreur de chargement" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Betöltési hiba" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "加载错误" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "載入錯誤" - } - } - } - }, "Manage applications" : { "extractionState" : "manual", "localizations" : { @@ -3425,90 +3202,6 @@ } } }, - "Pinentry" : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pinentry" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pinentry" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pinentry" - } - } - } - }, - "Pinentry is used to securely prompt for the admin password when it is required during the installation of an application." : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "PINEntry est utilisé pour demander en toute sécurité le mot de passe administrateur lorsqu'il est requis lors de l'installation d'une application." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A Pinentry segít biztonságosan elkérni a admin jelszót olyan alkalmazásoknál, amelyeknek szükségük van rá a telepítéshez." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pinentry 用于在安装应用程序时安全地提示输入管理员密码。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pinentry 用於在應用程式安裝過程中需要時安全地提示輸入管理員密碼。" - } - } - } - }, - "PINEntry not installed correctly" : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "PINEntry n’est pas installé correctement" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A PINEntry nincs letöltve" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "PINEntry 未正确安装" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "PINEntry 未正確安裝" - } - } - } - }, "Preferred proxy protocol" : { "localizations" : { "fr" : { @@ -5177,34 +4870,6 @@ } } }, - "You can safely continue, but you won't be able to install apps with .pkg installers." : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vous pouvez continuer en toute sécurité, mais vous ne pourrez pas installer d'applications avec des installateurs .pkg." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Továbbléphet, de a .pkg telepítőkkel rendelkező alkalmazásokat nem fogja tudni telepíteni." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "您可以安全地继续操作,但是无法安装使用 .pkg 安装程序的应用程序。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "您可以安全地繼續,但您將無法使用 .pkg 安裝程式安裝應用程式。" - } - } - } - }, "You will be prompted to install the Xcode Command Line Tools, please click \"Install\" as it is required for this application to work. It will take a few minutes, you can see the progress on the installation window." : { "localizations" : { "fr" : {