summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOpeOginni <[email protected]>2026-02-06 18:21:47 +0100
committerGitHub <[email protected]>2026-02-06 17:21:47 +0000
commit8069197329d2d1b958d8e7f63daaf9662a97027d (patch)
tree9f38c854ebca35fbfce7fafe21b73e619f08584f
parent3f7ca0494b065a52933d12f1965758405ca86c2e (diff)
downloadopencode-8069197329d2d1b958d8e7f63daaf9662a97027d.tar.gz
opencode-8069197329d2d1b958d8e7f63daaf9662a97027d.zip
feat(desktop): added Macos support for displaying only installed editors & added sublime text editor (#12501)
-rw-r--r--packages/app/src/components/session/session-header.tsx76
-rw-r--r--packages/app/src/context/platform.tsx3
-rw-r--r--packages/desktop/src-tauri/src/lib.rs60
-rw-r--r--packages/desktop/src/bindings.ts2
-rw-r--r--packages/desktop/src/index.tsx4
-rw-r--r--packages/ui/src/assets/icons/app/sublimetext.svg1
-rw-r--r--packages/ui/src/components/app-icon.tsx2
-rw-r--r--packages/ui/src/components/app-icons/types.ts1
8 files changed, 127 insertions, 22 deletions
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index ec2a231e4..805e69931 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/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
})
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx
index f5d20ff8e..127b9260b 100644
--- a/packages/app/src/context/platform.tsx
+++ b/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({
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index 135f7b072..14105e5dd 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/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);
diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts
index 46dcb7f44..562a98aca 100644
--- a/packages/desktop/src/bindings.ts
+++ b/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 */
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index 116689673..d3377e95a 100644
--- a/packages/desktop/src/index.tsx
+++ b/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)
diff --git a/packages/ui/src/assets/icons/app/sublimetext.svg b/packages/ui/src/assets/icons/app/sublimetext.svg
new file mode 100644
index 000000000..f482ec5bb
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/packages/ui/src/components/app-icon.tsx b/packages/ui/src/components/app-icon.tsx
index c698f32c6..e3f2a0fb2 100644
--- a/packages/ui/src/components/app-icon.tsx
+++ b/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"> & {
diff --git a/packages/ui/src/components/app-icons/types.ts b/packages/ui/src/components/app-icons/types.ts
index 0ad9f83d1..a0343c25b 100644
--- a/packages/ui/src/components/app-icons/types.ts
+++ b/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]