fix(desktop): open apps with executables on Windows (#13022)
This commit is contained in:
@@ -166,6 +166,7 @@ export function SessionHeader() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
||||||
|
const [menu, setMenu] = createStore({ open: false })
|
||||||
|
|
||||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||||
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
||||||
@@ -355,7 +356,12 @@ export function SessionHeader() {
|
|||||||
<span class="text-12-regular text-text-strong">Open</span>
|
<span class="text-12-regular text-text-strong">Open</span>
|
||||||
</Button>
|
</Button>
|
||||||
<div class="self-stretch w-px bg-border-base/70" />
|
<div class="self-stretch w-px bg-border-base/70" />
|
||||||
<DropdownMenu gutter={6} placement="bottom-end">
|
<DropdownMenu
|
||||||
|
gutter={6}
|
||||||
|
placement="bottom-end"
|
||||||
|
open={menu.open}
|
||||||
|
onOpenChange={(open) => setMenu("open", open)}
|
||||||
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
icon="chevron-down"
|
icon="chevron-down"
|
||||||
@@ -375,7 +381,13 @@ export function SessionHeader() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{options().map((o) => (
|
{options().map((o) => (
|
||||||
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
|
<DropdownMenu.RadioItem
|
||||||
|
value={o.id}
|
||||||
|
onSelect={() => {
|
||||||
|
setMenu("open", false)
|
||||||
|
openDir(o.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||||
<AppIcon id={o.icon} class={size(o.icon)} />
|
<AppIcon id={o.icon} class={size(o.icon)} />
|
||||||
</div>
|
</div>
|
||||||
@@ -388,7 +400,12 @@ export function SessionHeader() {
|
|||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item onSelect={copyPath}>
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
setMenu("open", false)
|
||||||
|
copyPath()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ use std::{
|
|||||||
env,
|
env,
|
||||||
net::TcpListener,
|
net::TcpListener,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
process::Command,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
process::Command,
|
|
||||||
};
|
};
|
||||||
use tauri::{AppHandle, Manager, RunEvent, State, ipc::Channel};
|
use tauri::{AppHandle, Manager, RunEvent, State, ipc::Channel};
|
||||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||||
@@ -165,11 +165,165 @@ fn check_app_exists(app_name: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn check_windows_app(app_name: &str) -> bool {
|
fn check_windows_app(_app_name: &str) -> bool {
|
||||||
// Check if command exists in PATH, including .exe
|
// Check if command exists in PATH, including .exe
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn resolve_windows_app_path(app_name: &str) -> Option<String> {
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
// Try to find the command using 'where'
|
||||||
|
let output = Command::new("where").arg(app_name).output().ok()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let paths = String::from_utf8_lossy(&output.stdout)
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let has_ext = |path: &Path, ext: &str| {
|
||||||
|
path.extension()
|
||||||
|
.and_then(|v| v.to_str())
|
||||||
|
.map(|v| v.eq_ignore_ascii_case(ext))
|
||||||
|
.unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) {
|
||||||
|
return Some(path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolve_cmd = |path: &Path| -> Option<String> {
|
||||||
|
let content = std::fs::read_to_string(path).ok()?;
|
||||||
|
|
||||||
|
for token in content.split('"') {
|
||||||
|
let lower = token.to_ascii_lowercase();
|
||||||
|
if !lower.contains(".exe") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(index) = lower.find("%~dp0") {
|
||||||
|
let base = path.parent()?;
|
||||||
|
let suffix = &token[index + 5..];
|
||||||
|
let mut resolved = PathBuf::from(base);
|
||||||
|
|
||||||
|
for part in suffix.replace('/', "\\").split('\\') {
|
||||||
|
if part.is_empty() || part == "." {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if part == ".." {
|
||||||
|
let _ = resolved.pop();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resolved.push(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolved.exists() {
|
||||||
|
return Some(resolved.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolved = PathBuf::from(token);
|
||||||
|
if resolved.exists() {
|
||||||
|
return Some(resolved.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
for path in &paths {
|
||||||
|
if has_ext(path, "cmd") || has_ext(path, "bat") {
|
||||||
|
if let Some(resolved) = resolve_cmd(path) {
|
||||||
|
return Some(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.extension().is_none() {
|
||||||
|
let cmd = path.with_extension("cmd");
|
||||||
|
if cmd.exists() {
|
||||||
|
if let Some(resolved) = resolve_cmd(&cmd) {
|
||||||
|
return Some(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bat = path.with_extension("bat");
|
||||||
|
if bat.exists() {
|
||||||
|
if let Some(resolved) = resolve_cmd(&bat) {
|
||||||
|
return Some(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = app_name
|
||||||
|
.chars()
|
||||||
|
.filter(|v| v.is_ascii_alphanumeric())
|
||||||
|
.flat_map(|v| v.to_lowercase())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
if !key.is_empty() {
|
||||||
|
for path in &paths {
|
||||||
|
let dirs = [
|
||||||
|
path.parent(),
|
||||||
|
path.parent().and_then(|dir| dir.parent()),
|
||||||
|
path.parent()
|
||||||
|
.and_then(|dir| dir.parent())
|
||||||
|
.and_then(|dir| dir.parent()),
|
||||||
|
];
|
||||||
|
|
||||||
|
for dir in dirs.into_iter().flatten() {
|
||||||
|
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let candidate = entry.path();
|
||||||
|
if !has_ext(&candidate, "exe") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(stem) = candidate.file_stem().and_then(|v| v.to_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = stem
|
||||||
|
.chars()
|
||||||
|
.filter(|v| v.is_ascii_alphanumeric())
|
||||||
|
.flat_map(|v| v.to_lowercase())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
if name.contains(&key) || key.contains(&name) {
|
||||||
|
return Some(candidate.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paths.first().map(|path| path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
fn resolve_app_path(app_name: &str) -> Option<String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
resolve_windows_app_path(app_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
// On macOS/Linux, just return the app_name as-is since
|
||||||
|
// the opener plugin handles them correctly
|
||||||
|
Some(app_name.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn check_macos_app(app_name: &str) -> bool {
|
fn check_macos_app(app_name: &str) -> bool {
|
||||||
// Check common installation locations
|
// Check common installation locations
|
||||||
@@ -251,7 +405,8 @@ pub fn run() {
|
|||||||
get_display_backend,
|
get_display_backend,
|
||||||
set_display_backend,
|
set_display_backend,
|
||||||
markdown::parse_markdown_command,
|
markdown::parse_markdown_command,
|
||||||
check_app_exists
|
check_app_exists,
|
||||||
|
resolve_app_path
|
||||||
])
|
])
|
||||||
.events(tauri_specta::collect_events![LoadingWindowComplete])
|
.events(tauri_specta::collect_events![LoadingWindowComplete])
|
||||||
.error_handling(tauri_specta::ErrorHandlingMode::Throw);
|
.error_handling(tauri_specta::ErrorHandlingMode::Throw);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const commands = {
|
|||||||
setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE<null>("set_display_backend", { backend }),
|
setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE<null>("set_display_backend", { backend }),
|
||||||
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
||||||
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
|
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
|
||||||
|
resolveAppPath: (appName: string) => __TAURI_INVOKE<string | null>("resolve_app_path", { appName }),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Events */
|
/** Events */
|
||||||
|
|||||||
@@ -98,7 +98,12 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
|
|||||||
void shellOpen(url).catch(() => undefined)
|
void shellOpen(url).catch(() => undefined)
|
||||||
},
|
},
|
||||||
|
|
||||||
openPath(path: string, app?: string) {
|
async openPath(path: string, app?: string) {
|
||||||
|
const os = ostype()
|
||||||
|
if (os === "windows" && app) {
|
||||||
|
const resolvedApp = await commands.resolveAppPath(app)
|
||||||
|
return openerOpenPath(path, resolvedApp || app)
|
||||||
|
}
|
||||||
return openerOpenPath(path, app)
|
return openerOpenPath(path, app)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user