Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com>
229 lines
7.3 KiB
TypeScript
229 lines
7.3 KiB
TypeScript
import { createEffect, createMemo } from "solid-js"
|
|
import { createStore } from "solid-js/store"
|
|
import { useLanguage } from "@/context/language"
|
|
import { usePlatform } from "@/context/platform"
|
|
import { useServer } from "@/context/server"
|
|
import { Persist, persisted } from "@/utils/persist"
|
|
import { showToast } from "@/utils/toast"
|
|
|
|
export const OPEN_APPS = [
|
|
"vscode",
|
|
"cursor",
|
|
"zed",
|
|
"textmate",
|
|
"antigravity",
|
|
"finder",
|
|
"terminal",
|
|
"iterm2",
|
|
"ghostty",
|
|
"warp",
|
|
"xcode",
|
|
"android-studio",
|
|
"powershell",
|
|
"sublime-text",
|
|
] as const
|
|
|
|
export type OpenApp = (typeof OPEN_APPS)[number]
|
|
export type OpenAppOS = "macos" | "windows" | "linux" | "unknown"
|
|
|
|
export const MAC_OPEN_APPS = [
|
|
{
|
|
id: "vscode",
|
|
label: "session.header.open.app.vscode",
|
|
icon: "vscode",
|
|
openWith: "Visual Studio Code",
|
|
},
|
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" },
|
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" },
|
|
{ id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" },
|
|
{
|
|
id: "antigravity",
|
|
label: "session.header.open.app.antigravity",
|
|
icon: "antigravity",
|
|
openWith: "Antigravity",
|
|
},
|
|
{ id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" },
|
|
{ id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" },
|
|
{ id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" },
|
|
{ id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" },
|
|
{ id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" },
|
|
{
|
|
id: "android-studio",
|
|
label: "session.header.open.app.androidStudio",
|
|
icon: "android-studio",
|
|
openWith: "Android Studio",
|
|
},
|
|
{
|
|
id: "sublime-text",
|
|
label: "session.header.open.app.sublimeText",
|
|
icon: "sublime-text",
|
|
openWith: "Sublime Text",
|
|
},
|
|
] as const
|
|
|
|
export const WINDOWS_OPEN_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: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
|
|
{
|
|
id: "powershell",
|
|
label: "session.header.open.app.powershell",
|
|
icon: "powershell",
|
|
openWith: "powershell",
|
|
},
|
|
{
|
|
id: "sublime-text",
|
|
label: "session.header.open.app.sublimeText",
|
|
icon: "sublime-text",
|
|
openWith: "Sublime Text",
|
|
},
|
|
] as const
|
|
|
|
export const LINUX_OPEN_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: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
|
|
{
|
|
id: "sublime-text",
|
|
label: "session.header.open.app.sublimeText",
|
|
icon: "sublime-text",
|
|
openWith: "Sublime Text",
|
|
},
|
|
] as const
|
|
|
|
export function detectOpenAppOS(platform: ReturnType<typeof usePlatform>): OpenAppOS {
|
|
if (platform.platform === "desktop" && platform.os) return platform.os
|
|
if (typeof navigator !== "object") return "unknown"
|
|
const value = navigator.platform || navigator.userAgent
|
|
if (/Mac/i.test(value)) return "macos"
|
|
if (/Win/i.test(value)) return "windows"
|
|
if (/Linux/i.test(value)) return "linux"
|
|
return "unknown"
|
|
}
|
|
|
|
export function openAppFileManager(os: OpenAppOS) {
|
|
if (os === "macos") return { label: "session.header.open.finder", icon: "finder" as const }
|
|
if (os === "windows") return { label: "session.header.open.fileExplorer", icon: "file-explorer" as const }
|
|
return { label: "session.header.open.fileManager", icon: "finder" as const }
|
|
}
|
|
|
|
export function openAppsForOS(os: OpenAppOS) {
|
|
if (os === "macos") return MAC_OPEN_APPS
|
|
if (os === "windows") return WINDOWS_OPEN_APPS
|
|
return LINUX_OPEN_APPS
|
|
}
|
|
|
|
const showRequestError = (language: ReturnType<typeof useLanguage>, err: unknown) => {
|
|
showToast({
|
|
variant: "error",
|
|
title: language.t("common.requestFailed"),
|
|
description: err instanceof Error ? err.message : String(err),
|
|
})
|
|
}
|
|
|
|
export function useOpenInApp(input: { directory: () => string }) {
|
|
const platform = usePlatform()
|
|
const server = useServer()
|
|
const language = useLanguage()
|
|
|
|
const os = createMemo(() => detectOpenAppOS(platform))
|
|
const apps = createMemo(() => openAppsForOS(os()))
|
|
const fileManager = createMemo(() => openAppFileManager(os()))
|
|
|
|
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
|
finder: true,
|
|
})
|
|
|
|
createEffect(() => {
|
|
if (platform.platform !== "desktop") return
|
|
if (!platform.checkAppExists) return
|
|
|
|
const list = apps()
|
|
|
|
setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial<Record<OpenApp, boolean>>)
|
|
|
|
void Promise.all(
|
|
list.map((app) =>
|
|
Promise.resolve(platform.checkAppExists?.(app.openWith))
|
|
.then((value) => Boolean(value))
|
|
.catch(() => false)
|
|
.then((ok) => [app.id, ok] as const),
|
|
),
|
|
).then((entries) => {
|
|
setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)
|
|
})
|
|
})
|
|
|
|
const options = createMemo(() => {
|
|
return [
|
|
{ id: "finder", label: language.t(fileManager().label), icon: fileManager().icon },
|
|
...apps()
|
|
.filter((app) => exists[app.id])
|
|
.map((app) => ({ ...app, label: language.t(app.label) })),
|
|
] as const
|
|
})
|
|
|
|
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp | "finder" }))
|
|
const [menu, setMenu] = createStore({ open: false })
|
|
const [openRequest, setOpenRequest] = createStore({
|
|
app: undefined as OpenApp | undefined,
|
|
})
|
|
|
|
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
|
const current = createMemo(
|
|
() =>
|
|
options().find((o) => o.id === prefs.app) ??
|
|
options()[0] ??
|
|
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
|
|
)
|
|
const opening = createMemo(() => openRequest.app !== undefined)
|
|
|
|
const selectApp = (app: OpenApp | "finder") => {
|
|
if (!options().some((item) => item.id === app)) return
|
|
setPrefs("app", app)
|
|
}
|
|
|
|
const openDir = (app: OpenApp | "finder") => {
|
|
if (opening() || !canOpen() || !platform.openPath) return
|
|
const directory = input.directory()
|
|
if (!directory) return
|
|
|
|
const item = options().find((o) => o.id === app)
|
|
const openWith = item && "openWith" in item ? item.openWith : undefined
|
|
setOpenRequest("app", app)
|
|
platform
|
|
.openPath(directory, openWith)
|
|
.catch((err: unknown) => showRequestError(language, err))
|
|
.finally(() => {
|
|
setOpenRequest("app", undefined)
|
|
})
|
|
}
|
|
|
|
const copyPath = () => {
|
|
const directory = input.directory()
|
|
if (!directory) return
|
|
navigator.clipboard
|
|
.writeText(directory)
|
|
.then(() => {
|
|
showToast({
|
|
variant: "success",
|
|
icon: "circle-check",
|
|
title: language.t("session.share.copy.copied"),
|
|
description: directory,
|
|
})
|
|
})
|
|
.catch((err: unknown) => showRequestError(language, err))
|
|
}
|
|
|
|
return {
|
|
canOpen,
|
|
opening,
|
|
current,
|
|
options,
|
|
menu,
|
|
setMenu,
|
|
openDir,
|
|
selectApp,
|
|
copyPath,
|
|
}
|
|
}
|