feat(desktop): added Macos support for displaying only installed editors & added sublime text editor (#12501)
parent
3f7ca0494b
commit
8069197329
|
|
@ -67,9 +67,39 @@ export function SessionHeader() {
|
||||||
"xcode",
|
"xcode",
|
||||||
"android-studio",
|
"android-studio",
|
||||||
"powershell",
|
"powershell",
|
||||||
|
"sublime-text",
|
||||||
] as const
|
] as const
|
||||||
type OpenApp = (typeof OPEN_APPS)[number]
|
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">(() => {
|
const os = createMemo<"macos" | "windows" | "linux" | "unknown">(() => {
|
||||||
if (platform.platform === "desktop" && platform.os) return platform.os
|
if (platform.platform === "desktop" && platform.os) return platform.os
|
||||||
if (typeof navigator !== "object") return "unknown"
|
if (typeof navigator !== "object") return "unknown"
|
||||||
|
|
@ -80,38 +110,44 @@ export function SessionHeader() {
|
||||||
return "unknown"
|
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(() => {
|
const options = createMemo(() => {
|
||||||
if (os() === "macos") {
|
if (os() === "macos") {
|
||||||
return [
|
return [{ id: "finder", label: "Finder", icon: "finder" }, ...MAC_APPS.filter((app) => exists[app.id])] as const
|
||||||
{ 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os() === "windows") {
|
if (os() === "windows") {
|
||||||
return [
|
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: "finder", label: "File Explorer", icon: "file-explorer" },
|
||||||
{ id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
|
...WINDOWS_APPS.filter((app) => exists[app.id]),
|
||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
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" },
|
{ id: "finder", label: "File Manager", icon: "finder" },
|
||||||
|
...LINUX_APPS.filter((app) => exists[app.id]),
|
||||||
] as const
|
] as const
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,9 @@ export type Platform = {
|
||||||
|
|
||||||
/** Webview zoom level (desktop only) */
|
/** Webview zoom level (desktop only) */
|
||||||
webviewZoom?: Accessor<number>
|
webviewZoom?: Accessor<number>
|
||||||
|
|
||||||
|
/** Check if an editor app exists (desktop only) */
|
||||||
|
checkAppExists?(appName: string): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
process::Command,
|
||||||
};
|
};
|
||||||
use tauri::{AppHandle, Manager, RunEvent, State, ipc::Channel};
|
use tauri::{AppHandle, Manager, RunEvent, State, ipc::Channel};
|
||||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
#[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())?
|
.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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let builder = tauri_specta::Builder::<tauri::Wry>::new()
|
let builder = tauri_specta::Builder::<tauri::Wry>::new()
|
||||||
|
|
@ -152,7 +209,8 @@ pub fn run() {
|
||||||
await_initialization,
|
await_initialization,
|
||||||
server::get_default_server_url,
|
server::get_default_server_url,
|
||||||
server::set_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])
|
.events(tauri_specta::collect_events![LoadingWindowComplete])
|
||||||
.error_handling(tauri_specta::ErrorHandlingMode::Throw);
|
.error_handling(tauri_specta::ErrorHandlingMode::Throw);
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ export const commands = {
|
||||||
killSidecar: () => __TAURI_INVOKE<void>("kill_sidecar"),
|
killSidecar: () => __TAURI_INVOKE<void>("kill_sidecar"),
|
||||||
installCli: () => __TAURI_INVOKE<string>("install_cli"),
|
installCli: () => __TAURI_INVOKE<string>("install_cli"),
|
||||||
awaitInitialization: (events: Channel) => __TAURI_INVOKE<ServerReadyData>("await_initialization", { events }),
|
awaitInitialization: (events: Channel) => __TAURI_INVOKE<ServerReadyData>("await_initialization", { events }),
|
||||||
|
|
||||||
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
|
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
|
||||||
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
|
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
|
||||||
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
||||||
|
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Events */
|
/** Events */
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,10 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||||
parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
|
parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
|
||||||
|
|
||||||
webviewZoom,
|
webviewZoom,
|
||||||
|
|
||||||
|
checkAppExists: async (appName: string) => {
|
||||||
|
return commands.checkAppExists(appName)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let menuTrigger = null as null | ((id: string) => void)
|
let menuTrigger = null as null | ((id: string) => void)
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -15,6 +15,7 @@ import textmate from "../assets/icons/app/textmate.png"
|
||||||
import vscode from "../assets/icons/app/vscode.svg"
|
import vscode from "../assets/icons/app/vscode.svg"
|
||||||
import xcode from "../assets/icons/app/xcode.png"
|
import xcode from "../assets/icons/app/xcode.png"
|
||||||
import zed from "../assets/icons/app/zed.svg"
|
import zed from "../assets/icons/app/zed.svg"
|
||||||
|
import sublimetext from "../assets/icons/app/sublimetext.svg"
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
vscode,
|
vscode,
|
||||||
|
|
@ -30,6 +31,7 @@ const icons = {
|
||||||
antigravity,
|
antigravity,
|
||||||
textmate,
|
textmate,
|
||||||
powershell,
|
powershell,
|
||||||
|
"sublime-text": sublimetext,
|
||||||
} satisfies Record<IconName, string>
|
} satisfies Record<IconName, string>
|
||||||
|
|
||||||
export type AppIconProps = Omit<ComponentProps<"img">, "src"> & {
|
export type AppIconProps = Omit<ComponentProps<"img">, "src"> & {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export const iconNames = [
|
||||||
"antigravity",
|
"antigravity",
|
||||||
"textmate",
|
"textmate",
|
||||||
"powershell",
|
"powershell",
|
||||||
|
"sublime-text",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type IconName = (typeof iconNames)[number]
|
export type IconName = (typeof iconNames)[number]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue