This is the full developer documentation for Kunkun
# Kunkun
> An open source, extensible, cross-platform app launcher. Alternative to Alfred and Raycast.
import { Card, CardGrid } from "@astrojs/starlight/components";
Supports macOS, Windows, and Linux. Extensions are designed to be
write-once, run-anywhere.
Create custom extensions using JavaScript. An extension store is available
for sharing and discovering extensions. You can also install extensions
freely from any source.
Extensions are designed with controlled access to your system, requesting
only the permissions necessary for their operation.
You have the ability to examine both application and extension code.
# Creation of Kunkun
> Why did I create Kunkun?
> Kunkun is designed to be a cross-platform and open source alternative for apps like Alfred, Raycast, uTools. I will discuss them in this blog.
These are very successful and popular products that I have used and loved. Kunkun was created to solve some of the problems I had with these products.
## Discussion
### Cross-platform
- Alfred and Raycast are macOS only
- uTools is cross-platform but doesn't seem to have good extension support for Linux
Mac native apps usually look better than that of the native apps on Windows and Linux.
However I really wish all good apps were cross-platform.
I use all three operating systems and I want to use the same app on all of them.
Electron (used by uTools) is a popular choice for cross-platform apps, but it is controversial because of its resource usage.
Bundling a whole chromium browser and NodeJS runtime for each Electron app doesn't sound like a good idea to me.
I don't like running multiple browsers simultaneously.
### Security Concerns
- **Closed Source Applications**: All three products are closed source. Given that these applications typically require access to the entire disk, an open source alternative would be preferable for transparency and security.
- **Trust in Extensions**: When we install an application, we trust it, but do we really trust the extensions we install? Installing an extension is significantly easier than installing a regular application. Installing extensions from the extension store of an app we trust may let our guard down. You may trust the app developers, but do you trust the extension developers?
- **NodeJS Runtime Risks**: Raycast and uTools extensions run in a NodeJS runtime. Unlike Deno, NodeJS lacks a built-in sandbox to restrict access to the file system, network, etc.
- **Potential Threats**: Extensions theoretically have the same capabilities as the user. They can read/write files, steal SSH private keys from `~/.ssh`, or even grant remote access to an attacker as the extension can execute any script in NodeJS runtime.
- **NodeJS as a Double-Edged Sword**: Using NodeJS makes it easy to write powerful extensions, but it also increases the risk of exposing users to security threats.
- **Raycast’s Approach**: Raycast mitigates this issue by open-sourcing all community extensions for public review. While open-sourcing is a good practice, it is not a perfect or scalable long-term solution. Open source does not inherently guarantee security; many well-known open source projects have significant vulnerabilities.
- **Scalability and Review Challenges**: As of now, Raycast has over 1600 extensions, and the number is growing. It is challenging to review all these extensions thoroughly. Regular users lack the time and expertise to scrutinize the code of each extension they install, relying instead on the community's vigilance.
- **Proposed Solution**: There needs to be a technical solution to this problem, akin to the permission systems in mobile operating systems. Extensions should only access the resources that the user has explicitly granted permission to, utilizing a sandboxing mechanism.
### Resource Usage
#### Bundle Size
| App | Installer Size | Installed App Size |
| ------- | -------------- | ------------------ |
| Alfred | 5.5MB | 17MB |
| Raycast | 99.5MB | 124MB |
| uTools | 93MB | 220MB |
The size is acceptable given they are extendable. The size of Alfred is actually surprisingly small given its features.
uTools is based on Electron which enables it to be cross-platform, no wonder it is so big.
I don't understand why raycast is so big as a native app. Its NodeJS runtime is not even bundled into the app itself. I remember the older version of raycast was much smaller.
### UI
- Alfred and Raycast are both native Mac apps, and look "Apple". Which is good.
- Native apps are usually more performant and look better (Mac Apps).
- However, I believe web is the future. Browser is the most important/successful cross-platform app. There are so many apps that doesn't need to be native.
- Web/JS Ecosystem is probably the largest and most active ecosystem. One can create pretty much any complex UI with web technologies. Animation, 3D, etc.
- Web apps are easy to develop and deploy. If I need to build native apps for all platforms, I have to be familiar with at least Swift, C++, C#, Java. Web, only JS.
- Most importantly, for such apps, extension ecosystem is very important, and JS is the most popular language for extensions. VSCode is a good example.
- Although Raycast extensioins are written with React/JS, the app itself is not web-based. React is used as a templating engine. The benefits of web technologies are not fully utilized.
### HCI
I love the design of Raycast, which offers a hands-on keyboard experience similar to Vim. I also appreciate the flexibility of uTools, which leverages web technologies to provide a customizable UI.
Using web technologies gives developers the freedom to tailor the UI, but this can lead to inconsistent experiences across extensions since different developers may have varying design skills. Raycast, on the other hand, has a more "Apple-style" extension system where developers use a provided React template components. The app then renders native components through reconciliation, ensuring a consistent UI and interaction, but at the cost of flexibility.
Alfred's GUI for creating workflows is fantastic, especially for non-developers. It allows anyone to create their own workflows without needing programming knowledge.
Overall, balancing the flexibility of web technologies with the consistency of native rendering can greatly enhance the user experience, making the app both versatile and user-friendly.
## My Vision
I used to use Ubuntu and Windows as my main machine, and one of the main reasons I use Mac as my main machine was Alfred.
I didn't find another app as good as Alfred on Linux and Windows.
Then I found Raycast, and it's now my favorite app on Mac. It's pretty much perfect for me, but I still need to use Windows and Linux.
What I want is,
- Cross-platform (Linux, Windows, Mac) like uTools
- Open source
- Beautiful UI like Raycast
- Secure (extensions restricted by a permission system)
- Small bundle size (don't bundle a whole browser)
- Extensible (Allow users to write extensions in JS)
- Support Raycast-style extensions (developers create extensions following a provided template) for consistent UI and interaction
- Flexible UI: Support web-based extensions (any SPA/SSG/CSR web apps can be converted into an extension) with maximum UI flexibility
- This means any existing open source web apps can be converted into an extension with a few lines of code.
When I explain my app to non-tech friends, I usually use the example of WeChat Mini Programs. Although I'm not a fan of WeChat, I have to admit that WeChat Mini Programs are a brilliant idea. They represent the largest-scale extension ecosystem I know of, used in everyday life by non-tech people. WeChat Mini Programs are like apps, but they are not installed. They are web apps that run within the WeChat app, offering good integration with WeChat to enable a seamless user experience.
I want to create an app that functions like WeChat Mini Programs, but for the desktop. There are many desktop apps that don't need to be native. For example, image/video converters, todo apps, file finders, video downloaders, and encoder/decoder tools for developers can all be implemented as lightweight, integrated extensions within a single desktop app, providing a unified and efficient user experience.
- If you build a native app, you have to know the native language of the platform. If you build a cross-platform app with Electron, do you really want to consume serveral hundred MBs of your precious disk space for each app (Mac disk space is like gold)?
- To implement a very simple feature native apps with GUI usually cost at least a few MBs. The smallest Electron app costs at least ~100MB. The same features can be implemented in Kunkun with as low as 40KB.
- Tauri or Wails are good alternatives, they are small (as low as a few MB), based on web, and cross-platform, but developers have to build the entire app from scratch, which is a little more difficult than building a web app. Code signing, promotion, advertisement, etc. takes a lot of effort, and could discourage developers to write a handy tool (Remember? Developers need to pay Apple tax to code sign Mac apps, even if the app is free).
- Desktop apps lives and runs directly on the OS. Web apps run in browsers. Browsers restrict the access to the OS. What if you can install a web app as a desktop app, with access to system APIs (under a permission system of course)? That's what I want to achieve with Kunkun (Kind of like PWA, but PWA also have many limitations as of now and has a different HCI design from app launchers). I know this sounds dangerous, browsers restrict access to the OS for security reasons. There is always a trade-off between security and functionality. But I believe it is possible to create a secure system that allows web apps to access the OS in a secure way. No need to be scared. Browser extensions are a good example. They can access the OS, but they are sandboxed.
## Tech Stack
- Desktop
- Framework: Tauri
- Frontend: Sveltekit
- Backend: Rust
- Frontend
- Tailwind CSS
- shadcn-svelte
- Backend
- Server: Axum + Tonic (grpc)
- Web Server
- Supabase: open source Firebase alternative
- Web Apps
- Astro + Starlight: Documentation Site
- Cloudflare pages: web app hosting
- Dev
- pnpm workspace
- GitHub Actions (CI/CD)
- turbo repo (monorepo management)
- `bun`
- `rollup`
### Explanation
- **Tauri**
- **Lightweight & Cross-Platform:** Tauri provides a lightweight, cross-platform framework using web technologies.
- **Rust Backend:** Rust is fast and memory-safe, though challenging to write. It is the hardest high-level language I've encountered.
- **Consideration for Wails:** Wails is another good alternative. Golang is much easier than rust. For this kind of app, Golang's GC is not a problem. I picked Tauri since Wails is not as mature as Tauri. Multi-window support is not stable yet. I may switch to Wails in the future if one day it becomes as good as Tauri.
- Tauri v2 has a window-based permission system. Although this is not the only way to implement a permission system, this feature is very important for Kunkun. I may use this feature to restrict access of extensions to the OS.
- **Sveltekit** is my favorite frontend stack. I am familiar with React, Vue and Svelte.
- In terms of ecosystem, my ranking is React > Vue > Svelte.
- In terms of the framework/language itself, my ranking is Svelte > Vue > React.
- There is no perfect framework, and there is always a trade-off. I picked Vue + Nuxt initially because it has a mature ecosystem and is easy to use. However, I then faced many problems with Nuxt when building a Tauri app, and eventually switched to Sveltekit, which I found to be the best choice for me.
- When I was using Nuxt, its auto import feature sounds very nice, but could be very inconvenient for debugging. When there is import error with a nuxt module, I don't know where is happens because all imports happen in background. I had to remove all nuxt modules and add them back one by one to see which one is causing the problem. I never had to do this again after switching to sveltekit.
- I always choose a **meta-framework** (Next, Nuxt, Sveltekit) rather than the vanilla framework when building a Tauri app. Metaframeworks provide lots of useful features out of the box. SSR isn't feasible for a desktop app, but other features like file-based routing, layouts, SSG, etc. are still useful. When an SPA grows, I have to do code splitting, lazy loading, etc. manually. A metaframework does this for me.
- React is probably the most future proof. If you are unsure what library you will need in the future, React is a good choice. Most UI libraries support React or have a React version.
- But I didn't have a good experience with React and especially Next.js when writing Tauri apps. The double mounting from React 18 is so annoying. `useEffect` doesn't support async. Tauri APIs are mostly async. A listener is usually obtained with `unlisten = await listen("event_name")`. I had to write a lot more boilerplate code in React to avoid listening twice than with Vue or Svelte. Next.js throws so many errors when I was building a Tauri app (I don't remember what they were but I wasted so much time on them rather than the app logic). I also have to keep thinking about using `use client;`. In Nuxt and Sveltekit, I never need to think about whether I am in the client or server (when building a pure frontend app).
- **Rust** is nice. Fast and memory safe. Extremely hard to write, slow to build. Huge cache. (60GB accumulated for Kunkun). `node_modules` is not `heavy` at all compring to rust `target`. `target` is the heaviest object in the universe. I don't get it. I use rust because of Tauri. If Wails is as good as Tauri, I will happily switch to use Golang.
- **TailwindCSS**: I don't think I need to explain this.
- **Shadcn-svelte**: a svelte port from the original react shadcn. I like the styling of Shadcn and philosophy of letting user customize the components. And, I plan to support any SSG/CSR web app as an extension, which means any JS frameworks are supported. Shadcn is based on React, and there is ports for Vue and Svelte, with the same styling. It's simpler to keep extension UI styling consistent with the app UI styling if shadcn is used.
- Axum+Tonic is probably the best choice in rust to support regular HTTP server and gRPC server together. I want to use gRPC because of its proto can guarantee the compatibility between the client and server. Reflection server is also easy for development, kind of like graphql.
# Convert neohtop to Extension
[neohtop](https://github.com/Abdenasser/neohtop) is a beautiful desktop version of the htop command.
> A modern, cross-platform system monitor built on top of Svelte, Rust, and Tauri.
In this blog, I will talk about how I converted neohtop to an extension.
It maybe simpler than you think.
## TC;SC
- A demo PR: https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files
- The extension: https://kunkun.sh/store/neohtop
- The repo: https://github.com/kunkunsh/kunkun-ext-neohtop
Here is a screenshot of the extension, exactly the same as the desktop version.

## Intro
[neohtop](https://github.com/Abdenasser/neohtop) is a Tauri app. It's frontend is built with Svelte, and backend is built with Rust.
> One of the reasons I created kunkun is to make it easy to write cross-platform apps and distribute them on different platforms.
>
> And neohtop is a perfect example for this.
It's a nice GUI wrapper over the [`sysinfo`](https://crates.io/crates/sysinfo) crate.
See its [Cargo.toml](https://github.com/Abdenasser/neohtop/blob/8269d6f95a41df5fd31b175f26113b1692b6163c/src-tauri/Cargo.toml#L15).
Luckily, Kunkun's extension API also provides a `sysInfo` API, see [docs](/developer/api/sysinfo/).
It is based on [tauri-plugin-system-info](https://github.com/HuakunShen/tauri-plugin-system-info), which is also a wrapper over the [`sysinfo`](https://crates.io/crates/sysinfo) crate. See [Cargo.toml](https://github.com/HuakunShen/tauri-plugin-system-info/blob/cb32fe842b8ba9a893a2aa4d6850b9a81516e7aa/Cargo.toml#L21).
Since the data source are the same, then it should be straightforward to convert it to an extension.
I set a 30 minutes goal for myself, but it took me 90 minutes to complete the conversion.
This is because `neohtop` did its data aggregation and preprocessing in rust, and I need to convert the logic to TypeScript.
Kind of like how TypeScript is converted to Golang, I just needed to convert the rust code to TypeScript, not hard at all, just time-consuming.
## Code
In this [PR](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files), I demonstrated all the changes I made for the conversion.
First of all, neohtop is written in frontend framework svelte, so obviously this is a custom UI extension.
### Manifest
[`package.json`](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519)
```json title="package.json" showLineNumbers {10-11} {20-22}
"kunkun": {
"name": "neohtop",
"shortDescription": "A modern, cross-platform system monitor",
"longDescription": "A modern, cross-platform system monitor",
"identifier": "neohtop",
"demoImages": [
"https://imgur.com/D8VHDEz.png"
],
"permissions": [
"system-info:all",
"shell:kill-any"
],
"icon": {
"type": "remote-url",
"value": "https://github.com/Abdenasser/neohtop/raw/main/app-icon.png"
},
"customUiCmds": [
{
"name": "neohtop",
"main": "/",
"dist": "build",
"devMain": "http://localhost:1420",
"cmds": [],
"window": {
"title": "Neohtop",
"hiddenTitle": true,
"titleBarStyle": "overlay",
"width": 1400,
"height": 1000
}
}
]
},
```
- `build` folder is the default output folder of svelte projects, thus `dist` is set to `build`.
- `devMain` is the local server that serves the svelte project, it is used when developing the extension with HMR.
- `system-info:all` permission is required to access the system info data.
- `shell:kill-any` permission is required to kill any process by a `pid`.
### Frontend
neohtop has no default window title bar, but users can drag the window by clicking on the top of the app.
This is achieved by simply including `data-tauri-drag-region` in a `div` element.
```html
```
In a KK extension, we use `data-kunkun-drag-region` instead, just remember to call `ui.registerDragRegion` in app initialization.
import { Code } from "@astrojs/starlight/components";
export const exampleCode = `console.log('This could come from a file or CMS!');`;
export const fileName = "example.js";
export const highlights = ["file", "CMS"];
{/* */}
```diff lang="svelte" title="TitleBar.svelte"
-
+
```
```diff lang="ts" title="+page.svelte"
+ import { ui } from "@kksh/api/ui/custom";
+ onMount(() => {
+ ui.registerDragRegion("kunkun");
+ });
```
- [TitleBar.svelte](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-5b930dbf5fac4f54096c3e2bc4e0a320b89a6010da94963ce000193b929e29a5)
- [+page.svelte](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-1e1270da740e81dd13f8a65057582b0fce8a5fb3817ef913f3e90d7f82c4e9dfR76)
### Data Preprocessing/Aggregation
> This is the most time-consuming part.
The source system info data is preprocessed before rendering.
The preprocessing logic is written in Rust, thus I need to convert them to TypeScript.
| neohtop (Rust) | kunkun-ext-neohtop (TypeScript) |
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [process_monitor.rs](https://github.com/Abdenasser/neohtop/blob/8269d6f95a41df5fd31b175f26113b1692b6163c/src-tauri/src/monitoring/process_monitor.rs#L19) | [processMonitor.ts](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-875f3104224e0084f31fd16597f39627b5bb41359aaa82ae34d408e90cd708d7) |
| [system_monitor.rs](https://github.com/Abdenasser/neohtop/blob/8269d6f95a41df5fd31b175f26113b1692b6163c/src-tauri/src/monitoring/system_monitor.rs#L19) | [systemMonitor.ts](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-e49245b092e4c4cac8ce0c2f7754bdc520c6bd16a1f8a1c1633554c86192ca95) |
## Acknowledgements
- [Abdenasser](https://github.com/Abdenasser) for the awesome [neohtop](https://github.com/Abdenasser/neohtop)
# Tauri + Drizzle Proxy
GitHub: https://github.com/HuakunShen/tauri-demo/tree/master/examples/drizzle-sqlite-proxy
> [!info]
> This demo let you use drizzle to control your sqlite DB in a Tauri app, without any sidecar.
> This is a Tauri v2 reproduction for the archived repo [https://github.com/tdwesten/tauri-drizzle-sqlite-proxy-demo](https://github.com/tdwesten/tauri-drizzle-sqlite-proxy-demo)
Tauri's backend is in Rust, so I always thought the only way to use sqlite ORM in a Tauri app is with projects like [diesel](https://diesel.rs/) or [prisma-client-rust](https://github.com/Brendonovich/prisma-client-rust), which could be hard because they are in rust.
What is even harder is sqlite db encryption. Both of the 2 ORMs don't support cipher encryption, thus I had to write raw sql queries in #kunkun [Example](https://github.com/kunkunsh/kunkun/blob/c39e98258c3bf287e17ce3d3d143459b590d0976/packages/db/src/lib.rs#L119)
Maintaining raw sql queries is a nightmare to me, especially when it comes to schema migration.
I have to rely on thorough testing to make sure everything is correct. There is no type checking.
I never knew it's possible to use TypeScript ORM like drizzle in Tauri without a sidecar.
In my project [[Projects/kkrpc|kkrpc]] I implemented a Tauri adapter and made it possible to use compiled TypeScript backend as a sidecar in a Tauri app. With kkrpc, it's easy to call TypeScript backend from frontend. I can use any TypeScript libraries in a compiled deno/bun/node binary, including drizzle.
However that introduces at least 60MB to the bundle size. It's a good deal if you can take advantage of many node packages, but not really worth it for just DB.
Then I found this project https://github.com/tdwesten/tauri-drizzle-sqlite-proxy-demo using drizzle proxy to send queries to Tauri's sql plugin.
Drizzle Proxy Docs: https://orm.drizzle.team/docs/connect-drizzle-proxy
Basically, it's a translator between the [tauri-plugin-sql](https://crates.io/crates/tauri-plugin-sql) and drizzle ORM.
drizzle computes the sql query in frontend and send the query + params to backend to execute.
The backend can be a http server, or Tauri core.
```ts
// Example of driver implementation
import { drizzle } from 'drizzle-orm/pg-proxy';
const db = drizzle(async (sql, params, method) => {
try {
const rows = await axios.post('http://localhost:3000/query', { sql, params, method });
return { rows: rows.data };
} catch (e: any) {
console.error('Error from pg proxy server: ', e.response.data)
return { rows: [] };
}
});
```
Here is real code
[Excalidraw Diagrams](https://excalidraw.com/#json=qGv0x2Lrxuj1hjT8WFaw-,W3_F55HzeA6QtgbmsPqJ6g)

```ts
import { drizzle } from "drizzle-orm/sqlite-proxy";
import Database from "@tauri-apps/plugin-sql";
import * as schema from "./schema";
export async function getDb() {
return await Database.load("sqlite:test.db");
}
export const db = drizzle(
async (sql, params, method) => {
const sqlite = await getDb();
let rows: any = [];
let results = [];
// If the query is a SELECT, use the select method
if (isSelectQuery(sql)) {
rows = await sqlite.select(sql, params).catch((e) => {
console.error("SQL Error:", e);
return [];
});
} else {
// Otherwise, use the execute method
rows = await sqlite.execute(sql, params).catch((e) => {
console.error("SQL Error:", e);
return [];
});
return { rows: [] };
}
rows = rows.map((row: any) => {
return Object.values(row);
});
// If the method is "all", return all rows
results = method === "all" ? rows : rows[0];
await sqlite.close();
return { rows: results };
},
// Pass the schema to the drizzle instance
{ schema: schema, logger: true }
);
```
And here is how it can be used (just like regular drizzle code)
```ts
const loadUsers = async () => {
db.query.users
.findMany()
.execute()
.then((results) => {
console.log("🚀 ~ FindMany response from Drizzle:", results);
users = results;
});
};
async function addUser() {
await db.insert(schema.users).values({ name: nameInput });
nameInput = "";
loadUsers();
}
```
```mermaid
graph TD
A[Frontend JavaScript/TypeScript] --> B[Drizzle ORM]
B --> C[Drizzle Proxy]
C --> D[Tauri SQL Plugin]
D --> E[SQLite Database]
subgraph "Frontend JavaScript/TypeScript"
A
B
C
end
subgraph "Backend Rust"
D
E
end
F[SQL Query + Params] -.Generated by Drizzle.-> C
C -.Translates and forwards.-> D
D -.Executes query.-> E
E -.Returns results.-> D
D -.Returns data.-> C
C -.Formats for ORM.-> B
B -.Type-safe results.-> A
```
# Kunkun API
[](https://www.npmjs.com/package/@kksh/api)
:::note
### Acknowledgement
A lot of the APIs provided in `@kksh/api` are alias of Tauri's official APIs, or inspired by Tauri's APIs.
:::
[Kunkun API](https://www.npmjs.com/package/@kksh/api) is an NPM package designed for developers to create extensions for KK.
It provides a set of APIs to interact with KK and access system resources.
As of now, there are 2 sandboxed environments to run the extension: web worker and iframe.
APIs for the two environments are almost identical.
I made separate subpackages for them for clarity and make tree-shaking easier.
The APIs are exposed in the following subpackages:
- `@kksh/api/ui/custom`
- `@kksh/api/ui/template`
The `/ui` subpackage means they are designed for UI extensions (i.e. extension has GUI).
This is to distinguish from "headless" extensions for non-UI extensions (which I plan to develop in the future).
See [API Reference](/api-docs) for more details.
https://docs.api.kunkun.sh/ is the generated API documentation with a full list of APIs and their type definitions.
## Playground
Here is a code editor playground with `@kksh/api` installed. You can try out the API,
but don't try to run it, as the API is not designed to run in the browser.
The sample code here is only for template extensions running in web worker environment.
APIs are imported from `@kksh/api/ui/template`.
The custom UI extension running in iframe have similar APIs, imported from `@kksh/api/ui/custom`.
The playground is only to give you a taste of how the API are called and what kind of APIs are available.
For more details and examples, please keep read the following pages.
## API Docs
https://docs.api.kunkun.sh/
# App
> This API provides information about the application
**Docs:**: https://docs.api.kunkun.sh/interfaces/index.IApp
## API
- `language()`: Get the language preference
import ImportCodeBlock from '@/components/api/import-code-block.astro'
```ts
app.language().then((lang) => {
console.log("Language:", lang);
});
```
# Clipboard
**Docs:** https://docs.api.kunkun.sh/interfaces/index.IClipboard
import ClipboardAPI from '@/components/api/clipboard.astro'
## API and Permissions
## Sample Usage
import ImportCodeBlock from '@/components/api/import-code-block.astro'
```ts
// Check if the clipboard has data
const hasFiles: boolean = await clipboard.hasFiles()
const hasHTML: boolean = await clipboard.hasHTML()
const hasImage: boolean = await clipboard.hasImage()
const hasRTF: boolean = await clipboard.hasRTF()
const hasText: boolean = await clipboard.hasText()
// Read data from the clipboard
const text: string = await clipboard.readText()
const imageBinary: Blob = await clipboard.readImageBinary('Blob') as Blob
const imageBase64: string = await clipboard.readImageBase64()
const html: string = await clipboard.readHtml()
const files: string[] = await clipboard.readFiles()
const rtf: string = await clipboard.readRtf()
// Write data to the clipboard
await clipboard.writeText('Hello, World!')
await clipboard.writeImageBinary(Array.from(new Uint8Array(await imageBinary.arrayBuffer())));
await clipboard.writeHtmlAndText('
Hello, World!
', 'Hello, World!')
await clipboard.writeFiles(['file1.txt', 'file2.txt'])
await clipboard.writeRtf(rtf)
await clipboard.writeImageBase64(imageBase64)
```
:::note
Clipboard API taken from https://github.com/CrossCopy/tauri-plugin-clipboard
:::
# Database
**Docs:** https://docs.api.kunkun.sh/interfaces/index.IDb
Database API is for extensions that need to store data in a database.
Please avoid using browser storage like `localStorage` to avoid data collisions with other extensions and data leaks.
**Database API doesn't require any permissions, but one extension can only access its own data.**
This database is based on sqlite, for a key-value style storage, see [KV API](/developer/api/kv).
The following APIs are now available:
- `add`
- `delete`
- `search`
- `retrieveAll`
- `retrieveAllByType`
- `deleteAll`
- `update`
CRUD are the basic operations provided by the database API. Let's talk about them one by one.
## Add
There are 3 parameters for each data entry:
- `data`
- Any data you want to store.
- `dataType`
- An arbitrary string to categorize the data.
- `searchText`
- A string to search the data. You can use this to store preview data as well.
- This field is searchable via sqlite full-text search.
Only `data` is required. `dataType` and `searchText` are optional, and everything is stored as a string.
You need to use `JSON.stringify` to store objects, and `JSON.parse` to deserialize them.
> Later I may provide a way to store objects directly, but for now, you need to serialize them.
import ImportCodeBlock from '@/components/api/import-code-block.astro'
```ts
await db.add({
data: JSON.stringify({ name: "Kunkun" }),
dataType: "preference",
searchText: "preference",
});
```
:::caution
Since the database API is not a key-value store. `dataType` isn't a unique key.
Running `.add()` multiple times with the same data will create multiple entries.
If you need to make sure there is a single entry, please use the [KV API](/developer/api/kv) instead.
:::
## Read/Search
There are multiple APIs to read data from the database.
- `db.retrieveAll` returns all data entries for the extension.
Sometimes the extension may store large amount of data in the `data` field, while you may only need the `searchText`.
So a `fields` parameter is provided to specify which fields you want to retrieve.
- `db.retrieveAllByType` returns all data entries by `dataType`.
- `db.search` run search query within the database instead of retrieving all data and filtering them with JS.
```ts
const allData = await db.retrieveAll({
fields: ["data", "search_text"],
});
const result = await db.search({ dataType: "preference" });
const result = await db.search({ dataType: "preference" });
```
### Search
`db.search` API supports searching on multiple fields. Use your IDE to go to the definition of `search()` to see the full type definition.
```ts
enum SQLSortOrder {
ASC = "ASC",
DESC = "DESC",
}
type SearchParams = {
dataId?: number;
fullTextSearch?: boolean;
dataType?: string;
searchText?: string;
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
limit?: number;
orderByCreatedAt?: SQLSortOrder;
orderByUpdatedAt?: SQLSortOrder;
fields?: ExtDataField[];
};
```
- `dataId` is the unique ID of the data entry.
- `fullTextSearch` is a boolean to enable full-text search on the `searchText` field.
- `limit` is the maximum number of results to return.
- `fields` is an array of fields to return.
## Update
You need to provide the `dataId` to update the data.
```ts
await db.update({ dataId: 803, data: "new data", searchText: "new data" });
```
## Delete
You need to provide the `dataId` to delete the data.
```ts
await db.delete(803);
```
# Deno
> Deno provides an API to run JavaScript/TypeScript in non-browser environment with more access to system resources,
> but still within a sandbox.
:::tip
Deno API is part of the shell API and works similarly but provides a easier abstraction, you may want to read [shell API](/developer/api/shell) first.
:::
## API and Permissions
All [Deno permissions](https://docs.deno.com/runtime/fundamentals/security/) must be explicitly declared in `package.json`.
`DenoCommand` works similarly to a regular [shell Command](/developer/api/shell#command). It can be created with `shell.createDenoCommand`.
The returned `DenoCommand` object has `execute` and `spawn` methods (just like regular `Command`).
To run a Deno command, you need one of the the following scoped permissions:
- `shell:deno:execute`
- `shell:deno:spawn`
Read [sample code](#sample-code) for more details.
### RPC Library Style API
> If you're looking to run Deno as a library for resource-intensive tasks or those requiring system access, the Deno API offers a solution. You can execute Deno scripts using `shell.createDenoCommand`, passing input through `argv` and `stdin`, and retrieving results via `stdout` and `stderr`. This approach essentially treats Deno like a CLI, as illustrated in an example (Transform Image with Sharp) provided below.
> However, using a CLI is not always ideal for those who prefer calling scripts directly like a typical library. The `@kksh/api` solves this by offering an abstraction in RPC style, allowing Deno scripts to be invoked as libraries rather than CLIs.
The shell API provides a function `createDenoRpcChannel` that creates a Deno RPC API.
```ts
function createDenoRpcChannel(
scriptPath: string,
args: string[],
config: Partial & SpawnOptions,
localAPIImplementation: LocalAPI
): Promise>
// ...
}
```
Then you can call the remote API functions directly.
- Since this is a bidirectional IPC channel, you can pass `LocalAPI` and `RemoteAPI` generics to define the API.
- `LocalAPI` is what you want to expose to the other side of the channel, and `RemoteAPI` is what you want to call from the other side.
- `args` are `argv` like regular commands
- `config` is the same as config in `shell.createDenoCommand`, used for configuring deno permissions and other options.
- `localAPIImplementation` is the implementation of the API you want to expose, pass `{}` if you don't want to expose any API.
#### Debug Log in Deno
:::caution
This RPC style API is implemented with `@kunkun/kkrpc` package, see https://jsr.io/@kunkun/kkrpc
It's using `stdin` and `stdout` to communicate under the hood.
If your Deno script needs to print log, use `console.error` to log to `stderr` instead of `console.log` to `stdout`.
All `stdout` will be parsed by `kkrpc` protocol, if you print something to `stdout`, it could potentially break the protocol and cause undefined behavior.
If an imported package logs to `stdout` and `kkrpc` cannot parse it, it will log an error to console, but the extension will not crash.
:::
To show `stderr` in the console, you have to manually listen to the `stderr` event.
```ts
const { rpcChannel, process, command } = await shell.createDenoRpcChannel