diff options
author | Ashelyn Rose <git@ashen.earth> | 2025-02-25 23:48:46 -0700 |
---|---|---|
committer | Ashelyn Rose <git@ashen.earth> | 2025-02-25 23:48:46 -0700 |
commit | bca380fcf242208e375dd509cbd7bcb4d643a400 (patch) | |
tree | 26e3be107f7be09741781733d623c9e6cddc6b3c /ui | |
parent | 29576c7c36f254d6a60b0d599e5f809c636acb4b (diff) |
Diffstat (limited to 'ui')
-rw-r--r-- | ui/src/root.tsx | 17 | ||||
-rw-r--r-- | ui/util/generate-tauri-bindings.js | 131 |
2 files changed, 141 insertions, 7 deletions
diff --git a/ui/src/root.tsx b/ui/src/root.tsx index 13b3021..2026dde 100644 --- a/ui/src/root.tsx +++ b/ui/src/root.tsx @@ -1,26 +1,29 @@ import { useEffect, useState } from 'react'; -import { invoke } from '@tauri-apps/api/core'; +import { Accounts } from '../bindings/' +import { Account } from '../bindings/Account'; export default function Root() { - const [existingAccounts, setExistingAccounts] = useState<any>([]) + const [existingAccounts, setExistingAccounts] = useState<Account[]>([]) const [signedIn, setSignedIn] = useState<{serverDomain: string, username: string} | null>(null) const [accountData, setAccountData] = useState('') async function signIn() { - let [serverDomain, username] = await invoke('start_account_auth', {instanceDomain: 'social.tempest.dev'}) as string[] + let [serverDomain, username] = await Accounts.startAccountAuth('social.tempest.dev') + setSignedIn({serverDomain, username}) } async function getSelf() { if (!signedIn) throw new Error("Not signed in") const {serverDomain, username} = signedIn; - let result = await invoke('get_self', {serverDomain, username}) as string + let result = await Accounts.getSelf(serverDomain, username) setAccountData(JSON.parse(result)) } useEffect(() => { (async () => { - setExistingAccounts(await invoke('get_all_accounts')) + const result = await Accounts.getAllAccounts() + setExistingAccounts(result) })() }, []) @@ -29,9 +32,9 @@ export default function Root() { {!signedIn ? ( <> <p>Existing accounts:</p> - {existingAccounts.map(({username, server_domain}: {username: string, server_domain: string}) => ( + {existingAccounts.map(({username, domain}) => ( <> - <button key={username + server_domain} onClick={() => setSignedIn({serverDomain: server_domain, username})}>@{username}@{server_domain}</button> + <button key={username + domain} onClick={() => setSignedIn({serverDomain: domain, username})}>@{username}@{domain}</button> <br/> </> ))} diff --git a/ui/util/generate-tauri-bindings.js b/ui/util/generate-tauri-bindings.js new file mode 100644 index 0000000..11c8b61 --- /dev/null +++ b/ui/util/generate-tauri-bindings.js @@ -0,0 +1,131 @@ +import path from 'node:path' +import {existsSync, readFileSync, writeFileSync} from 'node:fs' + +const typedefPath = path.join(import.meta.dirname, '../../target/doc/foxfleet_applib.json') +const outputPath = path.join(import.meta.dirname, '../bindings/index.ts') + +if (!existsSync(typedefPath)) { + console.error(`Could not find rustdoc type definitions`) + process.exit(1) +} + +const commandPrefix = '::foxfleet_applib::commands' +const {root, index} = JSON.parse(readFileSync(typedefPath)) + +function traverseModuleForFunctions(startIndex, namespace = '') { + const node = index[startIndex] + + if (!node) return [] + + // If it's a module + if (node.inner?.module) { + const childItems = node.inner.module.items + return childItems.map(childIndex => traverseModuleForFunctions(childIndex, namespace + '::' + node.name)) + } + + // If it's a function + if (node.inner?.function) { + return {...node, namespace} + } + + return [] +} + +function getParameters(functionDef) { + let params = [] + + const skipped = ['tauri::State', 'tauri::AppHandle'] + for (const [name, typedef] of functionDef.inner.function.sig.inputs) { + const typeName = typedef.resolved_path?.name + if (skipped.includes(typeName)) continue + + + const type = typedef.resolved_path ? convertType(typedef.resolved_path) : convertType(typedef.borrowed_ref) + params.push({name, type}) + } + + return params +} + +function getReturnType(functionDef) { + if (functionDef.inner.function.sig.output.resolved_path.name === "Result") + return convertType(functionDef.inner.function.sig.output.resolved_path.args.angle_bracketed.args[0].type.resolved_path) + + return convertType(functionDef.inner.function.sig.output.resolved_path) +} + +let imports = [] + +function convertType(typeDef) { + if (typeof typeDef === 'undefined') return undefined + + const {name, id, args: {angle_bracketed: {args = []} = {}} = {}} = typeDef + + if(args.length && name !== 'Vec') throw new Error(`Unknown generic type ${name}`) + if(name === 'Vec' && !args) throw new Errror(`Unknown inner type for vec`) + + if(name === 'Vec') return convertType(args[0].type.resolved_path) + '[]' + if(name === 'Result') return `Promise<${convertType(args[0].type.resolved_path)}>` + + if(name === 'String' || typeDef?.type?.primitive === 'str') return 'string' + + // Otherwise look up the definition and make sure it has the ![derive(TS)] + const def = index[id] + if (!def.attrs || !def.attrs.includes('#[ts(export)]')) { + throw new Error(`Complex type ${name} does not have Typescript definitions exported`) + } + + imports.push(name) + return name +} + +function convertCamelCase(name) { + return name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) +} + +function convertUpperCamelCase(name) { + return name.replace(/(_|^)([a-z])/g, (_, _first, letter) => letter.toUpperCase()) +} + +const publicFunctions = traverseModuleForFunctions(root).flat(Infinity) +const commandFunctions = publicFunctions + .filter(({namespace}) => namespace.startsWith(commandPrefix)) + .map(({namespace, ...rest}) => ({...rest, namespace: namespace.replace(commandPrefix + '::', '')})) + +const namespaces = {} +for(const func of commandFunctions) { + if(!namespaces[func.namespace]) + namespaces[func.namespace] = [] + + namespaces[func.namespace].push(func) +} + +const invokeLine = `import { invoke } from '@tauri-apps/api/core';` +const namespaceBlocks = Object.keys(namespaces).map(namespace => { + const functions = namespaces[namespace] + const functionBlocks = functions.map(func => { + const params = getParameters(func) + const parametersString = params.map(({name, type}) => `${name}: ${type}`).join(', ') + let returnTypeString = getReturnType(func) + if (!/^Promise<.*>$/.test(returnTypeString)) + returnTypeString = `Promise<${returnTypeString}>` + + return ` + export async function ${convertCamelCase(func.name)}(${parametersString}): ${returnTypeString} { + return await invoke('${func.name}', {${params.map(param => convertCamelCase(param.name) + ': ' + param.name).join(', ')}}) as any as ${returnTypeString} + }` + }) + + return `export namespace ${convertUpperCamelCase(namespace)} {\n${functionBlocks.join('\n')}\n}` +}) +// This needs to be last so it gets populated by the above calls +const importLines = imports.map(type => `import type {${type}} from './${type}.ts'`) + +const fileData = ` +${invokeLine} +${importLines} + +${namespaceBlocks.join('\n\n')} +` + +writeFileSync(outputPath, fileData) |