1+ import AppKit
12import Foundation
23
34enum WorktrunkAgent : String , CaseIterable , Identifiable {
@@ -132,12 +133,147 @@ enum WorktrunkOpenBehavior: String, CaseIterable, Identifiable {
132133 }
133134}
134135
136+ enum ExternalEditorCategory : String {
137+ case editors
138+ case git
139+ case finder
140+ }
141+
142+ enum ExternalEditor : String , CaseIterable , Identifiable {
143+ // Editors
144+ case cursor
145+ case vscode
146+ case vscodium
147+ case zed
148+ case sublime
149+ case nova
150+ case textmate
151+ case xcode
152+ // JetBrains
153+ case intellij
154+ case webstorm
155+ case pycharm
156+ case goland
157+ case rubymine
158+ case clion
159+ case rider
160+ case phpstorm
161+ case fleet
162+ // Git clients
163+ case tower
164+ case fork
165+ case gitkraken
166+ case sourcetree
167+ case githubDesktop
168+ // Finder
169+ case finder
170+
171+ var id : String { rawValue }
172+
173+ var category : ExternalEditorCategory {
174+ switch self {
175+ case . tower, . fork, . gitkraken, . sourcetree, . githubDesktop:
176+ return . git
177+ case . finder:
178+ return . finder
179+ default :
180+ return . editors
181+ }
182+ }
183+
184+ var title : String {
185+ switch self {
186+ case . cursor: return " Cursor "
187+ case . vscode: return " VS Code "
188+ case . vscodium: return " VSCodium "
189+ case . zed: return " Zed "
190+ case . sublime: return " Sublime Text "
191+ case . nova: return " Nova "
192+ case . xcode: return " Xcode "
193+ case . textmate: return " TextMate "
194+ case . intellij: return " IntelliJ IDEA "
195+ case . webstorm: return " WebStorm "
196+ case . pycharm: return " PyCharm "
197+ case . goland: return " GoLand "
198+ case . rubymine: return " RubyMine "
199+ case . clion: return " CLion "
200+ case . rider: return " Rider "
201+ case . phpstorm: return " PhpStorm "
202+ case . fleet: return " Fleet "
203+ case . tower: return " Tower "
204+ case . fork: return " Fork "
205+ case . gitkraken: return " GitKraken "
206+ case . sourcetree: return " Sourcetree "
207+ case . githubDesktop: return " GitHub Desktop "
208+ case . finder: return " Finder "
209+ }
210+ }
211+
212+ var bundleIdentifier : String {
213+ switch self {
214+ case . cursor: return " com.todesktop.230313mzl4w4u92 "
215+ case . vscode: return " com.microsoft.VSCode "
216+ case . vscodium: return " com.vscodium "
217+ case . zed: return " dev.zed.Zed "
218+ case . sublime: return " com.sublimetext.4 "
219+ case . nova: return " com.panic.Nova "
220+ case . xcode: return " com.apple.dt.Xcode "
221+ case . textmate: return " com.macromates.TextMate "
222+ case . intellij: return " com.jetbrains.intellij "
223+ case . webstorm: return " com.jetbrains.WebStorm "
224+ case . pycharm: return " com.jetbrains.pycharm "
225+ case . goland: return " com.jetbrains.goland "
226+ case . rubymine: return " com.jetbrains.rubymine "
227+ case . clion: return " com.jetbrains.CLion "
228+ case . rider: return " com.jetbrains.rider "
229+ case . phpstorm: return " com.jetbrains.PhpStorm "
230+ case . fleet: return " fleet.app "
231+ case . tower: return " com.fournova.Tower3 "
232+ case . fork: return " com.DanPristupov.Fork "
233+ case . gitkraken: return " com.axosoft.gitkraken "
234+ case . sourcetree: return " com.torusknot.SourceTreeNotMAS "
235+ case . githubDesktop: return " com.github.GitHubClient "
236+ case . finder: return " com.apple.finder "
237+ }
238+ }
239+
240+ var appURL : URL ? {
241+ NSWorkspace . shared. urlForApplication ( withBundleIdentifier: bundleIdentifier)
242+ }
243+
244+ var isInstalled : Bool {
245+ appURL != nil
246+ }
247+
248+ var appIcon : NSImage ? {
249+ guard let url = appURL else { return nil }
250+ return NSWorkspace . shared. icon ( forFile: url. path)
251+ }
252+
253+ static func installedEditors( ) -> [ ExternalEditor ] {
254+ allCases. filter { $0. isInstalled }
255+ }
256+
257+ static func installedByCategory( ) -> [ ( category: ExternalEditorCategory , editors: [ ExternalEditor ] ) ] {
258+ let installed = installedEditors ( )
259+ var result : [ ( category: ExternalEditorCategory , editors: [ ExternalEditor ] ) ] = [ ]
260+ for cat in [ ExternalEditorCategory . editors, . git, . finder] {
261+ let group = installed. filter { $0. category == cat }
262+ if !group. isEmpty {
263+ result. append ( ( category: cat, editors: group) )
264+ }
265+ }
266+ return result
267+ }
268+ }
269+
135270enum WorktrunkPreferences {
136271 static let openBehaviorKey = " GhosttyWorktrunkOpenBehavior.v1 "
137272 static let worktreeTabsKey = " GhosttyWorktreeTabs.v1 "
138273 static let sidebarTabsKey = " GhostreeWorktrunkSidebarTabs.v1 "
139274 static let defaultAgentKey = " GhosttyWorktrunkDefaultAgent.v1 "
140275 static let githubIntegrationKey = " GhostreeGitHubIntegration.v1 "
276+ static let lastEditorKey = " GhostreeLastEditor.v1 "
141277
142278 static var worktreeTabsEnabled : Bool {
143279 UserDefaults . standard. bool ( forKey: worktreeTabsKey)
@@ -157,4 +293,22 @@ enum WorktrunkPreferences {
157293 }
158294 return UserDefaults . standard. bool ( forKey: githubIntegrationKey)
159295 }
296+
297+ static var lastEditor : ExternalEditor ? {
298+ get {
299+ guard let raw = UserDefaults . standard. string ( forKey: lastEditorKey) else { return nil }
300+ return ExternalEditor ( rawValue: raw)
301+ }
302+ set {
303+ UserDefaults . standard. set ( newValue? . rawValue, forKey: lastEditorKey)
304+ }
305+ }
306+
307+ static var preferredEditor : ExternalEditor ? {
308+ let installed = ExternalEditor . installedEditors ( )
309+ if let last = lastEditor, installed. contains ( last) {
310+ return last
311+ }
312+ return installed. first
313+ }
160314}
0 commit comments