mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-09-13 09:26:22 +00:00
feat: basic sidecar functionality for rendering
This commit is contained in:
parent
dac77c76b7
commit
91c1b4e811
14 changed files with 3305 additions and 94 deletions
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
sidecar/plugin-host.js
|
|
@ -15,7 +15,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "~2.2.2",
|
"@tauri-apps/plugin-clipboard-manager": "~2.2.2",
|
||||||
"@tauri-apps/plugin-opener": "^2"
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-shell": "~2.2.1",
|
||||||
|
"msgpackr": "^1.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@internationalized/date": "^3.8.2",
|
"@internationalized/date": "^3.8.2",
|
||||||
|
@ -26,6 +28,7 @@
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/node": "^24.0.0",
|
||||||
"bits-ui": "^2.5.0",
|
"bits-ui": "^2.5.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
|
@ -35,7 +38,8 @@
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.3"
|
"vite": "^6.0.3",
|
||||||
|
"vite-plugin-node-polyfills": "^0.23.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|
1367
pnpm-lock.yaml
generated
1367
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
2
sidecar/.gitignore
vendored
Normal file
2
sidecar/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
plugin-host.js
|
||||||
|
dist/
|
27
sidecar/package.json
Normal file
27
sidecar/package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "sidecar",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "plugin-host.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "esbuild plugin-host.ts --bundle --outfile=plugin-host.js --platform=node && pkg plugin-host.js --output app --public && pnpm rename",
|
||||||
|
"rename": "mv \"app$([[ \"$OSTYPE\" =~ msys|win32 ]] && echo .exe)\" \"../src-tauri/binaries/app-$(rustc -vV | awk '/host:/ {print $2}')$([[ \"$OSTYPE\" =~ msys|win32 ]] && echo .exe)\""
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"packageManager": "pnpm@10.11.1",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.0.0",
|
||||||
|
"@types/react": "^19.1.7",
|
||||||
|
"@types/react-reconciler": "^0.32.0",
|
||||||
|
"@yao-pkg/pkg": "^6.5.1",
|
||||||
|
"esbuild": "^0.25.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"msgpackr": "^1.11.4",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-reconciler": "^0.32.0"
|
||||||
|
}
|
||||||
|
}
|
413
sidecar/plugin-host.ts
Normal file
413
sidecar/plugin-host.ts
Normal file
|
@ -0,0 +1,413 @@
|
||||||
|
import { createInterface } from "readline";
|
||||||
|
import React from "react";
|
||||||
|
import Reconciler from "react-reconciler";
|
||||||
|
import { jsx } from "react/jsx-runtime";
|
||||||
|
import plugin from "./dist/emoji.txt";
|
||||||
|
import { pack, PackrStream } from "msgpackr";
|
||||||
|
|
||||||
|
let commitBuffer: { type: string; payload: any }[] = [];
|
||||||
|
|
||||||
|
const sendingStream = new PackrStream();
|
||||||
|
sendingStream.pipe(process.stdout);
|
||||||
|
|
||||||
|
function writeOutput(data: object) {
|
||||||
|
function removeSymbols(obj: object) {
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(removeSymbols);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newObj = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
newObj[key] = removeSymbols(obj[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const symKey of Object.getOwnPropertySymbols(obj)) {
|
||||||
|
// do nothing if the key is a symbol
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in newObj) {
|
||||||
|
if (typeof newObj[key] === "symbol") {
|
||||||
|
newObj[key] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = pack(removeSymbols(data));
|
||||||
|
const header = Buffer.alloc(4);
|
||||||
|
header.writeUInt32BE(payload.length);
|
||||||
|
|
||||||
|
process.stdout.write(header);
|
||||||
|
process.stdout.write(payload);
|
||||||
|
} catch (e) {
|
||||||
|
writeOutput({ type: "log", payload: e.toString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLog(message) {
|
||||||
|
writeOutput({ type: "log", payload: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason, promise) => {
|
||||||
|
writeLog(`--- UNHANDLED PROMISE REJECTION ---`);
|
||||||
|
writeLog(reason.stack || reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
let instanceCounter = 0;
|
||||||
|
const instances: Map<number, any> = new Map();
|
||||||
|
|
||||||
|
function serializeProps(props) {
|
||||||
|
const serializable = {};
|
||||||
|
for (const key in props) {
|
||||||
|
if (key === "children" || typeof props[key] === "function") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSON.stringify(props[key]);
|
||||||
|
serializable[key] = props[key];
|
||||||
|
} catch (error) {
|
||||||
|
serializable[key] = `[Circular Reference in prop '${key}']`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serializable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostConfig = {
|
||||||
|
getPublicInstance(instance) {
|
||||||
|
return instance;
|
||||||
|
},
|
||||||
|
getRootHostContext(rootContainerInstance) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
getChildHostContext(parentHostContext, type, rootContainerInstance) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
createInstance(
|
||||||
|
type,
|
||||||
|
props,
|
||||||
|
rootContainer,
|
||||||
|
hostContext,
|
||||||
|
internalInstanceHandle
|
||||||
|
) {
|
||||||
|
const componentType =
|
||||||
|
typeof type === "string" ? type : type.name || "Anonymous";
|
||||||
|
const id = ++instanceCounter;
|
||||||
|
const stateNode = {
|
||||||
|
id,
|
||||||
|
type: componentType,
|
||||||
|
children: [],
|
||||||
|
props: serializeProps(props),
|
||||||
|
_internalFiber: internalInstanceHandle,
|
||||||
|
};
|
||||||
|
internalInstanceHandle.stateNode = stateNode;
|
||||||
|
instances.set(id, stateNode);
|
||||||
|
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "CREATE_INSTANCE",
|
||||||
|
payload: { id, type: componentType, props: stateNode.props },
|
||||||
|
});
|
||||||
|
return stateNode;
|
||||||
|
},
|
||||||
|
|
||||||
|
createTextInstance(text) {
|
||||||
|
const id = ++instanceCounter;
|
||||||
|
const instance = { id, type: "TEXT", text };
|
||||||
|
instances.set(id, instance);
|
||||||
|
|
||||||
|
commitBuffer.push({ type: "CREATE_TEXT_INSTANCE", payload: instance });
|
||||||
|
return instance;
|
||||||
|
},
|
||||||
|
|
||||||
|
appendInitialChild(parentInstance, child) {
|
||||||
|
parentInstance.children.push(child);
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "APPEND_CHILD",
|
||||||
|
payload: { parentId: parentInstance.id, childId: child.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
appendChild(parentInstance, child) {
|
||||||
|
parentInstance.children.push(child);
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "APPEND_CHILD",
|
||||||
|
payload: { parentId: parentInstance.id, childId: child.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
appendChildToContainer(container, child) {
|
||||||
|
container.children.push(child);
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "APPEND_CHILD",
|
||||||
|
payload: { parentId: container.id, childId: child.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
insertBefore(parentInstance, child, beforeChild) {
|
||||||
|
const index = parentInstance.children.findIndex(
|
||||||
|
(c) => c.id === beforeChild.id
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
parentInstance.children.splice(index, 0, child);
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "INSERT_BEFORE",
|
||||||
|
payload: {
|
||||||
|
parentId: parentInstance.id,
|
||||||
|
childId: child.id,
|
||||||
|
beforeId: beforeChild.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.appendChild(parentInstance, child);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
insertInContainerBefore(container, child, beforeChild) {
|
||||||
|
this.insertBefore(container, child, beforeChild);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeChild(parentInstance, child) {
|
||||||
|
parentInstance.children = parentInstance.children.filter(
|
||||||
|
(c) => c.id !== child.id
|
||||||
|
);
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "REMOVE_CHILD",
|
||||||
|
payload: { parentId: parentInstance.id, childId: child.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeChildFromContainer(container, child) {
|
||||||
|
container.children = container.children.filter((c) => c.id !== child.id);
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "REMOVE_CHILD",
|
||||||
|
payload: { parentId: container.id, childId: child.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
commitUpdate(stateNode, updatePayload, type, oldProps, newProps) {
|
||||||
|
stateNode.props = serializeProps(newProps);
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "UPDATE_PROPS",
|
||||||
|
payload: { id: stateNode.id, props: serializeProps(stateNode.props) },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareForCommit: () => null,
|
||||||
|
resetAfterCommit: (container) => {
|
||||||
|
if (commitBuffer.length > 0) {
|
||||||
|
writeOutput({
|
||||||
|
type: "BATCH_UPDATE",
|
||||||
|
payload: commitBuffer,
|
||||||
|
});
|
||||||
|
commitBuffer = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
finalizeInitialChildren: () => false,
|
||||||
|
prepareUpdate: () => true,
|
||||||
|
shouldSetTextContent: () => false,
|
||||||
|
clearContainer: (container) => {
|
||||||
|
container.children = [];
|
||||||
|
commitBuffer.push({
|
||||||
|
type: "CLEAR_CONTAINER",
|
||||||
|
payload: { containerId: container.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
detachDeletedInstance: () => {},
|
||||||
|
|
||||||
|
now: Date.now,
|
||||||
|
scheduleTimeout: setTimeout,
|
||||||
|
cancelTimeout: clearTimeout,
|
||||||
|
noTimeout: -1,
|
||||||
|
getCurrentUpdatePriority: () => 1,
|
||||||
|
setCurrentUpdatePriority: () => {},
|
||||||
|
resolveUpdatePriority: () => 1,
|
||||||
|
maySuspendCommit: () => false,
|
||||||
|
|
||||||
|
supportsMutation: true,
|
||||||
|
isPrimaryRenderer: true,
|
||||||
|
supportsPersistence: false,
|
||||||
|
supportsHydration: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const reconciler = Reconciler(hostConfig);
|
||||||
|
|
||||||
|
const createPluginRequire = () => (moduleName) => {
|
||||||
|
if (moduleName === "react") {
|
||||||
|
return React;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleName.startsWith("@raycast/api")) {
|
||||||
|
writeLog(`Plugin requested @raycast/api`);
|
||||||
|
|
||||||
|
const storage = new Map();
|
||||||
|
const LocalStorage = {
|
||||||
|
getItem: async (key) => storage.get(key),
|
||||||
|
setItem: async (key, value) => storage.set(key, value),
|
||||||
|
removeItem: async (key) => storage.delete(key),
|
||||||
|
clear: async () => storage.clear(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListComponent = ({ children, ...rest }) =>
|
||||||
|
jsx("List", { ...rest, children });
|
||||||
|
const ListSectionComponent = ({ children, ...rest }) =>
|
||||||
|
jsx("ListSection", { ...rest, children });
|
||||||
|
const ListDropdownComponent = ({ children, ...rest }) =>
|
||||||
|
jsx("ListDropdown", { ...rest, children });
|
||||||
|
const ActionPanelComponent = ({ children, ...rest }) =>
|
||||||
|
jsx("ActionPanel", { ...rest, children });
|
||||||
|
const ActionPanelSectionComponent = ({ children, ...rest }) =>
|
||||||
|
jsx("ActionPanelSection", { ...rest, children });
|
||||||
|
|
||||||
|
ListComponent.Item = "ListItem";
|
||||||
|
ListComponent.Section = ListSectionComponent;
|
||||||
|
ListComponent.Dropdown = ListDropdownComponent;
|
||||||
|
ListDropdownComponent.Item = "ListDropdownItem";
|
||||||
|
ActionPanelComponent.Section = ActionPanelSectionComponent;
|
||||||
|
|
||||||
|
return {
|
||||||
|
LocalStorage,
|
||||||
|
environment: {
|
||||||
|
assetsPath: "/home/byte/code/raycast-linux/sidecar/dist/assets/",
|
||||||
|
},
|
||||||
|
getPreferenceValues: () => ({
|
||||||
|
primaryAction: "paste",
|
||||||
|
unicodeVersion: "14.0",
|
||||||
|
shortCodes: true,
|
||||||
|
}),
|
||||||
|
usePersistentState: (key, initialValue) => {
|
||||||
|
const [state, setState] = React.useState(initialValue);
|
||||||
|
const isLoading = false;
|
||||||
|
return [state, setState, isLoading];
|
||||||
|
},
|
||||||
|
List: ListComponent,
|
||||||
|
ActionPanel: ActionPanelComponent,
|
||||||
|
Action: {
|
||||||
|
Paste: "Action.Paste",
|
||||||
|
CopyToClipboard: "Action.CopyToClipboard",
|
||||||
|
OpenInBrowser: "Action.OpenInBrowser",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return require(moduleName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = { id: "root", children: [] };
|
||||||
|
|
||||||
|
const onUncaughtError = (error, errorInfo) => {
|
||||||
|
writeLog(`--- REACT UNCAUGHT ERROR ---`);
|
||||||
|
writeLog(`Error: ${error.message}`);
|
||||||
|
if (errorInfo && errorInfo.componentStack) {
|
||||||
|
writeLog(
|
||||||
|
`Stack: ${errorInfo.componentStack.trim().replace(/\n/g, "\n ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCaughtError = (error, errorInfo) => {
|
||||||
|
writeLog(`--- REACT CAUGHT ERROR ---`);
|
||||||
|
writeLog(`Error: ${error.message}`);
|
||||||
|
if (errorInfo && errorInfo.componentStack) {
|
||||||
|
writeLog(
|
||||||
|
`Stack: ${errorInfo.componentStack.trim().replace(/\n/g, "\n ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRecoverableError = (error, errorInfo) => {
|
||||||
|
writeLog(`--- REACT RECOVERABLE ERROR ---`);
|
||||||
|
writeLog(`Error: ${error.message}`);
|
||||||
|
if (errorInfo && errorInfo.componentStack) {
|
||||||
|
writeLog(
|
||||||
|
`Stack: ${errorInfo.componentStack.trim().replace(/\n/g, "\n ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = reconciler.createContainer(
|
||||||
|
root,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
onUncaughtError,
|
||||||
|
onCaughtError,
|
||||||
|
onRecoverableError
|
||||||
|
);
|
||||||
|
|
||||||
|
function runPlugin() {
|
||||||
|
const scriptText = plugin;
|
||||||
|
const pluginModule = { exports: {} } as {
|
||||||
|
exports: {
|
||||||
|
default: null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const scriptFunction = new Function(
|
||||||
|
"require",
|
||||||
|
"module",
|
||||||
|
"exports",
|
||||||
|
"React",
|
||||||
|
scriptText
|
||||||
|
);
|
||||||
|
|
||||||
|
scriptFunction(
|
||||||
|
createPluginRequire(),
|
||||||
|
pluginModule,
|
||||||
|
pluginModule.exports,
|
||||||
|
React
|
||||||
|
);
|
||||||
|
|
||||||
|
const PluginRootComponent = pluginModule.exports.default;
|
||||||
|
|
||||||
|
if (PluginRootComponent) {
|
||||||
|
writeLog("Plugin loaded. Initializing React render...");
|
||||||
|
const AppElement = React.createElement(PluginRootComponent);
|
||||||
|
reconciler.updateContainer(AppElement, container, null, () => {
|
||||||
|
writeLog("Initial render complete.");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Plugin did not export a default component.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rl = createInterface({ input: process.stdin });
|
||||||
|
writeLog("Node.js Sidecar started successfully with React Reconciler.");
|
||||||
|
|
||||||
|
rl.on("line", (line) => {
|
||||||
|
try {
|
||||||
|
const command = JSON.parse(line);
|
||||||
|
if (command.action === "run-plugin") {
|
||||||
|
runPlugin();
|
||||||
|
} else if (command.action === "dispatch-event") {
|
||||||
|
const { instanceId, handlerName, args } = command.payload;
|
||||||
|
writeLog(
|
||||||
|
`Event received: instance ${instanceId}, handler ${handlerName}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const stateNode = instances.get(instanceId);
|
||||||
|
if (!stateNode) {
|
||||||
|
writeLog(`Instance ${instanceId} not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = stateNode._internalFiber.memoizedProps;
|
||||||
|
|
||||||
|
if (props && typeof props[handlerName] === "function") {
|
||||||
|
props[handlerName](...args);
|
||||||
|
} else {
|
||||||
|
writeLog(`Handler ${handlerName} not found on instance ${instanceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
writeLog(`ERROR: ${err.message} \n ${err.stack}`);
|
||||||
|
writeOutput({ type: "error", payload: err.message });
|
||||||
|
}
|
||||||
|
});
|
1267
sidecar/pnpm-lock.yaml
generated
Normal file
1267
sidecar/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
41
src-tauri/Cargo.lock
generated
41
src-tauri/Cargo.lock
generated
|
@ -880,6 +880,15 @@ version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "endi"
|
name = "endi"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -2992,6 +3001,7 @@ dependencies = [
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-shell",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3364,6 +3374,16 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shared_child"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e297bd52991bbe0686c086957bee142f13df85d1e79b0b21630a99d374ae9dc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -3803,6 +3823,27 @@ dependencies = [
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-shell"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"log",
|
||||||
|
"open",
|
||||||
|
"os_pipe",
|
||||||
|
"regex",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"shared_child",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
|
|
|
@ -23,4 +23,5 @@ tauri-plugin-opener = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tauri-plugin-clipboard-manager = "2"
|
tauri-plugin-clipboard-manager = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,16 @@
|
||||||
"clipboard-manager:default",
|
"clipboard-manager:default",
|
||||||
"clipboard-manager:allow-write-text",
|
"clipboard-manager:allow-write-text",
|
||||||
"clipboard-manager:allow-write-html",
|
"clipboard-manager:allow-write-html",
|
||||||
"clipboard-manager:allow-write-image"
|
"clipboard-manager:allow-write-image",
|
||||||
|
"shell:allow-stdin-write",
|
||||||
|
{
|
||||||
|
"identifier": "shell:allow-spawn",
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"name": "binaries/app",
|
||||||
|
"sidecar": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
]
|
],
|
||||||
|
"externalBin": ["binaries/app"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,242 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ResultsList from "$lib/components/ResultsList.svelte";
|
import { Command, type Child } from "@tauri-apps/plugin-shell";
|
||||||
import Toast from "$lib/components/Toast.svelte";
|
import { SvelteMap } from "svelte/reactivity";
|
||||||
import { Input } from "$lib/components/ui/input";
|
import { pack, unpack } from "msgpackr";
|
||||||
|
|
||||||
async function run() {
|
interface UINode {
|
||||||
const { runPlugin } = await import("$lib");
|
id: number;
|
||||||
|
type: string;
|
||||||
|
props: Record<string, any>;
|
||||||
|
children: number[];
|
||||||
|
}
|
||||||
|
|
||||||
await runPlugin();
|
let uiTree: SvelteMap<number, UINode> = $state(new SvelteMap());
|
||||||
|
let rootNodeId: number | null = $state(null);
|
||||||
|
let sidecarLogs: string[] = $state([]);
|
||||||
|
let sidecarChild: Child | null = null;
|
||||||
|
|
||||||
|
$inspect(uiTree.get(1)?.children);
|
||||||
|
|
||||||
|
// For debugging, to see updates happen
|
||||||
|
let updateCounter = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
let receiveBuffer = Buffer.alloc(0); // Buffer to store incoming data chunks
|
||||||
|
|
||||||
|
// This function tries to parse full messages from the buffer
|
||||||
|
function processReceiveBuffer() {
|
||||||
|
// As long as there's enough data for a header, try to process it
|
||||||
|
while (receiveBuffer.length >= 4) {
|
||||||
|
const messageLength = receiveBuffer.readUInt32BE(0);
|
||||||
|
const totalLength = 4 + messageLength;
|
||||||
|
|
||||||
|
// Do we have the full message yet?
|
||||||
|
if (receiveBuffer.length >= totalLength) {
|
||||||
|
// Yes: extract the message payload
|
||||||
|
const messagePayload = receiveBuffer.subarray(4, totalLength);
|
||||||
|
|
||||||
|
// CRITICAL: Update the buffer to remove the message we just processed
|
||||||
|
receiveBuffer = receiveBuffer.subarray(totalLength);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = unpack(messagePayload);
|
||||||
|
handleSidecarMessage(message);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to unpack sidecar message:", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No: break the loop and wait for more data
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectAndRun() {
|
||||||
|
const command = Command.sidecar("binaries/app", undefined, {
|
||||||
|
encoding: "raw",
|
||||||
|
});
|
||||||
|
|
||||||
|
command.stdout.on("data", (chunk) => {
|
||||||
|
try {
|
||||||
|
receiveBuffer = Buffer.concat([receiveBuffer, Buffer.from(chunk)]);
|
||||||
|
processReceiveBuffer();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse sidecar message:", chunk, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
command.stderr.on("data", (line) => {
|
||||||
|
// Using a function to update the array is safer for state
|
||||||
|
sidecarLogs = [...sidecarLogs, `STDERR: ${line}`];
|
||||||
|
});
|
||||||
|
|
||||||
|
sidecarChild = await command.spawn();
|
||||||
|
sidecarLogs = [
|
||||||
|
...sidecarLogs,
|
||||||
|
`Sidecar spawned with PID: ${sidecarChild.pid}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (sidecarChild) {
|
||||||
|
sidecarChild.write(JSON.stringify({ action: "run-plugin" }) + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectAndRun();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log("Component unmounting, killing sidecar...");
|
||||||
|
sidecarChild?.kill();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendToSidecar(message: object) {
|
||||||
|
if (sidecarChild) {
|
||||||
|
sidecarChild.write(JSON.stringify(message) + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a single command from the sidecar. This function is designed
|
||||||
|
* to be called either for a standalone message or within a batched update.
|
||||||
|
*/
|
||||||
|
function processSingleCommand(command: any) {
|
||||||
|
switch (command.type) {
|
||||||
|
case "log":
|
||||||
|
console.log("SIDECAR:", command.payload);
|
||||||
|
sidecarLogs = [...sidecarLogs, command.payload];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "CREATE_TEXT_INSTANCE":
|
||||||
|
case "CREATE_INSTANCE": {
|
||||||
|
const { id, type, props } = command.payload;
|
||||||
|
uiTree.set(id, { id, type, props, children: [] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "UPDATE_PROPS": {
|
||||||
|
const { id, props } = command.payload;
|
||||||
|
const node = uiTree.get(id);
|
||||||
|
if (node) {
|
||||||
|
const updatedNode = { ...node, props: { ...node.props, ...props } };
|
||||||
|
uiTree.set(id, updatedNode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "APPEND_CHILD": {
|
||||||
|
const { parentId, childId } = command.payload;
|
||||||
|
if (parentId === "root") {
|
||||||
|
rootNodeId = childId;
|
||||||
|
} else {
|
||||||
|
const parentNode = uiTree.get(parentId);
|
||||||
|
if (parentNode) {
|
||||||
|
const newChildren = parentNode.children.filter(
|
||||||
|
(id) => id !== childId
|
||||||
|
);
|
||||||
|
newChildren.push(childId);
|
||||||
|
uiTree.set(parentId, { ...parentNode, children: newChildren });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "REMOVE_CHILD": {
|
||||||
|
const { parentId, childId } = command.payload;
|
||||||
|
const parentNode = uiTree.get(parentId);
|
||||||
|
if (parentNode) {
|
||||||
|
const newChildren = parentNode.children.filter(
|
||||||
|
(id) => id !== childId
|
||||||
|
);
|
||||||
|
uiTree.set(parentId, { ...parentNode, children: newChildren });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "INSERT_BEFORE": {
|
||||||
|
const { parentId, childId, beforeId } = command.payload;
|
||||||
|
const parentNode = uiTree.get(parentId);
|
||||||
|
if (parentNode) {
|
||||||
|
const cleanChildren = parentNode.children.filter(
|
||||||
|
(id) => id !== childId
|
||||||
|
);
|
||||||
|
|
||||||
|
const index = cleanChildren.indexOf(beforeId);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
cleanChildren.splice(index, 0, childId);
|
||||||
|
uiTree.set(parentId, { ...parentNode, children: cleanChildren });
|
||||||
|
} else {
|
||||||
|
cleanChildren.push(childId);
|
||||||
|
uiTree.set(parentId, { ...parentNode, children: cleanChildren });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main message handler. It checks for a BATCH_UPDATE and processes it
|
||||||
|
* efficiently, or processes single messages otherwise.
|
||||||
|
*/
|
||||||
|
function handleSidecarMessage(message: any) {
|
||||||
|
updateCounter++;
|
||||||
|
|
||||||
|
if (message.type === "BATCH_UPDATE") {
|
||||||
|
for (const command of message.payload) {
|
||||||
|
processSingleCommand(command);
|
||||||
|
}
|
||||||
|
console.log(message.payload.length);
|
||||||
|
} else {
|
||||||
|
processSingleCommand(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchEvent(instanceId: number, handlerName: string, args: any[]) {
|
||||||
|
sendToSidecar({
|
||||||
|
action: "dispatch-event",
|
||||||
|
payload: { instanceId, handlerName, args },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col items-stretch h-screen">
|
<main class="flex flex-grow flex-col">
|
||||||
<Input
|
{#if rootNodeId}
|
||||||
class="rounded-none border-0 border-b focus-visible:border-b-foreground transition-colors duration-75"
|
{@const rootNode = uiTree.get(rootNodeId)}
|
||||||
oninput={run}
|
{#if rootNode?.type === "List"}
|
||||||
/>
|
<div class="flex h-full flex-col">
|
||||||
<div class="grow">
|
<input
|
||||||
<ResultsList />
|
type="text"
|
||||||
</div>
|
class="w-full border-b border-gray-300 px-4 py-3 text-lg focus:border-blue-500 focus:outline-none"
|
||||||
|
placeholder="Search Emojis..."
|
||||||
<Toast />
|
oninput={(e) =>
|
||||||
</div>
|
dispatchEvent(rootNode.id, "onSearchTextChange", [
|
||||||
|
e.currentTarget.value,
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
<div class="flex-grow overflow-y-auto">
|
||||||
|
{#each rootNode.children as childId (childId)}
|
||||||
|
{@const childNode = uiTree.get(childId)}
|
||||||
|
{#if childNode?.type === "ListSection"}
|
||||||
|
<div>
|
||||||
|
<h3
|
||||||
|
class="px-4 pb-1 pt-2.5 text-xs font-semibold uppercase text-gray-500"
|
||||||
|
>
|
||||||
|
{childNode.props.title}
|
||||||
|
</h3>
|
||||||
|
{#each childNode.children as itemId (itemId)}
|
||||||
|
{@const itemNode = uiTree.get(itemId)}
|
||||||
|
{#if itemNode?.type === "ListItem"}
|
||||||
|
<div class="flex items-center gap-3 px-4 py-2">
|
||||||
|
<span class="text-lg">{itemNode.props.icon}</span>
|
||||||
|
<span>{itemNode.props.title}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
|
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
||||||
|
|
||||||
// @ts-expect-error process is a nodejs global
|
// @ts-expect-error process is a nodejs global
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [tailwindcss(), sveltekit()],
|
plugins: [tailwindcss(), sveltekit(), nodePolyfills()],
|
||||||
|
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue