@@ -7,10 +7,282 @@ use crate::git::types::{
77 SubmoduleActionRequest ,
88} ;
99use crate :: { AppState , CloneCancelFlag , configure_command} ;
10+ use serde:: { Deserialize , Serialize } ;
1011use std:: io:: Read ;
11- use std:: process:: Stdio ;
12+ use std:: path:: { Path , PathBuf } ;
13+ use std:: process:: { Command , Stdio } ;
1214use std:: sync:: atomic:: Ordering ;
1315use tauri:: Manager ;
16+ use tauri_plugin_opener:: OpenerExt ;
17+
18+ #[ derive( Debug , Clone , Copy , Deserialize , Serialize , PartialEq , Eq ) ]
19+ #[ serde( rename_all = "camelCase" ) ]
20+ pub enum RepoOpenLocationKind {
21+ FileExplorer ,
22+ Terminal ,
23+ GitBash ,
24+ }
25+
26+ #[ derive( Debug , Clone , Serialize , PartialEq , Eq ) ]
27+ #[ serde( rename_all = "camelCase" ) ]
28+ pub struct RepoOpenLocation {
29+ kind : RepoOpenLocationKind ,
30+ label : String ,
31+ fallback_label : String ,
32+ icon_data_url : Option < String > ,
33+ }
34+
35+ #[ tauri:: command]
36+ pub fn get_repo_open_locations ( ) -> Vec < RepoOpenLocation > {
37+ let locations = vec ! [
38+ RepoOpenLocation {
39+ kind: RepoOpenLocationKind :: FileExplorer ,
40+ label: default_file_manager_label( ) . to_string( ) ,
41+ fallback_label: default_file_manager_label( ) . to_string( ) ,
42+ icon_data_url: None ,
43+ } ,
44+ RepoOpenLocation {
45+ kind: RepoOpenLocationKind :: Terminal ,
46+ label: default_terminal_label( ) . to_string( ) ,
47+ fallback_label: "Terminal" . to_string( ) ,
48+ icon_data_url: None ,
49+ } ,
50+ ] ;
51+
52+ #[ cfg( target_os = "windows" ) ]
53+ let locations = {
54+ let mut locations = locations;
55+ if crate :: resolve_system_git_bash_exe ( ) . is_some ( ) {
56+ locations. push ( RepoOpenLocation {
57+ kind : RepoOpenLocationKind :: GitBash ,
58+ label : "Git Bash" . to_string ( ) ,
59+ fallback_label : "Git Bash" . to_string ( ) ,
60+ icon_data_url : None ,
61+ } ) ;
62+ }
63+ locations
64+ } ;
65+
66+ locations
67+ }
68+
69+ #[ tauri:: command]
70+ pub fn open_repo_location (
71+ repo_path : String ,
72+ kind : RepoOpenLocationKind ,
73+ app : tauri:: AppHandle ,
74+ ) -> Result < OperationResult , String > {
75+ let path = validate_repo_open_path ( & repo_path) ?;
76+
77+ match kind {
78+ RepoOpenLocationKind :: FileExplorer => {
79+ app. opener ( )
80+ . open_path ( path. to_string_lossy ( ) . to_string ( ) , None :: < & str > )
81+ . map_err ( |e| format ! ( "Failed to open file manager: {e}" ) ) ?;
82+ Ok ( repo_open_result (
83+ format ! ( "Opened repository in {}" , default_file_manager_label( ) ) ,
84+ path,
85+ ) )
86+ }
87+ RepoOpenLocationKind :: Terminal => {
88+ open_terminal_at ( & path) ?;
89+ Ok ( repo_open_result (
90+ "Opened repository in Terminal" . to_string ( ) ,
91+ path,
92+ ) )
93+ }
94+ RepoOpenLocationKind :: GitBash => {
95+ open_git_bash_at ( & path) ?;
96+ Ok ( repo_open_result (
97+ "Opened repository in Git Bash" . to_string ( ) ,
98+ path,
99+ ) )
100+ }
101+ }
102+ }
103+
104+ fn validate_repo_open_path ( repo_path : & str ) -> Result < PathBuf , String > {
105+ let trimmed = repo_path. trim ( ) ;
106+ if trimmed. is_empty ( ) {
107+ return Err ( "Repository path cannot be empty" . to_string ( ) ) ;
108+ }
109+
110+ let path = PathBuf :: from ( trimmed) ;
111+ if !path. is_dir ( ) {
112+ return Err ( "Repository path must be an existing directory" . to_string ( ) ) ;
113+ }
114+ Ok ( path)
115+ }
116+
117+ fn repo_open_result ( message : String , path : PathBuf ) -> OperationResult {
118+ OperationResult {
119+ message,
120+ output : None ,
121+ repo_path : Some ( path. to_string_lossy ( ) . to_string ( ) ) ,
122+ backend_used : "git-cli" . to_string ( ) ,
123+ interpreted_error : None ,
124+ }
125+ }
126+
127+ fn default_file_manager_label ( ) -> & ' static str {
128+ #[ cfg( target_os = "windows" ) ]
129+ {
130+ "File Explorer"
131+ }
132+ #[ cfg( target_os = "macos" ) ]
133+ {
134+ "Finder"
135+ }
136+ #[ cfg( target_os = "linux" ) ]
137+ {
138+ "File Manager"
139+ }
140+ #[ cfg( not( any( target_os = "windows" , target_os = "macos" , target_os = "linux" ) ) ) ]
141+ {
142+ "File Manager"
143+ }
144+ }
145+
146+ fn default_terminal_label ( ) -> & ' static str {
147+ "Terminal"
148+ }
149+
150+ fn open_terminal_at ( path : & Path ) -> Result < ( ) , String > {
151+ #[ cfg( target_os = "windows" ) ]
152+ {
153+ return open_terminal_at_windows ( path) ;
154+ }
155+
156+ #[ cfg( target_os = "macos" ) ]
157+ {
158+ return open_terminal_at_macos ( path) ;
159+ }
160+
161+ #[ cfg( target_os = "linux" ) ]
162+ {
163+ return open_terminal_at_linux ( path) ;
164+ }
165+
166+ #[ cfg( not( any( target_os = "windows" , target_os = "macos" , target_os = "linux" ) ) ) ]
167+ {
168+ let _ = path;
169+ Err ( "Opening a terminal is not supported on this platform" . to_string ( ) )
170+ }
171+ }
172+
173+ fn open_git_bash_at ( path : & Path ) -> Result < ( ) , String > {
174+ #[ cfg( target_os = "windows" ) ]
175+ {
176+ return open_git_bash_at_windows ( path) ;
177+ }
178+
179+ #[ cfg( not( target_os = "windows" ) ) ]
180+ {
181+ let _ = path;
182+ Err ( "Git Bash is only available on Windows" . to_string ( ) )
183+ }
184+ }
185+
186+ #[ cfg( target_os = "windows" ) ]
187+ fn open_git_bash_at_windows ( path : & Path ) -> Result < ( ) , String > {
188+ let git_bash = crate :: resolve_system_git_bash_exe ( )
189+ . ok_or_else ( || "Git Bash from Git for Windows was not found" . to_string ( ) ) ?;
190+ Command :: new ( git_bash)
191+ . arg ( format ! ( "--cd={}" , path. display( ) ) )
192+ . spawn ( )
193+ . map ( |_| ( ) )
194+ . map_err ( |e| format ! ( "Failed to open Git Bash: {e}" ) )
195+ }
196+
197+ #[ cfg( target_os = "windows" ) ]
198+ fn open_terminal_at_windows ( path : & Path ) -> Result < ( ) , String > {
199+ let mut wt = Command :: new ( "wt.exe" ) ;
200+ wt. arg ( "-d" ) . arg ( path) ;
201+ if wt. spawn ( ) . is_ok ( ) {
202+ return Ok ( ( ) ) ;
203+ }
204+
205+ Command :: new ( "cmd.exe" )
206+ . arg ( "/C" )
207+ . arg ( "start" )
208+ . arg ( "" )
209+ . arg ( "/D" )
210+ . arg ( path)
211+ . arg ( "cmd.exe" )
212+ . spawn ( )
213+ . map ( |_| ( ) )
214+ . map_err ( |e| format ! ( "Failed to open terminal: {e}" ) )
215+ }
216+
217+ #[ cfg( target_os = "macos" ) ]
218+ fn open_terminal_at_macos ( path : & Path ) -> Result < ( ) , String > {
219+ Command :: new ( "open" )
220+ . arg ( "-a" )
221+ . arg ( "Terminal" )
222+ . arg ( path)
223+ . spawn ( )
224+ . map ( |_| ( ) )
225+ . map_err ( |e| format ! ( "Failed to open Terminal: {e}" ) )
226+ }
227+
228+ #[ cfg( target_os = "linux" ) ]
229+ fn open_terminal_at_linux ( path : & Path ) -> Result < ( ) , String > {
230+ let mut errors = Vec :: new ( ) ;
231+
232+ if let Some ( terminal) = std:: env:: var_os ( "TERMINAL" ) . and_then ( |value| value. into_string ( ) . ok ( ) )
233+ {
234+ if !terminal. trim ( ) . is_empty ( ) && spawn_terminal_command ( & terminal, path, & mut errors) {
235+ return Ok ( ( ) ) ;
236+ }
237+ }
238+
239+ for command in [
240+ "x-terminal-emulator" ,
241+ "gnome-terminal" ,
242+ "kgx" ,
243+ "konsole" ,
244+ "xfce4-terminal" ,
245+ "mate-terminal" ,
246+ "lxterminal" ,
247+ "alacritty" ,
248+ "kitty" ,
249+ "wezterm" ,
250+ "foot" ,
251+ "xterm" ,
252+ ] {
253+ if spawn_terminal_command ( command, path, & mut errors) {
254+ return Ok ( ( ) ) ;
255+ }
256+ }
257+
258+ Err ( if errors. is_empty ( ) {
259+ "No supported terminal emulator was found" . to_string ( )
260+ } else {
261+ format ! (
262+ "No supported terminal emulator was found ({})" ,
263+ errors. join( "; " )
264+ )
265+ } )
266+ }
267+
268+ #[ cfg( target_os = "linux" ) ]
269+ fn spawn_terminal_command ( command_spec : & str , path : & Path , errors : & mut Vec < String > ) -> bool {
270+ let mut parts = command_spec. split_whitespace ( ) ;
271+ let Some ( command_name) = parts. next ( ) else {
272+ return false ;
273+ } ;
274+
275+ let mut command = Command :: new ( command_name) ;
276+ command. args ( parts) . current_dir ( path) ;
277+
278+ match command. spawn ( ) {
279+ Ok ( _) => true ,
280+ Err ( error) => {
281+ errors. push ( format ! ( "{command_name}: {error}" ) ) ;
282+ false
283+ }
284+ }
285+ }
14286
15287#[ tauri:: command]
16288pub async fn get_commit_markers (
@@ -83,6 +355,7 @@ pub fn init_repo(repo_path: String) -> Result<OperationResult, String> {
83355 output : None ,
84356 repo_path : Some ( path. to_string_lossy ( ) . to_string ( ) ) ,
85357 backend_used : "git-cli" . to_string ( ) ,
358+ interpreted_error : None ,
86359 } ) ;
87360 }
88361
@@ -117,6 +390,7 @@ pub fn init_repo(repo_path: String) -> Result<OperationResult, String> {
117390 output : None ,
118391 repo_path : Some ( path. to_string_lossy ( ) . to_string ( ) ) ,
119392 backend_used : "git-cli" . to_string ( ) ,
393+ interpreted_error : None ,
120394 } )
121395}
122396
@@ -232,6 +506,7 @@ pub async fn clone_repo(
232506 output : None ,
233507 repo_path : Some ( final_dest_str) ,
234508 backend_used : "git-cli" . to_string ( ) ,
509+ interpreted_error : None ,
235510 } )
236511}
237512
0 commit comments