summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorFilip <[email protected]>2026-02-25 07:39:58 +0100
committerGitHub <[email protected]>2026-02-25 14:39:58 +0800
commitfc6e7934bd365ad1665dea68556dbfc80ac3b611 (patch)
treea79e707c7e2744d4adbaea16811defadc50e65a1 /packages
parentd7500b25b8eb84051e13bd930e234445352cc9a4 (diff)
downloadopencode-fc6e7934bd365ad1665dea68556dbfc80ac3b611.tar.gz
opencode-fc6e7934bd365ad1665dea68556dbfc80ac3b611.zip
feat(desktop): enhance Windows app resolution and UI loading states (#13320)
Co-authored-by: Brendan Allan <[email protected]> Co-authored-by: Brendan Allan <[email protected]>
Diffstat (limited to 'packages')
-rw-r--r--packages/app/src/components/session/session-header.tsx127
-rw-r--r--packages/desktop/src-tauri/Cargo.lock5
-rw-r--r--packages/desktop/src-tauri/Cargo.toml3
-rw-r--r--packages/desktop/src-tauri/src/cli.rs4
-rw-r--r--packages/desktop/src-tauri/src/lib.rs158
-rw-r--r--packages/desktop/src-tauri/src/os/mod.rs2
-rw-r--r--packages/desktop/src-tauri/src/os/windows.rs439
7 files changed, 556 insertions, 182 deletions
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index 825d1dab6..d531fa50a 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -1,28 +1,28 @@
+import { AppIcon } from "@opencode-ai/ui/app-icon"
+import { Button } from "@opencode-ai/ui/button"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { Icon } from "@opencode-ai/ui/icon"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { Keybind } from "@opencode-ai/ui/keybind"
+import { Popover } from "@opencode-ai/ui/popover"
+import { Spinner } from "@opencode-ai/ui/spinner"
+import { TextField } from "@opencode-ai/ui/text-field"
+import { showToast } from "@opencode-ai/ui/toast"
+import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
+import { getFilename } from "@opencode-ai/util/path"
+import { useParams } from "@solidjs/router"
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
-import { useParams } from "@solidjs/router"
-import { useLayout } from "@/context/layout"
import { useCommand } from "@/context/command"
+import { useGlobalSDK } from "@/context/global-sdk"
import { useLanguage } from "@/context/language"
+import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server"
import { useSync } from "@/context/sync"
-import { useGlobalSDK } from "@/context/global-sdk"
-import { getFilename } from "@opencode-ai/util/path"
import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist"
-
-import { Icon } from "@opencode-ai/ui/icon"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import { Button } from "@opencode-ai/ui/button"
-import { AppIcon } from "@opencode-ai/ui/app-icon"
-import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
-import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
-import { Popover } from "@opencode-ai/ui/popover"
-import { TextField } from "@opencode-ai/ui/text-field"
-import { Keybind } from "@opencode-ai/ui/keybind"
-import { showToast } from "@opencode-ai/ui/toast"
import { StatusPopover } from "../status-popover"
const OPEN_APPS = [
@@ -45,32 +45,67 @@ type OpenApp = (typeof OPEN_APPS)[number]
type OS = "macos" | "windows" | "linux" | "unknown"
const MAC_APPS = [
- { id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code" },
+ {
+ id: "vscode",
+ label: "VS Code",
+ icon: "vscode",
+ openWith: "Visual Studio Code",
+ },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
- { id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" },
+ {
+ id: "antigravity",
+ label: "Antigravity",
+ icon: "antigravity",
+ openWith: "Antigravity",
+ },
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
- { id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" },
- { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
+ {
+ id: "android-studio",
+ label: "Android Studio",
+ icon: "android-studio",
+ openWith: "Android Studio",
+ },
+ {
+ id: "sublime-text",
+ label: "Sublime Text",
+ icon: "sublime-text",
+ openWith: "Sublime Text",
+ },
] as const
const WINDOWS_APPS = [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
- { id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
- { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
+ {
+ id: "powershell",
+ label: "PowerShell",
+ icon: "powershell",
+ openWith: "powershell",
+ },
+ {
+ id: "sublime-text",
+ label: "Sublime Text",
+ icon: "sublime-text",
+ openWith: "Sublime Text",
+ },
] as const
const LINUX_APPS = [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
- { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
+ {
+ id: "sublime-text",
+ label: "Sublime Text",
+ icon: "sublime-text",
+ openWith: "Sublime Text",
+ },
] as const
type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number]
@@ -213,7 +248,9 @@ export function SessionHeader() {
const view = createMemo(() => layout.view(sessionKey))
const os = createMemo(() => detectOS(platform))
- const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
+ const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
+ finder: true,
+ })
const apps = createMemo(() => {
if (os() === "macos") return MAC_APPS
@@ -259,18 +296,34 @@ export function SessionHeader() {
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
const [menu, setMenu] = createStore({ open: false })
+ const [openRequest, setOpenRequest] = createStore({
+ app: undefined as OpenApp | undefined,
+ })
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
+ const opening = createMemo(() => openRequest.app !== undefined)
+
+ createEffect(() => {
+ const value = prefs.app
+ if (options().some((o) => o.id === value)) return
+ setPrefs("app", options()[0]?.id ?? "finder")
+ })
const openDir = (app: OpenApp) => {
+ if (opening() || !canOpen() || !platform.openPath) return
const directory = projectDirectory()
if (!directory) return
- if (!canOpen()) return
const item = options().find((o) => o.id === app)
const openWith = item && "openWith" in item ? item.openWith : undefined
- Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => showRequestError(language, err))
+ setOpenRequest("app", app)
+ platform
+ .openPath(directory, openWith)
+ .catch((err: unknown) => showRequestError(language, err))
+ .finally(() => {
+ setOpenRequest("app", undefined)
+ })
}
const copyPath = () => {
@@ -315,7 +368,9 @@ export function SessionHeader() {
<div class="flex min-w-0 flex-1 items-center gap-1.5 overflow-visible">
<Icon name="magnifying-glass" size="small" class="icon-base shrink-0 size-4" />
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
- {language.t("session.header.search.placeholder", { project: name() })}
+ {language.t("session.header.search.placeholder", {
+ project: name(),
+ })}
</span>
</div>
@@ -357,12 +412,21 @@ export function SessionHeader() {
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
<Button
variant="ghost"
- class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
+ class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none disabled:!cursor-default"
+ classList={{
+ "bg-surface-raised-base-active": opening(),
+ }}
onClick={() => openDir(current().id)}
+ disabled={opening()}
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
>
<div class="flex size-5 shrink-0 items-center justify-center">
- <AppIcon id={current().icon} class="size-4" />
+ <Show
+ when={opening()}
+ fallback={<AppIcon id={current().icon} class={openIconSize(current().icon)} />}
+ >
+ <Spinner class="size-3.5 text-icon-base" />
+ </Show>
</div>
<span class="text-12-regular text-text-strong">Open</span>
</Button>
@@ -377,7 +441,11 @@ export function SessionHeader() {
as={IconButton}
icon="chevron-down"
variant="ghost"
- class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-hover"
+ disabled={opening()}
+ class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
+ classList={{
+ "bg-surface-raised-base-active": opening(),
+ }}
aria-label={language.t("session.header.open.menu")}
/>
<DropdownMenu.Portal>
@@ -395,6 +463,7 @@ export function SessionHeader() {
{(o) => (
<DropdownMenu.RadioItem
value={o.id}
+ disabled={opening()}
onSelect={() => {
setMenu("open", false)
openDir(o.id)
diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock
index f9516350e..55f0d5f36 100644
--- a/packages/desktop/src-tauri/Cargo.lock
+++ b/packages/desktop/src-tauri/Cargo.lock
@@ -1988,7 +1988,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.62.2",
+ "windows-core 0.61.2",
]
[[package]]
@@ -3136,7 +3136,8 @@ dependencies = [
"tracing-subscriber",
"uuid",
"webkit2gtk",
- "windows 0.62.2",
+ "windows-core 0.62.2",
+ "windows-sys 0.61.2",
]
[[package]]
diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml
index e98b8965c..b228c7b61 100644
--- a/packages/desktop/src-tauri/Cargo.toml
+++ b/packages/desktop/src-tauri/Cargo.toml
@@ -55,7 +55,8 @@ tokio-stream = { version = "0.1.18", features = ["sync"] }
process-wrap = { version = "9.0.3", features = ["tokio1"] }
[target.'cfg(windows)'.dependencies]
-windows = { version = "0.62", features = ["Win32_System_Threading"] }
+windows-sys = { version = "0.61", features = ["Win32_System_Threading", "Win32_System_Registry"] }
+windows-core = "0.62"
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2"
diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs
index acab0fa70..0c5dfebaf 100644
--- a/packages/desktop/src-tauri/src/cli.rs
+++ b/packages/desktop/src-tauri/src/cli.rs
@@ -19,7 +19,7 @@ use tokio::{
use tokio_stream::wrappers::ReceiverStream;
use tracing::Instrument;
#[cfg(windows)]
-use windows::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
+use windows_sys::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
use crate::server::get_wsl_config;
@@ -32,7 +32,7 @@ struct WinCreationFlags;
#[cfg(windows)]
impl CommandWrapper for WinCreationFlags {
fn pre_spawn(&mut self, command: &mut Command, _core: &CommandWrap) -> std::io::Result<()> {
- command.creation_flags((CREATE_NO_WINDOW | CREATE_SUSPENDED).0);
+ command.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED);
Ok(())
}
}
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index 7ea3aaa8a..71fe8407f 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -6,6 +6,7 @@ pub mod linux_display;
pub mod linux_windowing;
mod logging;
mod markdown;
+mod os;
mod server;
mod window_customizer;
mod windows;
@@ -42,7 +43,7 @@ struct ServerReadyData {
url: String,
username: Option<String>,
password: Option<String>,
- is_sidecar: bool
+ is_sidecar: bool,
}
#[derive(Clone, Copy, serde::Serialize, specta::Type, Debug)]
@@ -148,7 +149,7 @@ async fn await_initialization(
fn check_app_exists(app_name: &str) -> bool {
#[cfg(target_os = "windows")]
{
- check_windows_app(app_name)
+ os::windows::check_windows_app(app_name)
}
#[cfg(target_os = "macos")]
@@ -162,156 +163,12 @@ fn check_app_exists(app_name: &str) -> bool {
}
}
-#[cfg(target_os = "windows")]
-fn check_windows_app(_app_name: &str) -> bool {
- // Check if command exists in PATH, including .exe
- 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)
+ os::windows::resolve_windows_app_path(app_name)
}
#[cfg(not(target_os = "windows"))]
@@ -634,7 +491,12 @@ async fn initialize(app: AppHandle) {
app.state::<ServerState>().set_child(Some(child));
- Ok(ServerReadyData { url, username,password, is_sidecar: true })
+ Ok(ServerReadyData {
+ url,
+ username,
+ password,
+ is_sidecar: true,
+ })
}
.map(move |res| {
let _ = server_ready_tx.send(res);
diff --git a/packages/desktop/src-tauri/src/os/mod.rs b/packages/desktop/src-tauri/src/os/mod.rs
new file mode 100644
index 000000000..8c36e53f7
--- /dev/null
+++ b/packages/desktop/src-tauri/src/os/mod.rs
@@ -0,0 +1,2 @@
+#[cfg(windows)]
+pub mod windows;
diff --git a/packages/desktop/src-tauri/src/os/windows.rs b/packages/desktop/src-tauri/src/os/windows.rs
new file mode 100644
index 000000000..cab265b62
--- /dev/null
+++ b/packages/desktop/src-tauri/src/os/windows.rs
@@ -0,0 +1,439 @@
+use std::{
+ ffi::c_void,
+ os::windows::process::CommandExt,
+ path::{Path, PathBuf},
+ process::Command,
+};
+use windows_sys::Win32::{
+ Foundation::ERROR_SUCCESS,
+ System::Registry::{
+ HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, REG_EXPAND_SZ, REG_SZ, RRF_RT_REG_EXPAND_SZ,
+ RRF_RT_REG_SZ, RegGetValueW,
+ },
+};
+
+pub fn check_windows_app(app_name: &str) -> bool {
+ resolve_windows_app_path(app_name).is_some()
+}
+
+pub fn resolve_windows_app_path(app_name: &str) -> Option<String> {
+ fn expand_env(value: &str) -> String {
+ let mut out = String::with_capacity(value.len());
+ let mut index = 0;
+
+ while let Some(start) = value[index..].find('%') {
+ let start = index + start;
+ out.push_str(&value[index..start]);
+
+ let Some(end_rel) = value[start + 1..].find('%') else {
+ out.push_str(&value[start..]);
+ return out;
+ };
+
+ let end = start + 1 + end_rel;
+ let key = &value[start + 1..end];
+ if key.is_empty() {
+ out.push('%');
+ index = end + 1;
+ continue;
+ }
+
+ if let Ok(v) = std::env::var(key) {
+ out.push_str(&v);
+ index = end + 1;
+ continue;
+ }
+
+ out.push_str(&value[start..=end]);
+ index = end + 1;
+ }
+
+ out.push_str(&value[index..]);
+ out
+ }
+
+ fn extract_exe(value: &str) -> Option<String> {
+ let value = value.trim();
+ if value.is_empty() {
+ return None;
+ }
+
+ if let Some(rest) = value.strip_prefix('"') {
+ if let Some(end) = rest.find('"') {
+ let inner = rest[..end].trim();
+ if inner.to_ascii_lowercase().contains(".exe") {
+ return Some(inner.to_string());
+ }
+ }
+ }
+
+ let lower = value.to_ascii_lowercase();
+ let end = lower.find(".exe")?;
+ Some(value[..end + 4].trim().trim_matches('"').to_string())
+ }
+
+ fn candidates(app_name: &str) -> Vec<String> {
+ let app_name = app_name.trim().trim_matches('"');
+ if app_name.is_empty() {
+ return vec![];
+ }
+
+ let mut out = Vec::<String>::new();
+ let mut push = |value: String| {
+ let value = value.trim().trim_matches('"').to_string();
+ if value.is_empty() {
+ return;
+ }
+ if out.iter().any(|v| v.eq_ignore_ascii_case(&value)) {
+ return;
+ }
+ out.push(value);
+ };
+
+ push(app_name.to_string());
+
+ let lower = app_name.to_ascii_lowercase();
+ if !lower.ends_with(".exe") {
+ push(format!("{app_name}.exe"));
+ }
+
+ let snake = {
+ let mut s = String::new();
+ let mut underscore = false;
+ for c in lower.chars() {
+ if c.is_ascii_alphanumeric() {
+ s.push(c);
+ underscore = false;
+ continue;
+ }
+ if underscore {
+ continue;
+ }
+ s.push('_');
+ underscore = true;
+ }
+ s.trim_matches('_').to_string()
+ };
+
+ if !snake.is_empty() {
+ push(snake.clone());
+ if !snake.ends_with(".exe") {
+ push(format!("{snake}.exe"));
+ }
+ }
+
+ let alnum = lower
+ .chars()
+ .filter(|c| c.is_ascii_alphanumeric())
+ .collect::<String>();
+
+ if !alnum.is_empty() {
+ push(alnum.clone());
+ push(format!("{alnum}.exe"));
+ }
+
+ match lower.as_str() {
+ "sublime text" | "sublime-text" | "sublime_text" | "sublime text.exe" => {
+ push("subl".to_string());
+ push("subl.exe".to_string());
+ push("sublime_text".to_string());
+ push("sublime_text.exe".to_string());
+ }
+ _ => {}
+ }
+
+ out
+ }
+
+ fn reg_app_path(exe: &str) -> Option<String> {
+ let exe = exe.trim().trim_matches('"');
+ if exe.is_empty() {
+ return None;
+ }
+
+ let query = |root: *mut c_void, subkey: &str| -> Option<String> {
+ let flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ;
+ let mut kind: u32 = 0;
+ let mut size = 0u32;
+
+ let mut key = subkey.encode_utf16().collect::<Vec<_>>();
+ key.push(0);
+
+ let status = unsafe {
+ RegGetValueW(
+ root,
+ key.as_ptr(),
+ std::ptr::null(),
+ flags,
+ &mut kind,
+ std::ptr::null_mut(),
+ &mut size,
+ )
+ };
+
+ if status != ERROR_SUCCESS || size == 0 {
+ return None;
+ }
+
+ if kind != REG_SZ && kind != REG_EXPAND_SZ {
+ return None;
+ }
+
+ let mut data = vec![0u8; size as usize];
+ let status = unsafe {
+ RegGetValueW(
+ root,
+ key.as_ptr(),
+ std::ptr::null(),
+ flags,
+ &mut kind,
+ data.as_mut_ptr() as *mut c_void,
+ &mut size,
+ )
+ };
+
+ if status != ERROR_SUCCESS || size < 2 {
+ return None;
+ }
+
+ let words = unsafe {
+ std::slice::from_raw_parts(data.as_ptr().cast::<u16>(), (size as usize) / 2)
+ };
+ let len = words.iter().position(|v| *v == 0).unwrap_or(words.len());
+ let value = String::from_utf16_lossy(&words[..len]).trim().to_string();
+
+ if value.is_empty() {
+ return None;
+ }
+
+ Some(value)
+ };
+
+ let keys = [
+ (
+ HKEY_CURRENT_USER,
+ format!(r"Software\Microsoft\Windows\CurrentVersion\App Paths\{exe}"),
+ ),
+ (
+ HKEY_LOCAL_MACHINE,
+ format!(r"Software\Microsoft\Windows\CurrentVersion\App Paths\{exe}"),
+ ),
+ (
+ HKEY_LOCAL_MACHINE,
+ format!(r"Software\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\{exe}"),
+ ),
+ ];
+
+ for (root, key) in keys {
+ let Some(value) = query(root, &key) else {
+ continue;
+ };
+
+ let Some(exe) = extract_exe(&value) else {
+ continue;
+ };
+
+ let exe = expand_env(&exe);
+ let path = Path::new(exe.trim().trim_matches('"'));
+ if path.exists() {
+ return Some(path.to_string_lossy().to_string());
+ }
+ }
+
+ None
+ }
+
+ let app_name = app_name.trim().trim_matches('"');
+ if app_name.is_empty() {
+ return None;
+ }
+
+ let direct = Path::new(app_name);
+ if direct.is_absolute() && direct.exists() {
+ return Some(direct.to_string_lossy().to_string());
+ }
+
+ let key = app_name
+ .chars()
+ .filter(|v| v.is_ascii_alphanumeric())
+ .flat_map(|v| v.to_lowercase())
+ .collect::<String>();
+
+ 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)
+ };
+
+ let resolve_cmd = |path: &Path| -> Option<String> {
+ let bytes = std::fs::read(path).ok()?;
+ let content = String::from_utf8_lossy(&bytes);
+
+ for token in content.split('"') {
+ let Some(exe) = extract_exe(token) else {
+ continue;
+ };
+
+ let lower = exe.to_ascii_lowercase();
+ if let Some(index) = lower.find("%~dp0") {
+ let base = path.parent()?;
+ let suffix = &exe[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());
+ }
+
+ continue;
+ }
+
+ let resolved = PathBuf::from(expand_env(&exe));
+ if resolved.exists() {
+ return Some(resolved.to_string_lossy().to_string());
+ }
+ }
+
+ None
+ };
+
+ let resolve_where = |query: &str| -> Option<String> {
+ let output = Command::new("where")
+ .creation_flags(0x08000000)
+ .arg(query)
+ .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<_>>();
+
+ if paths.is_empty() {
+ return None;
+ }
+
+ if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) {
+ return Some(path.to_string_lossy().to_string());
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ 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())
+ };
+
+ let list = candidates(app_name);
+ for query in &list {
+ if let Some(path) = resolve_where(query) {
+ return Some(path);
+ }
+ }
+
+ let mut exes = Vec::<String>::new();
+ for query in &list {
+ let query = query.trim().trim_matches('"');
+ if query.is_empty() {
+ continue;
+ }
+
+ let name = Path::new(query)
+ .file_name()
+ .and_then(|v| v.to_str())
+ .unwrap_or(query);
+
+ let exe = if name.to_ascii_lowercase().ends_with(".exe") {
+ name.to_string()
+ } else {
+ format!("{name}.exe")
+ };
+
+ if exes.iter().any(|v| v.eq_ignore_ascii_case(&exe)) {
+ continue;
+ }
+
+ exes.push(exe);
+ }
+
+ for exe in exes {
+ if let Some(path) = reg_app_path(&exe) {
+ return Some(path);
+ }
+ }
+
+ None
+}