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

This commit is contained in:
OpeOginni
2026-02-06 18:21:47 +01:00
committed by GitHub
parent 3f7ca0494b
commit 8069197329
8 changed files with 127 additions and 22 deletions

View File

@@ -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
})

View File

@@ -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({

View File

@@ -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);

View File

@@ -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 */

View File

@@ -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)

View File

@@ -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>

After

Width:  |  Height:  |  Size: 1008 B

View File

@@ -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"> & {

View File

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