返回 DeepSeek-Reasonix
external_opener_darwin.go
根目录 / desktop / external_opener_darwin.go
1 //go:build darwin
2
3 package main
4
5 import (
6 "context"
7 "io"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12 "time"
13 )
14
15 func darwinInstalledApplicationIndex() map[string]string {
16 home, _ := os.UserHomeDir()
17 roots := []string{
18 "/Applications",
19 "/System/Applications",
20 "/System/Applications/Utilities",
21 "/System/Library/CoreServices",
22 }
23 if home != "" {
24 roots = append(roots, filepath.Join(home, "Applications"))
25 }
26 index := make(map[string]string)
27 add := func(path string) {
28 path = strings.TrimSpace(path)
29 if info, err := os.Stat(path); err == nil && info.IsDir() && strings.HasSuffix(strings.ToLower(path), ".app") {
30 name := strings.ToLower(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)))
31 if _, exists := index[name]; !exists {
32 index[name] = path
33 }
34 }
35 }
36 for _, root := range roots {
37 entries, err := os.ReadDir(root)
38 if err != nil {
39 continue
40 }
41 for _, entry := range entries {
42 if strings.HasSuffix(strings.ToLower(entry.Name()), ".app") {
43 add(filepath.Join(root, entry.Name()))
44 }
45 }
46 }
47 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
48 defer cancel()
49 out, err := exec.CommandContext(ctx, "/usr/bin/mdfind", "kMDItemContentType == 'com.apple.application-bundle'").Output()
50 if err == nil {
51 for _, line := range strings.Split(string(out), "\n") {
52 add(line)
53 }
54 }
55 return index
56 }
57
58 func darwinApplicationPath(index map[string]string, names ...string) string {
59 for _, name := range names {
60 if path := index[strings.ToLower(strings.TrimSpace(name))]; path != "" {
61 return path
62 }
63 }
64 return ""
65 }
66
67 func platformExternalOpenerSpecs() []externalOpenerSpec {
68 installed := darwinInstalledApplicationIndex()
69 var specs []externalOpenerSpec
70 addMode := func(id, name, kind, mode, _ string, appNames ...string) {
71 if path := darwinApplicationPath(installed, appNames...); path != "" {
72 specs = append(specs, externalOpenerSpec{
73 View: ExternalOpenerView{ID: id, Name: name, Kind: kind},
74 Target: path,
75 LaunchMode: mode,
76 IconSource: path,
77 })
78 }
79 }
80 add := func(id, name, kind, bundleID string, appNames ...string) {
81 addMode(id, name, kind, "application", bundleID, appNames...)
82 }
83
84 // Keep the order stable and close to the native Codex menu: common editors,
85 // the platform browser, terminals, then the broader IDE catalog.
86 add("vscode", "VS Code", externalOpenerEditor, "com.microsoft.VSCode", "Visual Studio Code")
87 add("vscode-insiders", "VS Code Insiders", externalOpenerEditor, "com.microsoft.VSCodeInsiders", "Visual Studio Code - Insiders")
88 add("cursor", "Cursor", externalOpenerEditor, "com.todesktop.230313mzl4w4u92", "Cursor")
89 add("finder", "Finder", externalOpenerFileManager, "com.apple.finder", "Finder")
90 add("terminal", "Terminal", externalOpenerTerminal, "com.apple.Terminal", "Terminal")
91 add("iterm", "iTerm2", externalOpenerTerminal, "com.googlecode.iterm2", "iTerm", "iTerm2")
92 addMode("ghostty", "Ghostty", externalOpenerTerminal, "ghostty", "com.mitchellh.ghostty", "Ghostty")
93 add("xcode", "Xcode", externalOpenerEditor, "com.apple.dt.Xcode", "Xcode")
94 add("android-studio", "Android Studio", externalOpenerEditor, "com.google.android.studio", "Android Studio")
95 add("goland", "GoLand", externalOpenerEditor, "com.jetbrains.goland", "GoLand")
96 add("pycharm", "PyCharm", externalOpenerEditor, "com.jetbrains.pycharm", "PyCharm", "PyCharm CE")
97 add("intellij-idea", "IntelliJ IDEA", externalOpenerEditor, "com.jetbrains.intellij", "IntelliJ IDEA", "IntelliJ IDEA Ultimate", "IntelliJ IDEA CE")
98 add("webstorm", "WebStorm", externalOpenerEditor, "com.jetbrains.WebStorm", "WebStorm")
99 add("datagrip", "DataGrip", externalOpenerEditor, "com.jetbrains.datagrip", "DataGrip")
100 add("codebuddy", "CodeBuddy", externalOpenerEditor, "com.tencent.codebuddy", "CodeBuddy")
101 add("windsurf", "Windsurf", externalOpenerEditor, "com.exafunction.windsurf", "Windsurf")
102 add("zed", "Zed", externalOpenerEditor, "dev.zed.Zed", "Zed")
103 add("sublime-text", "Sublime Text", externalOpenerEditor, "com.sublimetext.4", "Sublime Text")
104 add("kiro", "Kiro", externalOpenerEditor, "dev.kiro.desktop", "Kiro")
105 return specs
106 }
107
108 func darwinBundleIconPath(appPath string) string {
109 resources := filepath.Join(appPath, "Contents", "Resources")
110 infoPlist := filepath.Join(appPath, "Contents", "Info.plist")
111 for _, key := range []string{"CFBundleIconFile", "CFBundleIconName"} {
112 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
113 out, err := exec.CommandContext(ctx, "/usr/libexec/PlistBuddy", "-c", "Print :"+key, infoPlist).Output()
114 cancel()
115 if err != nil {
116 continue
117 }
118 name := strings.TrimSpace(string(out))
119 if name == "" {
120 continue
121 }
122 if filepath.Ext(name) == "" {
123 name += ".icns"
124 }
125 path := filepath.Join(resources, name)
126 if info, statErr := os.Stat(path); statErr == nil && !info.IsDir() {
127 return path
128 }
129 }
130
131 // Older and some Electron bundles omit the icon key. The application icon
132 // is normally the largest ICNS resource; document-type icons are much smaller.
133 matches, _ := filepath.Glob(filepath.Join(resources, "*.icns"))
134 var best string
135 var bestSize int64
136 for _, candidate := range matches {
137 if info, err := os.Stat(candidate); err == nil && !info.IsDir() && info.Size() > bestSize {
138 best = candidate
139 bestSize = info.Size()
140 }
141 }
142 return best
143 }
144
145 func platformExternalOpenerIconDataURL(spec externalOpenerSpec) string {
146 iconPath := darwinBundleIconPath(spec.IconSource)
147 if iconPath == "" {
148 return ""
149 }
150 tempDir, err := os.MkdirTemp("", "reasonix-opener-icon-*")
151 if err != nil {
152 return ""
153 }
154 defer os.RemoveAll(tempDir)
155 outPath := filepath.Join(tempDir, "icon.png")
156 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
157 defer cancel()
158 cmd := exec.CommandContext(ctx, "/usr/bin/sips", "-s", "format", "png", "-z", "64", "64", iconPath, "--out", outPath)
159 cmd.Stdout = io.Discard
160 cmd.Stderr = io.Discard
161 if err := cmd.Run(); err != nil {
162 return ""
163 }
164 data, err := os.ReadFile(outPath)
165 if err != nil {
166 return ""
167 }
168 return externalOpenerPNGDataURL(data)
169 }
170
171 func darwinExternalOpenerCommand(spec externalOpenerSpec, path string) *exec.Cmd {
172 if spec.LaunchMode == "ghostty" {
173 return exec.Command("/usr/bin/open", "-na", spec.Target, "--args", "--working-directory="+path)
174 }
175 return exec.Command("/usr/bin/open", "-a", spec.Target, path)
176 }
177
178 func launchPlatformExternalOpener(spec externalOpenerSpec, path string) error {
179 return startDetachedExternalOpener(darwinExternalOpenerCommand(spec, path))
180 }
181
181 lines GO