├── .gitignore ├── CHANGELOG.md ├── NeoHub.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── Alex.xcuserdatad │ │ └── IDEFindNavigatorScopes.plist ├── xcshareddata │ └── xcschemes │ │ ├── NeoHub.xcscheme │ │ └── NeoHubCLI.xcscheme └── xcuserdata │ └── Alex.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── NeoHub ├── ActivationManager.swift ├── App.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── neovim-hicontrast-1024-1024.png │ │ ├── neovim-hicontrast-1024-128.png │ │ ├── neovim-hicontrast-1024-16.png │ │ ├── neovim-hicontrast-1024-256.png │ │ ├── neovim-hicontrast-1024-32.png │ │ ├── neovim-hicontrast-1024-512.png │ │ └── neovim-hicontrast-1024-64.png │ ├── Contents.json │ ├── EditorIcon.imageset │ │ ├── Contents.json │ │ └── MenuBarIcon.pdf │ └── MenuBarIcon.imageset │ │ ├── Contents.json │ │ └── MenuBarIcon.pdf ├── BugReporter.swift ├── CLI.swift ├── EditorStore.swift ├── Info.plist ├── Logger.swift ├── NeoHub.entitlements ├── NotificationManager.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── RegularWindow.swift ├── SocketServer.swift ├── components │ └── SettingsGroupModifier.swift └── views │ ├── AboutView.swift │ ├── InstallationView.swift │ ├── MenuBarView.swift │ ├── SettingsView.swift │ └── SwitcherView.swift ├── NeoHubCLI ├── CLI.swift ├── Logger.swift ├── NeoHubCLI.entitlements ├── Shell.swift └── SocketClient.swift ├── NeoHubLib ├── NeoHubLib.docc │ └── NeoHubLib.md ├── NeoHubLib.h └── Shared.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | *.xcodeproj 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # Pods/ 58 | # Add this line if you want to avoid checking in source code from the Xcode workspace 59 | # *.xcworkspace 60 | 61 | # Carthage 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build/ 66 | 67 | # Accio dependency management 68 | Dependencies/ 69 | .accio/ 70 | 71 | # fastlane 72 | # It is recommended to not store the screenshots in the git repo. 73 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 74 | # For more information about the recommended setup visit: 75 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 76 | 77 | fastlane/report.xml 78 | fastlane/Preview.html 79 | fastlane/screenshots/**/*.png 80 | fastlane/test_output 81 | 82 | # Code Injection 83 | # After new code Injection tools there's a generated folder /iOSInjectionProject 84 | # https://github.com/johnno1962/injectionforxcode 85 | 86 | iOSInjectionProject/ 87 | 88 | # End of https://www.toptal.com/developers/gitignore/api/swift 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.1 4 | - Add missing entitlement. 5 | 6 | ## 0.2.0 7 | - Update to the current Neovide. 8 | - Add hotkey to activate last used editor directly. 9 | - Add hotkey to restart current editor. 10 | - Use `⇥` to cycle through the editors in the switcher. 11 | - Fix installation script. 12 | 13 | ## 0.1.1 14 | - Fix CLI version. 15 | 16 | ## 0.1.0 17 | - Sort editors: 18 | - in menubar: by name 19 | - in switcher: by access time 20 | - Fix a few edge cases in the apps activation logic. 21 | 22 | ## 0.0.1 23 | Initial release. 24 | 25 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FA2AE44B2AEB980700D24008 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2AE44A2AEB980700D24008 /* App.swift */; }; 11 | FA2AE44F2AEB980800D24008 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA2AE44E2AEB980800D24008 /* Assets.xcassets */; }; 12 | FA2AE4522AEB980800D24008 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA2AE4512AEB980800D24008 /* Preview Assets.xcassets */; }; 13 | FA2AE4602AEB9CA000D24008 /* CLI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2AE45F2AEB9CA000D24008 /* CLI.swift */; }; 14 | FA2EAD912AEC4C49000F8F7A /* SocketServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2EAD902AEC4C49000F8F7A /* SocketServer.swift */; }; 15 | FA2EAD932AEC4CCD000F8F7A /* SocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2EAD922AEC4CCD000F8F7A /* SocketClient.swift */; }; 16 | FA2EAD972AED0C8B000F8F7A /* EditorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2EAD962AED0C8B000F8F7A /* EditorStore.swift */; }; 17 | FA2EAD9A2AED3906000F8F7A /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = FA2EAD992AED3906000F8F7A /* ArgumentParser */; }; 18 | FA2EADA62AED589F000F8F7A /* NeoHubLib.docc in Sources */ = {isa = PBXBuildFile; fileRef = FA2EADA52AED589F000F8F7A /* NeoHubLib.docc */; }; 19 | FA2EADA72AED589F000F8F7A /* NeoHubLib.h in Headers */ = {isa = PBXBuildFile; fileRef = FA2EADA42AED589F000F8F7A /* NeoHubLib.h */; settings = {ATTRIBUTES = (Public, ); }; }; 20 | FA2EADAA2AED589F000F8F7A /* NeoHubLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA2EADA22AED589F000F8F7A /* NeoHubLib.framework */; }; 21 | FA2EADAB2AED589F000F8F7A /* NeoHubLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FA2EADA22AED589F000F8F7A /* NeoHubLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 22 | FA2EADB02AED58C6000F8F7A /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2EADAF2AED58C6000F8F7A /* Shared.swift */; }; 23 | FA2EADB12AED65A6000F8F7A /* NeoHubLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA2EADA22AED589F000F8F7A /* NeoHubLib.framework */; }; 24 | FA30F21E2AEEB6F9004EC6E9 /* NIO in Frameworks */ = {isa = PBXBuildFile; productRef = FA30F21D2AEEB6F9004EC6E9 /* NIO */; }; 25 | FA30F2202AEEB70A004EC6E9 /* NIO in Frameworks */ = {isa = PBXBuildFile; productRef = FA30F21F2AEEB70A004EC6E9 /* NIO */; }; 26 | FA30F2282AEED1E7004EC6E9 /* MenuBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA30F2272AEED1E7004EC6E9 /* MenuBarView.swift */; }; 27 | FA30F22A2AEF7A7F004EC6E9 /* SwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA30F2292AEF7A7F004EC6E9 /* SwitcherView.swift */; }; 28 | FA30F22F2AEF828A004EC6E9 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = FA30F22E2AEF828A004EC6E9 /* KeyboardShortcuts */; }; 29 | FA3937172AF291A5001D9105 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3937162AF291A5001D9105 /* SettingsView.swift */; }; 30 | FA39371D2AF3A7CC001D9105 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA39371C2AF3A7CC001D9105 /* Shell.swift */; }; 31 | FA3937222AF41D84001D9105 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = FA3937212AF41D84001D9105 /* LaunchAtLogin */; }; 32 | FA54EB702AF4FD84001AE449 /* neohub in Embed CLI */ = {isa = PBXBuildFile; fileRef = FA2AE45D2AEB9CA000D24008 /* neohub */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 33 | FA54EB722AF5033D001AE449 /* CLI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB712AF5033D001AE449 /* CLI.swift */; }; 34 | FA54EB742AF53ED9001AE449 /* RegularWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB732AF53ED9001AE449 /* RegularWindow.swift */; }; 35 | FA54EB762AF654D7001AE449 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB752AF654D7001AE449 /* AboutView.swift */; }; 36 | FA54EB792AF6AE06001AE449 /* InstallationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB782AF6AE06001AE449 /* InstallationView.swift */; }; 37 | FA54EB7C2AF6E3E8001AE449 /* SettingsGroupModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB7B2AF6E3E8001AE449 /* SettingsGroupModifier.swift */; }; 38 | FA54EB7F2AF789D4001AE449 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = FA54EB7E2AF789D4001AE449 /* Logging */; }; 39 | FA54EB812AF789F6001AE449 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB802AF789F6001AE449 /* Logger.swift */; }; 40 | FA54EB832AF7A381001AE449 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB822AF7A381001AE449 /* NotificationManager.swift */; }; 41 | FA54EB852AF7CB7E001AE449 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = FA54EB842AF7CB7E001AE449 /* Logging */; }; 42 | FA54EB872AF7CB97001AE449 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB862AF7CB97001AE449 /* Logger.swift */; }; 43 | FA54EB8D2AF7F689001AE449 /* BugReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB8C2AF7F689001AE449 /* BugReporter.swift */; }; 44 | FA54EB8F2AF8ACDB001AE449 /* ActivationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA54EB8E2AF8ACDB001AE449 /* ActivationManager.swift */; }; 45 | FA54EB942AF90B52001AE449 /* LoggingSyslog in Frameworks */ = {isa = PBXBuildFile; productRef = FA54EB932AF90B52001AE449 /* LoggingSyslog */; }; 46 | /* End PBXBuildFile section */ 47 | 48 | /* Begin PBXContainerItemProxy section */ 49 | FA2EADA82AED589F000F8F7A /* PBXContainerItemProxy */ = { 50 | isa = PBXContainerItemProxy; 51 | containerPortal = FA2AE43F2AEB980700D24008 /* Project object */; 52 | proxyType = 1; 53 | remoteGlobalIDString = FA2EADA12AED589F000F8F7A; 54 | remoteInfo = NeoHubLib; 55 | }; 56 | FA2EADB32AED65A6000F8F7A /* PBXContainerItemProxy */ = { 57 | isa = PBXContainerItemProxy; 58 | containerPortal = FA2AE43F2AEB980700D24008 /* Project object */; 59 | proxyType = 1; 60 | remoteGlobalIDString = FA2EADA12AED589F000F8F7A; 61 | remoteInfo = NeoHubLib; 62 | }; 63 | FA54EB6E2AF4FD46001AE449 /* PBXContainerItemProxy */ = { 64 | isa = PBXContainerItemProxy; 65 | containerPortal = FA2AE43F2AEB980700D24008 /* Project object */; 66 | proxyType = 1; 67 | remoteGlobalIDString = FA2AE45C2AEB9CA000D24008; 68 | remoteInfo = NeoHubCLI; 69 | }; 70 | /* End PBXContainerItemProxy section */ 71 | 72 | /* Begin PBXCopyFilesBuildPhase section */ 73 | FA2AE45B2AEB9CA000D24008 /* CopyFiles */ = { 74 | isa = PBXCopyFilesBuildPhase; 75 | buildActionMask = 2147483647; 76 | dstPath = /usr/share/man/man1/; 77 | dstSubfolderSpec = 0; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 1; 81 | }; 82 | FA2AE4A42AEBDA1D00D24008 /* Embed Frameworks */ = { 83 | isa = PBXCopyFilesBuildPhase; 84 | buildActionMask = 2147483647; 85 | dstPath = ""; 86 | dstSubfolderSpec = 10; 87 | files = ( 88 | FA2EADAB2AED589F000F8F7A /* NeoHubLib.framework in Embed Frameworks */, 89 | ); 90 | name = "Embed Frameworks"; 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | FAA5D7372AEEB074002DF3F8 /* Embed CLI */ = { 94 | isa = PBXCopyFilesBuildPhase; 95 | buildActionMask = 12; 96 | dstPath = ""; 97 | dstSubfolderSpec = 12; 98 | files = ( 99 | FA54EB702AF4FD84001AE449 /* neohub in Embed CLI */, 100 | ); 101 | name = "Embed CLI"; 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | /* End PBXCopyFilesBuildPhase section */ 105 | 106 | /* Begin PBXFileReference section */ 107 | FA03D4C82AFA302A00790D2F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 108 | FA2AE4472AEB980700D24008 /* NeoHub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NeoHub.app; sourceTree = BUILT_PRODUCTS_DIR; }; 109 | FA2AE44A2AEB980700D24008 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 110 | FA2AE44E2AEB980800D24008 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 111 | FA2AE4512AEB980800D24008 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 112 | FA2AE4532AEB980800D24008 /* NeoHub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NeoHub.entitlements; sourceTree = ""; }; 113 | FA2AE45D2AEB9CA000D24008 /* neohub */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = neohub; sourceTree = BUILT_PRODUCTS_DIR; }; 114 | FA2AE45F2AEB9CA000D24008 /* CLI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLI.swift; sourceTree = ""; }; 115 | FA2AE4A92AEBDDB800D24008 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 116 | FA2AE4AA2AEBDF7800D24008 /* NeoHubCLI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NeoHubCLI.entitlements; sourceTree = ""; }; 117 | FA2EAD902AEC4C49000F8F7A /* SocketServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketServer.swift; sourceTree = ""; }; 118 | FA2EAD922AEC4CCD000F8F7A /* SocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketClient.swift; sourceTree = ""; }; 119 | FA2EAD962AED0C8B000F8F7A /* EditorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorStore.swift; sourceTree = ""; }; 120 | FA2EADA22AED589F000F8F7A /* NeoHubLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NeoHubLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 121 | FA2EADA42AED589F000F8F7A /* NeoHubLib.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NeoHubLib.h; sourceTree = ""; }; 122 | FA2EADA52AED589F000F8F7A /* NeoHubLib.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = NeoHubLib.docc; sourceTree = ""; }; 123 | FA2EADAF2AED58C6000F8F7A /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; 124 | FA30F2272AEED1E7004EC6E9 /* MenuBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = ""; }; 125 | FA30F2292AEF7A7F004EC6E9 /* SwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitcherView.swift; sourceTree = ""; }; 126 | FA3937162AF291A5001D9105 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 127 | FA39371C2AF3A7CC001D9105 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = ""; }; 128 | FA54EB712AF5033D001AE449 /* CLI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLI.swift; sourceTree = ""; }; 129 | FA54EB732AF53ED9001AE449 /* RegularWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularWindow.swift; sourceTree = ""; }; 130 | FA54EB752AF654D7001AE449 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 131 | FA54EB782AF6AE06001AE449 /* InstallationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationView.swift; sourceTree = ""; }; 132 | FA54EB7B2AF6E3E8001AE449 /* SettingsGroupModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupModifier.swift; sourceTree = ""; }; 133 | FA54EB802AF789F6001AE449 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 134 | FA54EB822AF7A381001AE449 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 135 | FA54EB862AF7CB97001AE449 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 136 | FA54EB8C2AF7F689001AE449 /* BugReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReporter.swift; sourceTree = ""; }; 137 | FA54EB8E2AF8ACDB001AE449 /* ActivationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivationManager.swift; sourceTree = ""; }; 138 | FA728EF32AFF716E0013610E /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 139 | /* End PBXFileReference section */ 140 | 141 | /* Begin PBXFrameworksBuildPhase section */ 142 | FA2AE4442AEB980700D24008 /* Frameworks */ = { 143 | isa = PBXFrameworksBuildPhase; 144 | buildActionMask = 2147483647; 145 | files = ( 146 | FA30F21E2AEEB6F9004EC6E9 /* NIO in Frameworks */, 147 | FA54EB942AF90B52001AE449 /* LoggingSyslog in Frameworks */, 148 | FA2EADAA2AED589F000F8F7A /* NeoHubLib.framework in Frameworks */, 149 | FA3937222AF41D84001D9105 /* LaunchAtLogin in Frameworks */, 150 | FA30F22F2AEF828A004EC6E9 /* KeyboardShortcuts in Frameworks */, 151 | FA54EB7F2AF789D4001AE449 /* Logging in Frameworks */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | FA2AE45A2AEB9CA000D24008 /* Frameworks */ = { 156 | isa = PBXFrameworksBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | FA30F2202AEEB70A004EC6E9 /* NIO in Frameworks */, 160 | FA2EAD9A2AED3906000F8F7A /* ArgumentParser in Frameworks */, 161 | FA54EB852AF7CB7E001AE449 /* Logging in Frameworks */, 162 | FA2EADB12AED65A6000F8F7A /* NeoHubLib.framework in Frameworks */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | FA2EAD9F2AED589F000F8F7A /* Frameworks */ = { 167 | isa = PBXFrameworksBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | }; 173 | /* End PBXFrameworksBuildPhase section */ 174 | 175 | /* Begin PBXGroup section */ 176 | FA2AE43E2AEB980700D24008 = { 177 | isa = PBXGroup; 178 | children = ( 179 | FA03D4C82AFA302A00790D2F /* README.md */, 180 | FA728EF32AFF716E0013610E /* CHANGELOG.md */, 181 | FA2AE4492AEB980700D24008 /* NeoHub */, 182 | FA2AE45E2AEB9CA000D24008 /* NeoHubCLI */, 183 | FA2EADA32AED589F000F8F7A /* NeoHubLib */, 184 | FA2AE4482AEB980700D24008 /* Products */, 185 | FA2AE46B2AEBB9B600D24008 /* Frameworks */, 186 | ); 187 | sourceTree = ""; 188 | }; 189 | FA2AE4482AEB980700D24008 /* Products */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | FA2AE4472AEB980700D24008 /* NeoHub.app */, 193 | FA2AE45D2AEB9CA000D24008 /* neohub */, 194 | FA2EADA22AED589F000F8F7A /* NeoHubLib.framework */, 195 | ); 196 | name = Products; 197 | sourceTree = ""; 198 | }; 199 | FA2AE4492AEB980700D24008 /* NeoHub */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | FA2AE4A92AEBDDB800D24008 /* Info.plist */, 203 | FA54EB7A2AF6E3C5001AE449 /* components */, 204 | FA30F2262AEED1C5004EC6E9 /* views */, 205 | FA2AE44A2AEB980700D24008 /* App.swift */, 206 | FA54EB712AF5033D001AE449 /* CLI.swift */, 207 | FA54EB802AF789F6001AE449 /* Logger.swift */, 208 | FA2EAD962AED0C8B000F8F7A /* EditorStore.swift */, 209 | FA54EB732AF53ED9001AE449 /* RegularWindow.swift */, 210 | FA54EB8E2AF8ACDB001AE449 /* ActivationManager.swift */, 211 | FA54EB822AF7A381001AE449 /* NotificationManager.swift */, 212 | FA54EB8C2AF7F689001AE449 /* BugReporter.swift */, 213 | FA2EAD902AEC4C49000F8F7A /* SocketServer.swift */, 214 | FA2AE44E2AEB980800D24008 /* Assets.xcassets */, 215 | FA2AE4532AEB980800D24008 /* NeoHub.entitlements */, 216 | FA2AE4502AEB980800D24008 /* Preview Content */, 217 | ); 218 | path = NeoHub; 219 | sourceTree = ""; 220 | }; 221 | FA2AE4502AEB980800D24008 /* Preview Content */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | FA2AE4512AEB980800D24008 /* Preview Assets.xcassets */, 225 | ); 226 | path = "Preview Content"; 227 | sourceTree = ""; 228 | }; 229 | FA2AE45E2AEB9CA000D24008 /* NeoHubCLI */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | FA2AE4AA2AEBDF7800D24008 /* NeoHubCLI.entitlements */, 233 | FA2AE45F2AEB9CA000D24008 /* CLI.swift */, 234 | FA39371C2AF3A7CC001D9105 /* Shell.swift */, 235 | FA54EB862AF7CB97001AE449 /* Logger.swift */, 236 | FA2EAD922AEC4CCD000F8F7A /* SocketClient.swift */, 237 | ); 238 | path = NeoHubCLI; 239 | sourceTree = ""; 240 | }; 241 | FA2AE46B2AEBB9B600D24008 /* Frameworks */ = { 242 | isa = PBXGroup; 243 | children = ( 244 | ); 245 | name = Frameworks; 246 | sourceTree = ""; 247 | }; 248 | FA2EADA32AED589F000F8F7A /* NeoHubLib */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | FA2EADA42AED589F000F8F7A /* NeoHubLib.h */, 252 | FA2EADA52AED589F000F8F7A /* NeoHubLib.docc */, 253 | FA2EADAF2AED58C6000F8F7A /* Shared.swift */, 254 | ); 255 | path = NeoHubLib; 256 | sourceTree = ""; 257 | }; 258 | FA30F2262AEED1C5004EC6E9 /* views */ = { 259 | isa = PBXGroup; 260 | children = ( 261 | FA30F2272AEED1E7004EC6E9 /* MenuBarView.swift */, 262 | FA30F2292AEF7A7F004EC6E9 /* SwitcherView.swift */, 263 | FA54EB782AF6AE06001AE449 /* InstallationView.swift */, 264 | FA3937162AF291A5001D9105 /* SettingsView.swift */, 265 | FA54EB752AF654D7001AE449 /* AboutView.swift */, 266 | ); 267 | path = views; 268 | sourceTree = ""; 269 | }; 270 | FA54EB7A2AF6E3C5001AE449 /* components */ = { 271 | isa = PBXGroup; 272 | children = ( 273 | FA54EB7B2AF6E3E8001AE449 /* SettingsGroupModifier.swift */, 274 | ); 275 | path = components; 276 | sourceTree = ""; 277 | }; 278 | /* End PBXGroup section */ 279 | 280 | /* Begin PBXHeadersBuildPhase section */ 281 | FA2EAD9D2AED589F000F8F7A /* Headers */ = { 282 | isa = PBXHeadersBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | FA2EADA72AED589F000F8F7A /* NeoHubLib.h in Headers */, 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | }; 289 | /* End PBXHeadersBuildPhase section */ 290 | 291 | /* Begin PBXNativeTarget section */ 292 | FA2AE4462AEB980700D24008 /* NeoHub */ = { 293 | isa = PBXNativeTarget; 294 | buildConfigurationList = FA2AE4562AEB980800D24008 /* Build configuration list for PBXNativeTarget "NeoHub" */; 295 | buildPhases = ( 296 | FA2AE4432AEB980700D24008 /* Sources */, 297 | FA2AE4442AEB980700D24008 /* Frameworks */, 298 | FA2AE4452AEB980700D24008 /* Resources */, 299 | FA2AE4A42AEBDA1D00D24008 /* Embed Frameworks */, 300 | FA54EB772AF678AA001AE449 /* Set Build Version */, 301 | FAA5D7372AEEB074002DF3F8 /* Embed CLI */, 302 | ); 303 | buildRules = ( 304 | ); 305 | dependencies = ( 306 | FA54EB6F2AF4FD46001AE449 /* PBXTargetDependency */, 307 | FA2EADA92AED589F000F8F7A /* PBXTargetDependency */, 308 | ); 309 | name = NeoHub; 310 | packageProductDependencies = ( 311 | FA30F21D2AEEB6F9004EC6E9 /* NIO */, 312 | FA30F22E2AEF828A004EC6E9 /* KeyboardShortcuts */, 313 | FA3937212AF41D84001D9105 /* LaunchAtLogin */, 314 | FA54EB7E2AF789D4001AE449 /* Logging */, 315 | FA54EB932AF90B52001AE449 /* LoggingSyslog */, 316 | ); 317 | productName = NeoHub; 318 | productReference = FA2AE4472AEB980700D24008 /* NeoHub.app */; 319 | productType = "com.apple.product-type.application"; 320 | }; 321 | FA2AE45C2AEB9CA000D24008 /* NeoHubCLI */ = { 322 | isa = PBXNativeTarget; 323 | buildConfigurationList = FA2AE4612AEB9CA000D24008 /* Build configuration list for PBXNativeTarget "NeoHubCLI" */; 324 | buildPhases = ( 325 | FA2AE4592AEB9CA000D24008 /* Sources */, 326 | FA2AE45A2AEB9CA000D24008 /* Frameworks */, 327 | FA2AE45B2AEB9CA000D24008 /* CopyFiles */, 328 | ); 329 | buildRules = ( 330 | ); 331 | dependencies = ( 332 | FA2EADB42AED65A6000F8F7A /* PBXTargetDependency */, 333 | ); 334 | name = NeoHubCLI; 335 | packageProductDependencies = ( 336 | FA2EAD992AED3906000F8F7A /* ArgumentParser */, 337 | FA30F21F2AEEB70A004EC6E9 /* NIO */, 338 | FA54EB842AF7CB7E001AE449 /* Logging */, 339 | ); 340 | productName = NeoHubCLI; 341 | productReference = FA2AE45D2AEB9CA000D24008 /* neohub */; 342 | productType = "com.apple.product-type.tool"; 343 | }; 344 | FA2EADA12AED589F000F8F7A /* NeoHubLib */ = { 345 | isa = PBXNativeTarget; 346 | buildConfigurationList = FA2EADAC2AED589F000F8F7A /* Build configuration list for PBXNativeTarget "NeoHubLib" */; 347 | buildPhases = ( 348 | FA2EAD9D2AED589F000F8F7A /* Headers */, 349 | FA2EAD9E2AED589F000F8F7A /* Sources */, 350 | FA2EAD9F2AED589F000F8F7A /* Frameworks */, 351 | FA2EADA02AED589F000F8F7A /* Resources */, 352 | ); 353 | buildRules = ( 354 | ); 355 | dependencies = ( 356 | ); 357 | name = NeoHubLib; 358 | packageProductDependencies = ( 359 | ); 360 | productName = NeoHubLib; 361 | productReference = FA2EADA22AED589F000F8F7A /* NeoHubLib.framework */; 362 | productType = "com.apple.product-type.framework"; 363 | }; 364 | /* End PBXNativeTarget section */ 365 | 366 | /* Begin PBXProject section */ 367 | FA2AE43F2AEB980700D24008 /* Project object */ = { 368 | isa = PBXProject; 369 | attributes = { 370 | BuildIndependentTargetsInParallel = 1; 371 | LastSwiftUpdateCheck = 1500; 372 | LastUpgradeCheck = 1500; 373 | TargetAttributes = { 374 | FA2AE4462AEB980700D24008 = { 375 | CreatedOnToolsVersion = 15.0.1; 376 | }; 377 | FA2AE45C2AEB9CA000D24008 = { 378 | CreatedOnToolsVersion = 15.0.1; 379 | }; 380 | FA2EADA12AED589F000F8F7A = { 381 | CreatedOnToolsVersion = 15.0.1; 382 | }; 383 | }; 384 | }; 385 | buildConfigurationList = FA2AE4422AEB980700D24008 /* Build configuration list for PBXProject "NeoHub" */; 386 | compatibilityVersion = "Xcode 14.0"; 387 | developmentRegion = en; 388 | hasScannedForEncodings = 0; 389 | knownRegions = ( 390 | en, 391 | Base, 392 | ); 393 | mainGroup = FA2AE43E2AEB980700D24008; 394 | packageReferences = ( 395 | FA2EAD982AED3906000F8F7A /* XCRemoteSwiftPackageReference "swift-argument-parser" */, 396 | FA30F21C2AEEB6F8004EC6E9 /* XCRemoteSwiftPackageReference "swift-nio" */, 397 | FA30F22D2AEF828A004EC6E9 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 398 | FA3937202AF41D84001D9105 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, 399 | FA54EB7D2AF789D4001AE449 /* XCRemoteSwiftPackageReference "swift-log" */, 400 | FA54EB922AF90B52001AE449 /* XCRemoteSwiftPackageReference "swift-log-syslog" */, 401 | ); 402 | productRefGroup = FA2AE4482AEB980700D24008 /* Products */; 403 | projectDirPath = ""; 404 | projectRoot = ""; 405 | targets = ( 406 | FA2AE4462AEB980700D24008 /* NeoHub */, 407 | FA2AE45C2AEB9CA000D24008 /* NeoHubCLI */, 408 | FA2EADA12AED589F000F8F7A /* NeoHubLib */, 409 | ); 410 | }; 411 | /* End PBXProject section */ 412 | 413 | /* Begin PBXResourcesBuildPhase section */ 414 | FA2AE4452AEB980700D24008 /* Resources */ = { 415 | isa = PBXResourcesBuildPhase; 416 | buildActionMask = 2147483647; 417 | files = ( 418 | FA2AE4522AEB980800D24008 /* Preview Assets.xcassets in Resources */, 419 | FA2AE44F2AEB980800D24008 /* Assets.xcassets in Resources */, 420 | ); 421 | runOnlyForDeploymentPostprocessing = 0; 422 | }; 423 | FA2EADA02AED589F000F8F7A /* Resources */ = { 424 | isa = PBXResourcesBuildPhase; 425 | buildActionMask = 2147483647; 426 | files = ( 427 | ); 428 | runOnlyForDeploymentPostprocessing = 0; 429 | }; 430 | /* End PBXResourcesBuildPhase section */ 431 | 432 | /* Begin PBXShellScriptBuildPhase section */ 433 | FA54EB772AF678AA001AE449 /* Set Build Version */ = { 434 | isa = PBXShellScriptBuildPhase; 435 | alwaysOutOfDate = 1; 436 | buildActionMask = 2147483647; 437 | files = ( 438 | ); 439 | inputFileListPaths = ( 440 | ); 441 | inputPaths = ( 442 | $BUILT_PRODUCTS_DIR/$INFOPLIST_PATH, 443 | ); 444 | name = "Set Build Version"; 445 | outputFileListPaths = ( 446 | ); 447 | outputPaths = ( 448 | ); 449 | runOnlyForDeploymentPostprocessing = 0; 450 | shellPath = /bin/sh; 451 | shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :GitCommitHash' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :GitCommitHash string\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :GitCommitHash `git rev-parse --short HEAD`\" \"${INFO_PLIST}\"\n"; 452 | }; 453 | /* End PBXShellScriptBuildPhase section */ 454 | 455 | /* Begin PBXSourcesBuildPhase section */ 456 | FA2AE4432AEB980700D24008 /* Sources */ = { 457 | isa = PBXSourcesBuildPhase; 458 | buildActionMask = 2147483647; 459 | files = ( 460 | FA3937172AF291A5001D9105 /* SettingsView.swift in Sources */, 461 | FA54EB742AF53ED9001AE449 /* RegularWindow.swift in Sources */, 462 | FA54EB762AF654D7001AE449 /* AboutView.swift in Sources */, 463 | FA2EAD912AEC4C49000F8F7A /* SocketServer.swift in Sources */, 464 | FA54EB8D2AF7F689001AE449 /* BugReporter.swift in Sources */, 465 | FA54EB7C2AF6E3E8001AE449 /* SettingsGroupModifier.swift in Sources */, 466 | FA2AE44B2AEB980700D24008 /* App.swift in Sources */, 467 | FA54EB722AF5033D001AE449 /* CLI.swift in Sources */, 468 | FA54EB812AF789F6001AE449 /* Logger.swift in Sources */, 469 | FA30F22A2AEF7A7F004EC6E9 /* SwitcherView.swift in Sources */, 470 | FA54EB832AF7A381001AE449 /* NotificationManager.swift in Sources */, 471 | FA54EB8F2AF8ACDB001AE449 /* ActivationManager.swift in Sources */, 472 | FA2EAD972AED0C8B000F8F7A /* EditorStore.swift in Sources */, 473 | FA54EB792AF6AE06001AE449 /* InstallationView.swift in Sources */, 474 | FA30F2282AEED1E7004EC6E9 /* MenuBarView.swift in Sources */, 475 | ); 476 | runOnlyForDeploymentPostprocessing = 0; 477 | }; 478 | FA2AE4592AEB9CA000D24008 /* Sources */ = { 479 | isa = PBXSourcesBuildPhase; 480 | buildActionMask = 2147483647; 481 | files = ( 482 | FA39371D2AF3A7CC001D9105 /* Shell.swift in Sources */, 483 | FA2AE4602AEB9CA000D24008 /* CLI.swift in Sources */, 484 | FA2EAD932AEC4CCD000F8F7A /* SocketClient.swift in Sources */, 485 | FA54EB872AF7CB97001AE449 /* Logger.swift in Sources */, 486 | ); 487 | runOnlyForDeploymentPostprocessing = 0; 488 | }; 489 | FA2EAD9E2AED589F000F8F7A /* Sources */ = { 490 | isa = PBXSourcesBuildPhase; 491 | buildActionMask = 2147483647; 492 | files = ( 493 | FA2EADA62AED589F000F8F7A /* NeoHubLib.docc in Sources */, 494 | FA2EADB02AED58C6000F8F7A /* Shared.swift in Sources */, 495 | ); 496 | runOnlyForDeploymentPostprocessing = 0; 497 | }; 498 | /* End PBXSourcesBuildPhase section */ 499 | 500 | /* Begin PBXTargetDependency section */ 501 | FA2EADA92AED589F000F8F7A /* PBXTargetDependency */ = { 502 | isa = PBXTargetDependency; 503 | target = FA2EADA12AED589F000F8F7A /* NeoHubLib */; 504 | targetProxy = FA2EADA82AED589F000F8F7A /* PBXContainerItemProxy */; 505 | }; 506 | FA2EADB42AED65A6000F8F7A /* PBXTargetDependency */ = { 507 | isa = PBXTargetDependency; 508 | target = FA2EADA12AED589F000F8F7A /* NeoHubLib */; 509 | targetProxy = FA2EADB32AED65A6000F8F7A /* PBXContainerItemProxy */; 510 | }; 511 | FA54EB6F2AF4FD46001AE449 /* PBXTargetDependency */ = { 512 | isa = PBXTargetDependency; 513 | target = FA2AE45C2AEB9CA000D24008 /* NeoHubCLI */; 514 | targetProxy = FA54EB6E2AF4FD46001AE449 /* PBXContainerItemProxy */; 515 | }; 516 | /* End PBXTargetDependency section */ 517 | 518 | /* Begin XCBuildConfiguration section */ 519 | FA2AE4542AEB980800D24008 /* Debug */ = { 520 | isa = XCBuildConfiguration; 521 | buildSettings = { 522 | ALWAYS_SEARCH_USER_PATHS = NO; 523 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 524 | CLANG_ANALYZER_NONNULL = YES; 525 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 526 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 527 | CLANG_ENABLE_MODULES = YES; 528 | CLANG_ENABLE_OBJC_ARC = YES; 529 | CLANG_ENABLE_OBJC_WEAK = YES; 530 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 531 | CLANG_WARN_BOOL_CONVERSION = YES; 532 | CLANG_WARN_COMMA = YES; 533 | CLANG_WARN_CONSTANT_CONVERSION = YES; 534 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 535 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 536 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 537 | CLANG_WARN_EMPTY_BODY = YES; 538 | CLANG_WARN_ENUM_CONVERSION = YES; 539 | CLANG_WARN_INFINITE_RECURSION = YES; 540 | CLANG_WARN_INT_CONVERSION = YES; 541 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 542 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 543 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 544 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 545 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 546 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 547 | CLANG_WARN_STRICT_PROTOTYPES = YES; 548 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 549 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 550 | CLANG_WARN_UNREACHABLE_CODE = YES; 551 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 552 | COPY_PHASE_STRIP = NO; 553 | DEBUG_INFORMATION_FORMAT = dwarf; 554 | ENABLE_CODE_COVERAGE = NO; 555 | ENABLE_STRICT_OBJC_MSGSEND = YES; 556 | ENABLE_TESTABILITY = YES; 557 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 558 | GCC_C_LANGUAGE_STANDARD = gnu17; 559 | GCC_DYNAMIC_NO_PIC = NO; 560 | GCC_NO_COMMON_BLOCKS = YES; 561 | GCC_OPTIMIZATION_LEVEL = 0; 562 | GCC_PREPROCESSOR_DEFINITIONS = ( 563 | "DEBUG=1", 564 | "$(inherited)", 565 | ); 566 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 567 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 568 | GCC_WARN_UNDECLARED_SELECTOR = YES; 569 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 570 | GCC_WARN_UNUSED_FUNCTION = YES; 571 | GCC_WARN_UNUSED_VARIABLE = YES; 572 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 573 | MACOSX_DEPLOYMENT_TARGET = 13.0; 574 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 575 | MTL_FAST_MATH = YES; 576 | ONLY_ACTIVE_ARCH = YES; 577 | SDKROOT = macosx; 578 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 579 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 580 | }; 581 | name = Debug; 582 | }; 583 | FA2AE4552AEB980800D24008 /* Release */ = { 584 | isa = XCBuildConfiguration; 585 | buildSettings = { 586 | ALWAYS_SEARCH_USER_PATHS = NO; 587 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 588 | CLANG_ANALYZER_NONNULL = YES; 589 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 590 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 591 | CLANG_ENABLE_MODULES = YES; 592 | CLANG_ENABLE_OBJC_ARC = YES; 593 | CLANG_ENABLE_OBJC_WEAK = YES; 594 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 595 | CLANG_WARN_BOOL_CONVERSION = YES; 596 | CLANG_WARN_COMMA = YES; 597 | CLANG_WARN_CONSTANT_CONVERSION = YES; 598 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 599 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 600 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 601 | CLANG_WARN_EMPTY_BODY = YES; 602 | CLANG_WARN_ENUM_CONVERSION = YES; 603 | CLANG_WARN_INFINITE_RECURSION = YES; 604 | CLANG_WARN_INT_CONVERSION = YES; 605 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 606 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 607 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 608 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 609 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 610 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 611 | CLANG_WARN_STRICT_PROTOTYPES = YES; 612 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 613 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 614 | CLANG_WARN_UNREACHABLE_CODE = YES; 615 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 616 | COPY_PHASE_STRIP = NO; 617 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 618 | ENABLE_CODE_COVERAGE = NO; 619 | ENABLE_NS_ASSERTIONS = NO; 620 | ENABLE_STRICT_OBJC_MSGSEND = YES; 621 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 622 | GCC_C_LANGUAGE_STANDARD = gnu17; 623 | GCC_NO_COMMON_BLOCKS = YES; 624 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 625 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 626 | GCC_WARN_UNDECLARED_SELECTOR = YES; 627 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 628 | GCC_WARN_UNUSED_FUNCTION = YES; 629 | GCC_WARN_UNUSED_VARIABLE = YES; 630 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 631 | MACOSX_DEPLOYMENT_TARGET = 13.0; 632 | MTL_ENABLE_DEBUG_INFO = NO; 633 | MTL_FAST_MATH = YES; 634 | SDKROOT = macosx; 635 | SWIFT_COMPILATION_MODE = wholemodule; 636 | }; 637 | name = Release; 638 | }; 639 | FA2AE4572AEB980800D24008 /* Debug */ = { 640 | isa = XCBuildConfiguration; 641 | buildSettings = { 642 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 643 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 644 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 645 | CODE_SIGN_ENTITLEMENTS = NeoHub/NeoHub.entitlements; 646 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 647 | CODE_SIGN_STYLE = Automatic; 648 | COMBINE_HIDPI_IMAGES = YES; 649 | CURRENT_PROJECT_VERSION = 1; 650 | DEVELOPMENT_ASSET_PATHS = "\"NeoHub/Preview Content\""; 651 | DEVELOPMENT_TEAM = Q62V4YP545; 652 | ENABLE_HARDENED_RUNTIME = YES; 653 | ENABLE_PREVIEWS = YES; 654 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 655 | GENERATE_INFOPLIST_FILE = YES; 656 | INFOPLIST_FILE = NeoHub/Info.plist; 657 | INFOPLIST_KEY_CFBundleDisplayName = NeoHub; 658 | INFOPLIST_KEY_LSUIElement = YES; 659 | INFOPLIST_KEY_NSHumanReadableCopyright = "© Alex Fedoseev. Icon by u/danbee."; 660 | LD_RUNPATH_SEARCH_PATHS = ( 661 | "$(inherited)", 662 | "@executable_path/../Frameworks", 663 | ); 664 | MARKETING_VERSION = 0.2.1; 665 | OTHER_SWIFT_FLAGS = ""; 666 | PRODUCT_BUNDLE_IDENTIFIER = com.alex35mil.NeoHub; 667 | PRODUCT_NAME = "$(TARGET_NAME)"; 668 | SWIFT_EMIT_LOC_STRINGS = YES; 669 | SWIFT_VERSION = 5.0; 670 | }; 671 | name = Debug; 672 | }; 673 | FA2AE4582AEB980800D24008 /* Release */ = { 674 | isa = XCBuildConfiguration; 675 | buildSettings = { 676 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 677 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 678 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 679 | CODE_SIGN_ENTITLEMENTS = NeoHub/NeoHub.entitlements; 680 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 681 | CODE_SIGN_STYLE = Automatic; 682 | COMBINE_HIDPI_IMAGES = YES; 683 | CURRENT_PROJECT_VERSION = 1; 684 | DEVELOPMENT_ASSET_PATHS = "\"NeoHub/Preview Content\""; 685 | DEVELOPMENT_TEAM = Q62V4YP545; 686 | ENABLE_HARDENED_RUNTIME = YES; 687 | ENABLE_PREVIEWS = YES; 688 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 689 | GENERATE_INFOPLIST_FILE = YES; 690 | INFOPLIST_FILE = NeoHub/Info.plist; 691 | INFOPLIST_KEY_CFBundleDisplayName = NeoHub; 692 | INFOPLIST_KEY_LSUIElement = YES; 693 | INFOPLIST_KEY_NSHumanReadableCopyright = "© Alex Fedoseev. Icon by u/danbee."; 694 | LD_RUNPATH_SEARCH_PATHS = ( 695 | "$(inherited)", 696 | "@executable_path/../Frameworks", 697 | ); 698 | MARKETING_VERSION = 0.2.1; 699 | OTHER_SWIFT_FLAGS = ""; 700 | PRODUCT_BUNDLE_IDENTIFIER = com.alex35mil.NeoHub; 701 | PRODUCT_NAME = "$(TARGET_NAME)"; 702 | SWIFT_EMIT_LOC_STRINGS = YES; 703 | SWIFT_VERSION = 5.0; 704 | }; 705 | name = Release; 706 | }; 707 | FA2AE4622AEB9CA000D24008 /* Debug */ = { 708 | isa = XCBuildConfiguration; 709 | buildSettings = { 710 | CODE_SIGN_ENTITLEMENTS = NeoHubCLI/NeoHubCLI.entitlements; 711 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 712 | CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; 713 | CODE_SIGN_STYLE = Automatic; 714 | DEVELOPMENT_TEAM = Q62V4YP545; 715 | ENABLE_HARDENED_RUNTIME = YES; 716 | GCC_PREPROCESSOR_DEFINITIONS = ( 717 | "DEBUG=1", 718 | "$(inherited)", 719 | ); 720 | LD_RUNPATH_SEARCH_PATHS = ( 721 | "@executable_path/../Frameworks", 722 | /usr/local/lib, 723 | "@loader_path/", 724 | ); 725 | MARKETING_VERSION = ""; 726 | OTHER_CODE_SIGN_FLAGS = ""; 727 | PRODUCT_BUNDLE_IDENTIFIER = com.alex35mil.NeoHub.CLI; 728 | PRODUCT_NAME = neohub; 729 | SKIP_INSTALL = YES; 730 | SWIFT_VERSION = 5.0; 731 | }; 732 | name = Debug; 733 | }; 734 | FA2AE4632AEB9CA000D24008 /* Release */ = { 735 | isa = XCBuildConfiguration; 736 | buildSettings = { 737 | CODE_SIGN_ENTITLEMENTS = NeoHubCLI/NeoHubCLI.entitlements; 738 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 739 | CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; 740 | CODE_SIGN_STYLE = Automatic; 741 | DEVELOPMENT_TEAM = Q62V4YP545; 742 | ENABLE_HARDENED_RUNTIME = YES; 743 | GCC_PREPROCESSOR_DEFINITIONS = ""; 744 | LD_RUNPATH_SEARCH_PATHS = ( 745 | "@executable_path/../Frameworks", 746 | /usr/local/lib, 747 | ); 748 | MARKETING_VERSION = ""; 749 | OTHER_CODE_SIGN_FLAGS = ""; 750 | PRODUCT_BUNDLE_IDENTIFIER = com.alex35mil.NeoHub.CLI; 751 | PRODUCT_NAME = neohub; 752 | SKIP_INSTALL = YES; 753 | SWIFT_VERSION = 5.0; 754 | }; 755 | name = Release; 756 | }; 757 | FA2EADAD2AED589F000F8F7A /* Debug */ = { 758 | isa = XCBuildConfiguration; 759 | buildSettings = { 760 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 761 | CODE_SIGN_STYLE = Automatic; 762 | COMBINE_HIDPI_IMAGES = YES; 763 | CURRENT_PROJECT_VERSION = 1; 764 | DEFINES_MODULE = YES; 765 | DEVELOPMENT_TEAM = Q62V4YP545; 766 | DYLIB_COMPATIBILITY_VERSION = 1; 767 | DYLIB_CURRENT_VERSION = 1; 768 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 769 | ENABLE_MODULE_VERIFIER = YES; 770 | GENERATE_INFOPLIST_FILE = YES; 771 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 772 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 773 | LD_RUNPATH_SEARCH_PATHS = ( 774 | "$(inherited)", 775 | "@executable_path/../Frameworks", 776 | "@loader_path/Frameworks", 777 | ); 778 | MARKETING_VERSION = 1.0; 779 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 780 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 781 | PRODUCT_BUNDLE_IDENTIFIER = com.alex35mil.NeoHub.Lib; 782 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 783 | SKIP_INSTALL = YES; 784 | SWIFT_EMIT_LOC_STRINGS = YES; 785 | SWIFT_VERSION = 5.0; 786 | VERSIONING_SYSTEM = "apple-generic"; 787 | VERSION_INFO_PREFIX = ""; 788 | }; 789 | name = Debug; 790 | }; 791 | FA2EADAE2AED589F000F8F7A /* Release */ = { 792 | isa = XCBuildConfiguration; 793 | buildSettings = { 794 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 795 | CODE_SIGN_STYLE = Automatic; 796 | COMBINE_HIDPI_IMAGES = YES; 797 | CURRENT_PROJECT_VERSION = 1; 798 | DEFINES_MODULE = YES; 799 | DEVELOPMENT_TEAM = Q62V4YP545; 800 | DYLIB_COMPATIBILITY_VERSION = 1; 801 | DYLIB_CURRENT_VERSION = 1; 802 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 803 | ENABLE_MODULE_VERIFIER = YES; 804 | GENERATE_INFOPLIST_FILE = YES; 805 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 806 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 807 | LD_RUNPATH_SEARCH_PATHS = ( 808 | "$(inherited)", 809 | "@executable_path/../Frameworks", 810 | "@loader_path/Frameworks", 811 | ); 812 | MARKETING_VERSION = 1.0; 813 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 814 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 815 | PRODUCT_BUNDLE_IDENTIFIER = com.alex35mil.NeoHub.Lib; 816 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 817 | SKIP_INSTALL = YES; 818 | SWIFT_EMIT_LOC_STRINGS = YES; 819 | SWIFT_VERSION = 5.0; 820 | VERSIONING_SYSTEM = "apple-generic"; 821 | VERSION_INFO_PREFIX = ""; 822 | }; 823 | name = Release; 824 | }; 825 | /* End XCBuildConfiguration section */ 826 | 827 | /* Begin XCConfigurationList section */ 828 | FA2AE4422AEB980700D24008 /* Build configuration list for PBXProject "NeoHub" */ = { 829 | isa = XCConfigurationList; 830 | buildConfigurations = ( 831 | FA2AE4542AEB980800D24008 /* Debug */, 832 | FA2AE4552AEB980800D24008 /* Release */, 833 | ); 834 | defaultConfigurationIsVisible = 0; 835 | defaultConfigurationName = Release; 836 | }; 837 | FA2AE4562AEB980800D24008 /* Build configuration list for PBXNativeTarget "NeoHub" */ = { 838 | isa = XCConfigurationList; 839 | buildConfigurations = ( 840 | FA2AE4572AEB980800D24008 /* Debug */, 841 | FA2AE4582AEB980800D24008 /* Release */, 842 | ); 843 | defaultConfigurationIsVisible = 0; 844 | defaultConfigurationName = Release; 845 | }; 846 | FA2AE4612AEB9CA000D24008 /* Build configuration list for PBXNativeTarget "NeoHubCLI" */ = { 847 | isa = XCConfigurationList; 848 | buildConfigurations = ( 849 | FA2AE4622AEB9CA000D24008 /* Debug */, 850 | FA2AE4632AEB9CA000D24008 /* Release */, 851 | ); 852 | defaultConfigurationIsVisible = 0; 853 | defaultConfigurationName = Release; 854 | }; 855 | FA2EADAC2AED589F000F8F7A /* Build configuration list for PBXNativeTarget "NeoHubLib" */ = { 856 | isa = XCConfigurationList; 857 | buildConfigurations = ( 858 | FA2EADAD2AED589F000F8F7A /* Debug */, 859 | FA2EADAE2AED589F000F8F7A /* Release */, 860 | ); 861 | defaultConfigurationIsVisible = 0; 862 | defaultConfigurationName = Release; 863 | }; 864 | /* End XCConfigurationList section */ 865 | 866 | /* Begin XCRemoteSwiftPackageReference section */ 867 | FA2EAD982AED3906000F8F7A /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { 868 | isa = XCRemoteSwiftPackageReference; 869 | repositoryURL = "https://github.com/apple/swift-argument-parser.git"; 870 | requirement = { 871 | kind = upToNextMajorVersion; 872 | minimumVersion = 1.2.3; 873 | }; 874 | }; 875 | FA30F21C2AEEB6F8004EC6E9 /* XCRemoteSwiftPackageReference "swift-nio" */ = { 876 | isa = XCRemoteSwiftPackageReference; 877 | repositoryURL = "https://github.com/apple/swift-nio.git"; 878 | requirement = { 879 | kind = upToNextMajorVersion; 880 | minimumVersion = 2.61.0; 881 | }; 882 | }; 883 | FA30F22D2AEF828A004EC6E9 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { 884 | isa = XCRemoteSwiftPackageReference; 885 | repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; 886 | requirement = { 887 | kind = upToNextMajorVersion; 888 | minimumVersion = 1.15.0; 889 | }; 890 | }; 891 | FA3937202AF41D84001D9105 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { 892 | isa = XCRemoteSwiftPackageReference; 893 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; 894 | requirement = { 895 | kind = upToNextMajorVersion; 896 | minimumVersion = 1.0.0; 897 | }; 898 | }; 899 | FA54EB7D2AF789D4001AE449 /* XCRemoteSwiftPackageReference "swift-log" */ = { 900 | isa = XCRemoteSwiftPackageReference; 901 | repositoryURL = "https://github.com/apple/swift-log.git"; 902 | requirement = { 903 | kind = upToNextMajorVersion; 904 | minimumVersion = 1.5.3; 905 | }; 906 | }; 907 | FA54EB922AF90B52001AE449 /* XCRemoteSwiftPackageReference "swift-log-syslog" */ = { 908 | isa = XCRemoteSwiftPackageReference; 909 | repositoryURL = "https://github.com/ianpartridge/swift-log-syslog.git"; 910 | requirement = { 911 | kind = upToNextMajorVersion; 912 | minimumVersion = 1.0.2; 913 | }; 914 | }; 915 | /* End XCRemoteSwiftPackageReference section */ 916 | 917 | /* Begin XCSwiftPackageProductDependency section */ 918 | FA2EAD992AED3906000F8F7A /* ArgumentParser */ = { 919 | isa = XCSwiftPackageProductDependency; 920 | package = FA2EAD982AED3906000F8F7A /* XCRemoteSwiftPackageReference "swift-argument-parser" */; 921 | productName = ArgumentParser; 922 | }; 923 | FA30F21D2AEEB6F9004EC6E9 /* NIO */ = { 924 | isa = XCSwiftPackageProductDependency; 925 | package = FA30F21C2AEEB6F8004EC6E9 /* XCRemoteSwiftPackageReference "swift-nio" */; 926 | productName = NIO; 927 | }; 928 | FA30F21F2AEEB70A004EC6E9 /* NIO */ = { 929 | isa = XCSwiftPackageProductDependency; 930 | package = FA30F21C2AEEB6F8004EC6E9 /* XCRemoteSwiftPackageReference "swift-nio" */; 931 | productName = NIO; 932 | }; 933 | FA30F22E2AEF828A004EC6E9 /* KeyboardShortcuts */ = { 934 | isa = XCSwiftPackageProductDependency; 935 | package = FA30F22D2AEF828A004EC6E9 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; 936 | productName = KeyboardShortcuts; 937 | }; 938 | FA3937212AF41D84001D9105 /* LaunchAtLogin */ = { 939 | isa = XCSwiftPackageProductDependency; 940 | package = FA3937202AF41D84001D9105 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; 941 | productName = LaunchAtLogin; 942 | }; 943 | FA54EB7E2AF789D4001AE449 /* Logging */ = { 944 | isa = XCSwiftPackageProductDependency; 945 | package = FA54EB7D2AF789D4001AE449 /* XCRemoteSwiftPackageReference "swift-log" */; 946 | productName = Logging; 947 | }; 948 | FA54EB842AF7CB7E001AE449 /* Logging */ = { 949 | isa = XCSwiftPackageProductDependency; 950 | package = FA54EB7D2AF789D4001AE449 /* XCRemoteSwiftPackageReference "swift-log" */; 951 | productName = Logging; 952 | }; 953 | FA54EB932AF90B52001AE449 /* LoggingSyslog */ = { 954 | isa = XCSwiftPackageProductDependency; 955 | package = FA54EB922AF90B52001AE449 /* XCRemoteSwiftPackageReference "swift-log-syslog" */; 956 | productName = LoggingSyslog; 957 | }; 958 | /* End XCSwiftPackageProductDependency section */ 959 | }; 960 | rootObject = FA2AE43F2AEB980700D24008 /* Project object */; 961 | } 962 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "keyboardshortcuts", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/sindresorhus/KeyboardShortcuts", 7 | "state" : { 8 | "revision" : "ac302e21da5883f4bd0490cbd0cb710b08740500", 9 | "version" : "1.15.0" 10 | } 11 | }, 12 | { 13 | "identity" : "launchatlogin-modern", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/sindresorhus/LaunchAtLogin-Modern", 16 | "state" : { 17 | "revision" : "638e6c426b8d113eae255baeba59ed2d9d9a7c4d", 18 | "version" : "1.0.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-argument-parser", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-argument-parser.git", 25 | "state" : { 26 | "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", 27 | "version" : "1.2.3" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-atomics", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-atomics.git", 34 | "state" : { 35 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 36 | "version" : "1.2.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-collections", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-collections.git", 43 | "state" : { 44 | "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", 45 | "version" : "1.0.5" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-log", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-log.git", 52 | "state" : { 53 | "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", 54 | "version" : "1.5.3" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-log-syslog", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/ianpartridge/swift-log-syslog.git", 61 | "state" : { 62 | "revision" : "654ea6e32b9802c45468c4c743ed507c36a775c2", 63 | "version" : "1.0.2" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-nio", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-nio.git", 70 | "state" : { 71 | "revision" : "9497e442486aab515c8486ef8153a506f93a5032", 72 | "version" : "2.61.0" 73 | } 74 | } 75 | ], 76 | "version" : 2 77 | } 78 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/project.xcworkspace/xcuserdata/Alex.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/xcshareddata/xcschemes/NeoHub.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 57 | 63 | 64 | 65 | 66 | 72 | 74 | 80 | 81 | 82 | 83 | 85 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/xcshareddata/xcschemes/NeoHubCLI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/xcuserdata/Alex.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /NeoHub.xcodeproj/xcuserdata/Alex.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NeoHub.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | NeoHubCLI.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | NeoHubLib.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 2 21 | 22 | NeoHubXPCService.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 2 26 | 27 | 28 | SuppressBuildableAutocreation 29 | 30 | FA2AE4462AEB980700D24008 31 | 32 | primary 33 | 34 | 35 | FA2AE45C2AEB9CA000D24008 36 | 37 | primary 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /NeoHub/ActivationManager.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | enum ActivationTarget { 4 | case neohub(NonSwitcherWindow) 5 | case neovide(Editor) 6 | case other(NSRunningApplication) 7 | } 8 | 9 | struct NonSwitcherWindow { 10 | let window: NSWindow 11 | 12 | init?(_ window: NSWindow, switcherWindow: SwitcherWindowRef) { 13 | guard !switcherWindow.isSameWindow(window) else { return nil } 14 | self.window = window 15 | } 16 | 17 | func activate() { 18 | window.makeKeyAndOrderFront(nil) 19 | NSApp.activate(ignoringOtherApps: true) 20 | } 21 | } 22 | 23 | final class ActivationManager { 24 | private(set) var activationTarget: ActivationTarget? 25 | 26 | public func setActivationTarget( 27 | currentApp: NSRunningApplication?, 28 | switcherWindow: SwitcherWindowRef, 29 | editors: [Editor] 30 | ) { 31 | let nextActivationTarget = currentApp.flatMap { app in 32 | if app.bundleIdentifier == APP_BUNDLE_ID { 33 | if 34 | let currentWindow = NSApplication.shared.mainWindow, 35 | let nonSwitcherWindow = NonSwitcherWindow(currentWindow, switcherWindow: switcherWindow) 36 | { 37 | return ActivationTarget.neohub(nonSwitcherWindow) 38 | } else { 39 | return nil 40 | } 41 | } 42 | 43 | if let editor = editors.first(where: { editor in editor.processIdentifier == app.processIdentifier }) { 44 | return .neovide(editor) 45 | } 46 | 47 | return .other(app) 48 | } 49 | 50 | self.activationTarget = nextActivationTarget 51 | } 52 | 53 | public func activateTarget() { 54 | switch self.activationTarget { 55 | case nil: () 56 | case .neohub(let window): window.activate() 57 | case .neovide(let editor): editor.activate() 58 | case .other(let app): app.activate() 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /NeoHub/App.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | import KeyboardShortcuts 4 | 5 | let APP_NAME = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as! String 6 | let APP_VERSION = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String 7 | let APP_BUILD = Bundle.main.object(forInfoDictionaryKey: "GitCommitHash") as! String 8 | let APP_BUNDLE_ID = Bundle.main.bundleIdentifier! 9 | 10 | extension KeyboardShortcuts.Name { 11 | static let toggleSwitcher = Self( 12 | "toggleSwitcher", 13 | default: .init(.n, modifiers: [.command, .control]) 14 | ) 15 | 16 | static let toggleLastActiveEditor = Self( 17 | "toggleLastActiveEditor", 18 | default: .init(.z, modifiers: [.command, .control]) 19 | ) 20 | 21 | static let restartEditor = Self("restartEditor") 22 | } 23 | 24 | @main 25 | struct NeoHubApp: App { 26 | @NSApplicationDelegateAdaptor(AppDelegate.self) var app 27 | 28 | var body: some Scene { 29 | MenuBarExtra( 30 | content: { 31 | MenuBarView( 32 | cli: app.cli, 33 | editorStore: app.editorStore, 34 | settingsWindow: app.settingsWindow, 35 | aboutWindow: app.aboutWindow 36 | ) 37 | }, 38 | label: { MenuBarIcon() } 39 | ) 40 | } 41 | } 42 | 43 | class AppDelegate: NSObject, NSApplicationDelegate { 44 | let cli: CLI 45 | let editorStore: EditorStore 46 | let server: SocketServer 47 | let switcherWindow: SwitcherWindow 48 | let installationWindow: RegularWindow 49 | let settingsWindow: RegularWindow 50 | let aboutWindow: RegularWindow 51 | let windowCounter: WindowCounter 52 | let activationManager: ActivationManager 53 | 54 | override init() { 55 | let cli = CLI() 56 | let windowCounter = WindowCounter() 57 | let activationManager = ActivationManager() 58 | 59 | let switcherWindowRef = SwitcherWindowRef() 60 | let installationWindowRef = RegularWindowRef() 61 | 62 | let editorStore = EditorStore( 63 | activationManager: activationManager, 64 | switcherWindow: switcherWindowRef 65 | ) 66 | 67 | self.cli = cli 68 | self.server = SocketServer(store: editorStore) 69 | self.editorStore = editorStore 70 | self.settingsWindow = RegularWindow( 71 | width: SettingsView.defaultWidth, 72 | content: { SettingsView(cli: cli) }, 73 | windowCounter: windowCounter 74 | ) 75 | self.aboutWindow = RegularWindow( 76 | width: AboutView.defaultWidth, 77 | content: { AboutView() }, 78 | windowCounter: windowCounter 79 | ) 80 | self.switcherWindow = SwitcherWindow( 81 | editorStore: editorStore, 82 | settingsWindow: settingsWindow, 83 | selfRef: switcherWindowRef, 84 | activationManager: activationManager 85 | ) 86 | self.windowCounter = windowCounter 87 | self.activationManager = activationManager 88 | 89 | self.installationWindow = RegularWindow( 90 | title: APP_NAME, 91 | width: InstallationView.defaultWidth, 92 | content: { 93 | InstallationView( 94 | cli: cli, 95 | installationWindow: installationWindowRef 96 | ) 97 | }, 98 | windowCounter: windowCounter 99 | ) 100 | 101 | switcherWindowRef.set(self.switcherWindow) 102 | installationWindowRef.set(self.installationWindow) 103 | 104 | super.init() 105 | } 106 | 107 | func applicationDidFinishLaunching(_ notification: Notification) { 108 | log.info("Application launched") 109 | 110 | log.info("Registering notification manager...") 111 | NotificationManager.shared.registerDelegate() 112 | 113 | log.info("Starting socket server...") 114 | self.server.start() 115 | 116 | log.info("Updating CLI status...") 117 | self.cli.updateStatusOnLaunch { status in 118 | if case .error(_) = status { 119 | self.installationWindow.open() 120 | } 121 | } 122 | } 123 | 124 | func applicationDidResignActive(_ notification: Notification) { 125 | log.trace("Application became inactive") 126 | switcherWindow.hide() 127 | } 128 | 129 | func applicationWillTerminate(_ notification: Notification) { 130 | log.info("Application is about to terminate") 131 | server.stop() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "neovim-hicontrast-1024-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "neovim-hicontrast-1024-32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "neovim-hicontrast-1024-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "neovim-hicontrast-1024-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "neovim-hicontrast-1024-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "neovim-hicontrast-1024-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "neovim-hicontrast-1024-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "neovim-hicontrast-1024-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "neovim-hicontrast-1024-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "neovim-hicontrast-1024-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-1024.png -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-128.png -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-16.png -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-256.png -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-32.png -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-512.png -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/AppIcon.appiconset/neovim-hicontrast-1024-64.png -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/EditorIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "MenuBarIcon.pdf", 9 | "idiom" : "mac", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | }, 17 | "properties" : { 18 | "preserves-vector-representation" : true, 19 | "template-rendering-intent" : "template" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/EditorIcon.imageset/MenuBarIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/EditorIcon.imageset/MenuBarIcon.pdf -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/MenuBarIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "MenuBarIcon.pdf", 9 | "idiom" : "mac", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | }, 17 | "properties" : { 18 | "compression-type" : "lossless", 19 | "preserves-vector-representation" : true, 20 | "template-rendering-intent" : "template" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NeoHub/Assets.xcassets/MenuBarIcon.imageset/MenuBarIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex35mil/NeoHub/6d5ab7f9763fef75219304f7e400e98776fa4432/NeoHub/Assets.xcassets/MenuBarIcon.imageset/MenuBarIcon.pdf -------------------------------------------------------------------------------- /NeoHub/BugReporter.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Foundation 3 | 4 | private struct GitHub { 5 | static let user = "alex35mil" 6 | static let repo = APP_NAME 7 | } 8 | 9 | struct BugReporter { 10 | static func report(_ error: ReportableError) { 11 | let url = BugReporter.buildUrl(title: error.message, error: String(describing: error)) 12 | NSWorkspace.shared.open(url) 13 | } 14 | 15 | // Since NotificationCenter can't reliably transfer ReportableError, we have to accept string'ish error 16 | static func report(title: String, error: String) { 17 | let url = BugReporter.buildUrl(title: title, error: error) 18 | NSWorkspace.shared.open(url) 19 | } 20 | 21 | private static func buildUrl(title: String, error: String) -> URL { 22 | let title = title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) 23 | let body = 24 | """ 25 | ## What happened? 26 | _Reproduction steps, context, etc._ 27 | 28 | ## Error details 29 | ``` 30 | \(error) 31 | ``` 32 | """ 33 | .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) 34 | 35 | let path = "https://github.com/\(GitHub.user)/\(GitHub.repo)/issues/new" 36 | let query = "title=\(title ?? "")&body=\(body ?? "")&labels=user-report" 37 | let url = "\(path)?\(query)" 38 | 39 | if let url = URL(string: url) { 40 | return url 41 | } else { 42 | log.warning("Failed to create the reporter url from: \(url)") 43 | return URL(string: path)! 44 | } 45 | } 46 | } 47 | 48 | struct ReportableError: Error { 49 | private(set) var message: String 50 | private let appVersion: String 51 | private let appBuild: String 52 | private let code: Int? 53 | private(set) var context: String 54 | private var meta: [String: Any]? 55 | private let osVersion: String 56 | private let arch: String? 57 | private let originalError: Error? 58 | 59 | init( 60 | _ message: String, 61 | code: Int? = nil, 62 | meta: [String: Any]? = nil, 63 | file: NSString = #file, 64 | function: NSString = #function, 65 | error: Error? = nil 66 | ) { 67 | if let error, var reportableError = error as? Self { 68 | if message != reportableError.message { 69 | reportableError.message = "\(message) → \(reportableError.message)" 70 | } 71 | 72 | let context = Self.buildContext(from: (file: file, function: function)) 73 | 74 | if context != reportableError.context { 75 | reportableError.context = "\(context) → \(reportableError.context)" 76 | } 77 | 78 | switch (meta, reportableError.meta) { 79 | case (.some(let meta), .none): 80 | reportableError.meta = meta 81 | case (.some(let meta), .some(var reportableErrorMeta)): 82 | reportableErrorMeta.merge(meta) { c, _ in c } 83 | case 84 | (.none, .some(_)), 85 | (.none, .none): 86 | () 87 | } 88 | 89 | self = reportableError 90 | } else { 91 | self.message = message 92 | self.appVersion = APP_VERSION 93 | self.appBuild = APP_BUILD 94 | self.osVersion = ProcessInfo.processInfo.operatingSystemVersionString 95 | self.arch = Self.getSystemArch() 96 | self.code = code 97 | self.context = Self.buildContext(from: (file: file, function: function)) 98 | self.originalError = error 99 | 100 | switch (meta, error.flatMap { err in err as NSError }) { 101 | case (.some(var meta), .some(let nsError)) where !nsError.userInfo.isEmpty: 102 | meta.merge(nsError.userInfo) { c, _ in c } 103 | self.meta = meta 104 | case (.some(let meta), _): 105 | self.meta = meta 106 | case (.none, .some(let nsError)) where !nsError.userInfo.isEmpty: 107 | self.meta = nsError.userInfo 108 | case (.none, _): 109 | self.meta = nil 110 | } 111 | } 112 | } 113 | 114 | var localizedDescription: String { 115 | String(describing: self) 116 | } 117 | 118 | private static func buildContext(from loc: (file: NSString, function: NSString)) -> String { 119 | "\((loc.file.lastPathComponent as NSString).deletingPathExtension)#\(loc.function)" 120 | } 121 | 122 | private static func getSystemArch() -> String? { 123 | var sysinfo = utsname() 124 | 125 | uname(&sysinfo) 126 | 127 | let data = Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)) 128 | 129 | return String(bytes: data, encoding: .ascii)?.trimmingCharacters(in: .controlCharacters) 130 | } 131 | } 132 | 133 | extension ReportableError: CustomStringConvertible { 134 | var description: String { 135 | var output = 136 | """ 137 | \(message) 138 | App: Version \(appVersion) (Build \(appBuild)) 139 | macOS: \(osVersion) 140 | Arch: \(arch ?? "?") 141 | Context: \(context) 142 | """ 143 | 144 | if let code { 145 | output.append("\n") 146 | output.append("Code: \(code)") 147 | } 148 | 149 | if let error = originalError { 150 | output.append("\n") 151 | output.append("Original Error: \(error)") 152 | } 153 | 154 | if let meta, !meta.isEmpty { 155 | output.append( 156 | """ 157 | 158 | Metadata: 159 | \(meta.debugDescription) 160 | """ 161 | ) 162 | } 163 | 164 | return output 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /NeoHub/CLI.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Foundation 3 | 4 | private struct Bin { 5 | static let source = Bundle.main.bundlePath + "/Contents/SharedSupport/neohub" 6 | static let destination = "/usr/local/bin/neohub" 7 | } 8 | 9 | private struct Lib { 10 | static let source = Bundle.main.bundlePath + "/Contents/Frameworks/NeoHubLib.framework" 11 | static let destination = "/usr/local/lib/NeoHubLib.framework" 12 | 13 | static var parent: String { 14 | return URL(fileURLWithPath: destination).deletingLastPathComponent().path 15 | } 16 | } 17 | 18 | enum CLIOperation { 19 | case install 20 | case uninstall 21 | } 22 | 23 | enum CLIStatus { 24 | case ok 25 | case error(reason: CLIError) 26 | } 27 | 28 | enum CLIError { 29 | case notInstalled 30 | case versionMismatch 31 | case unexpectedError(Error) 32 | } 33 | 34 | enum CLIInstallationError: Error { 35 | case failedToCreateAppleScript 36 | case userCanceledOperation 37 | case failedToExecuteAppleScript(error: NSDictionary) 38 | } 39 | 40 | final class CLI: ObservableObject { 41 | @Published private(set) var status: CLIStatus = .ok 42 | 43 | func updateStatusOnLaunch(_ cb: @escaping (CLIStatus) -> Void) { 44 | DispatchQueue.global().async { 45 | log.info("Getting the CLI status...") 46 | 47 | let status = Self.getStatus() 48 | 49 | log.info("CLI status: \(status)") 50 | 51 | DispatchQueue.main.async { 52 | if case .ok = status { 53 | cb(status) 54 | } else { 55 | self.status = status 56 | cb(status) 57 | } 58 | } 59 | } 60 | } 61 | 62 | static func getStatus() -> CLIStatus { 63 | let fs = FileManager.default 64 | 65 | let installed = fs.fileExists(atPath: Bin.destination) && fs.fileExists(atPath: Lib.destination) 66 | 67 | if !installed { 68 | return .error(reason: .notInstalled) 69 | } 70 | 71 | let version = Self.getVersion() 72 | 73 | switch version { 74 | case .success(let version): 75 | if version == APP_VERSION { 76 | return .ok 77 | } else { 78 | return .error(reason: .versionMismatch) 79 | } 80 | case .failure(let error): 81 | log.error("Failed to get a CLI version. \(error)") 82 | return .error(reason: .unexpectedError(error)) 83 | } 84 | } 85 | 86 | func perform(_ operation: CLIOperation, andThen callback: @escaping (Result, CLIStatus) -> Void) { 87 | DispatchQueue.global(qos: .background).async { 88 | let script = 89 | switch operation { 90 | case .install: 91 | "do shell script \"mkdir -p \(Lib.parent) && cp -Rf \(Lib.source) \(Lib.destination) && cp -f \(Bin.source) \(Bin.destination)\" with administrator privileges" 92 | case .uninstall: 93 | "do shell script \"rm \(Bin.destination) && rm -rf \(Lib.destination)\" with administrator privileges" 94 | } 95 | let result = CLI.runAppleScript(script) 96 | 97 | let status = 98 | switch result { 99 | case .success(): 100 | CLI.getStatus() 101 | case .failure(_): 102 | self.status 103 | } 104 | 105 | DispatchQueue.main.async { 106 | self.status = status 107 | callback(result, status) 108 | } 109 | } 110 | } 111 | 112 | private static func getVersion() -> Result { 113 | let process = Process() 114 | let stdoutPipe = Pipe() 115 | let stderrPipe = Pipe() 116 | 117 | process.executableURL = URL(filePath: Bin.destination) 118 | process.arguments = ["--version"] 119 | process.standardOutput = stdoutPipe 120 | process.standardError = stderrPipe 121 | 122 | do { 123 | try process.run() 124 | 125 | process.waitUntilExit() 126 | 127 | if process.terminationStatus == 0 { 128 | let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile() 129 | let result = String(data: data, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) 130 | return .success(result) 131 | } else { 132 | let errorData = stderrPipe.fileHandleForReading.readDataToEndOfFile() 133 | let errorOutput = String(data: errorData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) 134 | 135 | let error = ReportableError( 136 | "Failed to get CLI version", 137 | code: Int(process.terminationStatus), 138 | meta: [ 139 | "StdErr": errorOutput.isEmpty ? "-" : errorOutput 140 | ] 141 | ) 142 | return .failure(error) 143 | } 144 | } catch { 145 | return .failure(error) 146 | } 147 | } 148 | 149 | private static func runAppleScript(_ script: String) -> Result { 150 | var error: NSDictionary? 151 | 152 | if let scriptObject = NSAppleScript(source: script) { 153 | scriptObject.executeAndReturnError(&error) 154 | 155 | switch error { 156 | case .some(let error): 157 | if error["NSAppleScriptErrorNumber"] as? Int == -128 /* User canceled */ { 158 | return .failure(.userCanceledOperation) 159 | } else { 160 | return .failure(.failedToExecuteAppleScript(error: error)) 161 | } 162 | case .none: 163 | return .success(()) 164 | } 165 | } else { 166 | return .failure(.failedToCreateAppleScript) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /NeoHub/EditorStore.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | import KeyboardShortcuts 4 | import NeoHubLib 5 | 6 | final class EditorStore: ObservableObject { 7 | @Published private var editors: [EditorID:Editor] 8 | 9 | let switcherWindow: SwitcherWindowRef 10 | let activationManager: ActivationManager 11 | 12 | private var restartPoller: Timer? 13 | 14 | init(activationManager: ActivationManager, switcherWindow: SwitcherWindowRef) { 15 | self.editors = [:] 16 | self.switcherWindow = switcherWindow 17 | self.activationManager = activationManager 18 | 19 | KeyboardShortcuts.onKeyUp(for: .restartEditor) { [self] in 20 | self.restartActiveEditor() 21 | } 22 | } 23 | 24 | public enum SortTarget { 25 | case menubar 26 | case switcher 27 | case lastActiveEditor 28 | } 29 | 30 | public func getEditors() -> [Editor] { 31 | self.editors.values.map { $0 } 32 | } 33 | 34 | public func getEditors(sortedFor sortTarget: SortTarget) -> [Editor] { 35 | let editors = self.getEditors() 36 | 37 | switch sortTarget { 38 | case .menubar: 39 | return editors.sorted { $0.name > $1.name } 40 | case .lastActiveEditor: 41 | if let lastActiveEditor = editors.max(by: { $0.lastAcceessTime < $1.lastAcceessTime }) { 42 | return [lastActiveEditor] 43 | } else { 44 | return [] 45 | } 46 | case .switcher: 47 | var sorted = editors.sorted { $0.lastAcceessTime > $1.lastAcceessTime } 48 | 49 | if 50 | sorted.count > 1, 51 | let firstEditor = sorted.first, 52 | case .neovide(let prevEditor) = activationManager.activationTarget, 53 | firstEditor.processIdentifier == prevEditor.processIdentifier 54 | { 55 | // Swap the first editor with the second one 56 | // so it would require just Enter to switch between two editors 57 | sorted.swapAt(0, 1) 58 | } 59 | 60 | return sorted 61 | } 62 | } 63 | 64 | func runEditor(request: RunRequest) { 65 | log.info("Running an editor...") 66 | 67 | let editorID = switch request.path { 68 | case nil, "": 69 | EditorID(request.wd) 70 | case .some(let path): 71 | EditorID( 72 | URL( 73 | fileURLWithPath: path, 74 | relativeTo: request.wd 75 | ) 76 | ) 77 | } 78 | 79 | log.info("Editor ID: \(editorID)") 80 | 81 | let editorName = switch request.name { 82 | case nil, "": 83 | editorID.lastPathComponent 84 | case .some(let name): 85 | name 86 | } 87 | 88 | log.info("Editor name: \(editorName)") 89 | 90 | switch editors[editorID] { 91 | case .some(let editor): 92 | log.info("Editor at \(editorID) is already in the hub. Activating it.") 93 | editor.activate() 94 | case .none: 95 | log.info("No editors at \(editorID) found. Launching a new one.") 96 | 97 | do { 98 | log.info("Running editor at \(request.wd.path)") 99 | 100 | let process = Process() 101 | 102 | process.executableURL = request.bin 103 | 104 | let nofork = "--no-fork" 105 | 106 | process.arguments = request.opts 107 | 108 | if !process.arguments!.contains(nofork) { 109 | process.arguments!.append(nofork) 110 | } 111 | 112 | if let path = request.path { 113 | process.arguments!.append(path) 114 | } 115 | 116 | process.currentDirectoryURL = request.wd 117 | process.environment = request.env 118 | 119 | process.terminationHandler = { process in 120 | DispatchQueue.main.async { 121 | log.info("Removing editor from the hub") 122 | self.editors.removeValue(forKey: editorID) 123 | } 124 | } 125 | 126 | let currentApp = NSWorkspace.shared.frontmostApplication 127 | 128 | try process.run() 129 | 130 | activationManager.setActivationTarget( 131 | currentApp: currentApp, 132 | switcherWindow: self.switcherWindow, 133 | editors: self.getEditors() 134 | ) 135 | 136 | if process.isRunning { 137 | log.info("Editor is launched at \(editorID) with PID \(process.processIdentifier)") 138 | 139 | DispatchQueue.main.async { 140 | self.editors[editorID] = Editor( 141 | id: editorID, 142 | name: editorName, 143 | process: process, 144 | request: request 145 | ) 146 | } 147 | } else { 148 | throw ReportableError( 149 | "Editor process is not running", 150 | code: Int(process.terminationStatus), 151 | meta: [ 152 | "EditorID": editorID, 153 | "EditorPID": process.processIdentifier, 154 | "EditorTerminationStatus": process.terminationStatus, 155 | "EditorWorkingDirectory": request.wd, 156 | "EditorBinary": request.bin, 157 | "EditorPathArgument": request.path ?? "-", 158 | "EditorOptions": request.opts, 159 | ] 160 | ) 161 | } 162 | } catch { 163 | let error = ReportableError("Failed to run editor process", error: error) 164 | log.error("\(error)") 165 | FailedToRunEditorProcessNotification(error: error).send() 166 | } 167 | } 168 | } 169 | 170 | func restartActiveEditor() { 171 | log.info("Restarting the active editor...") 172 | 173 | guard let activeApp = NSWorkspace.shared.frontmostApplication else { 174 | log.info("There is no active app. Canceling restart.") 175 | return 176 | } 177 | 178 | guard let editor = self.editors.first(where: { id, editor in editor.processIdentifier == activeApp.processIdentifier })?.value else { 179 | log.info("The active app is not an editor. Canceling restart.") 180 | return 181 | } 182 | 183 | log.info("Quiting the editor") 184 | 185 | editor.quit() 186 | 187 | // Termination handler should remove the editor from the store, 188 | // so we should wait for that, then re-run the editor 189 | log.info("Starting polling until the old editor is removed from the store") 190 | 191 | let timeout = TimeInterval(5) 192 | let startTime = Date() 193 | 194 | self.restartPoller = Timer.scheduledTimer(withTimeInterval: 0.005, repeats: true) { [weak self] _ in 195 | log.trace("Starting the iteration...") 196 | 197 | guard let self = self else { return } 198 | 199 | log.trace("We have self. Checking the store.") 200 | if self.editors[editor.id] == nil { 201 | log.info("The old editor removed from the store. Starting the new instance.") 202 | self.invalidateRestartPoller() 203 | self.runEditor(request: editor.request) 204 | } else if -startTime.timeIntervalSinceNow > timeout { 205 | log.error("The editor wasn't removed from the store within the timeout. Canceling the restart.") 206 | self.invalidateRestartPoller() 207 | 208 | let alert = NSAlert() 209 | 210 | alert.messageText = "Failed to restart the editor" 211 | alert.informativeText = "Please, report the issue on GitHub." 212 | alert.alertStyle = .critical 213 | alert.addButton(withTitle: "Report") 214 | alert.addButton(withTitle: "Dismiss") 215 | 216 | switch alert.runModal() { 217 | case .alertFirstButtonReturn: 218 | let error = ReportableError("Failed to restart the editor") 219 | BugReporter.report(error) 220 | default: () 221 | } 222 | 223 | return 224 | } 225 | } 226 | } 227 | 228 | 229 | func quitAllEditors() async { 230 | await withTaskGroup(of: Void.self) { group in 231 | for (_, editor) in self.editors { 232 | group.addTask { editor.quit() } 233 | } 234 | } 235 | } 236 | 237 | private func invalidateRestartPoller() { 238 | log.debug("Stopping the restart poller") 239 | self.restartPoller?.invalidate() 240 | } 241 | 242 | deinit { 243 | self.invalidateRestartPoller() 244 | } 245 | } 246 | 247 | struct EditorID { 248 | private let loc: URL 249 | 250 | init(_ loc: URL) { 251 | self.loc = loc 252 | } 253 | 254 | public var path: String { 255 | loc.path(percentEncoded: false) 256 | } 257 | 258 | public var lastPathComponent: String { 259 | loc.lastPathComponent 260 | } 261 | } 262 | 263 | extension EditorID: Identifiable { 264 | var id: URL { self.loc } 265 | } 266 | 267 | extension EditorID: Hashable { 268 | func hash(into hasher: inout Hasher) { 269 | hasher.combine(loc) 270 | } 271 | } 272 | 273 | extension EditorID: Equatable { 274 | static func == (lhs: EditorID, rhs: EditorID) -> Bool { 275 | return lhs.loc == rhs.loc 276 | } 277 | } 278 | 279 | extension EditorID: CustomStringConvertible { 280 | var description: String { self.path } 281 | } 282 | 283 | final class Editor: Identifiable { 284 | let id: EditorID 285 | let name: String 286 | 287 | private let process: Process 288 | private(set) var lastAcceessTime: Date 289 | private(set) var request: RunRequest 290 | 291 | init(id: EditorID, name: String, process: Process, request: RunRequest) { 292 | self.id = id 293 | self.name = name 294 | self.process = process 295 | self.lastAcceessTime = Date() 296 | self.request = request 297 | } 298 | 299 | var displayPath: String { 300 | let fullPath = self.id.path 301 | let pattern = "^/Users/[^/]+/" 302 | 303 | guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { 304 | log.warning("Invalid display path regular expression.") 305 | return fullPath 306 | } 307 | 308 | let range = NSRange(fullPath.startIndex..., in: fullPath) 309 | let result = regex.stringByReplacingMatches( 310 | in: fullPath, 311 | options: [], 312 | range: range, 313 | withTemplate: "~/" 314 | ) 315 | 316 | return result 317 | } 318 | 319 | var processIdentifier: Int32 { 320 | self.process.processIdentifier 321 | } 322 | 323 | private func runningEditor() -> NSRunningApplication? { 324 | NSRunningApplication(processIdentifier: process.processIdentifier)! 325 | } 326 | 327 | func activate() { 328 | guard let app = self.runningEditor() else { 329 | let error = ReportableError("Failed to get Neovide NSRunningApplication instance") 330 | log.error("\(error)") 331 | FailedToGetRunningEditorAppNotification(error: error).send() 332 | return 333 | } 334 | 335 | DispatchQueue.main.async { 336 | // We have to activate NeoHub first so macOS would allow to activate Neovide 337 | NSApp.activate(ignoringOtherApps: true) 338 | 339 | let activated = app.activate() 340 | if !activated { 341 | let error = ReportableError("Failed to activate Neovide instance") 342 | log.error("\(error)") 343 | FailedToActivateEditorAppNotification(error: error).send() 344 | } else { 345 | self.lastAcceessTime = Date() 346 | } 347 | } 348 | } 349 | 350 | func quit() { 351 | log.info("Terminating editor...") 352 | process.terminate() 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /NeoHub/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GitCommitHash 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NeoHub/Logger.swift: -------------------------------------------------------------------------------- 1 | import Logging 2 | import LoggingSyslog 3 | 4 | private func bootstrapLogger() -> Logger { 5 | LoggingSystem.bootstrap(SyslogLogHandler.init) 6 | 7 | var logger = Logger(label: APP_BUNDLE_ID) 8 | 9 | #if DEBUG 10 | logger.logLevel = .debug 11 | #else 12 | logger.logLevel = .info 13 | #endif 14 | 15 | return logger 16 | } 17 | 18 | let log = bootstrapLogger() 19 | -------------------------------------------------------------------------------- /NeoHub/NeoHub.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | com.apple.security.app-sandbox 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeoHub/NotificationManager.swift: -------------------------------------------------------------------------------- 1 | import UserNotifications 2 | 3 | typealias NotificationMeta = [AnyHashable: Any] 4 | 5 | protocol NotificationProtocol { 6 | static var id: String { get } 7 | 8 | static var title: String { get } 9 | static var body: String { get } 10 | 11 | static var actions: [NotificationAction.Type] { get } 12 | static var category: UNNotificationCategory { get } 13 | 14 | var meta: NotificationMeta? { get } 15 | 16 | func send() 17 | } 18 | 19 | extension NotificationProtocol { 20 | static var category: UNNotificationCategory { 21 | UNNotificationCategory( 22 | identifier: Self.id, 23 | actions: Self.actions.map { action in action.built }, 24 | intentIdentifiers: [], 25 | hiddenPreviewsBodyPlaceholder: "", 26 | options: .customDismissAction 27 | ) 28 | } 29 | } 30 | 31 | protocol NotificationAction { 32 | static var id: String { get } 33 | static var button: String { get } 34 | 35 | static var built: UNNotificationAction { get } 36 | 37 | var meta: NotificationMeta { get } 38 | 39 | init?(from meta: NotificationMeta) 40 | 41 | func run() 42 | } 43 | 44 | extension NotificationAction { 45 | static var built: UNNotificationAction { 46 | UNNotificationAction( 47 | identifier: Self.id, 48 | title: Self.button, 49 | options: [] 50 | ) 51 | } 52 | } 53 | 54 | final class NotificationManager: NSObject { 55 | static let shared = NotificationManager() 56 | 57 | override init() { 58 | Self.registerCategories() 59 | super.init() 60 | } 61 | 62 | static func registerCategories() { 63 | // Probably, I should have gone with a simple enum so the compiler could generate all cases for me 64 | let categories = Set([ 65 | FailedToLaunchServerNotification.category, 66 | FailedToHandleRequestFromCLINotification.category, 67 | FailedToRunEditorProcessNotification.category, 68 | FailedToGetRunningEditorAppNotification.category, 69 | FailedToActivateEditorAppNotification.category 70 | ]) 71 | 72 | UNUserNotificationCenter.current().setNotificationCategories(categories) 73 | } 74 | 75 | private enum AuthStatus { 76 | case unknown 77 | case granted 78 | case rejected 79 | } 80 | 81 | private var status: AuthStatus = .unknown 82 | 83 | private func requestAuthorization(completion: @escaping (Bool) -> Void) { 84 | switch self.status { 85 | case .unknown: 86 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in 87 | DispatchQueue.main.async { 88 | switch (granted, error) { 89 | case (true, nil): 90 | log.info("Notification permission granted") 91 | self.status = .granted 92 | completion(true) 93 | case (true, .some(let error)): 94 | log.info("Notification permission granted") 95 | log.notice("There was an error during notification authorization request. \(error)") 96 | self.status = .granted 97 | completion(true) 98 | case (false, let error): 99 | log.info("Notification permission not granted. Details: \(String(describing: error))") 100 | self.status = .rejected 101 | completion(false) 102 | } 103 | } 104 | } 105 | case .granted: 106 | completion(true) 107 | case .rejected: 108 | completion(false) 109 | } 110 | } 111 | 112 | fileprivate func sendNotification(notification: NotificationProtocol) { 113 | log.debug("Sending notification") 114 | 115 | self.requestAuthorization { granted in 116 | guard granted else { 117 | log.debug("Notifications are not authorized") 118 | return 119 | } 120 | 121 | let content = UNMutableNotificationContent() 122 | 123 | let Notification = type(of: notification) 124 | 125 | content.categoryIdentifier = Notification.id 126 | 127 | content.title = Notification.title 128 | content.body = Notification.body 129 | 130 | if let meta = notification.meta { 131 | content.userInfo = meta 132 | } 133 | 134 | log.debug("Notification content: \(content)") 135 | 136 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) 137 | 138 | let request = UNNotificationRequest( 139 | identifier: UUID().uuidString, 140 | content: content, 141 | trigger: trigger 142 | ) 143 | 144 | UNUserNotificationCenter.current().add(request) { error in 145 | if let error { 146 | log.error("Error scheduling notification: \(error)") 147 | } else { 148 | log.debug("Notification scheduled") 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | extension NotificationManager: UNUserNotificationCenterDelegate { 156 | func registerDelegate() { 157 | UNUserNotificationCenter.current().delegate = self 158 | log.info("Notification manager delegate registered") 159 | } 160 | 161 | func userNotificationCenter(_ center: UNUserNotificationCenter, 162 | willPresent notification: UNNotification, 163 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { 164 | completionHandler([.banner, .sound]) 165 | } 166 | 167 | func userNotificationCenter(_ center: UNUserNotificationCenter, 168 | didReceive response: UNNotificationResponse, 169 | withCompletionHandler completionHandler: @escaping () -> Void) { 170 | switch response.actionIdentifier { 171 | case ReportAction.id: 172 | let meta = response.notification.request.content.userInfo 173 | if let action = ReportAction(from: meta) { 174 | DispatchQueue.global().async { 175 | action.run() 176 | } 177 | } 178 | break 179 | default: 180 | break 181 | } 182 | 183 | completionHandler() 184 | } 185 | } 186 | 187 | struct ReportAction: NotificationAction { 188 | static let id: String = "REPORT_ACTION" 189 | static let button: String = "Report" 190 | 191 | let title: String 192 | let error: String 193 | 194 | init(error: ReportableError) { 195 | self.title = error.message 196 | self.error = String(describing: error) 197 | } 198 | 199 | var meta: NotificationMeta { 200 | [ 201 | "REPORT_TITLE": title, 202 | "REPORT_ERROR": error, 203 | ] 204 | } 205 | 206 | init?(from meta: NotificationMeta) { 207 | guard let title = meta["REPORT_TITLE"] as? String, 208 | let error = meta["REPORT_ERROR"] as? String else { 209 | log.warning("Failed to get metadata from notification. Meta: \(meta)") 210 | return nil 211 | } 212 | 213 | self.title = title 214 | self.error = error 215 | } 216 | 217 | func run() { 218 | BugReporter.report( 219 | title: self.title, 220 | error: self.error 221 | ) 222 | } 223 | } 224 | 225 | struct FailedToLaunchServerNotification: NotificationProtocol { 226 | static let id = "FAILED_TO_LAUNCH_SERVER" 227 | 228 | static let title = "Failed to launch the NeoHub server" 229 | static let body = "NeoHub won't be able to function properly. Please, create an issue in the GitHub repo." 230 | 231 | static let actions: [NotificationAction.Type] = [ReportAction.self] 232 | 233 | var meta: NotificationMeta? 234 | 235 | init(error: ReportableError) { 236 | let action = ReportAction(error: error) 237 | self.meta = action.meta 238 | } 239 | 240 | func send() { 241 | NotificationManager.shared.sendNotification(notification: self) 242 | } 243 | } 244 | 245 | struct FailedToHandleRequestFromCLINotification: NotificationProtocol { 246 | static let id = "FAILED_TO_HANDLE_REQUEST_FROM_CLI" 247 | 248 | static let title = "Failed to open Neovide" 249 | static let body = "Please create an issue in the GitHub repo." 250 | 251 | static let actions: [NotificationAction.Type] = [ReportAction.self] 252 | 253 | var meta: NotificationMeta? 254 | 255 | init(error: ReportableError) { 256 | let action = ReportAction(error: error) 257 | self.meta = action.meta 258 | } 259 | 260 | func send() { 261 | NotificationManager.shared.sendNotification(notification: self) 262 | } 263 | } 264 | 265 | struct FailedToRunEditorProcessNotification: NotificationProtocol { 266 | static let id = "FAILED_TO_RUN_EDITOR_PROCESS" 267 | 268 | static let title = "Failed to open Neovide" 269 | static let body = "Please create an issue in the GitHub repo." 270 | 271 | static let actions: [NotificationAction.Type] = [ReportAction.self] 272 | 273 | var meta: NotificationMeta? 274 | 275 | init(error: ReportableError) { 276 | let action = ReportAction(error: error) 277 | self.meta = action.meta 278 | } 279 | 280 | func send() { 281 | NotificationManager.shared.sendNotification(notification: self) 282 | } 283 | } 284 | 285 | struct FailedToGetRunningEditorAppNotification: NotificationProtocol { 286 | static let id = "FAILED_TO_GET_RUNNING_EDITOR_APP" 287 | 288 | static let title = "Failed to activate Neovide" 289 | static let body = "Requested Neovide instance is not running." 290 | 291 | static let actions: [NotificationAction.Type] = [ReportAction.self] 292 | 293 | var meta: NotificationMeta? 294 | 295 | init(error: ReportableError) { 296 | let action = ReportAction(error: error) 297 | self.meta = action.meta 298 | } 299 | 300 | func send() { 301 | NotificationManager.shared.sendNotification(notification: self) 302 | } 303 | } 304 | 305 | struct FailedToActivateEditorAppNotification: NotificationProtocol { 306 | static let id = "FAILED_TO_ACTIVATE_EDITOR_APP" 307 | 308 | static let title = "Failed to activate Neovide" 309 | static let body = "Please create an issue in GitHub repo." 310 | 311 | static let actions: [NotificationAction.Type] = [ReportAction.self] 312 | 313 | var meta: NotificationMeta? 314 | 315 | init(error: ReportableError) { 316 | let action = ReportAction(error: error) 317 | self.meta = action.meta 318 | } 319 | 320 | func send() { 321 | NotificationManager.shared.sendNotification(notification: self) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /NeoHub/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeoHub/RegularWindow.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | final class WindowCounter { 5 | private var counter: UInt8 6 | 7 | init() { 8 | self.counter = 0 9 | } 10 | 11 | var now: UInt8 { self.counter } 12 | 13 | func inc() { 14 | self.counter += 1 15 | } 16 | 17 | func dec() { 18 | if self.counter > 0 { 19 | self.counter -= 1 20 | } 21 | } 22 | } 23 | 24 | final class RegularWindow { 25 | var window: NSWindow? 26 | var observer: NSObjectProtocol? 27 | 28 | let title: String? 29 | let width: CGFloat 30 | let content: () -> Content 31 | let windowCounter: WindowCounter 32 | 33 | init(title: String? = nil, width: CGFloat, content: @escaping () -> Content, windowCounter: WindowCounter) { 34 | self.window = nil 35 | self.title = title 36 | self.width = width 37 | self.content = content 38 | self.windowCounter = windowCounter 39 | } 40 | 41 | func open() { 42 | let window = NSWindow( 43 | contentRect: NSRect(x: 0, y: 0, width: self.width, height: 0), 44 | styleMask: [.titled, .closable], 45 | backing: .buffered, 46 | defer: false 47 | ) 48 | 49 | if let title = self.title { 50 | window.title = title 51 | } 52 | 53 | window.contentView = NSHostingView(rootView: self.content()) 54 | 55 | window.isReleasedWhenClosed = false 56 | 57 | // We want Settigs and other non-Switcher windows to be Cmd+Tab'able, so temporarily making app regular 58 | NSApp.setActivationPolicy(.regular) 59 | 60 | window.styleMask.remove(.resizable) 61 | 62 | // Ensuring that the window gets activated 63 | window.center() 64 | window.makeKeyAndOrderFront(nil) 65 | NSApp.activate(ignoringOtherApps: true) 66 | 67 | // When window gets closed, reverting the app to the accessory type 68 | self.observer = NotificationCenter.default.addObserver( 69 | forName: NSWindow.willCloseNotification, 70 | object: window, 71 | queue: nil 72 | ) { [weak self] notification in 73 | self?.onClose(notification) 74 | } 75 | 76 | self.window = window 77 | 78 | self.windowCounter.inc() 79 | } 80 | 81 | func isSameWindow(_ window: NSWindow) -> Bool { 82 | self.window == window 83 | } 84 | 85 | func close() { 86 | if let window = self.window { 87 | window.close() 88 | } 89 | } 90 | 91 | private func onClose(_ notification: Notification) { 92 | if windowCounter.now == 1 { 93 | NSApp.setActivationPolicy(.accessory) 94 | } 95 | windowCounter.dec() 96 | self.cleanUp() 97 | } 98 | 99 | private func cleanUp() { 100 | self.window = nil 101 | if let observer = self.observer { 102 | NotificationCenter.default.removeObserver(observer) 103 | } 104 | } 105 | 106 | deinit { self.cleanUp() } 107 | } 108 | 109 | final class RegularWindowRef { 110 | private var window: RegularWindow? 111 | 112 | func set(_ window: RegularWindow) { 113 | self.window = window 114 | } 115 | 116 | func isSameWindow(_ window: NSWindow) -> Bool { 117 | if let win = self.window { 118 | return win.isSameWindow(window) 119 | } else { 120 | return false 121 | } 122 | } 123 | 124 | func close() { 125 | if let window = self.window { 126 | window.close() 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /NeoHub/SocketServer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NeoHubLib 4 | 5 | final class SocketServer { 6 | let store: EditorStore 7 | 8 | private let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 9 | private var channel: Channel? 10 | 11 | init(store: EditorStore) { 12 | self.store = store 13 | } 14 | 15 | func start() { 16 | DispatchQueue.global(qos: .background).async { 17 | do { 18 | let bootstrap = ServerBootstrap(group: self.group) 19 | .serverChannelOption(ChannelOptions.backlog, value: 256) 20 | .childChannelInitializer { channel in 21 | channel.pipeline.addHandler(MessageHandler(store: self.store)) 22 | } 23 | 24 | if FileManager.default.fileExists(atPath: Socket.addr) { 25 | log.warning("Socket \(Socket.addr) exists. Removing it.") 26 | try? FileManager.default.removeItem(atPath: Socket.addr) 27 | } 28 | 29 | self.channel = try bootstrap.bind(unixDomainSocketPath: Socket.addr).wait() 30 | 31 | log.info("Bound to the \(Socket.addr) socket") 32 | 33 | try self.channel?.closeFuture.wait() 34 | } catch { 35 | let error = ReportableError("Failed to start the socket server", error: error) 36 | log.critical("\(error)") 37 | FailedToLaunchServerNotification(error: error).send() 38 | } 39 | } 40 | } 41 | 42 | func stop() { 43 | do { 44 | log.info("Stopping the socket server...") 45 | try group.syncShutdownGracefully() 46 | log.info("Socket server successfully stopped") 47 | if FileManager.default.fileExists(atPath: Socket.addr) { 48 | log.warning("The socket at \(Socket.addr) still exists. Removing it.") 49 | try? FileManager.default.removeItem(atPath: Socket.addr) 50 | log.info("Socket at \(Socket.addr) is removed") 51 | } 52 | } catch { 53 | log.error("There was an issue dunring the socket server termination. Details: \(error)") 54 | } 55 | } 56 | } 57 | 58 | enum MessageHandlerState { 59 | case ready 60 | case reading( 61 | length: UInt32, 62 | buffer: ByteBuffer 63 | ) 64 | } 65 | 66 | class MessageHandler: ChannelInboundHandler { 67 | typealias InboundIn = ByteBuffer 68 | 69 | let store: EditorStore 70 | var state: MessageHandlerState = .ready 71 | 72 | init(store: EditorStore) { 73 | self.store = store 74 | } 75 | 76 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 77 | log.trace("Incoming data from the CLI") 78 | 79 | var message = unwrapInboundIn(data) 80 | 81 | switch self.state { 82 | case .ready: 83 | log.trace("First packet. Getting message size.") 84 | 85 | var header = message.readSlice(length: 4)! 86 | let length = header.readInteger(endianness: .big, as: UInt32.self)! 87 | 88 | log.trace("Message size is \(length) bytes") 89 | 90 | if length > message.readableBytes { 91 | log.trace("There will be more packets. Waiting.") 92 | self.state = .reading(length: length, buffer: message) 93 | } else { 94 | log.trace("Message fully received") 95 | self.handleRequest(context: context, buffer: &message) 96 | } 97 | case .reading(let length, var buffer): 98 | log.trace("Next packet received") 99 | 100 | buffer.writeBuffer(&message) 101 | 102 | if length > buffer.readableBytes { 103 | log.trace("There will be more packets. Waiting.") 104 | self.state = .reading(length: length, buffer: buffer) 105 | } else { 106 | log.trace("Message fully received") 107 | self.handleRequest(context: context, buffer: &buffer) 108 | } 109 | } 110 | } 111 | 112 | func handleRequest(context: ChannelHandlerContext, buffer: inout ByteBuffer) { 113 | if let json = buffer.readDispatchData(length: buffer.readableBytes) { 114 | do { 115 | log.trace("Decoding incoming JSON...") 116 | 117 | let decoder = JSONDecoder() 118 | let req = try decoder.decode(RunRequest.self, from: Data(json)) 119 | 120 | log.debug( 121 | """ 122 | 123 | ====================== INCOMING REQUEST ====================== 124 | wd: \(req.wd) 125 | bin: \(req.bin) 126 | name: \(req.name ?? "-") 127 | path: \(req.path ?? "-") 128 | opts: \(req.opts) 129 | """ 130 | ) 131 | log.trace("env: \(req.env)") 132 | log.debug( 133 | """ 134 | 135 | ================== END OF INCOMING REQUEST =================== 136 | """ 137 | ) 138 | 139 | DispatchQueue.global().async { 140 | self.store.runEditor(request: req) 141 | } 142 | } catch { 143 | let error = ReportableError("Failed to decode request from the CLI", error: error) 144 | log.error("\(error)") 145 | FailedToHandleRequestFromCLINotification(error: error).send() 146 | } 147 | 148 | let response = "OK" 149 | 150 | var buffer = context.channel.allocator.buffer(capacity: response.count) 151 | 152 | buffer.writeString(response) 153 | 154 | let dataToSend = NIOAny(buffer) 155 | 156 | context.writeAndFlush(dataToSend, promise: nil) 157 | 158 | log.trace("Response sent") 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /NeoHub/components/SettingsGroupModifier.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsGroupModifier: ViewModifier { 4 | func body(content: Content) -> some View { 5 | content 6 | .background(Color.gray.opacity(0.05)) 7 | .clipShape(RoundedRectangle(cornerRadius: 10)) 8 | .overlay( 9 | RoundedRectangle(cornerRadius: 10) 10 | .stroke(Color.gray.opacity(0.1), lineWidth: 1) 11 | ) 12 | } 13 | } 14 | 15 | extension View { 16 | func settingsGroup() -> some View { 17 | self.modifier(SettingsGroupModifier()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /NeoHub/views/AboutView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AboutView: View { 4 | static let defaultWidth: CGFloat = 300 5 | static let defaultHeight: CGFloat = 220 6 | 7 | var body: some View { 8 | VStack(alignment: .center, spacing: 10) { 9 | Image(nsImage: NSApp.applicationIconImage) 10 | .resizable() 11 | .aspectRatio(contentMode: .fit) 12 | .frame(width: 64, height: 64) 13 | Text(APP_NAME) 14 | .font(.title) 15 | if #available(macOS 14, *) { 16 | Text("Version \(APP_VERSION) (\(APP_BUILD))") 17 | .foregroundColor(.gray) 18 | .selectionDisabled(false) 19 | } else { 20 | Text("Version \(APP_VERSION) (\(APP_BUILD))") 21 | .foregroundColor(.gray) 22 | } 23 | VStack { 24 | Text("© 2023 Alex Fedoseev") 25 | HStack(spacing: 1) { 26 | Text("Icon by ") 27 | Link( 28 | "u/danbee", 29 | destination: URL(string: "https://www.reddit.com/user/danbee/")! 30 | ) 31 | .focusable(false) 32 | } 33 | } 34 | .font(.caption) 35 | } 36 | .padding() 37 | .frame(width: Self.defaultWidth, height: Self.defaultHeight) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /NeoHub/views/InstallationView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import LaunchAtLogin 3 | import KeyboardShortcuts 4 | 5 | struct InstallationView: View { 6 | static let defaultWidth: CGFloat = 400 7 | 8 | enum Status { 9 | case ok 10 | case notInstalled(progress: Progress) 11 | case versionMismatch(progress: Progress) 12 | case unexpectedError(Error) 13 | 14 | init(from cliStatus: CLIStatus) { 15 | switch cliStatus { 16 | case .ok: 17 | self = .ok 18 | case .error(reason: .notInstalled): 19 | self = .notInstalled(progress: .zero) 20 | case .error(reason: .versionMismatch): 21 | self = .versionMismatch(progress: .zero) 22 | case .error(reason: .unexpectedError(let error)): 23 | self = .unexpectedError(error) 24 | } 25 | } 26 | } 27 | 28 | enum Progress: Equatable { 29 | case zero 30 | case busy 31 | case error(CLIInstallationError) 32 | 33 | static func == (lhs: InstallationView.Progress, rhs: InstallationView.Progress) -> Bool { 34 | switch (lhs, rhs) { 35 | case (.zero, .zero): true 36 | case (.busy, .busy): true 37 | case (.error(_), .error(_)): true 38 | default: false 39 | } 40 | } 41 | } 42 | 43 | let cli: CLI 44 | let installationWindow: RegularWindowRef 45 | 46 | @State var status: Status 47 | 48 | init(cli: CLI, installationWindow: RegularWindowRef) { 49 | self.cli = cli 50 | self.installationWindow = installationWindow 51 | self.status = Status(from: cli.status) 52 | } 53 | 54 | var body: some View { 55 | VStack(spacing: 20) { 56 | switch self.status { 57 | case .ok: 58 | Text("CLI is installed").font(.title) 59 | case .notInstalled(_): 60 | Text("Install CLI").font(.title) 61 | case .versionMismatch(_): 62 | Text("Update CLI").font(.title) 63 | case .unexpectedError(_): 64 | Text("Unexpected Error").font(.title) 65 | } 66 | VStack(spacing: 20) { 67 | switch self.status { 68 | case .ok: 69 | VStack(spacing: 20) { 70 | Image(systemName: "gear.badge.checkmark") 71 | .symbolRenderingMode(.palette) 72 | .foregroundStyle(Color.green, Color.gray) 73 | .font(.system(size: 32)) 74 | Text("Open your terminal and try to run `neohub` command") 75 | Button("Close") { installationWindow.close() } 76 | } 77 | case .notInstalled(let progress): 78 | VStack(spacing: 10) { 79 | Image(systemName: "gear") 80 | .foregroundStyle(Color.gray) 81 | .font(.system(size: 32)) 82 | Text("In order to manage Neovide instances through NeoHub, you will need the NeoHub CLI.") 83 | .multilineTextAlignment(.center) 84 | .lineLimit(4) 85 | .fixedSize(horizontal: false, vertical: true) 86 | Text("After successful installation, the `neohub` command should become available in your shell. Use it instead of `neovide` to launch editors.") 87 | .multilineTextAlignment(.center) 88 | .lineLimit(4) 89 | .fixedSize(horizontal: false, vertical: true) 90 | 91 | switch progress { 92 | case .zero: 93 | EmptyView() 94 | case .busy: 95 | Spinner() 96 | case .error(.userCanceledOperation): 97 | EmptyView() 98 | case .error(.failedToCreateAppleScript): 99 | VStack { 100 | Text("Something went wrong").foregroundColor(.red) 101 | Button("Report") { 102 | let error = ReportableError("Failed to build installation Apple Script") 103 | BugReporter.report(error) 104 | } 105 | } 106 | case .error(.failedToExecuteAppleScript(error: let error)): 107 | VStack { 108 | Text("Something went wrong").foregroundColor(.red) 109 | Button("Report") { 110 | let error = ReportableError( 111 | "Failed to execute installation Apple Script", 112 | meta: error as? [String: Any] 113 | ) 114 | BugReporter.report(error) 115 | } 116 | } 117 | } 118 | } 119 | VStack(spacing: 10) { 120 | Button("Install") { 121 | self.status = .notInstalled(progress: .busy) 122 | cli.perform(.install) { result, _ in 123 | switch result { 124 | case .success(()): 125 | self.status = .ok 126 | case .failure(let error): 127 | self.status = .notInstalled(progress: .error(error)) 128 | } 129 | } 130 | } 131 | .disabled(progress == .busy) 132 | ButtonNote() 133 | } 134 | case .versionMismatch(let progress): 135 | VStack(spacing: 10) { 136 | Image(systemName: "gear.badge") 137 | .symbolRenderingMode(.palette) 138 | .foregroundStyle(Color.yellow, Color.gray) 139 | .font(.system(size: 32)) 140 | Text("NeoHub CLI needs to be updated.") 141 | 142 | switch progress { 143 | case .zero: 144 | EmptyView() 145 | case .busy: 146 | Spinner() 147 | case .error(.userCanceledOperation): 148 | EmptyView() 149 | case .error(.failedToCreateAppleScript): 150 | VStack { 151 | Text("Something went wrong").foregroundColor(.red) 152 | Button("Report") { 153 | let error = ReportableError("Failed to build installation Apple Script") 154 | BugReporter.report(error) 155 | } 156 | } 157 | case .error(.failedToExecuteAppleScript(error: let error)): 158 | VStack { 159 | Text("Something went wrong").foregroundColor(.red) 160 | Button("Report") { 161 | let error = ReportableError( 162 | "Failed to execute installation Apple Script", 163 | meta: error as? [String: Any] 164 | ) 165 | BugReporter.report(error) 166 | } 167 | } 168 | } 169 | } 170 | VStack(spacing: 10) { 171 | Button("Update") { 172 | self.status = .versionMismatch(progress: .busy) 173 | cli.perform(.install) { result, _ in 174 | switch result { 175 | case .success(()): 176 | self.status = .ok 177 | case .failure(let error): 178 | self.status = .versionMismatch(progress: .error(error)) 179 | } 180 | } 181 | } 182 | .disabled(progress == .busy) 183 | ButtonNote() 184 | } 185 | case .unexpectedError(let error): 186 | VStack(spacing: 20) { 187 | VStack(spacing: 10) { 188 | Image(systemName: "gear.badge.xmark") 189 | .symbolRenderingMode(.palette) 190 | .foregroundStyle(Color.red, Color.gray) 191 | .font(.system(size: 32)) 192 | Text("Something went terribly wrong.") 193 | Text("Please create an issue in the GitHub repo.") 194 | } 195 | Button("Create an Issue") { 196 | BugReporter.report(ReportableError("CLI failed to report a status", error: error)) 197 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 198 | NSApplication.shared.terminate(nil) 199 | } 200 | } 201 | } 202 | } 203 | } 204 | .padding() 205 | .frame(maxWidth: .infinity) 206 | .settingsGroup() 207 | } 208 | .padding(.horizontal, 20) 209 | .padding( 210 | .vertical, 211 | { 212 | // FIXME: Multiline Text produces odd paddings around window content 213 | switch self.status { 214 | case 215 | .unexpectedError(_): 216 | return 50 217 | case 218 | .versionMismatch(progress: .busy): 219 | return 40 220 | case 221 | .versionMismatch(progress: .zero), 222 | .versionMismatch(progress: .error(_)): 223 | return 30 224 | case 225 | .ok, 226 | .notInstalled(progress: .error(.failedToExecuteAppleScript(_))): 227 | return 20 228 | case 229 | .notInstalled(_): 230 | return 10 231 | } 232 | }() 233 | ) 234 | .frame(maxWidth: .infinity) 235 | } 236 | 237 | struct Spinner: View { 238 | var body: some View { 239 | ProgressView("Working...").progressViewStyle(CircularProgressViewStyle()) 240 | } 241 | } 242 | 243 | struct ButtonNote: View { 244 | var body: some View { 245 | Text( 246 | """ 247 | In the dialog box that appears, 248 | you will need to enter an administrator password 249 | """ 250 | ) 251 | .font(.caption2) 252 | .foregroundColor(.gray) 253 | .multilineTextAlignment(.center) 254 | .lineLimit(3) 255 | .fixedSize(horizontal: false, vertical: true) 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /NeoHub/views/MenuBarView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuBarIcon: View { 4 | private let icon: NSImage 5 | 6 | init() { 7 | let icon: NSImage = NSImage(named: "MenuBarIcon")! 8 | 9 | let ratio = icon.size.height / icon.size.width 10 | icon.size.height = 15 11 | icon.size.width = 15 / ratio 12 | 13 | self.icon = icon 14 | } 15 | 16 | var body: some View { 17 | Image(nsImage: icon) 18 | } 19 | } 20 | 21 | 22 | struct MenuBarView: View { 23 | @ObservedObject var cli: CLI 24 | @ObservedObject var editorStore: EditorStore 25 | 26 | let settingsWindow: RegularWindow 27 | let aboutWindow: RegularWindow 28 | 29 | var body: some View { 30 | let editors = editorStore.getEditors(sortedFor: .menubar) 31 | 32 | Group { 33 | if editors.count == 0 { 34 | Text("No editors").font(.headline) 35 | } else { 36 | Text("Editors").font(.headline) 37 | ForEach(editors) { editor in 38 | Button(editor.name) { editor.activate() } 39 | } 40 | } 41 | switch cli.status { 42 | case .error(reason:.notInstalled): 43 | Divider() 44 | Button("⚠️ Install CLI") { 45 | cli.perform(.install) { result, status in 46 | Self.showCLIInstallationAlert(with: (result, status)) 47 | } 48 | } 49 | case .error(reason: .versionMismatch): 50 | Divider() 51 | Button("⚠️ Update CLI") { 52 | cli.perform(.install) { result, status in 53 | Self.showCLIInstallationAlert(with: (result, status)) 54 | } 55 | } 56 | case .error(reason: .unexpectedError(_)): 57 | Divider() 58 | Button("❗ CLI Error") { settingsWindow.open() } 59 | case .ok: 60 | EmptyView() 61 | } 62 | Divider() 63 | Button("Settings") { settingsWindow.open() } 64 | Button("About") { aboutWindow.open() } 65 | Divider() 66 | Button("Quit All Editors") { Task { await editorStore.quitAllEditors() } }.disabled(editors.count == 0) 67 | Button("Quit NeoHub") { NSApplication.shared.terminate(nil) } 68 | } 69 | } 70 | 71 | static func showCLIInstallationAlert(with response: (result: Result, status: CLIStatus)) { 72 | switch response.result { 73 | case .success(()): 74 | let alert = NSAlert() 75 | 76 | alert.messageText = "Boom!" 77 | alert.informativeText = "The CLI is ready to roll 🚀" 78 | alert.alertStyle = .informational 79 | alert.addButton(withTitle: "OK") 80 | 81 | alert.runModal() 82 | 83 | case .failure(.userCanceledOperation): () 84 | 85 | case .failure(.failedToCreateAppleScript): 86 | let alert = NSAlert() 87 | 88 | alert.messageText = "Oh no!" 89 | alert.informativeText = "There was an issue during installation." 90 | alert.alertStyle = .critical 91 | alert.addButton(withTitle: "Report") 92 | alert.addButton(withTitle: "Dismiss") 93 | 94 | switch alert.runModal() { 95 | case .alertFirstButtonReturn: 96 | let error = ReportableError("Failed to build installation Apple Script") 97 | BugReporter.report(error) 98 | default: () 99 | } 100 | 101 | case .failure(.failedToExecuteAppleScript(error: let error)): 102 | let alert = NSAlert() 103 | 104 | alert.messageText = "Oh no!" 105 | alert.informativeText = "There was an issue during installation." 106 | alert.alertStyle = .critical 107 | alert.addButton(withTitle: "Report") 108 | alert.addButton(withTitle: "Dismiss") 109 | 110 | switch alert.runModal() { 111 | case .alertFirstButtonReturn: 112 | let error = ReportableError( 113 | "Failed to execute installation Apple Script", 114 | meta: error as? [String: Any] 115 | ) 116 | BugReporter.report(error) 117 | default: () 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /NeoHub/views/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import LaunchAtLogin 3 | import KeyboardShortcuts 4 | 5 | struct SettingsView: View { 6 | static let defaultWidth: CGFloat = 400 7 | 8 | @ObservedObject var cli: CLI 9 | 10 | @State var runningCLIAction: Bool = false 11 | 12 | var body: some View { 13 | VStack(spacing: 20) { 14 | HStack { 15 | Image("EditorIcon") 16 | .resizable() 17 | .scaledToFit() 18 | .frame(width: 32, height: 32) 19 | .foregroundColor(.gray) 20 | Text("NeoHub").font(.title) 21 | } 22 | VStack(spacing: 0) { 23 | HStack { 24 | Text("Launch at Login") 25 | Spacer() 26 | LaunchAtLogin.Toggle("").toggleStyle(SwitchToggleStyle()) 27 | } 28 | .padding(.horizontal) 29 | .padding(.vertical, 10) 30 | 31 | Divider().padding(.horizontal) 32 | 33 | HStack { 34 | Text("Toggle Editor Selector") 35 | Spacer() 36 | KeyboardShortcuts.Recorder("", name: .toggleSwitcher) 37 | } 38 | .padding(.horizontal) 39 | .padding(.vertical, 10) 40 | 41 | HStack { 42 | Text("Toggle Last Active Editor") 43 | Spacer() 44 | KeyboardShortcuts.Recorder("", name: .toggleLastActiveEditor) 45 | } 46 | .padding(.horizontal) 47 | .padding(.vertical, 10) 48 | 49 | Divider().padding(.horizontal) 50 | 51 | HStack { 52 | Text("Restart Active Editor") 53 | Spacer() 54 | KeyboardShortcuts.Recorder("", name: .restartEditor) 55 | } 56 | .padding(.horizontal) 57 | .padding(.vertical, 10) 58 | } 59 | .settingsGroup() 60 | Text("CLI").font(.title) 61 | VStack(spacing: 20) { 62 | switch cli.status { 63 | case .ok: 64 | VStack(spacing: 10) { 65 | if self.runningCLIAction { 66 | InstallationView.Spinner() 67 | } else { 68 | Image(systemName: "gear.badge.checkmark") 69 | .symbolRenderingMode(.palette) 70 | .foregroundStyle(Color.green, Color.gray) 71 | .font(.system(size: 32)) 72 | Text("Installed") 73 | } 74 | } 75 | Divider() 76 | VStack(spacing: 10) { 77 | Button("Uninstall") { 78 | self.runningCLIAction = true 79 | cli.perform(.uninstall) { _, _ in 80 | self.runningCLIAction = false 81 | } 82 | } 83 | .buttonStyle(LinkButtonStyle()) 84 | .disabled(self.runningCLIAction) 85 | .focusable() 86 | InstallationView.ButtonNote() 87 | } 88 | case .error(reason: .notInstalled): 89 | VStack(spacing: 10) { 90 | if self.runningCLIAction { 91 | InstallationView.Spinner() 92 | } else { 93 | Image(systemName: "gear.badge.xmark") 94 | .symbolRenderingMode(.palette) 95 | .foregroundStyle(Color.red, Color.gray) 96 | .font(.system(size: 32)) 97 | Text("Not Installed") 98 | } 99 | } 100 | VStack(spacing: 10) { 101 | Button("Install") { 102 | self.runningCLIAction = true 103 | cli.perform(.install) { _, _ in 104 | self.runningCLIAction = false 105 | } 106 | } 107 | .disabled(self.runningCLIAction) 108 | .focusable() 109 | InstallationView.ButtonNote() 110 | } 111 | case .error(reason: .versionMismatch): 112 | VStack(spacing: 10) { 113 | if self.runningCLIAction { 114 | InstallationView.Spinner() 115 | } else { 116 | Image(systemName: "gear.badge") 117 | .symbolRenderingMode(.palette) 118 | .foregroundStyle(Color.yellow, Color.gray) 119 | .font(.system(size: 32)) 120 | Text("Needs Update") 121 | } 122 | } 123 | VStack(spacing: 20) { 124 | Button("Update") { 125 | self.runningCLIAction = true 126 | cli.perform(.install) { _, _ in 127 | self.runningCLIAction = false 128 | } 129 | } 130 | .disabled(self.runningCLIAction) 131 | .focusable() 132 | Divider() 133 | Button("Uninstall") { 134 | self.runningCLIAction = true 135 | cli.perform(.uninstall) { _, _ in 136 | self.runningCLIAction = false 137 | } 138 | } 139 | .buttonStyle(LinkButtonStyle()) 140 | .disabled(self.runningCLIAction) 141 | .focusable() 142 | InstallationView.ButtonNote() 143 | } 144 | case .error(reason: .unexpectedError(let error)): 145 | VStack(spacing: 10) { 146 | Image(systemName: "gear.badge.xmark") 147 | .symbolRenderingMode(.palette) 148 | .foregroundStyle(Color.red, Color.gray) 149 | .font(.system(size: 32)) 150 | Text("Unexpected Error") 151 | } 152 | Button("Create an Issue on GitHub") { 153 | BugReporter.report(ReportableError("CLI failed to report a status", error: error)) 154 | } 155 | } 156 | } 157 | .padding() 158 | .frame(maxWidth: .infinity) 159 | .settingsGroup() 160 | } 161 | .padding(.horizontal, 20) 162 | .padding(.vertical, 40) 163 | .frame(maxWidth: .infinity) 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /NeoHub/views/SwitcherView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import KeyboardShortcuts 3 | 4 | struct Key { 5 | static let ESC: UInt16 = 53 6 | static let TAB: UInt16 = 48 7 | static let ENTER: UInt16 = 36 8 | static let ARROW_UP: UInt16 = 126 9 | static let ARROW_DOWN: UInt16 = 125 10 | static let BACKSPACE: UInt16 = 51 11 | static let COMMA: UInt16 = 43 12 | static let W: UInt16 = 13 13 | static let Q: UInt16 = 12 14 | } 15 | 16 | private final class KeyboardEventHandler: ObservableObject { 17 | var monitor: Any? 18 | } 19 | 20 | struct Layout { 21 | static let windowWidth: Int = 600 22 | static let windowHeight: Int = 320 23 | static let titleOriginalHight: Int = 28 24 | static let titleAdjustment: Int = titleOriginalHight - searchFieldVerticalPadding 25 | static let horisontalPadding: Int = 20 26 | static let searchFieldFontSize: Int = 20 27 | static let resultsFontSize: Int = 16 28 | static let searchFieldVerticalPadding: Int = 20 29 | static let resultItemOuterPadding: Int = 6 30 | static let resultItemInnerPadding: CGFloat = CGFloat(horisontalPadding - resultItemOuterPadding) 31 | static let resultsBottomPadding: Int = 20 32 | static let bottomBarVerticalPadding: Int = 6 33 | static let bottomBarHorizontalPadding: CGFloat = CGFloat(horisontalPadding - bottomBarButtonTrailingPadding) 34 | static let bottomBarFontSize: Int = 12 35 | static let bottomBarShortcutFontSize: Int = bottomBarFontSize + 3 36 | static let bottomBarButtonVerticalPadding: Int = 2 37 | static let bottomBarButtonLeadingPadding: Int = 8 38 | static let bottomBarButtonTrailingPadding: Int = 2 39 | 40 | static let resultsContainerHeight: CGFloat = CGFloat( 41 | windowHeight 42 | - (searchFieldVerticalPadding * 2 + searchFieldFontSize) 43 | - (bottomBarVerticalPadding * 2 + bottomBarFontSize + bottomBarButtonVerticalPadding * 2) 44 | - 16 // magic number b/c I didn't consider something in the calculation above 45 | ) 46 | } 47 | 48 | final class SwitcherWindow: ObservableObject { 49 | private let editorStore: EditorStore 50 | private let settingsWindow: RegularWindow 51 | private let selfRef: SwitcherWindowRef 52 | private let activationManager: ActivationManager 53 | 54 | private var window: NSWindow! 55 | 56 | @Published private var hidden: Bool = true 57 | 58 | init( 59 | editorStore: EditorStore, 60 | settingsWindow: RegularWindow, 61 | selfRef: SwitcherWindowRef, 62 | activationManager: ActivationManager 63 | ) { 64 | self.editorStore = editorStore 65 | self.settingsWindow = settingsWindow 66 | self.selfRef = selfRef 67 | self.activationManager = activationManager 68 | 69 | let contentView = SwitcherView( 70 | editorStore: editorStore, 71 | switcherWindow: self, 72 | settingsWindow: settingsWindow, 73 | activationManager: activationManager 74 | ) 75 | 76 | window = NSWindow( 77 | contentRect: NSRect(x: 0, y: 0, width: Layout.windowWidth, height: Layout.windowHeight), 78 | styleMask: [.titled, .closable, .fullSizeContentView], 79 | backing: .buffered, 80 | defer: false 81 | ) 82 | 83 | window.contentView = NSHostingView(rootView: contentView) 84 | 85 | window.setFrameAutosaveName(APP_NAME) 86 | window.isReleasedWhenClosed = false 87 | 88 | window.level = .floating 89 | window.collectionBehavior = .canJoinAllSpaces 90 | 91 | window.hasShadow = true 92 | window.isOpaque = false 93 | 94 | window.titlebarAppearsTransparent = true 95 | window.titleVisibility = .hidden 96 | window.showsToolbarButton = false 97 | 98 | let titleAdjustment = CGFloat(Layout.titleAdjustment) 99 | window.contentView!.frame = window.contentView!.frame.offsetBy(dx: 0, dy: titleAdjustment) 100 | window.contentView!.frame.size.height -= titleAdjustment 101 | 102 | window.isMovableByWindowBackground = true 103 | 104 | window.standardWindowButton(.miniaturizeButton)?.isHidden = true 105 | window.standardWindowButton(.closeButton)?.isHidden = true 106 | window.standardWindowButton(.zoomButton)?.isHidden = true 107 | 108 | window.center() 109 | 110 | KeyboardShortcuts.onKeyDown(for: .toggleLastActiveEditor) { [self] in 111 | self.handleLastActiveEditorToggle() 112 | } 113 | 114 | KeyboardShortcuts.onKeyDown(for: .toggleSwitcher) { [self] in 115 | self.handleSwitcherToggle() 116 | } 117 | } 118 | 119 | private func handleLastActiveEditorToggle() { 120 | let editors = editorStore.getEditors(sortedFor: .lastActiveEditor) 121 | 122 | if !editors.isEmpty { 123 | let editor = editors.first! 124 | let application = NSRunningApplication(processIdentifier: editor.processIdentifier) 125 | switch NSWorkspace.shared.frontmostApplication { 126 | case .some(let app): 127 | if app.processIdentifier == editor.processIdentifier { 128 | application?.hide() 129 | } else { 130 | activationManager.setActivationTarget( 131 | currentApp: app, 132 | switcherWindow: self.selfRef, 133 | editors: editors 134 | ) 135 | application?.activate() 136 | } 137 | case .none: 138 | let application = NSRunningApplication(processIdentifier: editor.processIdentifier) 139 | application?.hide() 140 | } 141 | } else { 142 | self.toggle() 143 | } 144 | } 145 | 146 | private func handleSwitcherToggle() { 147 | let editors = editorStore.getEditors() 148 | 149 | if editors.count == 1 { 150 | let editor = editors.first! 151 | 152 | switch NSWorkspace.shared.frontmostApplication { 153 | case .some(let app): 154 | if app.processIdentifier == editor.processIdentifier { 155 | activationManager.activateTarget() 156 | activationManager.setActivationTarget( 157 | currentApp: app, 158 | switcherWindow: self.selfRef, 159 | editors: editors 160 | ) 161 | } else { 162 | activationManager.setActivationTarget( 163 | currentApp: app, 164 | switcherWindow: self.selfRef, 165 | editors: editors 166 | ) 167 | editor.activate() 168 | } 169 | case .none: 170 | editor.activate() 171 | } 172 | } else { 173 | self.toggle() 174 | } 175 | } 176 | 177 | private func toggle() { 178 | if window.isVisible { 179 | self.hide() 180 | } else { 181 | self.show() 182 | } 183 | } 184 | 185 | private func show() { 186 | activationManager.setActivationTarget( 187 | currentApp: NSWorkspace.shared.frontmostApplication, 188 | switcherWindow: self.selfRef, 189 | editors: self.editorStore.getEditors() 190 | ) 191 | 192 | self.hidden = false 193 | 194 | window.center() 195 | window.makeKeyAndOrderFront(nil) 196 | NSApp.activate(ignoringOtherApps: true) 197 | } 198 | 199 | public func hide() { 200 | if self.hidden { 201 | return 202 | } 203 | 204 | self.hidden = true 205 | window.orderOut(nil) 206 | 207 | let currentApp = NSWorkspace.shared.frontmostApplication 208 | if currentApp?.bundleIdentifier == APP_BUNDLE_ID { 209 | activationManager.activateTarget() 210 | } 211 | } 212 | 213 | public func isHidden() -> Bool { 214 | return self.hidden 215 | } 216 | 217 | public func isSameWindow(_ window: NSWindow) -> Bool { 218 | self.window == window 219 | } 220 | } 221 | 222 | struct SwitcherView: View { 223 | @ObservedObject var editorStore: EditorStore 224 | @ObservedObject var switcherWindow: SwitcherWindow 225 | 226 | let settingsWindow: RegularWindow 227 | let activationManager: ActivationManager 228 | 229 | private enum State { 230 | case noEditors 231 | case oneEditor 232 | case manyEditors 233 | } 234 | 235 | private var state: State? { 236 | if switcherWindow.isHidden() { 237 | return nil 238 | } 239 | 240 | let editors = editorStore.getEditors() 241 | 242 | switch editors.count { 243 | case 0: 244 | return .noEditors 245 | case 1: 246 | return .oneEditor 247 | default: 248 | return .manyEditors 249 | } 250 | } 251 | 252 | var body: some View { 253 | Group { 254 | switch state { 255 | case .noEditors: 256 | SwitcherEmptyView( 257 | switcherWindow: self.switcherWindow, 258 | settingsWindow: self.settingsWindow 259 | ) 260 | case .oneEditor, .manyEditors: 261 | SwitcherListView( 262 | editorStore: self.editorStore, 263 | switcherWindow: self.switcherWindow, 264 | settingsWindow: self.settingsWindow, 265 | activationManager: self.activationManager 266 | ) 267 | case .none: 268 | EmptyView() 269 | } 270 | } 271 | .frame(maxWidth: .infinity, maxHeight: .infinity) 272 | } 273 | } 274 | 275 | struct SwitcherEmptyView: View { 276 | @ObservedObject var switcherWindow: SwitcherWindow 277 | 278 | let settingsWindow: RegularWindow 279 | 280 | @StateObject private var keyboard = KeyboardEventHandler() 281 | 282 | @FocusState private var focused: Bool 283 | 284 | var body: some View { 285 | VStack(alignment: .center, spacing: 26) { 286 | Image(systemName: "exclamationmark.shield.fill") 287 | .font(.system(size: 70)) 288 | .foregroundColor(.gray) 289 | VStack(spacing: 6) { 290 | Text("No Neovide instances in NeoHub") 291 | .font(.system(size: 20)) 292 | .foregroundColor(.gray) 293 | Text("Use CLI to launch some") 294 | .font(.system(size: 12)) 295 | .foregroundColor(.gray) 296 | } 297 | Button("Close") { switcherWindow.hide() }.focused($focused) 298 | } 299 | .onAppear { 300 | log.trace("SwitcherEmptyView: appears") 301 | 302 | self.focused = true 303 | 304 | self.keyboard.monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in 305 | switch event.keyCode { 306 | case Key.ESC: 307 | switcherWindow.hide() 308 | return nil 309 | case Key.COMMA where event.modifierFlags.contains(.command): 310 | switcherWindow.hide() 311 | settingsWindow.open() 312 | return nil 313 | case Key.W where event.modifierFlags.contains(.command): 314 | switcherWindow.hide() 315 | return nil 316 | default: 317 | break 318 | } 319 | return event 320 | } 321 | } 322 | .onDisappear() { 323 | log.trace("SwitcherEmptyView: disappears") 324 | if let monitor = keyboard.monitor { 325 | log.trace("SwitcherEmptyView: removing monitor") 326 | NSEvent.removeMonitor(monitor) 327 | } 328 | } 329 | } 330 | } 331 | 332 | struct SwitcherListView: View { 333 | @ObservedObject var editorStore: EditorStore 334 | @ObservedObject var switcherWindow: SwitcherWindow 335 | 336 | let settingsWindow: RegularWindow 337 | let activationManager: ActivationManager 338 | 339 | @StateObject private var keyboard = KeyboardEventHandler() 340 | 341 | @State private var searchText = "" 342 | @State private var selectedIndex: Int = 0 343 | 344 | @FocusState private var focused: Bool 345 | 346 | var body: some View { 347 | let editors = self.filterEditors() 348 | 349 | VStack(spacing: 0) { 350 | Form { 351 | TextField("Search", text: $searchText, prompt: Text("Search...")) 352 | .font(.system(size: CGFloat(Layout.searchFieldFontSize))) 353 | .textFieldStyle(PlainTextFieldStyle()) 354 | .labelsHidden() 355 | .focused($focused) 356 | .padding(.horizontal, CGFloat(Layout.horisontalPadding)) 357 | .padding(.bottom, CGFloat(Layout.searchFieldVerticalPadding)) 358 | .onChange(of: searchText) { _ in 359 | selectedIndex = 0 360 | } 361 | } 362 | Divider().padding(.bottom, CGFloat(Layout.resultItemOuterPadding)) 363 | ScrollView(.vertical) { 364 | VStack(spacing: 0) { 365 | ForEach(Array(editors.enumerated()), id: \.1.id) { index, editor in 366 | Button(action: { editor.activate() }) { 367 | HStack(spacing: 16) { 368 | Image("EditorIcon") 369 | .resizable() 370 | .scaledToFit() 371 | .frame(width: 16, height: 16) 372 | .foregroundColor(.gray) 373 | Text(editor.name).font(.system(size: CGFloat(Layout.resultsFontSize))) 374 | Spacer() 375 | Text(editor.displayPath) 376 | .font(.system(size: CGFloat(Layout.resultsFontSize))) 377 | .foregroundColor(.gray) 378 | } 379 | } 380 | .buttonStyle(PlainButtonStyle()) 381 | .frame(maxWidth: .infinity) 382 | .padding(Layout.resultItemInnerPadding) 383 | .background( 384 | Color.gray.opacity(selectedIndex == index ? 0.1 : 0.0) 385 | ) 386 | .cornerRadius(6) 387 | .focusable(false) 388 | } 389 | } 390 | .padding(.horizontal, CGFloat(Layout.resultItemOuterPadding)) 391 | .padding(.bottom, CGFloat(Layout.resultsBottomPadding)) 392 | } 393 | .frame(height: Layout.resultsContainerHeight) 394 | HStack(spacing: 5) { 395 | Spacer() 396 | BottomBarButton(text: "Quit Selected", shortcut: ["⌘", "⌫"], action: { self.quitSelectedEditor() }) 397 | BottomBarButton(text: "Quit All", shortcut: ["⌘", "Q"], action: { self.quitAllEditors() }) 398 | } 399 | .padding(.vertical, CGFloat(Layout.bottomBarVerticalPadding)) 400 | .padding(.horizontal, CGFloat(Layout.bottomBarHorizontalPadding)) 401 | .background(Color.black.opacity(0.1)) 402 | } 403 | .frame(maxWidth: .infinity, maxHeight: .infinity) 404 | .onAppear { 405 | log.trace("SwitcherListView: appears") 406 | 407 | self.focused = true 408 | 409 | self.keyboard.monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in 410 | switch event.keyCode { 411 | case Key.ARROW_UP: 412 | if selectedIndex > 0 { 413 | selectedIndex -= 1 414 | } 415 | return nil 416 | case Key.ARROW_DOWN: 417 | if selectedIndex < self.filterEditors().count - 1 { 418 | selectedIndex += 1 419 | } 420 | return nil 421 | case Key.TAB: 422 | selectedIndex = (selectedIndex + 1) % self.filterEditors().count 423 | return nil 424 | case Key.ENTER: 425 | let editors = self.filterEditors() 426 | if editors.indices.contains(selectedIndex) { 427 | let editor = editors[selectedIndex] 428 | editor.activate() 429 | } 430 | return nil 431 | case Key.BACKSPACE where event.modifierFlags.contains(.command): 432 | self.quitSelectedEditor() 433 | return nil 434 | case Key.ESC: 435 | switcherWindow.hide() 436 | return nil 437 | case Key.COMMA where event.modifierFlags.contains(.command): 438 | switcherWindow.hide() 439 | settingsWindow.open() 440 | return nil 441 | case Key.W where event.modifierFlags.contains(.command): 442 | switcherWindow.hide() 443 | return nil 444 | case Key.Q where event.modifierFlags.contains(.command): 445 | self.quitAllEditors() 446 | return nil 447 | default: 448 | break 449 | } 450 | return event 451 | } 452 | } 453 | .onDisappear() { 454 | log.trace("SwitcherListView: disappears") 455 | if let monitor = keyboard.monitor { 456 | log.trace("SwitcherListView: removing monitor") 457 | NSEvent.removeMonitor(monitor) 458 | } 459 | } 460 | } 461 | 462 | func filterEditors() -> [Editor] { 463 | editorStore.getEditors(sortedFor: .switcher).filter { editor in 464 | searchText.isEmpty 465 | || editor.name.contains(searchText) 466 | || editor.displayPath.localizedCaseInsensitiveContains(searchText) 467 | } 468 | } 469 | 470 | func quitSelectedEditor() { 471 | let editors = self.filterEditors() 472 | if editors.indices.contains(selectedIndex) { 473 | let editor = editors[selectedIndex] 474 | let totalEditors = editors.count 475 | 476 | if totalEditors == selectedIndex + 1 && selectedIndex != 0 { 477 | selectedIndex -= 1 478 | } 479 | 480 | if totalEditors == 1 { 481 | activationManager.activateTarget() 482 | } 483 | 484 | editor.quit() 485 | } 486 | } 487 | 488 | func quitAllEditors() { 489 | Task { 490 | activationManager.activateTarget() 491 | await editorStore.quitAllEditors() 492 | } 493 | } 494 | 495 | struct BottomBarButton: View { 496 | let text: String 497 | let shortcut: [Character] 498 | let action: () -> Void 499 | 500 | private static let background = Color.clear 501 | 502 | @State private var background: Color = Self.background 503 | 504 | var body: some View { 505 | Button(action: action) { 506 | HStack(spacing: 8) { 507 | Text(text) 508 | .foregroundColor(.gray) 509 | HStack(spacing: 2) { 510 | ForEach(shortcut, id: \.self) { key in 511 | Text(String(key)) 512 | .padding(.horizontal, 6) 513 | .padding(.vertical, 1) 514 | .font(.system(size: CGFloat(Layout.bottomBarShortcutFontSize), design: .monospaced)) 515 | .foregroundColor(.gray) 516 | .background(Color.white.opacity(0.05)) 517 | .cornerRadius(2) 518 | } 519 | } 520 | } 521 | } 522 | .font(.system(size: CGFloat(Layout.bottomBarFontSize))) 523 | .buttonStyle(PlainButtonStyle()) 524 | .padding(.vertical, CGFloat(Layout.bottomBarButtonVerticalPadding)) 525 | .padding(.leading, CGFloat(Layout.bottomBarButtonLeadingPadding)) 526 | .padding(.trailing, CGFloat(Layout.bottomBarButtonTrailingPadding)) 527 | .background(background) 528 | .cornerRadius(3) 529 | .focusable(false) 530 | .onHover(perform: { hovering in 531 | withAnimation(.easeInOut(duration: 0.1)) { 532 | background = hovering ? Color.white.opacity(0.1) : Self.background 533 | } 534 | }) 535 | } 536 | } 537 | } 538 | 539 | final class SwitcherWindowRef { 540 | private var window: SwitcherWindow? 541 | 542 | init(window: SwitcherWindow? = nil) { 543 | self.window = window 544 | } 545 | 546 | func set(_ window: SwitcherWindow) { 547 | self.window = window 548 | } 549 | 550 | func isSameWindow(_ window: NSWindow) -> Bool { 551 | if let win = self.window { 552 | return win.isSameWindow(window) 553 | } else { 554 | return false 555 | } 556 | } 557 | } 558 | -------------------------------------------------------------------------------- /NeoHubCLI/CLI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ArgumentParser 3 | import NeoHubLib 4 | 5 | let APP_BUNDLE_ID = "com.alex35mil.NeoHub.CLI" 6 | 7 | enum CLIError: Error { 8 | case failedToGetBin(Error) 9 | case failedToCommunicateWithNeoHub(SendError) 10 | } 11 | 12 | extension CLIError: LocalizedError { 13 | var errorDescription: String? { 14 | switch self { 15 | case .failedToGetBin(let error): 16 | return 17 | """ 18 | Failed to get a path to Neovide binary. Make sure it is available in your PATH. 19 | \(error.localizedDescription) 20 | """ 21 | case .failedToCommunicateWithNeoHub(let error): 22 | return 23 | """ 24 | Failed to communicate with NeoHub. 25 | \(error.localizedDescription) 26 | """ 27 | } 28 | } 29 | } 30 | 31 | @main 32 | struct CLI: ParsableCommand { 33 | static var configuration = CommandConfiguration( 34 | commandName: "neohub", 35 | abstract: "A CLI interface to NeoHub. Launch new or activate already running Neovide instance.", 36 | version: "0.2.1" 37 | ) 38 | 39 | @Argument(help: "Optional path passed to Neovide.") 40 | var path: String? 41 | 42 | @Option(help: "Optional editor name. Used for display only. If not provided, a file or directory name will be used.") 43 | var name: String? 44 | 45 | @Option(parsing: .remaining, help: "Options passed to Neovide") 46 | var opts: [String] = [] 47 | 48 | mutating func run() { 49 | let wd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) 50 | 51 | let bin: URL 52 | switch Shell.run("command -v neovide") { 53 | case .success(let path): 54 | bin = URL(fileURLWithPath: path) 55 | case .failure(let error): 56 | Self.exit(withError: CLIError.failedToGetBin(error)) 57 | } 58 | 59 | let path: String? = switch self.path { 60 | case nil, "": nil 61 | case .some(let path): .some(path) 62 | } 63 | 64 | let env = ProcessInfo.processInfo.environment 65 | 66 | let req = RunRequest( 67 | wd: wd, 68 | bin: bin, 69 | name: self.name, 70 | path: path, 71 | opts: self.opts, 72 | env: env 73 | ) 74 | 75 | log.debug( 76 | """ 77 | 78 | ====================== OUTGOING REQUEST ====================== 79 | wd: \(req.wd) 80 | bin: \(req.bin) 81 | name: \(req.name ?? "-") 82 | path: \(req.path ?? "-") 83 | opts: \(req.opts) 84 | """ 85 | ) 86 | log.trace("env: \(req.env)") 87 | log.debug( 88 | """ 89 | 90 | =================== END OF OUTGOING REQUEST ================== 91 | """ 92 | ) 93 | 94 | let client = SocketClient() 95 | let result = client.send(req) 96 | 97 | switch result { 98 | case .success(let res): 99 | log.debug("Response: \(res ?? "-")") 100 | Self.exit(withError: nil) 101 | case .failure(let error): 102 | Self.exit(withError: CLIError.failedToCommunicateWithNeoHub(error)) 103 | } 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /NeoHubCLI/Logger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | private let envVar = "NEOHUB_LOG" 5 | private let defaultLevel: Logger.Level = .info 6 | 7 | private func bootstrapLogger() -> Logger { 8 | var logger = Logger(label: APP_BUNDLE_ID) 9 | 10 | let level = 11 | switch ProcessInfo.processInfo.environment[envVar] { 12 | case .some(let value): Logger.Level(rawValue: value) ?? defaultLevel 13 | case .none: defaultLevel 14 | } 15 | 16 | logger.logLevel = level 17 | 18 | return logger 19 | } 20 | 21 | let log = bootstrapLogger() 22 | -------------------------------------------------------------------------------- /NeoHubCLI/NeoHubCLI.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NeoHubCLI/Shell.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Shell { 4 | static func run(_ command: String) -> Result { 5 | let process = Process() 6 | let stdoutPipe = Pipe() 7 | let stderrPipe = Pipe() 8 | 9 | process.executableURL = URL(fileURLWithPath: "/bin/sh") 10 | process.arguments = ["-c", command] 11 | process.environment = ProcessInfo.processInfo.environment 12 | process.standardOutput = stdoutPipe 13 | process.standardError = stderrPipe 14 | 15 | do { 16 | try process.run() 17 | 18 | process.waitUntilExit() 19 | 20 | if process.terminationStatus == 0 { 21 | let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile() 22 | let result = String(data: data, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) 23 | return .success(result) 24 | } else { 25 | let errorData = stderrPipe.fileHandleForReading.readDataToEndOfFile() 26 | let errorOutput = String(data: errorData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) 27 | 28 | let error = NSError( 29 | domain: "NeoHubCLI", 30 | code: Int(process.terminationStatus), 31 | userInfo: [ 32 | NSLocalizedDescriptionKey: 33 | """ 34 | Exit code: \(process.terminationStatus) 35 | Output: \(errorOutput != "" ? errorOutput : "-") 36 | """ 37 | ] 38 | ) 39 | return .failure(error) 40 | } 41 | } catch { 42 | return .failure(error) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /NeoHubCLI/SocketClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NeoHubLib 4 | 5 | enum SendError: Error { 6 | case appIsNotRunning 7 | case failedToSendRequest(Error) 8 | } 9 | 10 | extension SendError: LocalizedError { 11 | var errorDescription: String? { 12 | switch self { 13 | case .appIsNotRunning: 14 | return "NeoHub app is not running. Start the app and retry." 15 | case .failedToSendRequest(let error): 16 | return error.localizedDescription 17 | } 18 | } 19 | } 20 | 21 | class SocketClient { 22 | private let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 23 | 24 | func send(_ request: Codable) -> Result { 25 | if !FileManager.default.fileExists(atPath: Socket.addr) { 26 | return .failure(.appIsNotRunning) 27 | } 28 | 29 | do { 30 | let encoder = JSONEncoder() 31 | let json = try encoder.encode(request) 32 | 33 | let responsePromise = group.next().makePromise(of: String.self) 34 | 35 | let bootstrap = ClientBootstrap(group: group).channelInitializer { channel in 36 | let responseHandler = ResponsePromiseHandler(promise: responsePromise) 37 | return channel.pipeline.addHandlers([ResponseHandler(), responseHandler]) 38 | } 39 | 40 | let channel = try bootstrap.connect(unixDomainSocketPath: Socket.addr).wait() 41 | 42 | let length = UInt32(bigEndian: UInt32(json.count)) 43 | let header = withUnsafeBytes(of: length) { Data($0) } 44 | var buffer = channel.allocator.buffer(capacity: header.count + json.count) 45 | 46 | buffer.writeBytes(header + json) 47 | 48 | try channel.writeAndFlush(buffer).wait() 49 | 50 | let response = try responsePromise.futureResult.wait() 51 | 52 | try channel.close().wait() 53 | 54 | return .success(response) 55 | } catch { 56 | return .failure(.failedToSendRequest(error)) 57 | } 58 | } 59 | } 60 | 61 | class ResponseHandler: ChannelInboundHandler { 62 | typealias InboundIn = ByteBuffer 63 | typealias OutboundOut = String 64 | 65 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 66 | var buffer = unwrapInboundIn(data) 67 | if let response = buffer.readString(length: buffer.readableBytes) { 68 | context.fireChannelRead(self.wrapOutboundOut(response)) 69 | } 70 | } 71 | } 72 | 73 | class ResponsePromiseHandler: ChannelInboundHandler { 74 | typealias InboundIn = String 75 | private let promise: EventLoopPromise 76 | 77 | init(promise: EventLoopPromise) { 78 | self.promise = promise 79 | } 80 | 81 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 82 | let response = self.unwrapInboundIn(data) 83 | promise.succeed(response) 84 | } 85 | 86 | func errorCaught(context: ChannelHandlerContext, error: Error) { 87 | promise.fail(error) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /NeoHubLib/NeoHubLib.docc/NeoHubLib.md: -------------------------------------------------------------------------------- 1 | # ``NeoHubLib`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` -------------------------------------------------------------------------------- /NeoHubLib/NeoHubLib.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for NeoHubLib. 4 | FOUNDATION_EXPORT double NeoHubLibVersionNumber; 5 | 6 | //! Project version string for NeoHubLib. 7 | FOUNDATION_EXPORT const unsigned char NeoHubLibVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | 12 | -------------------------------------------------------------------------------- /NeoHubLib/Shared.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Socket { 4 | public static let addr = "/tmp/neohub.sock" 5 | } 6 | 7 | public struct RunRequest: Codable { 8 | public let wd: URL 9 | public let bin: URL 10 | public let name: String? 11 | public let path: String? 12 | public let opts: [String] 13 | public let env: [String:String] 14 | 15 | public init( 16 | wd: URL, 17 | bin: URL, 18 | name: String?, 19 | path: String?, 20 | opts: [String], 21 | env: [String:String] 22 | ) { 23 | self.wd = wd 24 | self.bin = bin 25 | self.name = name 26 | self.path = path 27 | self.opts = opts 28 | self.env = env 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

