@@ -17,6 +17,17 @@ pub struct EditorInfo {
1717 pub installed : bool ,
1818}
1919
20+ #[ derive( Serialize , Clone ) ]
21+ #[ serde( rename_all = "camelCase" ) ]
22+ pub struct GitToolInfo {
23+ pub id : String ,
24+ pub name : String ,
25+ pub command : String ,
26+ pub installed : bool ,
27+ pub supports_diff : bool ,
28+ pub supports_merge : bool ,
29+ }
30+
2031// ── Known Editors ──
2132// Each entry: (id, display_name, cli_command, optional macOS app bundle path).
2233
@@ -27,6 +38,15 @@ struct EditorCandidate {
2738 mac_bundle_bin : Option < & ' static str > ,
2839}
2940
41+ struct GitToolCandidate {
42+ id : & ' static str ,
43+ name : & ' static str ,
44+ command : & ' static str ,
45+ mac_bundle_bin : Option < & ' static str > ,
46+ supports_diff : bool ,
47+ supports_merge : bool ,
48+ }
49+
3050fn known_editors ( ) -> Vec < EditorCandidate > {
3151 vec ! [
3252 EditorCandidate {
@@ -94,6 +114,113 @@ fn known_editors() -> Vec<EditorCandidate> {
94114 ]
95115}
96116
117+ fn known_git_tools ( ) -> Vec < GitToolCandidate > {
118+ vec ! [
119+ GitToolCandidate {
120+ id: "vscode" ,
121+ name: "VS Code" ,
122+ command: "code" ,
123+ mac_bundle_bin: Some (
124+ "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" ,
125+ ) ,
126+ supports_diff: true ,
127+ supports_merge: true ,
128+ } ,
129+ GitToolCandidate {
130+ id: "cursor" ,
131+ name: "Cursor" ,
132+ command: "cursor" ,
133+ mac_bundle_bin: Some ( "/Applications/Cursor.app/Contents/Resources/app/bin/cursor" ) ,
134+ supports_diff: true ,
135+ supports_merge: true ,
136+ } ,
137+ GitToolCandidate {
138+ id: "windsurf" ,
139+ name: "Windsurf" ,
140+ command: "windsurf" ,
141+ mac_bundle_bin: Some (
142+ "/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf" ,
143+ ) ,
144+ supports_diff: true ,
145+ supports_merge: true ,
146+ } ,
147+ GitToolCandidate {
148+ id: "zed" ,
149+ name: "Zed" ,
150+ command: "zed" ,
151+ mac_bundle_bin: Some ( "/Applications/Zed.app/Contents/MacOS/cli" ) ,
152+ supports_diff: true ,
153+ supports_merge: true ,
154+ } ,
155+ GitToolCandidate {
156+ id: "sublime" ,
157+ name: "Sublime Text" ,
158+ command: "subl" ,
159+ mac_bundle_bin: Some (
160+ "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" ,
161+ ) ,
162+ supports_diff: true ,
163+ supports_merge: true ,
164+ } ,
165+ GitToolCandidate {
166+ id: "vimdiff" ,
167+ name: "Vimdiff" ,
168+ command: "vimdiff" ,
169+ mac_bundle_bin: None ,
170+ supports_diff: true ,
171+ supports_merge: true ,
172+ } ,
173+ GitToolCandidate {
174+ id: "nvimdiff" ,
175+ name: "Neovim diff" ,
176+ command: "nvimdiff" ,
177+ mac_bundle_bin: None ,
178+ supports_diff: true ,
179+ supports_merge: true ,
180+ } ,
181+ GitToolCandidate {
182+ id: "meld" ,
183+ name: "Meld" ,
184+ command: "meld" ,
185+ mac_bundle_bin: None ,
186+ supports_diff: true ,
187+ supports_merge: true ,
188+ } ,
189+ GitToolCandidate {
190+ id: "kdiff3" ,
191+ name: "KDiff3" ,
192+ command: "kdiff3" ,
193+ mac_bundle_bin: None ,
194+ supports_diff: true ,
195+ supports_merge: true ,
196+ } ,
197+ GitToolCandidate {
198+ id: "bcompare" ,
199+ name: "Beyond Compare" ,
200+ command: "bcompare" ,
201+ mac_bundle_bin: None ,
202+ supports_diff: true ,
203+ supports_merge: true ,
204+ } ,
205+ GitToolCandidate {
206+ id: "p4merge" ,
207+ name: "P4Merge" ,
208+ command: "p4merge" ,
209+ mac_bundle_bin: None ,
210+ supports_diff: true ,
211+ supports_merge: true ,
212+ } ,
213+ GitToolCandidate {
214+ id: "opendiff" ,
215+ name: "FileMerge (opendiff)" ,
216+ command: "opendiff" ,
217+ mac_bundle_bin: None ,
218+ supports_diff: true ,
219+ supports_merge: true ,
220+ } ,
221+ ]
222+ }
223+
97224fn is_command_available ( cmd : & str ) -> bool {
98225 command_exists ( cmd)
99226}
@@ -112,6 +239,18 @@ fn resolve_editor(editor: &EditorCandidate) -> Option<String> {
112239 None
113240}
114241
242+ fn resolve_git_tool ( tool : & GitToolCandidate ) -> Option < String > {
243+ if is_command_available ( tool. command ) {
244+ return Some ( tool. command . to_string ( ) ) ;
245+ }
246+ if let Some ( bundle_path) = tool. mac_bundle_bin {
247+ if Path :: new ( bundle_path) . exists ( ) {
248+ return Some ( bundle_path. to_string ( ) ) ;
249+ }
250+ }
251+ None
252+ }
253+
115254/// Parse a shell-like command string, respecting double and single quotes.
116255/// e.g. `"/path/with spaces/code" --wait` → ["/path/with spaces/code", "--wait"]
117256/// Also handles unquoted paths by probing the filesystem for known editors.
@@ -231,6 +370,26 @@ pub fn detect_editors() -> Vec<EditorInfo> {
231370 . collect ( )
232371}
233372
373+ #[ tauri:: command]
374+ pub fn detect_git_tools ( ) -> Vec < GitToolInfo > {
375+ known_git_tools ( )
376+ . into_iter ( )
377+ . map ( |tool| {
378+ let resolved = resolve_git_tool ( & tool) ;
379+ GitToolInfo {
380+ id : tool. id . to_string ( ) ,
381+ name : tool. name . to_string ( ) ,
382+ command : resolved
383+ . clone ( )
384+ . unwrap_or_else ( || tool. command . to_string ( ) ) ,
385+ installed : resolved. is_some ( ) ,
386+ supports_diff : tool. supports_diff ,
387+ supports_merge : tool. supports_merge ,
388+ }
389+ } )
390+ . collect ( )
391+ }
392+
234393#[ tauri:: command]
235394pub fn get_git_config ( key : String ) -> Result < String , String > {
236395 let key = validate_git_config_key ( & key) ?;
0 commit comments