mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-08-04 10:38:38 +00:00
feat: early raycast support
This commit is contained in:
parent
3542eec277
commit
2a4400dff3
19 changed files with 2403 additions and 682 deletions
|
@ -19,6 +19,7 @@
|
|||
"@inlang/paraglide-sveltekit": "0.16.0",
|
||||
"@kksh/drizzle": "workspace:*",
|
||||
"@kksh/extension": "workspace:*",
|
||||
"@kksh/raycast": "workspace:*",
|
||||
"@kksh/svelte5": "^0.1.15",
|
||||
"@kksh/ui": "workspace:*",
|
||||
"@kksh/utils": "workspace:*",
|
||||
|
@ -30,6 +31,7 @@
|
|||
"@tauri-apps/plugin-shell": "^2.2.0",
|
||||
"@tauri-apps/plugin-sql": "^2.2.0",
|
||||
"@tauri-apps/plugin-stronghold": "^2.2.0",
|
||||
"@tauri-apps/plugin-websocket": "^2.3.0",
|
||||
"@tauri-store/svelte": "^2.1.1",
|
||||
"dompurify": "^3.2.4",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
|
|
|
@ -59,6 +59,7 @@ obfstr = { workspace = true }
|
|||
base64 = { workspace = true }
|
||||
tauri-plugin-stronghold = "2.2.0"
|
||||
tauri-plugin-sql = "2"
|
||||
tauri-plugin-websocket = "2"
|
||||
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
|
|
|
@ -130,6 +130,7 @@
|
|||
"system-info:allow-all",
|
||||
"user-input:default",
|
||||
"shell:default",
|
||||
"shell:allow-stdin-write",
|
||||
"keyring:default",
|
||||
"stronghold:default",
|
||||
{
|
||||
|
@ -143,12 +144,22 @@
|
|||
"validator": ".+"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"cmd": "node",
|
||||
"args": [
|
||||
{
|
||||
"validator": ".+"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"deep-link:default",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled"
|
||||
"autostart:allow-is-enabled",
|
||||
"websocket:default"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -136,6 +136,7 @@ pub fn run() {
|
|||
.plugin(tauri_plugin_keyring::init())
|
||||
.plugin(tauri_plugin_network::init())
|
||||
.plugin(tauri_plugin_system_info::init())
|
||||
.plugin(tauri_plugin_websocket::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::keyring::get_stronghold_key,
|
||||
]);
|
||||
|
|
|
@ -207,3 +207,62 @@ export async function onCustomUiCmdSelect(
|
|||
}
|
||||
appState.clearSearchTerm()
|
||||
}
|
||||
|
||||
export async function onRaycastCmdSelect(
|
||||
ext: ExtPackageJsonExtra,
|
||||
cmd: CustomUiCmd,
|
||||
{ isDev, hmr }: { isDev: boolean; hmr: boolean }
|
||||
) {
|
||||
// console.log("onCustomUiCmdSelect", ext, cmd, isDev, hmr)
|
||||
await createExtSupportDir(ext.extPath)
|
||||
let url = cmd.main
|
||||
const useDevMain = hmr && isDev && cmd.devMain
|
||||
if (useDevMain) {
|
||||
url = cmd.devMain
|
||||
} else {
|
||||
url = cmd.main.startsWith("http")
|
||||
? cmd.main
|
||||
: decodeURIComponent(convertFileSrc(`${trimSlash(cmd.main)}`, "ext"))
|
||||
}
|
||||
let url2 = `/app/extension/raycast?url=${encodeURIComponent(url)}&extPath=${encodeURIComponent(ext.extPath)}`
|
||||
// url2 = `/dev?url=${encodeURIComponent(url)}&extPath=${encodeURIComponent(ext.extPath)}`
|
||||
|
||||
setIframeExtParams(ext.extPath, url)
|
||||
if (cmd.window) {
|
||||
const winLabel = await winExtMap.registerExtensionWithWindow({
|
||||
extPath: ext.extPath,
|
||||
dist: cmd.dist
|
||||
})
|
||||
if (platform() === "windows" && !useDevMain) {
|
||||
const addr = await spawnExtensionFileServer(winLabel)
|
||||
const newUrl = `http://${addr}`
|
||||
url2 = `/app/extension/raycast?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}`
|
||||
setIframeExtParams(ext.extPath, newUrl)
|
||||
}
|
||||
localStorage.setItem(
|
||||
"kunkun-iframe-ext-params",
|
||||
JSON.stringify({ url, extPath: ext.extPath } satisfies KunkunIframeExtParams)
|
||||
)
|
||||
const window = launchNewExtWindow(winLabel, url2, cmd.window)
|
||||
window.onCloseRequested(async (event) => {
|
||||
await winExtMap.unregisterExtensionFromWindow(winLabel)
|
||||
})
|
||||
} else {
|
||||
console.log("Launch main window")
|
||||
const winLabel = await winExtMap.registerExtensionWithWindow({
|
||||
windowLabel: "main",
|
||||
extPath: ext.extPath,
|
||||
dist: cmd.dist
|
||||
})
|
||||
const _platform = platform()
|
||||
if ((_platform === "windows" || _platform === "linux") && !useDevMain) {
|
||||
const addr = await spawnExtensionFileServer(winLabel) // addr has format "127.0.0.1:<port>"
|
||||
console.log("Extension file server address: ", addr)
|
||||
const newUrl = `http://${addr}`
|
||||
url2 = `/app/extension/raycast?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}`
|
||||
setIframeExtParams(ext.extPath, newUrl)
|
||||
}
|
||||
goto(i18n.resolveRoute(url2))
|
||||
}
|
||||
appState.clearSearchTerm()
|
||||
}
|
||||
|
|
|
@ -7,7 +7,12 @@ import {
|
|||
} from "@kksh/api/models"
|
||||
import type { CommandLaunchers, OnExtCmdSelect } from "@kksh/ui/types"
|
||||
import * as v from "valibot"
|
||||
import { onCustomUiCmdSelect, onHeadlessCmdSelect, onTemplateUiCmdSelect } from "./ext"
|
||||
import {
|
||||
onCustomUiCmdSelect,
|
||||
onHeadlessCmdSelect,
|
||||
onRaycastCmdSelect,
|
||||
onTemplateUiCmdSelect
|
||||
} from "./ext"
|
||||
import { onQuickLinkSelect } from "./quick-links"
|
||||
|
||||
const onExtCmdSelect: OnExtCmdSelect = (
|
||||
|
@ -25,6 +30,9 @@ const onExtCmdSelect: OnExtCmdSelect = (
|
|||
case CmdTypeEnum.HeadlessWorker:
|
||||
onHeadlessCmdSelect(ext, v.parse(HeadlessCmd, cmd), { isDev, hmr })
|
||||
break
|
||||
case CmdTypeEnum.Raycast:
|
||||
onRaycastCmdSelect(ext, v.parse(CustomUiCmd, cmd), { isDev, hmr })
|
||||
break
|
||||
default:
|
||||
console.error("Unknown command type", cmd.type)
|
||||
}
|
||||
|
|
473
apps/desktop/src/routes/app/extension/raycast/+page.svelte
Normal file
473
apps/desktop/src/routes/app/extension/raycast/+page.svelte
Normal file
|
@ -0,0 +1,473 @@
|
|||
<script lang="ts">
|
||||
import DanceTransition from "@/components/dance/dance-transition.svelte"
|
||||
import { i18n } from "@/i18n"
|
||||
import { appConfig, winExtMap } from "@/stores"
|
||||
import { helperAPI } from "@/utils/helper"
|
||||
import { paste } from "@/utils/hotkey"
|
||||
import { goBackOnEscape } from "@/utils/key"
|
||||
import { goHome } from "@/utils/route"
|
||||
import { positionToCssStyleString, positionToTailwindClasses } from "@/utils/style"
|
||||
import { sleep } from "@/utils/time"
|
||||
import { isInMainWindow } from "@/utils/window"
|
||||
import { ThemeColor, type Position } from "@kksh/api/models"
|
||||
import {
|
||||
constructJarvisServerAPIWithPermissions,
|
||||
// exposeApiToWindow,
|
||||
type IApp
|
||||
} from "@kksh/api/ui"
|
||||
import { toast, type IUiCustomServer1, type IUiCustomServer2 } from "@kksh/api/ui/custom"
|
||||
import { db } from "@kksh/drizzle"
|
||||
import raycastScript from "@kksh/raycast/host?raw"
|
||||
import { Button } from "@kksh/svelte5"
|
||||
import { cn } from "@kksh/ui/utils"
|
||||
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
|
||||
import {
|
||||
RECORD_EXTENSION_PROCESS_EVENT,
|
||||
type IRecordExtensionProcessEvent
|
||||
} from "@kunkunapi/src/events"
|
||||
import { emitTo } from "@tauri-apps/api/event"
|
||||
import { appCacheDir, BaseDirectory, join } from "@tauri-apps/api/path"
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||
import { mkdir, writeTextFile } from "@tauri-apps/plugin-fs"
|
||||
import WebSocket from "@tauri-apps/plugin-websocket"
|
||||
import { goto } from "$app/navigation"
|
||||
import { WebSocketClientIO } from "kkrpc"
|
||||
import { IframeParentIO, RPCChannel, type IoInterface } from "kkrpc/browser"
|
||||
import { ArrowLeftIcon, MoveIcon, RefreshCcwIcon, XIcon } from "lucide-svelte"
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import { Command } from "tauri-plugin-shellx-api"
|
||||
import type { PageData } from "./$types"
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
const { loadedExt, extPath, extInfoInDB } = data
|
||||
|
||||
let root
|
||||
|
||||
let extSpawnedProcesses = $state<number[]>([])
|
||||
const appWin = getCurrentWindow()
|
||||
let iframeRef: HTMLIFrameElement
|
||||
let uiControl = $state<{
|
||||
iframeLoaded: boolean
|
||||
showBackBtn: boolean
|
||||
showMoveBtn: boolean
|
||||
showRefreshBtn: boolean
|
||||
backBtnPosition: Position
|
||||
moveBtnPosition: Position
|
||||
refreshBtnPosition: Position
|
||||
transparentBg: boolean
|
||||
}>({
|
||||
iframeLoaded: true,
|
||||
showBackBtn: true, // if open in new window, hide back button
|
||||
showMoveBtn: false,
|
||||
showRefreshBtn: false,
|
||||
backBtnPosition: "top-left",
|
||||
moveBtnPosition: "bottom-left",
|
||||
refreshBtnPosition: "top-right",
|
||||
transparentBg: false
|
||||
})
|
||||
|
||||
const iframeUiAPI: IUiCustomServer2 = {
|
||||
goBack: async () => {
|
||||
if (isInMainWindow()) {
|
||||
goto(i18n.resolveRoute("/app/"))
|
||||
} else {
|
||||
appWin.close()
|
||||
}
|
||||
},
|
||||
hideBackButton: async () => {
|
||||
uiControl.showBackBtn = false
|
||||
},
|
||||
hideMoveButton: async () => {
|
||||
uiControl.showMoveBtn = false
|
||||
},
|
||||
hideRefreshButton: async () => {
|
||||
console.log("hideRefreshButton")
|
||||
uiControl.showRefreshBtn = false
|
||||
},
|
||||
showBackButton: async (position?: Position) => {
|
||||
console.log("showBackBtn", position)
|
||||
|
||||
uiControl.showBackBtn = true
|
||||
uiControl.backBtnPosition = position ?? "top-left"
|
||||
},
|
||||
showMoveButton: async (position?: Position) => {
|
||||
uiControl.showMoveBtn = true
|
||||
uiControl.moveBtnPosition = position ?? "bottom-left"
|
||||
},
|
||||
showRefreshButton: async (position?: Position) => {
|
||||
uiControl.showRefreshBtn = true
|
||||
uiControl.refreshBtnPosition = position ?? "top-right"
|
||||
},
|
||||
getTheme: () => {
|
||||
const theme = $appConfig.theme
|
||||
return Promise.resolve({
|
||||
theme: theme.theme as ThemeColor,
|
||||
radius: theme.radius,
|
||||
lightMode: theme.lightMode
|
||||
})
|
||||
},
|
||||
async reloadPage() {
|
||||
location.reload()
|
||||
},
|
||||
async setTransparentWindowBackground(transparent: boolean) {
|
||||
if (isInMainWindow()) {
|
||||
throw new Error("Cannot set background in main window")
|
||||
}
|
||||
if (transparent) {
|
||||
document.body.style.backgroundColor = "transparent"
|
||||
} else {
|
||||
document.body.style.backgroundColor = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions(
|
||||
loadedExt.kunkun.permissions,
|
||||
loadedExt.extPath,
|
||||
{
|
||||
recordSpawnedProcess: async (pid: number) => {
|
||||
extSpawnedProcesses = [...extSpawnedProcesses, pid]
|
||||
// winExtMap.registerProcess(appWin.label, pid)
|
||||
const curWin = await getCurrentWindow()
|
||||
await emitTo("main", RECORD_EXTENSION_PROCESS_EVENT, {
|
||||
windowLabel: curWin.label,
|
||||
pid
|
||||
} satisfies IRecordExtensionProcessEvent)
|
||||
// TODO: record process in a store
|
||||
},
|
||||
getSpawnedProcesses: () => Promise.resolve(extSpawnedProcesses),
|
||||
paste: async () => {
|
||||
await appWin.hide()
|
||||
await sleep(200)
|
||||
return paste()
|
||||
}
|
||||
}
|
||||
)
|
||||
const serverAPI2 = {
|
||||
...serverAPI,
|
||||
iframeUi: {
|
||||
...serverAPI.iframeUi,
|
||||
...iframeUiAPI
|
||||
} satisfies IUiCustomServer1 & IUiCustomServer2,
|
||||
helper: helperAPI,
|
||||
db: new db.JarvisExtDB(extInfoInDB.extId),
|
||||
kv: new db.KV(extInfoInDB.extId),
|
||||
app: {
|
||||
language: () => Promise.resolve("en") // TODO: get locale
|
||||
} satisfies IApp
|
||||
}
|
||||
|
||||
const elements = new Map()
|
||||
const waiting = new Map()
|
||||
|
||||
function getElement(id: number): Promise<HTMLElement> {
|
||||
return new Promise((resolve) => {
|
||||
if (elements.has(id)) return resolve(elements.get(id))
|
||||
if (!waiting.has(id)) waiting.set(id, [])
|
||||
waiting.get(id).push(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
function getElements(...ids: number[]) {
|
||||
return Promise.all(ids.map(getElement))
|
||||
}
|
||||
|
||||
let eventId = 0
|
||||
const events = new Map()
|
||||
|
||||
;(async () => {
|
||||
await mkdir("", { baseDir: BaseDirectory.AppCache, recursive: true })
|
||||
await writeTextFile("raycast.js", raycastScript, {
|
||||
baseDir: BaseDirectory.AppCache
|
||||
})
|
||||
const path = await join(await appCacheDir(), "raycast.js")
|
||||
|
||||
const node = Command.create("node", [path], {
|
||||
cwd: extPath
|
||||
})
|
||||
const nodeProcess = await node.spawn()
|
||||
const { pid } = nodeProcess
|
||||
extSpawnedProcesses = [...extSpawnedProcesses, pid]
|
||||
const curWin = await getCurrentWindow()
|
||||
await emitTo("main", RECORD_EXTENSION_PROCESS_EVENT, {
|
||||
windowLabel: curWin.label,
|
||||
pid
|
||||
} satisfies IRecordExtensionProcessEvent)
|
||||
node.on("error", console.error)
|
||||
node.stderr.on("data", console.error)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
node.stdout.on("data", (data) => {
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
|
||||
const { readable, writable } = new TransformStream()
|
||||
const reader = readable.getReader()
|
||||
const writer = writable.getWriter()
|
||||
node.stdout.on("data", (data) => writer.write(data))
|
||||
|
||||
const RPC_METHOD: string = "stdio"
|
||||
let stdio: IoInterface
|
||||
if (RPC_METHOD === "websocket") {
|
||||
const ws = await WebSocket.connect("ws://127.0.0.1:5000")
|
||||
ws.addListener((message) => {
|
||||
writer.write(message.data)
|
||||
})
|
||||
stdio = {
|
||||
name: "websocket",
|
||||
async read() {
|
||||
const { value } = await reader.read()
|
||||
return value
|
||||
},
|
||||
write: (data) => ws.send(data)
|
||||
}
|
||||
} else {
|
||||
stdio = {
|
||||
name: "stdio",
|
||||
async read(): Promise<string | Uint8Array | null> {
|
||||
const { value } = await reader.read()
|
||||
return value
|
||||
},
|
||||
async write(data: string): Promise<void> {
|
||||
return nodeProcess.write(data + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new RPCChannel<{}, {}>(stdio, {
|
||||
expose: {
|
||||
createInstance(id, type) {
|
||||
// console.log("set element", id)
|
||||
let element
|
||||
if (["svg", "path"].includes(type))
|
||||
element = document.createElementNS("http://www.w3.org/2000/svg", type)
|
||||
else element = document.createElement(type)
|
||||
element._id = id
|
||||
|
||||
elements.set(id, element)
|
||||
|
||||
for (const resolve of waiting.get(id) ?? []) {
|
||||
resolve(element)
|
||||
}
|
||||
waiting.delete(id)
|
||||
},
|
||||
createTextInstance(id, text) {
|
||||
const element = document.createTextNode(text)
|
||||
elements.set(id, element)
|
||||
element._id = id
|
||||
|
||||
for (const resolve of waiting.get(id) ?? []) {
|
||||
resolve(element)
|
||||
}
|
||||
waiting.delete(id)
|
||||
},
|
||||
async appendChild(parentId, childId) {
|
||||
const [parent, child] = await getElements(parentId, childId)
|
||||
parent.appendChild(child)
|
||||
},
|
||||
async removeChild(parentId, childId) {
|
||||
const [parent, child] = await getElements(parentId, childId)
|
||||
parent.removeChild(child)
|
||||
|
||||
function removeRecursive(elementId, element) {
|
||||
for (const child of element.children) {
|
||||
removeRecursive(child._id, child)
|
||||
}
|
||||
|
||||
elements.delete(elementId)
|
||||
if (waiting.has(elementId)) waiting.delete(elementId)
|
||||
}
|
||||
|
||||
removeRecursive(childId, child)
|
||||
},
|
||||
async insertBefore(parentId, childId, beforeChildId) {
|
||||
const [parent, child, beforeChild] = await getElements(parentId, childId, beforeChildId)
|
||||
parent.insertBefore(child, beforeChild)
|
||||
},
|
||||
async setText(elementId, text) {
|
||||
const element = await getElement(elementId)
|
||||
element.textContent = text
|
||||
},
|
||||
async applyProps(elementId, props) {
|
||||
const element = await getElement(elementId)
|
||||
for (let [propName, propValue] of Object.entries(props)) {
|
||||
if (propName === "className") propName = "class"
|
||||
|
||||
if (propName === "children") {
|
||||
if (typeof propValue === "string" || typeof propValue === "number") {
|
||||
element.textContent = propValue.toString()
|
||||
}
|
||||
} else if (propName.startsWith("on")) {
|
||||
element.addEventListener(propName.slice(2).toLowerCase(), async (event) => {
|
||||
const id = eventId++
|
||||
const serialized = {
|
||||
target: {
|
||||
value: event.target.value
|
||||
},
|
||||
key: event.key,
|
||||
ctrlKey: event.ctrlKey,
|
||||
keyCode: event.keyCode,
|
||||
id
|
||||
}
|
||||
|
||||
events.set(id, event)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
event.__resolve = resolve
|
||||
propValue(serialized)
|
||||
})
|
||||
|
||||
event.__resolve()
|
||||
events.delete(id)
|
||||
})
|
||||
} else if (propName === "style") {
|
||||
for (const [rule, value] of Object.entries(propValue)) {
|
||||
element.style[rule] = value
|
||||
}
|
||||
} else if (propValue === undefined) element.removeAttribute(propName)
|
||||
else element.setAttribute(propName, propValue)
|
||||
}
|
||||
},
|
||||
async addEventListener(elementId, type, listener) {
|
||||
const element = await getElement(elementId)
|
||||
|
||||
// Since React only allows one event listener per type, we can take advantage of that
|
||||
// by storing the listener in the element itself
|
||||
if (element._listeners == null) element._listeners = {}
|
||||
const existing = element._listeners[type]
|
||||
if (existing) element.removeEventListener(type, existing)
|
||||
|
||||
const realListener = async (event) => {
|
||||
const id = eventId++
|
||||
const serialized = {
|
||||
target: {
|
||||
value: event.target.value
|
||||
},
|
||||
key: event.key,
|
||||
ctrlKey: event.ctrlKey,
|
||||
keyCode: event.keyCode,
|
||||
id
|
||||
}
|
||||
|
||||
events.set(id, event)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
event.__resolve = resolve
|
||||
listener(serialized)
|
||||
})
|
||||
}
|
||||
element.addEventListener(type, realListener)
|
||||
element._listeners[type] = realListener
|
||||
},
|
||||
async preventDefault(id) {
|
||||
const event = events.get(id)
|
||||
if (event) {
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
async clearEvent(id) {
|
||||
const event = events.get(id)
|
||||
if (event) {
|
||||
event.__resolve()
|
||||
events.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
function onBackBtnClicked() {
|
||||
if (isInMainWindow()) {
|
||||
goHome()
|
||||
} else {
|
||||
appWin.close()
|
||||
}
|
||||
}
|
||||
|
||||
function onIframeLoaded() {
|
||||
setTimeout(() => {
|
||||
iframeRef.focus()
|
||||
uiControl.iframeLoaded = true
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
elements.set(-1, root)
|
||||
setTimeout(() => {
|
||||
appWin.show()
|
||||
}, 200)
|
||||
if (iframeRef?.contentWindow) {
|
||||
const io = new IframeParentIO(iframeRef.contentWindow)
|
||||
const rpc = new RPCChannel(io, { expose: serverAPI2 })
|
||||
} else {
|
||||
toast.warning("iframeRef.contentWindow not available")
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (!uiControl.iframeLoaded) {
|
||||
toast.error("Extension failed to load")
|
||||
}
|
||||
}, 3_000)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
winExtMap.unregisterExtensionFromWindow(appWin.label)
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={goBackOnEscape} />
|
||||
{#if uiControl.backBtnPosition && uiControl.showBackBtn}
|
||||
<Button
|
||||
class={cn("absolute", positionToTailwindClasses(uiControl.backBtnPosition))}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onclick={onBackBtnClicked}
|
||||
style={`${positionToCssStyleString(uiControl.backBtnPosition)}`}
|
||||
>
|
||||
{#if appWin.label === "main"}
|
||||
<ArrowLeftIcon class="w-4" />
|
||||
{:else}
|
||||
<XIcon class="w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if uiControl.moveBtnPosition && uiControl.showMoveBtn}
|
||||
<Button
|
||||
class={cn("absolute", positionToTailwindClasses(uiControl.moveBtnPosition))}
|
||||
style={`${positionToCssStyleString(uiControl.moveBtnPosition)}`}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<MoveIcon data-tauri-drag-region class="w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if uiControl.refreshBtnPosition && uiControl.showRefreshBtn}
|
||||
<Button
|
||||
class={cn("absolute", positionToTailwindClasses(uiControl.refreshBtnPosition))}
|
||||
style={`${positionToCssStyleString(uiControl.refreshBtnPosition)}`}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onclick={iframeUiAPI.reloadPage}
|
||||
>
|
||||
<RefreshCcwIcon class="w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<main class="h-screen">
|
||||
<DanceTransition delay={300} autoHide={false} show={!uiControl.iframeLoaded} />
|
||||
<div bind:this={root}></div>
|
||||
<!-- <iframe
|
||||
bind:this={iframeRef}
|
||||
class={cn("h-full", {
|
||||
hidden: !uiControl.iframeLoaded
|
||||
})}
|
||||
onload={onIframeLoaded}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameborder="0"
|
||||
src={data.url}
|
||||
title={data.extPath}
|
||||
></iframe> -->
|
||||
</main>
|
71
apps/desktop/src/routes/app/extension/raycast/+page.ts
Normal file
71
apps/desktop/src/routes/app/extension/raycast/+page.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { KunkunIframeExtParams } from "@/cmds/ext"
|
||||
import { i18n } from "@/i18n"
|
||||
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models"
|
||||
import { db } from "@kksh/drizzle"
|
||||
import { loadExtensionManifestFromDisk } from "@kksh/extension"
|
||||
import { error as svError } from "@sveltejs/kit"
|
||||
import { join } from "@tauri-apps/api/path"
|
||||
import { error } from "@tauri-apps/plugin-log"
|
||||
import { goto } from "$app/navigation"
|
||||
import { toast } from "svelte-sonner"
|
||||
import * as v from "valibot"
|
||||
import type { PageLoad } from "./$types"
|
||||
|
||||
export const load: PageLoad = async ({
|
||||
url
|
||||
}): Promise<{
|
||||
extPath: string
|
||||
url: string
|
||||
loadedExt: ExtPackageJsonExtra
|
||||
extInfoInDB: ExtInfoInDB
|
||||
}> => {
|
||||
// both query parameter must exist
|
||||
const rawKunkunIframeExtParams = localStorage.getItem("kunkun-iframe-ext-params")
|
||||
if (!rawKunkunIframeExtParams) {
|
||||
toast.error("Invalid extension path or url")
|
||||
return svError(404, "Invalid extension path or url")
|
||||
}
|
||||
// localStorage.removeItem("kunkun-iframe-ext-params")
|
||||
const parsed = v.safeParse(KunkunIframeExtParams, JSON.parse(rawKunkunIframeExtParams))
|
||||
if (!parsed.success) {
|
||||
toast.error("Fail to parse extension params from local storage", {
|
||||
description: `${v.flatten<typeof KunkunIframeExtParams>(parsed.issues)}`
|
||||
})
|
||||
return svError(400, "Fail to parse extension params from local storage")
|
||||
}
|
||||
const { url: extUrl, extPath } = parsed.output
|
||||
console.log("extUrl extPath", extUrl, extPath)
|
||||
|
||||
const _extPath = url.searchParams.get("extPath")
|
||||
const _extUrl = url.searchParams.get("url")
|
||||
console.log("_extPath", _extPath)
|
||||
console.log("_extUrl", _extUrl)
|
||||
// if (!_extPath || !_extUrl) {
|
||||
// toast.error("Invalid extension path or url", {
|
||||
// description: `_extPath: ${_extPath}; _extUrl: ${_extUrl}`
|
||||
// })
|
||||
// error("Invalid extension path or url")
|
||||
// goto(i18n.resolveRoute("/app/"))
|
||||
// }
|
||||
// const extPath = z.string().parse(_extPath)
|
||||
// const extUrl = z.string().parse(_extUrl)
|
||||
let _loadedExt: ExtPackageJsonExtra | undefined
|
||||
try {
|
||||
_loadedExt = await loadExtensionManifestFromDisk(await join(extPath, "package.json"))
|
||||
} catch (err) {
|
||||
error(`Error loading extension manifest: ${err}`)
|
||||
toast.error("Error loading extension manifest", {
|
||||
description: `${err}`
|
||||
})
|
||||
goto(i18n.resolveRoute("/app/"))
|
||||
}
|
||||
const loadedExt = _loadedExt!
|
||||
const extInfoInDB = await db.getUniqueExtensionByPath(loadedExt.extPath)
|
||||
if (!extInfoInDB) {
|
||||
toast.error("Unexpected Error", {
|
||||
description: `Extension ${loadedExt.kunkun.identifier} not found in database. Run Troubleshooter.`
|
||||
})
|
||||
goto(i18n.resolveRoute("/app/"))
|
||||
}
|
||||
return { extPath, url: extUrl, loadedExt, extInfoInDB: extInfoInDB! }
|
||||
}
|
73
flake.nix
Normal file
73
flake.nix
Normal file
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
description = "Kunkun development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
toolchain
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
gobject-introspection
|
||||
cargo
|
||||
cargo-tauri
|
||||
|
||||
pnpm
|
||||
nodejs_22
|
||||
cmake
|
||||
xorg.libXtst
|
||||
protobuf
|
||||
xdotool
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
at-spi2-atk
|
||||
atkmm
|
||||
cairo
|
||||
gdk-pixbuf
|
||||
glib
|
||||
gtk3
|
||||
harfbuzz
|
||||
librsvg
|
||||
libsoup_3
|
||||
pango
|
||||
webkitgtk_4_1
|
||||
openssl
|
||||
deno
|
||||
ffmpeg
|
||||
libayatana-appindicator
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export WEBKIT_DISABLE_COMPOSITING_MODE=1
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
|
@ -245,7 +245,7 @@ export class TauriShellStdio implements IoInterface {
|
|||
|
||||
read(): Promise<string | Uint8Array | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.readStream.on("data", (chunk) => {
|
||||
this.readStream.once("data", (chunk) => {
|
||||
resolve(chunk)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -32,7 +32,8 @@ export const CmdTypeEnum = {
|
|||
UiWorker: "ui_worker",
|
||||
UiIframe: "ui_iframe",
|
||||
QuickLink: "quick_link",
|
||||
Remote: "remote"
|
||||
Remote: "remote",
|
||||
Raycast: "raycast"
|
||||
}
|
||||
|
||||
export const CmdType = v.picklist(Object.values(CmdTypeEnum))
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import { ExtPackageJson, ExtPackageJsonExtra, License } from "@kksh/api/models"
|
||||
import {
|
||||
CmdTypeEnum,
|
||||
ExtPackageJson,
|
||||
ExtPackageJsonExtra,
|
||||
IconEnum,
|
||||
KunkunExtManifest,
|
||||
License
|
||||
} from "@kksh/api/models"
|
||||
import { db } from "@kksh/drizzle"
|
||||
import { basename, dirname, join } from "@tauri-apps/api/path"
|
||||
import { readDir, readTextFile } from "@tauri-apps/plugin-fs"
|
||||
import { readDir, readFile, readTextFile } from "@tauri-apps/plugin-fs"
|
||||
import { debug, error } from "@tauri-apps/plugin-log"
|
||||
import semver from "semver"
|
||||
import * as v from "valibot"
|
||||
|
@ -12,6 +19,35 @@ const OptionalExtPackageJson = v.object({
|
|||
license: v.optional(License, "MIT") // TODO: remove this optional package json later
|
||||
})
|
||||
|
||||
const RaycastExtPackageJson = v.object({
|
||||
...v.omit(OptionalExtPackageJson, ["kunkun"]).entries,
|
||||
title: v.string(),
|
||||
description: v.string(),
|
||||
icon: v.string(),
|
||||
version: v.optional(v.string(), "Version of the extension"),
|
||||
commands: v.array(
|
||||
v.object({
|
||||
name: v.string(),
|
||||
title: v.string(),
|
||||
subtitle: v.optional(v.string()),
|
||||
description: v.string(),
|
||||
mode: v.picklist(["view", "no-view"])
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// https://stackoverflow.com/a/66046176
|
||||
async function bufferToBase64(buffer) {
|
||||
// use a FileReader to generate a base64 data URI:
|
||||
const base64url = await new Promise((r) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => r(reader.result)
|
||||
reader.readAsDataURL(new Blob([buffer]))
|
||||
})
|
||||
// remove the `data:...;base64,` part from the start
|
||||
return base64url.slice(base64url.indexOf(",") + 1)
|
||||
}
|
||||
|
||||
export function parseAPIVersion(dependencies: Record<string, string>) {
|
||||
const stripPrefix = (version: string) => version.replace(/^[^0-9]+/, "") // Remove leading ^, ~, etc.
|
||||
const apiVersion = dependencies["@kksh/api"]
|
||||
|
@ -30,6 +66,40 @@ export function loadExtensionManifestFromDisk(manifestPath: string): Promise<Ext
|
|||
debug(`loadExtensionManifestFromDisk: ${manifestPath}`)
|
||||
return readTextFile(manifestPath).then(async (content) => {
|
||||
const json = JSON.parse(content)
|
||||
|
||||
const raycastParse = v.safeParse(RaycastExtPackageJson, json)
|
||||
if (!raycastParse.issues) {
|
||||
const raycast = raycastParse.output
|
||||
json.kunkun = {
|
||||
name: raycast.title,
|
||||
shortDescription: raycast.description,
|
||||
longDescription: "",
|
||||
identifier: raycast.name,
|
||||
permissions: ["shell:deno:execute", "shell:deno:spawn", "shell:all", "shell:execute"],
|
||||
demoImages: [],
|
||||
icon: {
|
||||
// TODO: is this the best way to do this?
|
||||
type: IconEnum.Base64PNG,
|
||||
value: await bufferToBase64(
|
||||
await readFile(await join(await dirname(manifestPath), "assets", raycast.icon))
|
||||
)
|
||||
},
|
||||
customUiCmds: raycast.commands.map((cmd) => ({
|
||||
main: "/",
|
||||
dist: "dist",
|
||||
name: cmd.title,
|
||||
cmds: [],
|
||||
type: CmdTypeEnum.Raycast,
|
||||
description: cmd.description,
|
||||
platforms: [],
|
||||
devMain: ""
|
||||
}))
|
||||
} satisfies KunkunExtManifest
|
||||
delete json.commands
|
||||
|
||||
json.version = "1.0.0"
|
||||
}
|
||||
|
||||
const parse = v.safeParse(OptionalExtPackageJson, json)
|
||||
if (parse.issues) {
|
||||
error(`Fail to load extension from ${manifestPath}. See console for parse error.`)
|
||||
|
|
538
packages/raycast/api.tsx
Normal file
538
packages/raycast/api.tsx
Normal file
|
@ -0,0 +1,538 @@
|
|||
import {
|
||||
Button,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandFooter,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
VertifcalSeparator
|
||||
} from "@kksh/react"
|
||||
import { CogIcon, Icons } from "@raycast/icons"
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import React, { useCallback } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function List(props) {
|
||||
const { className, children } = props
|
||||
const [value, setValue] = React.useState("")
|
||||
const onInput = useCallback((event) => {
|
||||
const v = event.target.value
|
||||
setValue(v)
|
||||
props.onSearchTextChange?.(v)
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<Command className={cn("rounded-lg border shadow-md", className)} shouldFilter={false}>
|
||||
<CommandInput placeholder="Type a command or search..." onInput={onInput} value={value} />
|
||||
<CommandList className="h-full">
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{children}
|
||||
</CommandList>
|
||||
<CommandFooter>
|
||||
<CogIcon className="ml-2 size-4" />
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
Open Application
|
||||
<kbd className="ml-1">↵</kbd>
|
||||
</Button>
|
||||
<VertifcalSeparator />
|
||||
{/* <ActionPanel
|
||||
listRef={listRef}
|
||||
selectedValue={value}
|
||||
inputRef={inputRef}
|
||||
actionItems={[
|
||||
{ label: "Open Application", value: "open" },
|
||||
{ label: "Show in Finder", value: "finder" },
|
||||
{ label: "Show Info in Finder", value: "info" },
|
||||
{ label: "Add to Favorites", value: "favorites" }
|
||||
]}
|
||||
></ActionPanel> */}
|
||||
</div>
|
||||
</CommandFooter>
|
||||
</Command>
|
||||
</>
|
||||
)
|
||||
}
|
||||
List.Item = function (props) {
|
||||
const Icon = Icons[props.icon?.slice(0, -3)]
|
||||
return (
|
||||
<CommandItem className="gap-3">
|
||||
{Icon && <Icon style={{ width: "16px" }} />}
|
||||
<span className="truncate">{props.title}</span>
|
||||
<span className="text-muted-foreground">{props.subtitle}</span>
|
||||
</CommandItem>
|
||||
)
|
||||
}
|
||||
List.Section = CommandGroup
|
||||
|
||||
export function ActionPanel() {
|
||||
return <div></div>
|
||||
}
|
||||
ActionPanel.Section = ActionPanel
|
||||
|
||||
export function Action() {}
|
||||
Action.CopyToClipboard = Action
|
||||
|
||||
export function showToast() {
|
||||
console.error("showToast stub")
|
||||
return {}
|
||||
}
|
||||
|
||||
enum Style {
|
||||
Animated
|
||||
}
|
||||
|
||||
export const Toast = {
|
||||
Style
|
||||
}
|
||||
|
||||
export function useNavigation() {
|
||||
return {
|
||||
push: () => console.error("push stub"),
|
||||
pop: () => console.error("pop stub")
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreferenceValues() {
|
||||
return {}
|
||||
}
|
||||
|
||||
// TODO: this should probably just be re-exported from @raycast/api
|
||||
export enum Icon {
|
||||
AddPerson = "add-person-16",
|
||||
Airplane = "airplane-16",
|
||||
AirplaneFilled = "airplane-filled-16",
|
||||
AirplaneLanding = "airplane-landing-16",
|
||||
AirplaneTakeoff = "airplane-takeoff-16",
|
||||
Airpods = "airpods-16",
|
||||
Alarm = "alarm-16",
|
||||
AlarmRinging = "alarm-ringing-16",
|
||||
AlignCentre = "align-centre-16",
|
||||
AlignLeft = "align-left-16",
|
||||
AlignRight = "align-right-16",
|
||||
AmericanFootball = "american-football-16",
|
||||
Anchor = "anchor-16",
|
||||
AppWindow = "app-window-16",
|
||||
AppWindowGrid2x2 = "app-window-grid-2x2-16",
|
||||
AppWindowGrid3x3 = "app-window-grid-3x3-16",
|
||||
AppWindowList = "app-window-list-16",
|
||||
AppWindowSidebarLeft = "app-window-sidebar-left-16",
|
||||
AppWindowSidebarRight = "app-window-sidebar-right-16",
|
||||
ArrowClockwise = "arrow-clockwise-16",
|
||||
ArrowCounterClockwise = "arrow-counter-clockwise-16",
|
||||
ArrowDown = "arrow-down-16",
|
||||
ArrowDownCircle = "arrow-down-circle-16",
|
||||
ArrowDownCircleFilled = "arrow-down-circle-filled-16",
|
||||
ArrowLeft = "arrow-left-16",
|
||||
ArrowLeftCircle = "arrow-left-circle-16",
|
||||
ArrowLeftCircleFilled = "arrow-left-circle-filled-16",
|
||||
ArrowNe = "arrow-ne-16",
|
||||
ArrowRight = "arrow-right-16",
|
||||
ArrowRightCircle = "arrow-right-circle-16",
|
||||
ArrowRightCircleFilled = "arrow-right-circle-filled-16",
|
||||
ArrowUp = "arrow-up-16",
|
||||
ArrowUpCircle = "arrow-up-circle-16",
|
||||
ArrowUpCircleFilled = "arrow-up-circle-filled-16",
|
||||
AtSymbol = "at-symbol-16",
|
||||
BandAid = "band-aid-16",
|
||||
BankNote = "bank-note-16",
|
||||
BarChart = "bar-chart-16",
|
||||
BarCode = "bar-code-16",
|
||||
BathTub = "bath-tub-16",
|
||||
Battery = "battery-16",
|
||||
BatteryCharging = "battery-charging-16",
|
||||
BatteryDisabled = "battery-disabled-16",
|
||||
Bell = "bell-16",
|
||||
BellDisabled = "bell-disabled-16",
|
||||
Bike = "bike-16",
|
||||
Binoculars = "binoculars-16",
|
||||
Bird = "bird-16",
|
||||
BlankDocument = "blank-document-16",
|
||||
Bluetooth = "bluetooth-16",
|
||||
Boat = "boat-16",
|
||||
Bold = "bold-16",
|
||||
Bolt = "bolt-16",
|
||||
BoltDisabled = "bolt-disabled-16",
|
||||
Book = "book-16",
|
||||
Bookmark = "bookmark-16",
|
||||
Box = "box-16",
|
||||
Brush = "brush-16",
|
||||
Bubble = "speech-bubble-16",
|
||||
Bug = "bug-16",
|
||||
BulletPoints = "bullet-points-16",
|
||||
BullsEye = "bulls-eye-16",
|
||||
Buoy = "buoy-16",
|
||||
Calculator = "calculator-16",
|
||||
Calendar = "calendar-16",
|
||||
Camera = "camera-16",
|
||||
Car = "car-16",
|
||||
Cart = "cart-16",
|
||||
Cd = "cd-16",
|
||||
Center = "center-16",
|
||||
Check = "check-16",
|
||||
CheckCircle = "check-circle-16",
|
||||
Checkmark = "check-circle-16",
|
||||
ChessPiece = "chess-piece-16",
|
||||
ChevronDown = "chevron-down-16",
|
||||
ChevronDownSmall = "chevron-down-small-16",
|
||||
ChevronLeft = "chevron-left-16",
|
||||
ChevronLeftSmall = "chevron-left-small-16",
|
||||
ChevronRight = "chevron-right-16",
|
||||
ChevronRightSmall = "chevron-right-small-16",
|
||||
ChevronUp = "chevron-up-16",
|
||||
ChevronUpSmall = "chevron-up-small-16",
|
||||
Circle = "circle-16",
|
||||
CircleEllipsis = "circle-ellipsis-16",
|
||||
CircleFilled = "circle-filled-16",
|
||||
CircleProgress = "circle-progress-16",
|
||||
CircleProgress100 = "circle-progress-100-16",
|
||||
CircleProgress25 = "circle-progress-25-16",
|
||||
CircleProgress50 = "circle-progress-50-16",
|
||||
CircleProgress75 = "circle-progress-75-16",
|
||||
ClearFormatting = "clear-formatting-16",
|
||||
Clipboard = "copy-clipboard-16",
|
||||
Clock = "clock-16",
|
||||
Cloud = "cloud-16",
|
||||
CloudLightning = "cloud-lightning-16",
|
||||
CloudRain = "cloud-rain-16",
|
||||
CloudSnow = "cloud-snow-16",
|
||||
CloudSun = "cloud-sun-16",
|
||||
Code = "code-16",
|
||||
CodeBlock = "code-block-16",
|
||||
Cog = "cog-16",
|
||||
Coin = "coin-16",
|
||||
Coins = "coins-16",
|
||||
Compass = "compass-16",
|
||||
ComputerChip = "computer-chip-16",
|
||||
Contrast = "contrast-16",
|
||||
CopyClipboard = "copy-clipboard-16",
|
||||
CreditCard = "credit-card-16",
|
||||
CricketBall = "cricket-ball-16",
|
||||
Crop = "crop-16",
|
||||
Crown = "crown-16",
|
||||
Crypto = "crypto-16",
|
||||
DeleteDocument = "delete-document-16",
|
||||
Desktop = "desktop-16",
|
||||
Dna = "dna-16",
|
||||
Document = "blank-document-16",
|
||||
Dot = "dot-16",
|
||||
Download = "download-16",
|
||||
EditShape = "edit-shape-16",
|
||||
Eject = "eject-16",
|
||||
Ellipsis = "ellipsis-16",
|
||||
Emoji = "emoji-16",
|
||||
Envelope = "envelope-16",
|
||||
Eraser = "eraser-16",
|
||||
ExclamationMark = "important-01-16",
|
||||
Exclamationmark = "exclamationmark-16",
|
||||
Exclamationmark2 = "exclamationmark-2-16",
|
||||
Exclamationmark3 = "exclamationmark-3-16",
|
||||
Eye = "eye-16",
|
||||
EyeDisabled = "eye-disabled-16",
|
||||
EyeDropper = "eye-dropper-16",
|
||||
Female = "female-16",
|
||||
FilmStrip = "film-strip-16",
|
||||
Filter = "filter-16",
|
||||
Finder = "finder-16",
|
||||
Fingerprint = "fingerprint-16",
|
||||
Folder = "folder-16",
|
||||
Footprints = "footprints-16",
|
||||
Forward = "forward-16",
|
||||
ForwardFilled = "forward-filled-16",
|
||||
FountainTip = "fountain-tip-16",
|
||||
FullSignal = "full-signal-16",
|
||||
GameController = "game-controller-16",
|
||||
Gauge = "gauge-16",
|
||||
Gear = "cog-16",
|
||||
Geopin = "geopin-16",
|
||||
Germ = "germ-16",
|
||||
Gift = "gift-16",
|
||||
Glasses = "glasses-16",
|
||||
Globe = "globe-01-16",
|
||||
Goal = "goal-16",
|
||||
Hammer = "hammer-16",
|
||||
HardDrive = "hard-drive-16",
|
||||
Hashtag = "hashtag-16",
|
||||
Headphones = "headphones-16",
|
||||
Heart = "heart-16",
|
||||
HeartDisabled = "heart-disabled-16",
|
||||
Heartbeat = "heartbeat-16",
|
||||
Highlight = "highlight-16",
|
||||
Hourglass = "hourglass-16",
|
||||
House = "house-16",
|
||||
Image = "image-16",
|
||||
Important = "important-01-16",
|
||||
Info = "info-01-16",
|
||||
Italics = "italics-16",
|
||||
Key = "key-16",
|
||||
Keyboard = "keyboard-16",
|
||||
Layers = "layers-16",
|
||||
Leaderboard = "leaderboard-16",
|
||||
Leaf = "leaf-16",
|
||||
LevelMeter = "signal-2-16",
|
||||
LightBulb = "light-bulb-16",
|
||||
LightBulbOff = "light-bulb-off-16",
|
||||
LineChart = "line-chart-16",
|
||||
Link = "link-16",
|
||||
List = "app-window-list-16",
|
||||
Livestream = "livestream-01-16",
|
||||
LivestreamDisabled = "livestream-disabled-01-16",
|
||||
Lock = "lock-16",
|
||||
LockDisabled = "lock-disabled-16",
|
||||
LockUnlocked = "lock-unlocked-16",
|
||||
Logout = "logout-16",
|
||||
Lorry = "lorry-16",
|
||||
Lowercase = "lowercase-16",
|
||||
MagnifyingGlass = "magnifying-glass-16",
|
||||
Male = "male-16",
|
||||
Map = "map-16",
|
||||
Mask = "mask-16",
|
||||
Maximize = "maximize-16",
|
||||
MedicalSupport = "medical-support-16",
|
||||
Megaphone = "megaphone-16",
|
||||
MemoryChip = "computer-chip-16",
|
||||
MemoryStick = "memory-stick-16",
|
||||
Message = "speech-bubble-16",
|
||||
Microphone = "microphone-16",
|
||||
MicrophoneDisabled = "microphone-disabled-16",
|
||||
Minimize = "minimize-16",
|
||||
Minus = "minus-16",
|
||||
MinusCircle = "minus-circle-16",
|
||||
MinusCircleFilled = "minus-circle-filled-16",
|
||||
Mobile = "mobile-16",
|
||||
Monitor = "monitor-16",
|
||||
Moon = "moon-16",
|
||||
Mountain = "mountain-16",
|
||||
Mouse = "mouse-16",
|
||||
Multiply = "multiply-16",
|
||||
Music = "music-16",
|
||||
Network = "network-16",
|
||||
NewDocument = "new-document-16",
|
||||
NewFolder = "new-folder-16",
|
||||
Number00 = "number-00-16",
|
||||
Number01 = "number-01-16",
|
||||
Number02 = "number-02-16",
|
||||
Number03 = "number-03-16",
|
||||
Number04 = "number-04-16",
|
||||
Number05 = "number-05-16",
|
||||
Number06 = "number-06-16",
|
||||
Number07 = "number-07-16",
|
||||
Number08 = "number-08-16",
|
||||
Number09 = "number-09-16",
|
||||
Number10 = "number-10-16",
|
||||
Number11 = "number-11-16",
|
||||
Number12 = "number-12-16",
|
||||
Number13 = "number-13-16",
|
||||
Number14 = "number-14-16",
|
||||
Number15 = "number-15-16",
|
||||
Number16 = "number-16-16",
|
||||
Number17 = "number-17-16",
|
||||
Number18 = "number-18-16",
|
||||
Number19 = "number-19-16",
|
||||
Number20 = "number-20-16",
|
||||
Number21 = "number-21-16",
|
||||
Number22 = "number-22-16",
|
||||
Number23 = "number-23-16",
|
||||
Number24 = "number-24-16",
|
||||
Number25 = "number-25-16",
|
||||
Number26 = "number-26-16",
|
||||
Number27 = "number-27-16",
|
||||
Number28 = "number-28-16",
|
||||
Number29 = "number-29-16",
|
||||
Number30 = "number-30-16",
|
||||
Number31 = "number-31-16",
|
||||
Number32 = "number-32-16",
|
||||
Number33 = "number-33-16",
|
||||
Number34 = "number-34-16",
|
||||
Number35 = "number-35-16",
|
||||
Number36 = "number-36-16",
|
||||
Number37 = "number-37-16",
|
||||
Number38 = "number-38-16",
|
||||
Number39 = "number-39-16",
|
||||
Number40 = "number-40-16",
|
||||
Number41 = "number-41-16",
|
||||
Number42 = "number-42-16",
|
||||
Number43 = "number-43-16",
|
||||
Number44 = "number-44-16",
|
||||
Number45 = "number-45-16",
|
||||
Number46 = "number-46-16",
|
||||
Number47 = "number-47-16",
|
||||
Number48 = "number-48-16",
|
||||
Number49 = "number-49-16",
|
||||
Number50 = "number-50-16",
|
||||
Number51 = "number-51-16",
|
||||
Number52 = "number-52-16",
|
||||
Number53 = "number-53-16",
|
||||
Number54 = "number-54-16",
|
||||
Number55 = "number-55-16",
|
||||
Number56 = "number-56-16",
|
||||
Number57 = "number-57-16",
|
||||
Number58 = "number-58-16",
|
||||
Number59 = "number-59-16",
|
||||
Number60 = "number-60-16",
|
||||
Number61 = "number-61-16",
|
||||
Number62 = "number-62-16",
|
||||
Number63 = "number-63-16",
|
||||
Number64 = "number-64-16",
|
||||
Number65 = "number-65-16",
|
||||
Number66 = "number-66-16",
|
||||
Number67 = "number-67-16",
|
||||
Number68 = "number-68-16",
|
||||
Number69 = "number-69-16",
|
||||
Number70 = "number-70-16",
|
||||
Number71 = "number-71-16",
|
||||
Number72 = "number-72-16",
|
||||
Number73 = "number-73-16",
|
||||
Number74 = "number-74-16",
|
||||
Number75 = "number-75-16",
|
||||
Number76 = "number-76-16",
|
||||
Number77 = "number-77-16",
|
||||
Number78 = "number-78-16",
|
||||
Number79 = "number-79-16",
|
||||
Number80 = "number-80-16",
|
||||
Number81 = "number-81-16",
|
||||
Number82 = "number-82-16",
|
||||
Number83 = "number-83-16",
|
||||
Number84 = "number-84-16",
|
||||
Number85 = "number-85-16",
|
||||
Number86 = "number-86-16",
|
||||
Number87 = "number-87-16",
|
||||
Number88 = "number-88-16",
|
||||
Number89 = "number-89-16",
|
||||
Number90 = "number-90-16",
|
||||
Number91 = "number-91-16",
|
||||
Number92 = "number-92-16",
|
||||
Number93 = "number-93-16",
|
||||
Number94 = "number-94-16",
|
||||
Number95 = "number-95-16",
|
||||
Number96 = "number-96-16",
|
||||
Number97 = "number-97-16",
|
||||
Number98 = "number-98-16",
|
||||
Number99 = "number-99-16",
|
||||
Paperclip = "paperclip-16",
|
||||
Patch = "patch-16",
|
||||
Pause = "pause-16",
|
||||
PauseFilled = "pause-filled-16",
|
||||
Pencil = "pencil-16",
|
||||
Person = "person-16",
|
||||
PersonCircle = "person-circle-16",
|
||||
PersonLines = "person-lines-16",
|
||||
Phone = "phone-16",
|
||||
PhoneRinging = "phone-ringing-16",
|
||||
PieChart = "pie-chart-16",
|
||||
Pill = "pill-16",
|
||||
Pin = "pin-16",
|
||||
PinDisabled = "pin-disabled-16",
|
||||
Play = "play-16",
|
||||
PlayFilled = "play-filled-16",
|
||||
Plug = "plug-16",
|
||||
Plus = "plus-16",
|
||||
PlusCircle = "plus-circle-16",
|
||||
PlusCircleFilled = "plus-circle-filled-16",
|
||||
PlusMinusDivideMultiply = "plus-minus-divide-multiply-16",
|
||||
Power = "power-16",
|
||||
Print = "print-16",
|
||||
QuestionMark = "question-mark-circle-16",
|
||||
QuestionMarkCircle = "question-mark-circle-16",
|
||||
QuotationMarks = "quotation-marks-16",
|
||||
QuoteBlock = "quote-block-16",
|
||||
Racket = "racket-16",
|
||||
Raindrop = "raindrop-16",
|
||||
RaycastLogoNeg = "raycast-logo-neg-16",
|
||||
RaycastLogoPos = "raycast-logo-pos-16",
|
||||
Receipt = "receipt-16",
|
||||
Redo = "redo-16",
|
||||
RemovePerson = "remove-person-16",
|
||||
Repeat = "repeat-16",
|
||||
Reply = "reply-16",
|
||||
Rewind = "rewind-16",
|
||||
RewindFilled = "rewind-filled-16",
|
||||
Rocket = "rocket-16",
|
||||
Rosette = "rosette-16",
|
||||
RotateAntiClockwise = "rotate-anti-clockwise-16",
|
||||
RotateClockwise = "rotate-clockwise-16",
|
||||
Ruler = "ruler-16",
|
||||
SaveDocument = "save-document-16",
|
||||
Shield = "shield-01-16",
|
||||
Shuffle = "shuffle-16",
|
||||
Sidebar = "app-window-sidebar-right-16",
|
||||
Signal1 = "signal-1-16",
|
||||
Signal2 = "signal-2-16",
|
||||
Signal3 = "signal-3-16",
|
||||
Snippets = "snippets-16",
|
||||
Snowflake = "snowflake-16",
|
||||
SoccerBall = "soccer-ball-16",
|
||||
SpeakerDown = "speaker-down-16",
|
||||
SpeakerHigh = "speaker-high-16",
|
||||
SpeakerLow = "speaker-low-16",
|
||||
SpeakerOff = "speaker-off-16",
|
||||
SpeakerOn = "speaker-on-16",
|
||||
SpeakerUp = "speaker-up-16",
|
||||
SpeechBubble = "speech-bubble-16",
|
||||
SpeechBubbleActive = "speech-bubble-active-16",
|
||||
SpeechBubbleImportant = "speech-bubble-important-16",
|
||||
Star = "star-16",
|
||||
StarCircle = "star-circle-16",
|
||||
StarDisabled = "star-disabled-16",
|
||||
Stars = "stars-16",
|
||||
Stop = "stop-16",
|
||||
StopFilled = "stop-filled-16",
|
||||
Stopwatch = "stopwatch-16",
|
||||
Store = "store-16",
|
||||
StrikeThrough = "strike-through-16",
|
||||
Sun = "sun-16",
|
||||
Sunrise = "sunrise-16",
|
||||
Swatch = "swatch-16",
|
||||
Switch = "switch-16",
|
||||
Syringe = "syringe-16",
|
||||
Tag = "tag-16",
|
||||
Temperature = "temperature-16",
|
||||
TennisBall = "tennis-ball-16",
|
||||
Terminal = "terminal-16",
|
||||
Text = "text-16",
|
||||
TextCursor = "text-cursor-16",
|
||||
Torch = "torch-16",
|
||||
Train = "train-16",
|
||||
Trash = "trash-16",
|
||||
Tray = "tray-16",
|
||||
Tree = "tree-16",
|
||||
Trophy = "trophy-16",
|
||||
TwoPeople = "two-people-16",
|
||||
Umbrella = "umbrella-16",
|
||||
Underline = "underline-16",
|
||||
Undo = "undo-16",
|
||||
Upload = "upload-16",
|
||||
Uppercase = "uppercase-16",
|
||||
Video = "video-16",
|
||||
Wallet = "wallet-16",
|
||||
Wand = "wand-16",
|
||||
Warning = "warning-16",
|
||||
Weights = "weights-16",
|
||||
Wifi = "wifi-16",
|
||||
WifiDisabled = "wifi-disabled-16",
|
||||
Window = "app-window-16",
|
||||
WrenchScrewdriver = "wrench-screwdriver-16",
|
||||
WristWatch = "wrist-watch-16",
|
||||
XMarkCircle = "x-mark-circle-16",
|
||||
XMarkCircleFilled = "x-mark-circle-filled-16",
|
||||
/** @deprecated Use {@link Icon.ArrowClockwise} instead. */
|
||||
TwoArrowsClockwise = "arrow-clockwise-16",
|
||||
/** @deprecated Use {@link Icon.EyeDisabled} instead. */
|
||||
EyeSlash = "eye-disabled-16",
|
||||
/** @deprecated Use {@link Icon.SpeakerDown} instead. */
|
||||
SpeakerArrowDown = "speaker-down-16",
|
||||
/** @deprecated Use {@link Icon.SpeakerUp} instead. */
|
||||
SpeakerArrowUp = "speaker-up-16",
|
||||
/** @deprecated Use {@link Icon.SpeakerOff} instead. */
|
||||
SpeakerSlash = "speaker-off-16",
|
||||
/** @deprecated Use {@link Icon.BlankDocument} instead. */
|
||||
TextDocument = "blank-document-16",
|
||||
/** @deprecated Use {@link Icon.XMarkCircle} instead. */
|
||||
XmarkCircle = "x-mark-circle-16"
|
||||
}
|
70
packages/raycast/host.tsx
Normal file
70
packages/raycast/host.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import module from "module"
|
||||
import { TransformStream } from "stream/web"
|
||||
import { IoInterface, NodeIo, RPCChannel, WebSocketServerIO } from "kkrpc"
|
||||
import react from "react"
|
||||
import reactRuntime from "react/jsx-runtime"
|
||||
import { WebSocketServer } from "ws"
|
||||
import * as raycast from "./api"
|
||||
import reconciler from "./reconciler"
|
||||
|
||||
module.prototype.require = new Proxy(module.prototype.require, {
|
||||
apply(target, thisArg, argumentsList) {
|
||||
const replacements = {
|
||||
react,
|
||||
"react/jsx-runtime": reactRuntime,
|
||||
"@raycast/api": raycast
|
||||
}
|
||||
|
||||
const replacement = replacements[argumentsList[0]]
|
||||
if (replacement) {
|
||||
return replacement
|
||||
}
|
||||
|
||||
return Reflect.apply(target, thisArg, argumentsList)
|
||||
}
|
||||
})
|
||||
|
||||
const { readable, writable } = new TransformStream()
|
||||
const reader = readable.getReader()
|
||||
const writer = writable.getWriter()
|
||||
process.stdin.on("data", (data) => writer.write(data))
|
||||
|
||||
const RPC_METHOD = "stdio"
|
||||
|
||||
if (RPC_METHOD === "stdio") {
|
||||
// const stdio = new NodeIo(process.stdin, process.stdout)
|
||||
const stdio: IoInterface = {
|
||||
name: "stdio",
|
||||
async read() {
|
||||
const { value } = await reader.read()
|
||||
return value
|
||||
},
|
||||
write(data) {
|
||||
return new Promise((resolve) => process.stdout.write(data, () => resolve()))
|
||||
}
|
||||
}
|
||||
init(stdio)
|
||||
} else {
|
||||
const wss = new WebSocketServer({ port: 5000 })
|
||||
console.log("WebSocket server started on ws://localhost:5000")
|
||||
wss.on("connection", (ws) => {
|
||||
const stdio = new WebSocketServerIO(ws)
|
||||
init(stdio)
|
||||
})
|
||||
}
|
||||
|
||||
function init(stdio) {
|
||||
const child = new RPCChannel(stdio)
|
||||
|
||||
const originalCallMethod = child.callMethod
|
||||
child.callMethod = function (method, args) {
|
||||
console.log("callMethod", method, JSON.stringify(args))
|
||||
return originalCallMethod.call(this, method, args)
|
||||
}
|
||||
|
||||
const api = child.getAPI()
|
||||
;(async () => {
|
||||
const app = await import(`${process.cwd()}/dist/index.js`)
|
||||
reconciler.render(<app.default.default></app.default.default>, api)
|
||||
})()
|
||||
}
|
31
packages/raycast/package.json
Normal file
31
packages/raycast/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@kksh/raycast",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./api.tsx",
|
||||
"./host": "./dist/host.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun build --target=node --external=./index.js --format=cjs --outdir=dist ./host.tsx",
|
||||
"dev": "bun run build -- --watch"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kksh/react": "^0.1.1",
|
||||
"@raycast/icons": "^0.4.7",
|
||||
"clsx": "^2.1.1",
|
||||
"kkrpc": "^0.2.2",
|
||||
"react": "^18.0.0",
|
||||
"react-reconciler": "^0.28.0",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/react": "^18.0.10",
|
||||
"@types/react-reconciler": "^0.28.0"
|
||||
}
|
||||
}
|
150
packages/raycast/reconciler.ts
Normal file
150
packages/raycast/reconciler.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import ReactReconciler from "react-reconciler"
|
||||
|
||||
const rootHostContext = {}
|
||||
const childHostContext = {}
|
||||
|
||||
let elementID = 0
|
||||
|
||||
function shallowDiff(oldObj, newObj) {
|
||||
// Return a diff between the new and the old object
|
||||
const uniqueProps = new Set([...Object.keys(oldObj), ...Object.keys(newObj)])
|
||||
const changedProps = Array.from(uniqueProps).filter(
|
||||
(propName) => oldObj[propName] !== newObj[propName]
|
||||
)
|
||||
|
||||
return changedProps.length === 0 ? null : changedProps
|
||||
}
|
||||
|
||||
function makeHostConfig(
|
||||
api
|
||||
): ReactReconciler.HostConfig<string, {}, any, any, any, any, any, any, any, any, any, any, any> {
|
||||
function applyProps(element, props2) {
|
||||
const props = { ...props2 }
|
||||
if (typeof props.children !== "string" && typeof props.children !== "number") {
|
||||
delete props.children
|
||||
}
|
||||
|
||||
for (let [propName, propValue] of Object.entries(props)) {
|
||||
if (propName.startsWith("on")) {
|
||||
api.addEventListener(element, propName.slice(2).toLowerCase(), (event) => {
|
||||
event.preventDefault = () => api.preventDefault(event.id)
|
||||
propValue(event)
|
||||
api.clearEvent(event.id)
|
||||
})
|
||||
|
||||
delete props[propName]
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(props).length > 0) api.applyProps(element, props)
|
||||
}
|
||||
|
||||
return {
|
||||
now: Date.now,
|
||||
getRootHostContext: () => {
|
||||
return rootHostContext
|
||||
},
|
||||
prepareForCommit: () => {},
|
||||
resetAfterCommit: () => {},
|
||||
getChildHostContext: () => {
|
||||
return childHostContext
|
||||
},
|
||||
shouldSetTextContent: (type, props) => {
|
||||
return typeof props.children === "string" || typeof props.children === "number"
|
||||
},
|
||||
/**
|
||||
This is where react-reconciler wants to create an instance of UI element in terms of the target. Since our target here is the DOM, we will create document.createElement and type is the argument that contains the type string like div or img or h1 etc. The initial values of domElement attributes can be set in this function from the newProps argument
|
||||
*/
|
||||
createInstance: (
|
||||
type,
|
||||
newProps,
|
||||
rootContainerInstance,
|
||||
_currentHostContext,
|
||||
workInProgress
|
||||
) => {
|
||||
const id = elementID++
|
||||
// console.log("createInstance", id, type)
|
||||
api.createInstance(id, type)
|
||||
applyProps(id, newProps)
|
||||
return id
|
||||
},
|
||||
createTextInstance: (text) => {
|
||||
const id = elementID++
|
||||
// console.log("createTextInstance", id, text)
|
||||
api.createTextInstance(id, text)
|
||||
return id
|
||||
},
|
||||
appendInitialChild: (parent, child) => {
|
||||
api.appendChild(parent, child)
|
||||
},
|
||||
appendChild(parent, child) {
|
||||
api.appendChild(parent, child)
|
||||
},
|
||||
insertBefore(parentInstance, child, beforeChild) {
|
||||
api.insertBefore(parentInstance, child, beforeChild)
|
||||
},
|
||||
insertInContainerBefore(parentInstance, child, beforeChild) {
|
||||
api.insertBefore(parentInstance, child, beforeChild)
|
||||
},
|
||||
finalizeInitialChildren: (domElement, type, props) => false,
|
||||
supportsMutation: true,
|
||||
appendChildToContainer: (parent, child) => {
|
||||
api.appendChild(parent, child)
|
||||
},
|
||||
prepareUpdate(domElement, type, oldProps, newProps) {
|
||||
// Return a diff between the new and the old props
|
||||
return shallowDiff(oldProps, newProps)
|
||||
},
|
||||
commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
|
||||
const serialized = {}
|
||||
for (const prop of updatePayload) {
|
||||
if (prop === "style") {
|
||||
for (const key of shallowDiff(oldProps.style, newProps.style) ?? []) {
|
||||
serialized.style ??= {}
|
||||
serialized.style[key] = newProps.style[key]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
serialized[prop] = newProps[prop]
|
||||
}
|
||||
|
||||
applyProps(domElement, serialized)
|
||||
},
|
||||
commitTextUpdate(textInstance, oldText, newText) {
|
||||
textInstance.nodeValue = newText
|
||||
},
|
||||
resetTextContent(domElement) {
|
||||
api.setText(domElement, "")
|
||||
},
|
||||
removeChild(parentInstance, child) {
|
||||
api.removeChild(parentInstance, child)
|
||||
},
|
||||
supportsPersistence: false,
|
||||
getPublicInstance() {},
|
||||
preparePortalMount() {},
|
||||
scheduleTimeout: setTimeout,
|
||||
isPrimaryRenderer: true,
|
||||
getCurrentEventPriority() {},
|
||||
getInstanceFromNode() {},
|
||||
beforeActiveInstanceBlur() {},
|
||||
afterActiveInstanceBlur() {},
|
||||
prepareScopeUpdate() {},
|
||||
getInstanceFromScope() {},
|
||||
detachDeletedInstance() {},
|
||||
supportsHydration: false,
|
||||
cancelTimeout: clearTimeout,
|
||||
noTimeout: -1,
|
||||
clearContainer(container) {}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
render: (reactElement, api, callback = undefined) => {
|
||||
const hostConfig = makeHostConfig(api)
|
||||
const ReactReconcilerInst = ReactReconciler(hostConfig)
|
||||
const root = ReactReconcilerInst.createContainer(-1, false)
|
||||
|
||||
// update the root Container
|
||||
return ReactReconcilerInst.updateContainer(reactElement, root, null, callback)
|
||||
}
|
||||
}
|
6
packages/raycast/tsconfig.json
Normal file
6
packages/raycast/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "../typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
1504
pnpm-lock.yaml
generated
1504
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
4
rust-toolchain.toml
Normal file
4
rust-toolchain.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.85"
|
||||
components = [ "rustfmt", "rust-src" ]
|
||||
profile = "minimal"
|
Loading…
Add table
Add a link
Reference in a new issue