Просмотр исходного кода

feat(desktop): added Macos support for displaying only installed editors & added sublime text editor (#12501)

OpeOginni 2 месяцев назад
Родитель
Сommit
8069197329

+ 56 - 20
packages/app/src/components/session/session-header.tsx

@@ -67,9 +67,39 @@ export function SessionHeader() {
     "xcode",
     "android-studio",
     "powershell",
+    "sublime-text",
   ] as const
   type OpenApp = (typeof OPEN_APPS)[number]
 
+  const MAC_APPS = [
+    { 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: "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" },
+  ] 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" },
+  ] 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" },
+  ] as const
+
   const os = createMemo<"macos" | "windows" | "linux" | "unknown">(() => {
     if (platform.platform === "desktop" && platform.os) return platform.os
     if (typeof navigator !== "object") return "unknown"
@@ -80,38 +110,44 @@ export function SessionHeader() {
     return "unknown"
   })
 
+  const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
+
+  createEffect(() => {
+    if (platform.platform !== "desktop") return
+    if (!platform.checkAppExists) return
+
+    const list = os()
+    const apps = list === "macos" ? MAC_APPS : list === "windows" ? WINDOWS_APPS : list === "linux" ? LINUX_APPS : []
+    if (apps.length === 0) return
+
+    void Promise.all(
+      apps.map((app) =>
+        Promise.resolve(platform.checkAppExists?.(app.openWith)).then((value) => {
+          const ok = Boolean(value)
+          console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`)
+          return [app.id, ok] as const
+        }),
+      ),
+    ).then((entries) => {
+      setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)
+    })
+  })
+
   const options = createMemo(() => {
     if (os() === "macos") {
-      return [
-        { 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: "finder", label: "Finder", icon: "finder" },
-        { 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" },
-      ] as const
+      return [{ id: "finder", label: "Finder", icon: "finder" }, ...MAC_APPS.filter((app) => exists[app.id])] as const
     }
 
     if (os() === "windows") {
       return [
-        { 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: "finder", label: "File Explorer", icon: "file-explorer" },
-        { id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
+        ...WINDOWS_APPS.filter((app) => exists[app.id]),
       ] as const
     }
 
     return [
-      { 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: "finder", label: "File Manager", icon: "finder" },
+      ...LINUX_APPS.filter((app) => exists[app.id]),
     ] as const
   })
 

+ 3 - 0
packages/app/src/context/platform.tsx

@@ -62,6 +62,9 @@ export type Platform = {
 
   /** Webview zoom level (desktop only) */
   webviewZoom?: Accessor<number>
+
+  /** Check if an editor app exists (desktop only) */
+  checkAppExists?(appName: string): Promise<boolean>
 }
 
 export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({

+ 59 - 1
packages/desktop/src-tauri/src/lib.rs

@@ -20,6 +20,7 @@ use std::{
     path::PathBuf,
     sync::{Arc, Mutex},
     time::Duration,
+    process::Command,
 };
 use tauri::{AppHandle, Manager, RunEvent, State, ipc::Channel};
 #[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
@@ -142,6 +143,62 @@ async fn await_initialization(
         .map_err(|_| "Failed to get server status".to_string())?
 }
 
+#[tauri::command]
+#[specta::specta]
+fn check_app_exists(app_name: &str) -> bool {
+    #[cfg(target_os = "windows")]
+    {
+        check_windows_app(app_name)
+    }
+    
+    #[cfg(target_os = "macos")]
+    {
+        check_macos_app(app_name)
+    }
+    
+    #[cfg(target_os = "linux")]
+    {
+        check_linux_app(app_name)
+    }
+}
+
+#[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 = "macos")]
+fn check_macos_app(app_name: &str) -> bool {
+    // Check common installation locations
+    let mut app_locations = vec![
+        format!("/Applications/{}.app", app_name),
+        format!("/System/Applications/{}.app", app_name),
+    ];
+
+    if let Ok(home) = std::env::var("HOME") {
+        app_locations.push(format!("{}/Applications/{}.app", home, app_name));
+    }
+    
+    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)
+}
+
+#[cfg(target_os = "linux")]
+fn check_linux_app(app_name: &str) -> bool {
+    return true;
+}
+
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
     let builder = tauri_specta::Builder::<tauri::Wry>::new()
@@ -152,7 +209,8 @@ pub fn run() {
             await_initialization,
             server::get_default_server_url,
             server::set_default_server_url,
-            markdown::parse_markdown_command
+            markdown::parse_markdown_command,
+            check_app_exists
         ])
         .events(tauri_specta::collect_events![LoadingWindowComplete])
         .error_handling(tauri_specta::ErrorHandlingMode::Throw);

+ 1 - 1
packages/desktop/src/bindings.ts

@@ -8,10 +8,10 @@ export const commands = {
 	killSidecar: () => __TAURI_INVOKE<void>("kill_sidecar"),
 	installCli: () => __TAURI_INVOKE<string>("install_cli"),
 	awaitInitialization: (events: Channel) => __TAURI_INVOKE<ServerReadyData>("await_initialization", { events }),
-
 	getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
 	setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
 	parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
+	checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
 };
 
 /** Events */

+ 4 - 0
packages/desktop/src/index.tsx

@@ -340,6 +340,10 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
   parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
 
   webviewZoom,
+
+  checkAppExists: async (appName: string) => {
+    return commands.checkAppExists(appName)
+  },
 })
 
 let menuTrigger = null as null | ((id: string) => void)

+ 1 - 0
packages/ui/src/assets/icons/app/sublimetext.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 256 332" width="256" height="332" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><defs><linearGradient x1="55.117%" y1="58.68%" x2="63.68%" y2="39.597%" id="sublimetext__a"><stop stop-color="#FF9700" offset="0%"/><stop stop-color="#F48E00" offset="53%"/><stop stop-color="#D06F00" offset="100%"/></linearGradient></defs><path d="M255.288 166.795c0-3.887-2.872-6.128-6.397-5.015L6.397 238.675C2.865 239.796 0 243.86 0 247.74v78.59c0 3.887 2.865 6.135 6.397 5.015l242.494-76.888c3.525-1.12 6.397-5.185 6.397-9.071v-78.59Z" fill="url(#sublimetext__a)"/><path d="M0 164.291c0 3.887 2.865 7.95 6.397 9.071l242.53 76.902c3.531 1.12 6.397-1.127 6.397-5.007V166.66c0-3.88-2.866-7.944-6.397-9.064L6.397 80.694C2.865 79.574 0 81.814 0 85.7v78.59Z" fill="#FF9800"/><path d="M255.288 5.302c0-3.886-2.872-6.135-6.397-5.014L6.397 77.176C2.865 78.296 0 82.36 0 86.247v78.59c0 3.887 2.865 6.128 6.397 5.014l242.494-76.895c3.525-1.12 6.397-5.184 6.397-9.064V5.302Z" fill="#FF9800"/></svg>

+ 2 - 0
packages/ui/src/components/app-icon.tsx

@@ -15,6 +15,7 @@ import textmate from "../assets/icons/app/textmate.png"
 import vscode from "../assets/icons/app/vscode.svg"
 import xcode from "../assets/icons/app/xcode.png"
 import zed from "../assets/icons/app/zed.svg"
+import sublimetext from "../assets/icons/app/sublimetext.svg"
 
 const icons = {
   vscode,
@@ -30,6 +31,7 @@ const icons = {
   antigravity,
   textmate,
   powershell,
+  "sublime-text": sublimetext,
 } satisfies Record<IconName, string>
 
 export type AppIconProps = Omit<ComponentProps<"img">, "src"> & {

+ 1 - 0
packages/ui/src/components/app-icons/types.ts

@@ -14,6 +14,7 @@ export const iconNames = [
   "antigravity",
   "textmate",
   "powershell",
+  "sublime-text",
 ] as const
 
 export type IconName = (typeof iconNames)[number]