@@ -68,15 +68,32 @@ pub fn reveal(path: &str, tpl: &str) {
6868/// 委托给已运行的用户态 shell(中完整性级别),因此即便 App 以管理员身份运行也
6969/// 能激活 UWP 关联应用(提权进程直接激活 UWP 会被系统拒绝),且无 cmd 黑框闪
7070/// 烁。文件场景不存在“强制用 Explorer 当文件管理器”的顾虑(那是打开目录才有)。
71+ /// - **压缩包例外**:explorer.exe 收到 .zip/.rar 等路径会直接以“压缩文件夹”
72+ /// 浏览进去,无视第三方压缩软件的关联。检测到第三方默认 handler 时直接调
73+ /// `ShellExecuteW`(详见 `shell_execute_open` / `archive_handler_is_third_party`)。
7174///
7275/// | 平台 | 命令 |
7376/// |---------|---------------------|
74- /// | Windows | `explorer.exe path` |
77+ /// | Windows | `explorer.exe path`(第三方关联的压缩包 → `ShellExecuteW`) |
7578/// | macOS | `open path` |
7679/// | Linux | `xdg-open path` |
7780pub fn open_file ( path : & str ) {
7881 #[ cfg( target_os = "windows" ) ]
7982 {
83+ // 压缩包特例:explorer.exe 收到 .zip/.rar 等路径会直接以"压缩文件夹"
84+ // 浏览进去,无视用户注册的第三方压缩软件关联。若检测到该扩展名的默认
85+ // 处理程序是第三方(7-Zip/WinRAR/Bandizip 等),直接调 ShellExecuteW
86+ // (与 Telegram/Chromium/Qt 的做法一致,即"双击"的 API 本体),尊重
87+ // 关联。压缩软件均为 Win32 程序,不受"提权进程无法激活 UWP"的限制。
88+ if is_archive ( path) && archive_handler_is_third_party ( path) {
89+ if shell_execute_open ( path) {
90+ return ;
91+ }
92+ crate :: logger:: log_info!(
93+ "[open_file] ShellExecuteW failed; falling back to explorer.exe"
94+ ) ;
95+ }
96+
8097 if let Err ( e) = std:: process:: Command :: new ( "explorer.exe" ) . arg ( path) . spawn ( ) {
8198 crate :: logger:: log_info!( "[open_file] explorer.exe failed: {e}" ) ;
8299 }
@@ -99,6 +116,113 @@ pub fn open_file(path: &str) {
99116 }
100117}
101118
119+ /// 直接调 Win32 `ShellExecuteW`("open" 默认 verb)打开路径——这是双击的
120+ /// API 本体,Telegram Desktop / Chromium / Qt `QDesktopServices` 的同款实现。
121+ /// 相比 `cmd /c start`:不额外起 cmd 进程、无引号转义问题、天然无黑框。
122+ /// 返回值 > 32 表示成功(Win32 约定)。
123+ #[ cfg( target_os = "windows" ) ]
124+ fn shell_execute_open ( path : & str ) -> bool {
125+ use windows_sys:: Win32 :: UI :: Shell :: ShellExecuteW ;
126+ use windows_sys:: Win32 :: UI :: WindowsAndMessaging :: SW_SHOWNORMAL ;
127+
128+ let wide: Vec < u16 > = path. encode_utf16 ( ) . chain ( std:: iter:: once ( 0 ) ) . collect ( ) ;
129+ let verb: Vec < u16 > = "open" . encode_utf16 ( ) . chain ( std:: iter:: once ( 0 ) ) . collect ( ) ;
130+ // SAFETY: wide/verb 均为有效的 NUL 结尾 UTF-16 缓冲,在调用期间存活;
131+ // 其余参数按文档允许为空。
132+ let h = unsafe {
133+ ShellExecuteW (
134+ std:: ptr:: null_mut ( ) ,
135+ verb. as_ptr ( ) ,
136+ wide. as_ptr ( ) ,
137+ std:: ptr:: null ( ) ,
138+ std:: ptr:: null ( ) ,
139+ SW_SHOWNORMAL ,
140+ )
141+ } ;
142+ h as usize > 32
143+ }
144+
145+ /// 常见压缩包扩展名(含复合扩展名 .tar.gz 等——只需匹配末段即可)。
146+ #[ cfg( target_os = "windows" ) ]
147+ fn is_archive ( path : & str ) -> bool {
148+ const ARCHIVE_EXTS : & [ & str ] = & [
149+ "zip" , "rar" , "7z" , "tar" , "gz" , "tgz" , "bz2" , "tbz2" , "xz" , "txz" , "zst" , "lz4" , "cab" ,
150+ "arj" , "lzh" , "wim" ,
151+ ] ;
152+ std:: path:: Path :: new ( path)
153+ . extension ( )
154+ . and_then ( |e| e. to_str ( ) )
155+ . map ( |e| {
156+ let e = e. to_ascii_lowercase ( ) ;
157+ ARCHIVE_EXTS . contains ( & e. as_str ( ) )
158+ } )
159+ . unwrap_or ( false )
160+ }
161+
162+ /// Windows:该文件扩展名的默认打开程序是否为第三方(非 Explorer)。
163+ ///
164+ /// 解析顺序与 Explorer 双击一致:
165+ /// 1. `HKCU\...\Explorer\FileExts\<.ext>\UserChoice` 的 `ProgId`(用户在
166+ /// "打开方式→始终"里选择的结果,优先级最高)
167+ /// 2. 回退 `HKCR\<.ext>` 默认值指向的 ProgId
168+ ///
169+ /// 再读 `HKCR\<ProgId>\shell\open\command` 解析可执行文件名。ProgId 为
170+ /// `CompressedFolder`(Explorer 内建 zip)或命令解析为 explorer.exe /
171+ /// 解析失败时返回 `false`(保持 explorer.exe 现状,行为不回退)。
172+ #[ cfg( target_os = "windows" ) ]
173+ fn archive_handler_is_third_party ( path : & str ) -> bool {
174+ use winreg:: RegKey ;
175+ use winreg:: enums:: { HKEY_CLASSES_ROOT , HKEY_CURRENT_USER } ;
176+
177+ let Some ( ext) = std:: path:: Path :: new ( path)
178+ . extension ( )
179+ . and_then ( |e| e. to_str ( ) )
180+ else {
181+ return false ;
182+ } ;
183+ let ext = format ! ( ".{}" , ext. to_ascii_lowercase( ) ) ;
184+
185+ // 1) UserChoice ProgId(用户显式选择)
186+ let hkcu = RegKey :: predef ( HKEY_CURRENT_USER ) ;
187+ let user_choice = hkcu
188+ . open_subkey ( format ! (
189+ r"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{ext}\UserChoice"
190+ ) )
191+ . and_then ( |k| k. get_value :: < String , _ > ( "ProgId" ) )
192+ . ok ( ) ;
193+
194+ // 2) 回退 HKCR\<.ext> 默认值
195+ let hkcr = RegKey :: predef ( HKEY_CLASSES_ROOT ) ;
196+ let progid = match user_choice. filter ( |p| !p. trim ( ) . is_empty ( ) ) {
197+ Some ( p) => p,
198+ None => match hkcr
199+ . open_subkey ( & ext)
200+ . and_then ( |k| k. get_value :: < String , _ > ( "" ) )
201+ {
202+ Ok ( p) if !p. trim ( ) . is_empty ( ) => p,
203+ _ => return false ,
204+ } ,
205+ } ;
206+
207+ // Explorer 内建压缩文件夹 handler(zip/cab 默认)。
208+ if progid. eq_ignore_ascii_case ( "CompressedFolder" )
209+ || progid. eq_ignore_ascii_case ( "CABFolder" )
210+ {
211+ return false ;
212+ }
213+
214+ let Ok ( cmd) = hkcr
215+ . open_subkey ( format ! ( r"{progid}\shell\open\command" ) )
216+ . and_then ( |k| k. get_value :: < String , _ > ( "" ) )
217+ else {
218+ return false ;
219+ } ;
220+ match exe_basename ( & cmd) {
221+ Some ( name) => !name. eq_ignore_ascii_case ( "explorer.exe" ) ,
222+ None => false ,
223+ }
224+ }
225+
102226// ---------------------------------------------------------------------------
103227// 模板执行:占位符替换 + shell 解析
104228// ---------------------------------------------------------------------------
0 commit comments