diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 495b3234058..f67fa56d543 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -28,6 +28,7 @@ import { StatusPopover } from "../status-popover" const OPEN_APPS = [ "vscode", "cursor", + "github-desktop", "zed", "textmate", "antigravity", @@ -53,6 +54,12 @@ const MAC_APPS = [ openWith: "Visual Studio Code", }, { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" }, + { + id: "github-desktop", + label: "session.header.open.app.githubDesktop", + icon: "github-desktop", + openWith: "GitHub Desktop", + }, { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" }, { id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" }, { @@ -83,6 +90,12 @@ const MAC_APPS = [ const WINDOWS_APPS = [ { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" }, { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" }, + { + id: "github-desktop", + label: "session.header.open.app.githubDesktop", + icon: "github-desktop", + openWith: "GitHub Desktop", + }, { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" }, { id: "powershell", @@ -101,6 +114,12 @@ const WINDOWS_APPS = [ const LINUX_APPS = [ { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" }, { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" }, + { + id: "github-desktop", + label: "session.header.open.app.githubDesktop", + icon: "github-desktop", + openWith: "github-desktop", + }, { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" }, { id: "sublime-text", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 72caed40ad9..497996ce121 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -581,6 +581,7 @@ export const dict = { "session.header.open.fileManager": "File Manager", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", + "session.header.open.app.githubDesktop": "GitHub Desktop", "session.header.open.app.zed": "Zed", "session.header.open.app.textmate": "TextMate", "session.header.open.app.antigravity": "Antigravity", diff --git a/packages/desktop-electron/src/main/apps.ts b/packages/desktop-electron/src/main/apps.ts index 2b460378948..b4ae2b5f506 100644 --- a/packages/desktop-electron/src/main/apps.ts +++ b/packages/desktop-electron/src/main/apps.ts @@ -32,20 +32,34 @@ export function wslPath(path: string, mode: "windows" | "linux" | null): string } } +function aliases(appName: string) { + const trimmed = appName.trim() + if (!trimmed) return [] + + return [trimmed, trimmed.replaceAll(" ", ""), trimmed.replaceAll(" ", "-"), trimmed.replaceAll(" ", "_")].filter( + (name, index, list) => list.findIndex((item) => item.toLowerCase() === name.toLowerCase()) === index, + ) +} + function checkMacosApp(appName: string) { - const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`] + const names = aliases(appName) + if (!names.length) return false + + const locs = names.flatMap((name) => [`/Applications/${name}.app`, `/System/Applications/${name}.app`]) const home = process.env.HOME - if (home) locations.push(`${home}/Applications/${appName}.app`) + if (home) locs.push(...names.map((name) => `${home}/Applications/${name}.app`)) - if (locations.some((location) => existsSync(location))) return true + if (locs.some((path) => existsSync(path))) return true - try { - execFileSync("which", [appName]) - return true - } catch { - return false - } + return names.some((name) => { + try { + execFileSync("which", [name]) + return true + } catch { + return false + } + }) } function resolveWindowsAppPath(appName: string): string | null { diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index a843ac8174e..c860cf7cb85 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -187,35 +187,90 @@ fn open_path(_app: AppHandle, path: String, app_name: Option) -> Result< .map_err(|e| format!("Failed to open path: {e}")); } - #[cfg(not(target_os = "windows"))] + #[cfg(target_os = "macos")] + { + if let Some(app_name) = app_name { + return Command::new("open") + .args(["-a", &app_name, &path]) + .output() + .map_err(|e| format!("Failed to run open: {e}")) + .and_then(|output| { + if output.status.success() { + return Ok(()); + } + + Err(format!( + "Failed to open path: {}", + String::from_utf8_lossy(&output.stderr).trim() + )) + }); + } + + return tauri_plugin_opener::open_path(path, Option::<&str>::None) + .map_err(|e| format!("Failed to open path: {e}")); + } + + #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))] tauri_plugin_opener::open_path(path, app_name.as_deref()) .map_err(|e| format!("Failed to open path: {e}")) } #[cfg(target_os = "macos")] fn check_macos_app(app_name: &str) -> bool { + fn names(app_name: &str) -> Vec { + let trimmed = app_name.trim(); + if trimmed.is_empty() { + return vec![]; + } + + let mut list = vec![trimmed.to_string()]; + let mut push = |value: String| { + let value = value.trim().to_string(); + if value.is_empty() || list.iter().any(|item| item.eq_ignore_ascii_case(&value)) { + return; + } + list.push(value); + }; + + push(trimmed.replace(' ', "")); + push(trimmed.replace(' ', "-")); + push(trimmed.replace(' ', "_")); + + list + } + + let names = names(app_name); + // Check common installation locations - let mut app_locations = vec![ - format!("/Applications/{}.app", app_name), - format!("/System/Applications/{}.app", app_name), - ]; + let mut app_locations = names + .iter() + .flat_map(|name| { + [ + format!("/Applications/{}.app", name), + format!("/System/Applications/{}.app", name), + ] + }) + .collect::>(); if let Ok(home) = std::env::var("HOME") { - app_locations.push(format!("{}/Applications/{}.app", home, app_name)); + app_locations.extend(names.iter().map(|name| format!("{}/Applications/{}.app", home, name))); } - for location in app_locations { - if std::path::Path::new(&location).exists() { + for location in &app_locations { + if std::path::Path::new(location).exists() { return true; } } // Also check if command exists in PATH - Command::new("which") - .arg(app_name) - .output() - .map(|output| output.status.success()) - .unwrap_or(false) + let out = names.iter().any(|name| { + Command::new("which") + .arg(name) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) + }); + out } #[derive(serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/packages/ui/src/assets/icons/app/github-desktop.svg b/packages/ui/src/assets/icons/app/github-desktop.svg new file mode 100644 index 00000000000..244558d5c3d --- /dev/null +++ b/packages/ui/src/assets/icons/app/github-desktop.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/components/app-icon.tsx b/packages/ui/src/components/app-icon.tsx index f8b587ff260..7cca85f3441 100644 --- a/packages/ui/src/components/app-icon.tsx +++ b/packages/ui/src/components/app-icon.tsx @@ -8,6 +8,7 @@ import cursor from "../assets/icons/app/cursor.svg" import fileExplorer from "../assets/icons/app/file-explorer.svg" import finder from "../assets/icons/app/finder.png" import ghostty from "../assets/icons/app/ghostty.svg" +import githubDesktop from "../assets/icons/app/github-desktop.svg" import iterm2 from "../assets/icons/app/iterm2.svg" import powershell from "../assets/icons/app/powershell.svg" import terminal from "../assets/icons/app/terminal.png" @@ -22,6 +23,7 @@ import sublimetext from "../assets/icons/app/sublimetext.svg" const icons = { vscode, cursor, + "github-desktop": githubDesktop, zed, "file-explorer": fileExplorer, finder, diff --git a/packages/ui/src/components/app-icons/types.ts b/packages/ui/src/components/app-icons/types.ts index 4fb3abf39c3..b988ec1ebcb 100644 --- a/packages/ui/src/components/app-icons/types.ts +++ b/packages/ui/src/components/app-icons/types.ts @@ -3,6 +3,7 @@ export const iconNames = [ "vscode", "cursor", + "github-desktop", "zed", "file-explorer", "finder",