@@ -20,9 +20,9 @@ use std::{
2020 env,
2121 net:: TcpListener ,
2222 path:: PathBuf ,
23+ process:: Command ,
2324 sync:: { Arc , Mutex } ,
2425 time:: Duration ,
25- process:: Command ,
2626} ;
2727use tauri:: { AppHandle , Manager , RunEvent , State , ipc:: Channel } ;
2828#[ cfg( any( target_os = "linux" , all( debug_assertions, windows) ) ) ]
@@ -152,24 +152,178 @@ fn check_app_exists(app_name: &str) -> bool {
152152 {
153153 check_windows_app ( app_name)
154154 }
155-
155+
156156 #[ cfg( target_os = "macos" ) ]
157157 {
158158 check_macos_app ( app_name)
159159 }
160-
160+
161161 #[ cfg( target_os = "linux" ) ]
162162 {
163163 check_linux_app ( app_name)
164164 }
165165}
166166
167167#[ cfg( target_os = "windows" ) ]
168- fn check_windows_app ( app_name : & str ) -> bool {
168+ fn check_windows_app ( _app_name : & str ) -> bool {
169169 // Check if command exists in PATH, including .exe
170170 return true ;
171171}
172172
173+ #[ cfg( target_os = "windows" ) ]
174+ fn resolve_windows_app_path ( app_name : & str ) -> Option < String > {
175+ use std:: path:: { Path , PathBuf } ;
176+
177+ // Try to find the command using 'where'
178+ let output = Command :: new ( "where" ) . arg ( app_name) . output ( ) . ok ( ) ?;
179+
180+ if !output. status . success ( ) {
181+ return None ;
182+ }
183+
184+ let paths = String :: from_utf8_lossy ( & output. stdout )
185+ . lines ( )
186+ . map ( str:: trim)
187+ . filter ( |line| !line. is_empty ( ) )
188+ . map ( PathBuf :: from)
189+ . collect :: < Vec < _ > > ( ) ;
190+
191+ let has_ext = |path : & Path , ext : & str | {
192+ path. extension ( )
193+ . and_then ( |v| v. to_str ( ) )
194+ . map ( |v| v. eq_ignore_ascii_case ( ext) )
195+ . unwrap_or ( false )
196+ } ;
197+
198+ if let Some ( path) = paths. iter ( ) . find ( |path| has_ext ( path, "exe" ) ) {
199+ return Some ( path. to_string_lossy ( ) . to_string ( ) ) ;
200+ }
201+
202+ let resolve_cmd = |path : & Path | -> Option < String > {
203+ let content = std:: fs:: read_to_string ( path) . ok ( ) ?;
204+
205+ for token in content. split ( '"' ) {
206+ let lower = token. to_ascii_lowercase ( ) ;
207+ if !lower. contains ( ".exe" ) {
208+ continue ;
209+ }
210+
211+ if let Some ( index) = lower. find ( "%~dp0" ) {
212+ let base = path. parent ( ) ?;
213+ let suffix = & token[ index + 5 ..] ;
214+ let mut resolved = PathBuf :: from ( base) ;
215+
216+ for part in suffix. replace ( '/' , "\\ " ) . split ( '\\' ) {
217+ if part. is_empty ( ) || part == "." {
218+ continue ;
219+ }
220+ if part == ".." {
221+ let _ = resolved. pop ( ) ;
222+ continue ;
223+ }
224+ resolved. push ( part) ;
225+ }
226+
227+ if resolved. exists ( ) {
228+ return Some ( resolved. to_string_lossy ( ) . to_string ( ) ) ;
229+ }
230+ }
231+
232+ let resolved = PathBuf :: from ( token) ;
233+ if resolved. exists ( ) {
234+ return Some ( resolved. to_string_lossy ( ) . to_string ( ) ) ;
235+ }
236+ }
237+
238+ None
239+ } ;
240+
241+ for path in & paths {
242+ if has_ext ( path, "cmd" ) || has_ext ( path, "bat" ) {
243+ if let Some ( resolved) = resolve_cmd ( path) {
244+ return Some ( resolved) ;
245+ }
246+ }
247+
248+ if path. extension ( ) . is_none ( ) {
249+ let cmd = path. with_extension ( "cmd" ) ;
250+ if cmd. exists ( ) {
251+ if let Some ( resolved) = resolve_cmd ( & cmd) {
252+ return Some ( resolved) ;
253+ }
254+ }
255+
256+ let bat = path. with_extension ( "bat" ) ;
257+ if bat. exists ( ) {
258+ if let Some ( resolved) = resolve_cmd ( & bat) {
259+ return Some ( resolved) ;
260+ }
261+ }
262+ }
263+ }
264+
265+ let key = app_name
266+ . chars ( )
267+ . filter ( |v| v. is_ascii_alphanumeric ( ) )
268+ . flat_map ( |v| v. to_lowercase ( ) )
269+ . collect :: < String > ( ) ;
270+
271+ if !key. is_empty ( ) {
272+ for path in & paths {
273+ let dirs = [
274+ path. parent ( ) ,
275+ path. parent ( ) . and_then ( |dir| dir. parent ( ) ) ,
276+ path. parent ( )
277+ . and_then ( |dir| dir. parent ( ) )
278+ . and_then ( |dir| dir. parent ( ) ) ,
279+ ] ;
280+
281+ for dir in dirs. into_iter ( ) . flatten ( ) {
282+ if let Ok ( entries) = std:: fs:: read_dir ( dir) {
283+ for entry in entries. flatten ( ) {
284+ let candidate = entry. path ( ) ;
285+ if !has_ext ( & candidate, "exe" ) {
286+ continue ;
287+ }
288+
289+ let Some ( stem) = candidate. file_stem ( ) . and_then ( |v| v. to_str ( ) ) else {
290+ continue ;
291+ } ;
292+
293+ let name = stem
294+ . chars ( )
295+ . filter ( |v| v. is_ascii_alphanumeric ( ) )
296+ . flat_map ( |v| v. to_lowercase ( ) )
297+ . collect :: < String > ( ) ;
298+
299+ if name. contains ( & key) || key. contains ( & name) {
300+ return Some ( candidate. to_string_lossy ( ) . to_string ( ) ) ;
301+ }
302+ }
303+ }
304+ }
305+ }
306+ }
307+
308+ paths. first ( ) . map ( |path| path. to_string_lossy ( ) . to_string ( ) )
309+ }
310+
311+ #[ tauri:: command]
312+ #[ specta:: specta]
313+ fn resolve_app_path ( app_name : & str ) -> Option < String > {
314+ #[ cfg( target_os = "windows" ) ]
315+ {
316+ resolve_windows_app_path ( app_name)
317+ }
318+
319+ #[ cfg( not( target_os = "windows" ) ) ]
320+ {
321+ // On macOS/Linux, just return the app_name as-is since
322+ // the opener plugin handles them correctly
323+ Some ( app_name. to_string ( ) )
324+ }
325+ }
326+
173327#[ cfg( target_os = "macos" ) ]
174328fn check_macos_app ( app_name : & str ) -> bool {
175329 // Check common installation locations
@@ -181,13 +335,13 @@ fn check_macos_app(app_name: &str) -> bool {
181335 if let Ok ( home) = std:: env:: var ( "HOME" ) {
182336 app_locations. push ( format ! ( "{}/Applications/{}.app" , home, app_name) ) ;
183337 }
184-
338+
185339 for location in app_locations {
186340 if std:: path:: Path :: new ( & location) . exists ( ) {
187341 return true ;
188342 }
189343 }
190-
344+
191345 // Also check if command exists in PATH
192346 Command :: new ( "which" )
193347 . arg ( app_name)
@@ -251,7 +405,8 @@ pub fn run() {
251405 get_display_backend,
252406 set_display_backend,
253407 markdown:: parse_markdown_command,
254- check_app_exists
408+ check_app_exists,
409+ resolve_app_path
255410 ] )
256411 . events ( tauri_specta:: collect_events![ LoadingWindowComplete ] )
257412 . error_handling ( tauri_specta:: ErrorHandlingMode :: Throw ) ;
0 commit comments