From 3625785ea79408fce27b1a039e916a378023e9b4 Mon Sep 17 00:00:00 2001 From: anduimagui Date: Wed, 18 Mar 2026 17:31:31 +0000 Subject: [PATCH 1/2] feat(app): add GitHub Desktop to open menu --- .../src/components/session/session-header.tsx | 25 ++++- packages/app/src/i18n/en.ts | 1 + packages/desktop-electron/src/main/apps.ts | 36 ++++-- .../desktop-electron/src/renderer/index.tsx | 4 +- packages/desktop/src-tauri/src/lib.rs | 103 ++++++++++++++---- .../src/assets/icons/app/github-desktop.svg | 43 ++++++++ packages/ui/src/components/app-icon.tsx | 2 + packages/ui/src/components/app-icons/types.ts | 1 + 8 files changed, 172 insertions(+), 43 deletions(-) create mode 100644 packages/ui/src/assets/icons/app/github-desktop.svg diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 495b3234058..cce2cf0af3f 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", @@ -156,11 +175,7 @@ export function SessionHeader() { finder: true, }) - const apps = createMemo(() => { - if (os() === "macos") return MAC_APPS - if (os() === "windows") return WINDOWS_APPS - return LINUX_APPS - }) + const apps = createMemo(() => (os() === "macos" ? MAC_APPS : os() === "windows" ? WINDOWS_APPS : LINUX_APPS)) const fileManager = createMemo(() => { if (os() === "macos") return { label: "session.header.open.finder", icon: "finder" as const } diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 7f6816de9e3..4a40a0a477c 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -580,6 +580,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..2ad247bae1c 100644 --- a/packages/desktop-electron/src/main/apps.ts +++ b/packages/desktop-electron/src/main/apps.ts @@ -3,9 +3,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs" import { dirname, extname, join } from "node:path" export function checkAppExists(appName: string): boolean { - if (process.platform === "win32") return true - if (process.platform === "linux") return true - return checkMacosApp(appName) + return process.platform === "win32" ? true : process.platform === "linux" ? true : checkMacosApp(appName) } export function resolveAppPath(appName: string): string | null { @@ -32,20 +30,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-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index 6719c04bee2..87648804d6e 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -219,9 +219,7 @@ const createPlatform = (): Platform => { webviewZoom, - checkAppExists: async (appName: string) => { - return window.api.checkAppExists(appName) - }, + checkAppExists: async (appName: string) => window.api.checkAppExists(appName), async readClipboardImage() { const image = await window.api.readClipboardImage().catch(() => null) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index a843ac8174e..4642d4c0969 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -131,19 +131,21 @@ async fn await_initialization( #[tauri::command] #[specta::specta] fn check_app_exists(app_name: &str) -> bool { - #[cfg(target_os = "windows")] { - os::windows::check_windows_app(app_name) - } + #[cfg(target_os = "windows")] + { + os::windows::check_windows_app(app_name) + } - #[cfg(target_os = "macos")] - { - check_macos_app(app_name) - } + #[cfg(target_os = "macos")] + { + check_macos_app(app_name) + } - #[cfg(target_os = "linux")] - { - check_linux_app(app_name) + #[cfg(target_os = "linux")] + { + check_linux_app(app_name) + } } } @@ -187,35 +189,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", From f031a0719fcaad81bcba76f176f02bd9d2b4ee71 Mon Sep 17 00:00:00 2001 From: anduimagui Date: Wed, 18 Mar 2026 20:02:39 +0000 Subject: [PATCH 2/2] refactor(app): remove cosmetic open menu diffs --- .../src/components/session/session-header.tsx | 6 ++++- packages/desktop-electron/src/main/apps.ts | 4 +++- .../desktop-electron/src/renderer/index.tsx | 4 +++- packages/desktop/src-tauri/src/lib.rs | 22 +++++++++---------- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index cce2cf0af3f..f67fa56d543 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -175,7 +175,11 @@ export function SessionHeader() { finder: true, }) - const apps = createMemo(() => (os() === "macos" ? MAC_APPS : os() === "windows" ? WINDOWS_APPS : LINUX_APPS)) + const apps = createMemo(() => { + if (os() === "macos") return MAC_APPS + if (os() === "windows") return WINDOWS_APPS + return LINUX_APPS + }) const fileManager = createMemo(() => { if (os() === "macos") return { label: "session.header.open.finder", icon: "finder" as const } diff --git a/packages/desktop-electron/src/main/apps.ts b/packages/desktop-electron/src/main/apps.ts index 2ad247bae1c..b4ae2b5f506 100644 --- a/packages/desktop-electron/src/main/apps.ts +++ b/packages/desktop-electron/src/main/apps.ts @@ -3,7 +3,9 @@ import { existsSync, readFileSync, readdirSync } from "node:fs" import { dirname, extname, join } from "node:path" export function checkAppExists(appName: string): boolean { - return process.platform === "win32" ? true : process.platform === "linux" ? true : checkMacosApp(appName) + if (process.platform === "win32") return true + if (process.platform === "linux") return true + return checkMacosApp(appName) } export function resolveAppPath(appName: string): string | null { diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index 87648804d6e..6719c04bee2 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -219,7 +219,9 @@ const createPlatform = (): Platform => { webviewZoom, - checkAppExists: async (appName: string) => window.api.checkAppExists(appName), + checkAppExists: async (appName: string) => { + return window.api.checkAppExists(appName) + }, async readClipboardImage() { const image = await window.api.readClipboardImage().catch(() => null) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 4642d4c0969..c860cf7cb85 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -131,21 +131,19 @@ async fn await_initialization( #[tauri::command] #[specta::specta] fn check_app_exists(app_name: &str) -> bool { + #[cfg(target_os = "windows")] { - #[cfg(target_os = "windows")] - { - os::windows::check_windows_app(app_name) - } + os::windows::check_windows_app(app_name) + } - #[cfg(target_os = "macos")] - { - check_macos_app(app_name) - } + #[cfg(target_os = "macos")] + { + check_macos_app(app_name) + } - #[cfg(target_os = "linux")] - { - check_linux_app(app_name) - } + #[cfg(target_os = "linux")] + { + check_linux_app(app_name) } }