NeoHub

2 | 3 |

4 | NeoHub 5 |

6 | 7 | ## Why? 8 | [Neovide](https://neovide.dev/) is a sweeeeet GUI for [Neovim](https://neovim.io/), but I had two annoying issues. 9 | 1. When multiple instances are running, it is hard to switch between them as each instance is a separate macOS process, and all the processes are named `neovide` in the `⌘ ⇥` list. 10 | 2. Often, I accidentally run a project that is already running, resulting in a Neovim error related to existing swap files. 11 | 12 | ## Features 13 | So, what NeoHub offers? 14 | 1. Global hotkey to show a switcher between Neovide instances. You can hit this hotkey from anywhere and activate a project you need. 15 | 2. Global hotkey to activate last used editor, if there are mulitple. 16 | 3. Hotkey to restart current editor. 17 | 4. CLI, which executes new Neovide instances, and if an instance at the current path is already running, it activates it instead of spawning a new one. 18 | 19 | ## Requirements 20 | - `macOS 13+`. 21 | - Administrative privileges to install the CLI. 22 | - `neovide` available in your `PATH`. 23 | 24 | ## Download 25 | Get it from the [Releases](https://github.com/alex35mil/NeoHub/releases). 26 | 27 | ## Installation 28 | This a macOS app. Unzip the `NeoHub.zip` and move `NeoHub.app` to the `Applications` folder. 29 | 30 | On the very first launch, you will be asked to install the CLI. You will need to enter an administrative password. 31 | 32 | ## Usage 33 | ### CLI 34 | Once installed, the `neohub` command should become available in your shell. This is the only way to launch a Neovide through the NeoHub, so use `neohub` instead of `neovide` to launch editors. Otherwise, things won't work. 35 | 36 | See `neovide --help` for available options. 37 | 38 | ### App 39 | Hit `⌘ ⌃ N` (`Command + Control + N`) to open the editor switcher. 40 | Hit `⌘ ⌃ Z` (`Command + Control + Z`) to activate last active editor. 41 | 42 | All hotkeys are configurable. 43 | 44 | When in the switcher, you can quit all editors at once by pressing `⌘ Q`, or just a selected one with `⌘ ⌫`. Also, you can use `⇥` (`Tab`) to cycle through the editors in the list. 45 | 46 | You can set a hotkey to restart current editor in the Settings. Keep in mind, that if you updated env vars and restart the editor - it won't pick up the new env. In this case, you need to shut down the editor and relaunch it from the CLI. 47 | 48 | P.S. When you press the editor switcher hotkey and there is only one Neovide instance is running, NeoHub will activate it instead of showing the switcher. 49 | 50 | ## Credits 51 | App icon is by [u/danbee](https://www.reddit.com/user/danbee/). 52 | 53 | ## License 54 | MIT. 55 | --------------------------------------------------------------------------------