Compare commits

...

35 Commits

Author SHA1 Message Date
Huakun Shen
3542eec277 feat: add GitHub Actions workflow for desktop test builds 2025-04-03 16:46:40 -04:00
Huakun
de3886d416
feat: implement clipboard data cleaning for data older than 10 days (#267)
* Update drizzle-orm to version 0.41.0 and implement clipboard cleanup functionality

- Updated drizzle-orm dependency in package.json and pnpm-lock.yaml to version 0.41.0.
- Added a new utility function `cleanClipboard` to remove clipboard entries older than 10 days.
- Integrated clipboard cleanup into the initialization process, logging success or failure.
- Refactored drizzle exports to include `proxyDB` for better access to the database proxy.
- Minor cleanup in the proxy.ts file to remove commented-out debug logs.

* Refactor clipboard cleanup logic to use configurable days parameter

- Introduced a variable `nDays` to allow dynamic adjustment of the clipboard cleanup threshold.
- Updated logging to reflect the configurable number of days for clipboard entry deletion instead of a hardcoded value.

* Enhance clipboard and database management in initialization process

- Added logging to `cleanClipboard` to indicate the number of clipboard entries older than a specified number of days.
- Introduced a new utility function `vacuumSqlite` for database maintenance, which is now called during app initialization.
- Updated the `init` function to await the completion of `cleanClipboard` and `vacuumSqlite` for better error handling and flow control.
- Ensured that the console attachment in `onMount` is awaited for proper synchronization.

* Update version in package.json from 0.1.37-beta.1 to 0.1.37

* Adds C11 standard flag for builds

Try to fix windows build beta CI
Adds the C11 standard flag to the build environment. This ensures
that the code is compiled using the C11 standard, potentially
improving compatibility and avoiding compiler-specific behavior.

* Remove CFLAGS for C11 standard from beta build workflow

This change eliminates the CFLAGS environment variable previously set for C11 standard compliance in the beta build process, streamlining the build configuration.
2025-04-03 12:42:27 -04:00
Huakun
bb9a46935c
Feature: add drizzle (#264)
* feat: add drizzle orm

* feat: update drizzle configuration and schema management

- Added a check for DB_FILE_NAME in drizzle.config.ts to ensure it's set.
- Updated package.json to change the package name to @kksh/drizzle and added exports for schema and relations.
- Enhanced README.md with instructions for using the schema generation.
- Refactored schema.ts for improved readability and organization of imports.

* add tauri-plugin-sql

* feat: add database select and execute commands

- Introduced `select` and `execute` functions in the database module to facilitate querying and executing SQL commands.
- Updated the Tauri plugin to expose these commands, allowing for database interactions from the frontend.
- Added corresponding permissions for the new commands in the permissions configuration.
- Enhanced the database library with JSON value handling for query parameters.

* fix: sqlite select command

* drizzle ORM verified working

* refactor: clean up database module by removing unused SelectQueryResult type and disabling eslint for explicit any usage

* pnpm lock update

* Update enum definition for type safety

- Changed enum to use 'as const' for better type inference
- Ensured more robust handling of extension publish sources

* reimplemented most db command functions with ORM (migrate from tauri command invoke

* fixed searchExtensionData orm function

* Refactor ORM commands and searchExtensionData function for improved readability and consistency

- Reformatted import statements for better organization.
- Cleaned up whitespace and indentation in searchExtensionData function.
- Enhanced readability of SQL conditions and query building logic.
- Disabled eslint for explicit any usage in the troubleshooters page.

* Fix test assertions in database module to use array indexing for results

format rust code

* update deno lock

* move drizzle from desktop to drizzle package

* update pnpm version and lock

* refactor: migrate db tauri commands to drizzle

* refactor: remove unused extension and command CRUD operations from db module
2025-04-01 06:15:10 -04:00
Huakun Shen
bf51fdadbc
Update version to 0.1.37-beta.1 and add loading animation translations for multiple languages 2025-03-28 08:54:18 -04:00
Huakun
9cf06b1835
Feature: custom transition animation (#266)
* Add loading animation to general settings

* Update dependencies and integrate @tauri-store/svelte

- Added `bon` and `bon-macros` packages to Cargo.lock.
- Upgraded `tauri-plugin-svelte`, `tauri-store`, and related packages to their latest versions.
- Updated `@tauri-store/svelte` integration in the desktop app, including changes to app configuration and layout handling.
- Adjusted pnpm-lock.yaml to reflect updated package versions and added new dependencies.
- Introduced a new app configuration file for development.

* Enhance loading animation handling in FullScreenLoading component

- Integrated conditional rendering for loading animations based on app configuration.
- Updated default loading animation to "kunkun-dancing" in app configuration.
- Adjusted general settings to ensure proper type handling for language labels.
- Modified ui-iframe component to manage full-screen loading state more effectively.

* remove a mis-placed config file

* Refactor window handling to ensure focus after showing

- Updated various components to use promise chaining with `show()` and `setFocus()` for better window management.
- Introduced `data.win` in multiple places to streamline access to the current webview window.
- Enhanced splashscreen and app layout handling to improve user experience by ensuring the window is focused after being shown.

* Refactor window handling to improve safety and consistency

- Introduced optional chaining for `data.win` to prevent potential runtime errors when accessing window methods.
- Updated various components to ensure proper handling of window focus and visibility.
- Enhanced the layout and extension pages to utilize the current webview window more effectively, improving overall user experience.
2025-03-28 07:45:25 -04:00
Huakun
48e2e47f96
Remove supabase (#263)
* remove supabase package

* upgrade valibot

* removed supabase package

Migration not complete yet

* update submodule

* fixed some supabase errors

* Add new fields to extension models

- Added `id` field to `ExtPublish`
- Expanded `DBExtension` with multiple new properties:
  - `api_version`, `author_id`, `created_at`,
  - `downloads`, `icon`, `identifier`,
  - `long_description`, `name`,
  - `readme`, `short_description`,
  - and `tarball_size`

* Refactor: clean up unused Supabase imports

- Removed commented-out Supabase imports from various files to streamline the codebase.
- Updated `created_at` type in `ExtPublish` model from `date` to `string` for consistency.

* update icon enum to union

* fix type errors after removing supabase

* format

* more types fixed

* feat: enhance command handling and update SDK version
2025-03-26 08:50:55 -04:00
Huakun
9fe51f6260
Feat: gitea mirror (#262)
* Update component props and add GitLab link

- Made `ref` prop optional in TauriLink component
- Added GitLab mirror URL to GitHubProvenanceCard
- Included a link to the GitLab mirror in the card layout
- Adjusted layout for StoreExtDetail component for better responsiveness
- Imported Tooltip component for potential future use

* chore: add parse-github-url dependency and update GitHub parsing logic

- Added `parse-github-url` package as a dependency in `package.json`.
- Updated `parseGitHubRepoFromUri` function to utilize `parse-github-url` for improved URI parsing.
- Introduced `getGitHubRepoMetadata` function to fetch repository metadata using Octokit.
- Updated validation data structure to include optional `repoId`.
- Enhanced tests to cover new functionality and error handling for invalid URIs.

* fix typo

* refactor: update validation data structure and improve function documentation

- Removed optional `repoId` from `ExtensionPublishValidationData` and adjusted related function to reflect this change.
- Added a note in the `validateJsrPackageAsKunkunExtension` function documentation to clarify frontend/backend verification logic.
- Updated `ExtPublishMetadata` to rename `repoId` to `repoNodeId` for clarity.

* refactor: remove GitLab mirror link from GitHubProvenanceCard

- Removed the GitLab mirror URL and its associated link from the GitHubProvenanceCard component.
- Commented out the layout for the GitLab mirror instead of deleting it, preserving the structure for potential future use.

* refactor: simplify GitHub repository URI parsing

- Removed dependency on `parse-github-url` and implemented a regex-based approach for parsing GitHub repository URIs in the `parseGitHubRepoFromUri` function.
- Enhanced error handling for invalid URIs while maintaining the function's output structure.

* feat: add Gitea mirror link to GitHubProvenanceCard

- Introduced a new link to the Gitea mirror repository in the GitHubProvenanceCard component.
- Updated the layout to reflect the new mirror link while removing the commented-out GitLab mirror section.

* refactor: enhance Globe component's location handling

- Updated the Globe component to conditionally render markers based on the provided locations prop.
- Simplified the destructuring of props for better readability.
- Retained default marker locations for cases where no locations are provided.

* pnpm lock
2025-03-26 01:08:16 -04:00
Huakun Shen
7759e615dd
Merge remote-tracking branch 'origin/develop' into develop 2025-03-23 10:26:22 -04:00
Huakun Shen
11226ee2ef
fix: jsr API for cloudflare worker env
Without this header will get html format instead of json in cf worker
2025-03-23 10:26:18 -04:00
Huakun
c39e98258c
Fix: kkrpc serialization backward compatibility (#256)
* update deno lock

* chore: update kkrpc and tauri-api-adapter versions, enhance serialization handling

- Bump kkrpc version to 0.2.2 in multiple packages including desktop and api.
- Update tauri-api-adapter version to 0.3.27.
- Introduce a new utility function to determine kkrpc serialization based on API version.
- Refactor RPC channel initialization to include serialization version in desktop extension handling.
- Increment desktop package version to 0.1.36 and api package version to 0.1.7.

* chore: update dependencies in pnpm-lock and package.json

- Upgrade postcss version for autoprefixer to 8.5.3 in pnpm-lock.yaml.
- Add semver package with version 7.7.1 in package.json.
- Update CHANGELOG.md to reflect recent kkrpc upgrades and changes.
2025-03-19 03:01:40 -04:00
Huakun Shen
d27731d0e6
hotfix: hard coded debug logic in shell spawn API 2025-03-18 21:09:44 -04:00
Huakun
0bca6739a7
[feat] New sysinfo api, update dep (#251)
* update tauri-plugin-system-info submodule to latest commit cb32fe8

* Update dependencies to latest versions, including valibot to 1.0.0-rc.4 and kkrpc to 0.2.1 across multiple packages. Bump api package version to 0.1.6.

* Update desktop and API package versions; change development server port and URL
2025-03-18 08:42:39 -04:00
Joel Stüdle
993e276e72
add translations for german (de) (#249) 2025-03-17 01:14:12 -04:00
Huakun
310969e597
UI Updates (#246)
* minor ui updates to shiki

* feat: add markdown renderer

* feat(ui): add scroll area component and expand markdown renderer

* feat(ui): expand markdown syntax highlighting with additional language support

* feat(ui): add markdown language support to syntax highlighting

* feat(ui): update markdown syntax highlighting theme to GitHub Dark Default

* feat(ui): add bash language support to markdown syntax highlighting

* feat: add globe component

* Change RetroGrid bg color

* feat: add headless command list to store detail component

* feat: update markdown renderer

Replace svelte-markdown with svelte-exmarkdown, with custom tauri link renderer and code highlight support

* format and fix eslint
2025-03-13 21:30:52 -04:00
Huakun
cd7301255b
perf: remove deno and ffmpeg instruction from onboard; use tauri-plugin-svelte store (#245) 2025-03-13 21:06:33 -04:00
Huakun
b4afcaac6c
UI updates/fixes (#244)
* fix: change icon in manifest

* refactor(ui): move BorderBeam component to ui package and update imports

* feat(ui): add new animation components and keyframes utility

* chore(deps): remove svelte-motion and related dependencies

* chore(deps): add svelte-motion and related dependencies

* fix(ui): eslint

* fix: extension store demo image display

* fix(ui): go to settings item in dropdown menu

* format
2025-03-10 13:59:19 -04:00
Huakun
234f245a9c
Improve: add global loading screen (#237)
* refactor(desktop): move ext loading code in store from +page.ts to +page.svelte

try to solve blank screen on slow network

* Revert "refactor(desktop): move ext loading code in store from +page.ts to +page.svelte"

This reverts commit 4a0a695ce615cee695849c64746ba569680ff8c4.

* feat(desktop): add full-screen loading state and border beam animation

- Implement full-screen loading component with BorderBeam animation
- Add fullScreenLoading flag to appState store
- Update extension store pages to use full-screen loading
- Add border beam animation to Tailwind config
- Enhance page loading experience with visual feedback

* feat(desktop): add dance animation to loading screen and update imports

- Add Dance component to FullScreenLoading with subtle background effect
- Remove unused fade transition import from layout
- Update lz-string import in utils to use default import
- Clean up compress test imports

* feat(desktop): add back button to full-screen loading component

- Import ArrowLeftIcon and Constants from @kksh/ui
- Add back button with absolute positioning
- Remove "Go Home" text button
- Enhance loading screen with improved navigation

* refactor(desktop): update BorderBeam component to use Svelte 5 runes
2025-03-07 13:00:15 -05:00
Huakun Shen
cc7cea7fe9
fix(desktop): add extension store item selection handler
fix issue https://github.com/kunkunsh/kunkun/issues/233
2025-03-03 21:31:24 -05:00
Huakun
90ba943fb6
fix: windows app detect (#231)
* fix: applications-rs upgrade submodule

* chore(desktop): bump package version to 0.1.33
2025-03-03 08:41:05 -05:00
Huakun
6ffc6f1543
fix(api): update matchPathAndScope (#229)
Translate windows style back slash to posix style slash in order for minimatch to work.
https://www.npmjs.com/package/minimatch#windows
2025-03-03 05:22:20 -05:00
Huakun
2cbe45f6d1
feat(desktop): improve app icon handling for cross-platform support (#230)
- Add platform-specific icon path selection for Windows
- Enhance app command item rendering with dynamic icon resolution
- Modify icon loading to use applications-rs for Windows icon extraction
- Update Rust icon loading utility to provide more robust error handling
2025-03-03 05:22:08 -05:00
Huakun
a42d4d97eb
Fix: windows powershell window (#228)
* chore: bump desktop package version to 0.1.30

* chore: increase Node.js memory limit for desktop build process

* chore: configure Node.js memory limit for desktop build

* update tauri-plugin-shellx, hide powershell window in whereIsCommand
2025-03-03 05:21:42 -05:00
Huakun
5fc99ca26c
Fix: listview filter (#225)
* chore: bump desktop package version to 0.1.30

* chore: increase Node.js memory limit for desktop build process

* chore: configure Node.js memory limit for desktop build

* fix(desktop): list view filter mode
2025-03-02 12:51:53 -05:00
Huakun
41302a29ff
fix: list view item's action panel and listview undefined error (#224)
* fix: list view item's action panel and listview undefined error

* chore: increase Node.js memory limit for build processes

* chore: configure Node.js memory limit for Tauri build process

* refactor: delete unecessary ui component code
2025-03-01 21:43:23 -05:00
Huakun
8751fbeff4
feat: add custom configurable app search paths (#221)
* feat: add custom configurable app search paths

* feat(i18n): add English and Chinese translation for app search path settings

* format
2025-03-01 12:36:37 -05:00
Huakun Shen
6df1c9865a
chore: upgrade applications-rs submodule 2025-03-01 08:12:06 -05:00
Anshul Raj Verma
f09b2832e9
fix: show action icon in action panel (#219) 2025-03-01 05:57:21 -05:00
Huakun Shen
6555ebcfcb
chore: upgrade applications-rs submodule 2025-03-01 03:47:19 -05:00
Luca Giannini
9e52ea331e
exit and clear search term after onSelect (#217)
* clean path and exit after onSelect

* format

* do not clean linux app path, and clear search term

---------

Co-authored-by: Huakun Shen <huakun.shen@huakunshen.com>
2025-03-01 01:59:09 -05:00
Huakun
70f7d4131e
[feat] Improve list view with fuse search and virtual list (#215)
* chore: comment out auto install for on boarding, its behavior and speed is unpredictable

* fix: clear action when ui template exits

* fix: update extension command search store references

* feat(ui): implement virtual list with advanced search and section handling

- Add @tanstack/svelte-virtual for efficient list rendering
- Integrate Fuse.js for advanced search across list items and sections
- Create dynamic virtual list with support for section headers
- Enhance list view with flexible search and filtering capabilities
- Add new types and components for virtual list management

* chore(desktop): bump package version to 0.1.29
2025-02-28 07:47:09 -05:00
Huakun
97cd20906f
Feature: add extension api (hide and paste) (#210)
* feat: add paste API to extension API

* feat(desktop): enhance clipboard and hotkey utilities

- Add `hideAndPaste` utility function to simplify window hiding and clipboard pasting
- Adjust key press and sleep timings for more reliable input simulation
- Implement window focus listener in clipboard extension
- Bind input element reference for automatic focus management

* feat(permissions): enhance clipboard permission handling

- Update clipboard permission schema to include paste permission
- Modify clipboard API to check for paste permission before executing
- Refactor permission map and schema for more flexible permission management

* feat(desktop): refactor extension command search and rendering

- Add `svelte-inspect-value` for debugging
- Implement new `ExtCmds` component to replace `ExtCmdsGroup`
- Enhance extension command search with separate Fuse.js instances for installed and dev extensions
- Simplify extension command filtering and rendering logic
- Add derived stores for extension commands with improved type safety

* feat(desktop): improve extension command search filtering

* bump @kksh/api version

* feat(permissions): add clipboard paste permission description
2025-02-26 04:47:43 -05:00
Huakun
a92c266d32
Feature: fine grain kill API for extension (#201)
* upgrade tauri-plugin-shellx

* feat(shell): add killPid method to shell API with new permission

* fix: extension new window

* feat(shell): enhance process management and logging in extensions

- Add debug logging for extension process events
- Implement process tracking in UI worker
- Update shell API to support custom process recording
- Modify extension template to demonstrate process spawning
- Refactor shell command handling with improved error handling

* Add killPid extension API to @kksh/api

* chore(deps): update tauri-plugin-shellx-api to version 2.0.15

* pnpm lock

* chore(deps): update dependencies and lock file

- Upgrade ESLint to version 9.21.0
- Update @types/bun to version 1.2.3
- Bump various development dependencies
- Reorganize package.json dependencies in ui package

* chore(deps): update SvelteKit template dependencies

- Add @eslint/js version 9.21.0
- Update package.json dependency order
- Sync pnpm-lock.yaml with package.json changes

* chore: add eslint to desktop

* chore(deps): remove local tauri-plugin-shellx and use published version 2.0.15

* bump desktop to 0.1.28
2025-02-26 02:06:06 -05:00
Huakun
66135624b9
feat(desktop) Improve search (#202)
* feat: add lockHideOnBlur to prevent app hiding during dialogs

* feat: add Fuse.js for advanced search filtering across app sections

* chore: update .prettierignore and clean up imports in AddDevExtForm
2025-02-25 10:21:20 -05:00
Luca Giannini
8940d25245
fix: only show apps with name (#194)
* only show apps with name

* format

* update pnpm lock

---------

Co-authored-by: Huakun Shen <huakun.shen@huakunshen.com>
2025-02-22 09:54:06 -05:00
Luca Giannini
8d49f50495
fix: handle failing icon loading linux gracefully (#193) 2025-02-22 09:12:47 -05:00
246 changed files with 12027 additions and 4128 deletions

View File

@ -11,11 +11,9 @@
"jarvis", "jarvis",
"form-view", "form-view",
"@kksh/desktop", "@kksh/desktop",
"@kksh/supabase",
"@kksh/utils", "@kksh/utils",
"@kksh/extension", "@kksh/extension",
"@kksh/schema", "@kksh/schema",
"@kksh/supabase",
"@kksh/ui" "@kksh/ui"
] ]
} }

View File

@ -205,12 +205,15 @@ jobs:
run: pnpm prepare run: pnpm prepare
- name: Build Packages - name: Build Packages
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build run: pnpm build
- name: Build the App - name: Build the App
working-directory: apps/desktop working-directory: apps/desktop
env: env:
CI: false CI: false
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm tauri build ${{ env.BUILD_MODE}} ${{ matrix.os == 'windows-latest' && '-b nsis' || '' }} run: pnpm tauri build ${{ env.BUILD_MODE}} ${{ matrix.os == 'windows-latest' && '-b nsis' || '' }}
- name: Rename macos-aarch64 - name: Rename macos-aarch64

View File

@ -52,6 +52,8 @@ jobs:
- name: Setup - name: Setup
run: pnpm prepare run: pnpm prepare
- name: Build - name: Build
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build run: pnpm build
- name: JS Test - name: JS Test
if: matrix.os == 'ubuntu-24.04' if: matrix.os == 'ubuntu-24.04'

View File

@ -87,6 +87,8 @@ jobs:
# pnpm --filter=@kksh/ci run ci-env-check # pnpm --filter=@kksh/ci run ci-env-check
bun packages/ci/scripts/ci-env-check.ts bun packages/ci/scripts/ci-env-check.ts
- name: Build Packages - name: Build Packages
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build run: pnpm build
- name: Get App Version - name: Get App Version
if: matrix.settings.platform == 'windows-latest' if: matrix.settings.platform == 'windows-latest'
@ -98,6 +100,7 @@ jobs:
CI: false CI: false
KUNKUN_PUBLISH: true KUNKUN_PUBLISH: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=4096
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}

121
.github/workflows/test-build.yml vendored Normal file
View File

@ -0,0 +1,121 @@
name: "Desktop Test build"
on:
push:
branches:
- "test-build"
tags:
- "v*"
workflow_dispatch:
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
settings:
- platform: "macos-14" # for Arm based macs (M1 and above).
args: "--target aarch64-apple-darwin --verbose --config src-tauri/tauri.conf.publish.json"
- platform: "macos-13" # for Intel based macs.
args: "--target x86_64-apple-darwin --verbose --config src-tauri/tauri.conf.publish.json"
# Universal Build no longer supported after adding openssl, which is not cross-compilable.
- platform: "macos-14" # for Both Arm and Intel based macs.
args: "--target universal-apple-darwin --verbose --config src-tauri/tauri.conf.publish.json"
- platform: "ubuntu-22.04" # for Tauri v1 you could replace this with ubuntu-20.04.
args: "--verbose --config src-tauri/tauri.conf.publish.json"
- platform: "windows-latest"
args: "--verbose --config src-tauri/tauri.conf.publish.json"
runs-on: ${{ matrix.settings.platform }}
steps:
- uses: actions/checkout@v4
with:
submodules: "true"
- name: Install Dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libxdo-dev
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
- name: Install protobuf (Mac)
if: startsWith(matrix.settings.platform, 'macos')
run: |
brew install protobuf
brew install openssl
- name: Install Protobuf (Ubuntu)
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt install -y protobuf-compiler
- uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm" # Set this to npm, yarn or pnpm.
cache-dependency-path: ./pnpm-lock.yaml
- name: Install protoc and openssl for windows
if: matrix.settings.platform == 'windows-latest'
run: |
choco install protoc
choco install openssl
echo OPENSSL_DIR='C:\Program Files\OpenSSL' >> $env:GITHUB_ENV
echo OPENSSL_INCLUDE_DIR='C:\Program Files\OpenSSL\include' >> $env:GITHUB_ENV
echo OPENSSL_LIB_DIR='C:\Program Files\OpenSSL\lib\VC\x64\MDd' >> $env:GITHUB_ENV
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.settings.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Add rust target (macos only)
if: matrix.settings.platform == 'macos-14'
run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
- name: Rust Cache
uses: swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install Dependencies
run: pnpm install
- name: Environment Check
run: |
# pnpm --filter=@kksh/ci run ci-env-check
bun packages/ci/scripts/ci-env-check.ts
- name: Build Packages
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build
- name: Get App Version
if: matrix.settings.platform == 'windows-latest'
id: appversion
run: |
echo "version=$(node -p "require('./apps/desktop/package.json').version")" >> $env:GITHUB_OUTPUT
- uses: tauri-apps/tauri-action@v0
env:
CI: false
KUNKUN_PUBLISH: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=4096
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: Kunkun-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
releaseName: "Kunkun v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: false
prerelease: false
args: ${{ matrix.settings.args }} ${{ contains(steps.appversion.outputs.version, 'beta') && matrix.settings.platform == 'windows-latest' && '-b nsis' || '' }}
projectPath: "./apps/desktop"

2
.gitmodules vendored
View File

@ -12,4 +12,4 @@
url = https://github.com/kunkunsh/tauri-plugin-user-input.git url = https://github.com/kunkunsh/tauri-plugin-user-input.git
[submodule "vendors/tauri-plugin-keyring"] [submodule "vendors/tauri-plugin-keyring"]
path = vendors/tauri-plugin-keyring path = vendors/tauri-plugin-keyring
url = https://github.com/HuakunShen/tauri-plugin-keyring.git url = https://github.com/HuakunShen/tauri-plugin-keyring.git

View File

@ -1,4 +1,5 @@
.svelte-kit/ .svelte-kit/
target/ target/
vendors/**
vendors vendors
.nuxt/ .nuxt/

View File

@ -10,5 +10,10 @@
"titleBar.activeForeground": "#FFFBFC" "titleBar.activeForeground": "#FFFBFC"
}, },
"svelte.enable-ts-plugin": true, "svelte.enable-ts-plugin": true,
"deno.enable": false "deno.enable": false,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true
}
} }

View File

@ -34,7 +34,7 @@ If you are interested in contributing to the project, please read the following
```bash ```bash
git clone https://github.com/kunkunsh/kunkun.git --recursive git clone https://github.com/kunkunsh/kunkun.git --recursive
pnpm install pnpm install
pnpm prepare pnpm build # build submodules
``` ```
### Run Desktop App ### Run Desktop App

704
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,8 @@ tokio-util = "0.7.12"
mdns-sd = "0.11.1" mdns-sd = "0.11.1"
tauri-plugin-network = { path = "./vendors/tauri-plugin-network" } tauri-plugin-network = { path = "./vendors/tauri-plugin-network" }
tauri-plugin-keyring = { path = "./vendors/tauri-plugin-keyring" } tauri-plugin-keyring = { path = "./vendors/tauri-plugin-keyring" }
tauri-plugin-clipboard = "2.1.8" tauri-plugin-shellx = { version = "2.0.16" }
tauri-plugin-clipboard = "2.1.11"
mac-security-rs = { path = "./packages/mac-security-rs" } mac-security-rs = { path = "./packages/mac-security-rs" }
log = "0.4.22" log = "0.4.22"
strum = "0.26" strum = "0.26"

View File

@ -1,5 +1,19 @@
# kksh # kksh
## 0.1.3
### Patch Changes
- Updated dependencies
- @kksh/api@0.1.5
## 0.1.2
### Patch Changes
- Updated dependencies
- @kksh/api@0.1.4
## 0.1.1 ## 0.1.1
### Patch Changes ### Patch Changes

View File

@ -1,7 +1,7 @@
{ {
"name": "kksh", "name": "kksh",
"module": "dist/cli.js", "module": "dist/cli.js",
"version": "0.1.1", "version": "0.1.3",
"type": "module", "type": "module",
"bin": { "bin": {
"kksh": "./dist/cli.js", "kksh": "./dist/cli.js",
@ -31,7 +31,7 @@
"debug": "^4.4.0", "debug": "^4.4.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"inquirer": "^10.1.2", "inquirer": "^10.1.2",
"valibot": "^1.0.0-beta.10" "valibot": "^1.0.0"
}, },
"files": [ "files": [
"dist" "dist"

View File

@ -1,5 +1,19 @@
# create-kunkun # create-kunkun
## 0.1.49
### Patch Changes
- Updated dependencies
- @kksh/api@0.1.5
## 0.1.48
### Patch Changes
- Updated dependencies
- @kksh/api@0.1.4
## 0.1.45 ## 0.1.45
### Patch Changes ### Patch Changes

View File

@ -1,7 +1,7 @@
{ {
"name": "create-kunkun", "name": "create-kunkun",
"type": "module", "type": "module",
"version": "0.1.47", "version": "0.1.49",
"bin": { "bin": {
"create-kunkun": "dist/index.mjs" "create-kunkun": "dist/index.mjs"
}, },
@ -27,7 +27,7 @@
"commander": "^12.1.0", "commander": "^12.1.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"valibot": "^1.0.0-beta.10" "valibot": "^1.0.0"
}, },
"files": [ "files": [
"dist" "dist"

13
apps/desktop/dev.ts Normal file
View File

@ -0,0 +1,13 @@
import { IconType } from "@kksh/api/models"
import { getExtensionsLatestPublishByIdentifier } from "@kksh/sdk"
const latestPublish = await getExtensionsLatestPublishByIdentifier({
path: {
identifier: "RAG1"
}
})
console.log(latestPublish)
// latestPublish
// console.log(typeof IconEnum.Iconify)
console.log(IconType.options)

View File

@ -0,0 +1,91 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"app_name": "KunKun",
"secondary_app_name": "KunKun",
"common_edit": "Bearbeiten",
"common_clear": "Löschen",
"common_check": "Prüfen",
"common_install": "Installieren",
"home_command_input_placeholder": "Suchen…",
"home_command_input_dropdown_quit": "Beenden",
"home_command_input_dropdown_developer_title": "Entwickler",
"home_command_input_dropdown_close_window": "Fenster schließen",
"home_command_input_dropdown_toggle_devtools": "Entwicklertools umschalten",
"home_command_input_dropdown_reload_window": "Fenster neu laden",
"home_command_input_dropdown_open_preference": "Einstellungen öffnen",
"home_command_input_dropdown_toggle_dev_extension_hmr": "Entwicklererweiterungen HMR umschalten",
"command_group_heading_dev_ext": "Entwicklererweiterungen",
"command_group_heading_ext": "Erweiterungen",
"command_group_heading_quick_links": "Quick Links",
"settings_menu_settings": "Einstellungen",
"settings_menu_general": "Allgemein",
"settings_menu_app_search_paths": "Verzeichnisse für Programme",
"settings_menu_developer": "Entwickler",
"settings_menu_extensions": "Erweiterungen",
"settings_menu_set_dev_ext": "Dev-Erweiterung festlegen",
"settings_menu_add_dev_ext": "Dev-Erweiterung hinzufügen",
"settings_menu_about": "Über",
"settings_general_launch_at_login": "Beim Systemstart öffnen",
"settings_general_hotkey": "Tastenkombination",
"settings_general_menu_bar_icon": "Menüleiste-Symbol",
"settings_general_hide_on_blur": "Automatisch ausblenden",
"settings_general_extension_auto_upgrade": "Erweiterungen automatisch aktualisieren",
"settings_general_dev_extension_hmr": "Entwicklererweiterungen HMR",
"settings_general_join_beta_updates": "Beta-Updates nutzen",
"settings_general_developer_mode": "Entwickler-Modus",
"settings_general_language": "Sprache",
"settings_general_loading_animation": "Ladeanimation",
"settings_app_search_paths_title": "Zusätzliche Verzeichnisse für die Programm-Suche",
"settings_app_search_paths_add_app_search_path": "Verzeichnis für Programm-Suche hinzufügen",
"settings_app_search_paths_table_col_search_path": "Suchpfad",
"settings_app_search_paths_table_col_depth": "Tiefe",
"settings_app_search_paths_table_col_actions": "Aktionen",
"settings_about_version": "Version",
"settings_about_author": "Autor",
"settings_about_source_code": "Quellcode",
"settings_about_extensions_source_code": "Quellcode für Erweiterungen",
"settings_about_check_for_updates": "Nach Updates suchen",
"settings_set_dev_ext_title": "Verzeichnis der Entwicklererweiterungen",
"settings_set_dev_ext_description": "Hier werden Entwicklererweiterungen installiert.",
"settings_set_dev_ext_enter_path": "Verzeichnis eingeben",
"settings_extensions_title": "Deine Erweiterungen",
"settings_extensions_table_col_name": "Name",
"settings_extensions_table_col_identifier": "Identifikator",
"settings_extensions_table_col_type": "Typ",
"settings_extensions_table_col_version": "Version",
"settings_extensions_table_col_uninstall": "Deinstallieren",
"settings_add_dev_ext_title": "Entwicklererweiterung hinzufügen",
"settings_add_dev_ext_description": "Es gibt vier Möglichkeiten, eine Erweiterung als Entwicklererweiterung zu installieren. Tarball-Archiv, lokales Verzeichnis, URL zu Tarball-Archiv oder NPM-Paketnamen.",
"settings_add_dev_ext_install_from_ext_folders": "Verzeichnis",
"settings_add_dev_ext_install_from_ext_files": "Tarball-Archiv",
"settings_add_dev_ext_drag_and_drop": "Drag and Drop",
"settings_add_dev_ext_drag_and_drop_strike": "Drag and Drop",
"settings_add_dev_ext_drag_and_drop2": "Verzeichnis oder Tarball-Archiv",
"settings_add_dev_ext_install_tarball_from_url": "Tarball-Archiv aus URL installieren",
"troubleshooters_sidebar_title": "Fehlerbehebung",
"troubleshooters_sidebar_extension_loading_title": "Ladevorgang",
"troubleshooters_sidebar_extension_window_title": "Darstellung",
"troubleshooters_sidebar_mdns_debugger_title": "MDNS-Debugger",
"troubleshooters_extension_window_title": "Fehlerbehebung für die Darstellung von Erweiterungen",
"troubleshooters_extension_window_refresh_every_second": "Jede Sekunde neu laden",
"troubleshooters_extension_window_refresh": "Neu laden",
"troubleshooters_extension_window_refreshed": "{count}x neu geladen",
"troubleshooters_extension_loading_title": "Fehlerbehebung für den Ladevorgang von Erweiterungen",
"troubleshooters_extension_loading_table_col_identifier": "Identifikator",
"troubleshooters_extension_loading_table_col_path": "Verzeichnis",
"troubleshooters_extension_loading_table_col_error": "Fehler"
}

View File

@ -24,6 +24,7 @@
"settings_menu_settings": "Settings", "settings_menu_settings": "Settings",
"settings_menu_general": "General", "settings_menu_general": "General",
"settings_menu_app_search_paths": "App Search Paths",
"settings_menu_developer": "Developer", "settings_menu_developer": "Developer",
"settings_menu_extensions": "Extensions", "settings_menu_extensions": "Extensions",
"settings_menu_set_dev_ext": "Set Dev Extension", "settings_menu_set_dev_ext": "Set Dev Extension",
@ -39,6 +40,13 @@
"settings_general_join_beta_updates": "Join Beta Updates", "settings_general_join_beta_updates": "Join Beta Updates",
"settings_general_developer_mode": "Developer Mode", "settings_general_developer_mode": "Developer Mode",
"settings_general_language": "Language", "settings_general_language": "Language",
"settings_general_loading_animation": "Loading Animation",
"settings_app_search_paths_title": "Extra App Search Paths",
"settings_app_search_paths_add_app_search_path": "Add App Search Path",
"settings_app_search_paths_table_col_search_path": "Search Path",
"settings_app_search_paths_table_col_depth": "Depth",
"settings_app_search_paths_table_col_actions": "Actions",
"settings_about_version": "Version", "settings_about_version": "Version",
"settings_about_author": "Author", "settings_about_author": "Author",

View File

@ -39,6 +39,7 @@
"settings_general_join_beta_updates": "Participar das Atualizações Beta", "settings_general_join_beta_updates": "Participar das Atualizações Beta",
"settings_general_developer_mode": "Modo Desenvolvedor", "settings_general_developer_mode": "Modo Desenvolvedor",
"settings_general_language": "Idioma", "settings_general_language": "Idioma",
"settings_general_loading_animation": "Animação de Carregamento",
"settings_about_version": "Versão", "settings_about_version": "Versão",
"settings_about_author": "Autor", "settings_about_author": "Autor",

View File

@ -39,6 +39,7 @@
"settings_general_join_beta_updates": "Получать бета-обновления", "settings_general_join_beta_updates": "Получать бета-обновления",
"settings_general_developer_mode": "Режим разработчика", "settings_general_developer_mode": "Режим разработчика",
"settings_general_language": "Язык", "settings_general_language": "Язык",
"settings_general_loading_animation": "Анимация загрузки",
"settings_about_version": "Версия", "settings_about_version": "Версия",
"settings_about_author": "Автор", "settings_about_author": "Автор",

View File

@ -39,6 +39,7 @@
"settings_general_join_beta_updates": "Cài đặt cập nhật thử nghiệm (beta)", "settings_general_join_beta_updates": "Cài đặt cập nhật thử nghiệm (beta)",
"settings_general_developer_mode": "Chế độ nhà phát triển", "settings_general_developer_mode": "Chế độ nhà phát triển",
"settings_general_language": "Ngôn ngữ", "settings_general_language": "Ngôn ngữ",
"settings_general_loading_animation": "Hình ảnh tải",
"settings_about_version": "Phiên bản", "settings_about_version": "Phiên bản",
"settings_about_author": "Tác giả", "settings_about_author": "Tác giả",

View File

@ -24,6 +24,7 @@
"settings_menu_settings": "设置", "settings_menu_settings": "设置",
"settings_menu_general": "通用", "settings_menu_general": "通用",
"settings_menu_app_search_paths": "应用搜索路径",
"settings_menu_developer": "开发者", "settings_menu_developer": "开发者",
"settings_menu_extensions": "插件", "settings_menu_extensions": "插件",
"settings_menu_set_dev_ext": "设置开发插件", "settings_menu_set_dev_ext": "设置开发插件",
@ -39,6 +40,13 @@
"settings_general_join_beta_updates": "加入 Beta 更新", "settings_general_join_beta_updates": "加入 Beta 更新",
"settings_general_developer_mode": "开发者模式", "settings_general_developer_mode": "开发者模式",
"settings_general_language": "语言", "settings_general_language": "语言",
"settings_general_loading_animation": "加载动画",
"settings_app_search_paths_title": "额外应用搜索路径",
"settings_app_search_paths_add_app_search_path": "添加应用搜索路径",
"settings_app_search_paths_table_col_search_path": "搜索路径",
"settings_app_search_paths_table_col_depth": "深度",
"settings_app_search_paths_table_col_actions": "操作",
"settings_about_version": "版本", "settings_about_version": "版本",
"settings_about_author": "作者", "settings_about_author": "作者",

View File

@ -1,6 +1,6 @@
{ {
"name": "@kksh/desktop", "name": "@kksh/desktop",
"version": "0.1.27", "version": "0.1.37",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -16,47 +16,55 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^0.8.2", "@formkit/auto-animate": "^0.8.2",
"@inlang/paraglide-sveltekit": "0.15.5", "@inlang/paraglide-sveltekit": "0.16.0",
"@kksh/drizzle": "workspace:*",
"@kksh/extension": "workspace:*", "@kksh/extension": "workspace:*",
"@kksh/supabase": "workspace:*",
"@kksh/svelte5": "^0.1.15", "@kksh/svelte5": "^0.1.15",
"@kksh/ui": "workspace:*", "@kksh/ui": "workspace:*",
"@kksh/utils": "workspace:*", "@kksh/utils": "workspace:*",
"@std/semver": "npm:@jsr/std__semver@^1.0.3", "@std/semver": "npm:@jsr/std__semver@^1.0.4",
"@supabase/supabase-js": "^2.48.0", "@supabase/supabase-js": "^2.49.1",
"@tanstack/table-core": "^8.20.5", "@tanstack/table-core": "^8.21.2",
"@tauri-apps/api": "^2.1.1", "@tauri-apps/api": "^2.3.0",
"@tauri-apps/plugin-autostart": "^2.2.0", "@tauri-apps/plugin-autostart": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0", "@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-sql": "^2.2.0",
"@tauri-apps/plugin-stronghold": "^2.2.0", "@tauri-apps/plugin-stronghold": "^2.2.0",
"dompurify": "^3.2.3", "@tauri-store/svelte": "^2.1.1",
"gsap": "^3.12.5", "dompurify": "^3.2.4",
"kkrpc": "^0.1.1", "drizzle-orm": "^0.41.0",
"eslint": "^9.21.0",
"fuse.js": "^7.1.0",
"gsap": "^3.12.7",
"kkrpc": "^0.2.2",
"lz-string": "^1.5.0", "lz-string": "^1.5.0",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"semver": "^7.6.3", "semver": "^7.7.1",
"svelte-inspect-value": "^0.5.0",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.22.1", "sveltekit-superforms": "^2.23.1",
"tauri-plugin-clipboard-api": "^2.1.11", "tauri-plugin-clipboard-api": "^2.1.11",
"tauri-plugin-shellx-api": "^2.0.16",
"tauri-plugin-svelte": "1.2.1",
"tauri-plugin-user-input-api": "workspace:*", "tauri-plugin-user-input-api": "workspace:*",
"uuid": "^11.0.3" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.21.0",
"@inlang/paraglide-js": "1.11.8", "@inlang/paraglide-js": "1.11.8",
"@kksh/types": "workspace:*", "@kksh/types": "workspace:*",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.17.1", "@sveltejs/kit": "^2.17.3",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tauri-apps/cli": "^2.2.7", "@tauri-apps/cli": "^2.3.1",
"@types/bun": "latest", "@types/bun": "latest",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^8.23.0", "@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.23.0", "@typescript-eslint/parser": "^8.25.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.86", "bits-ui": "1.0.0-next.86",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -64,15 +72,15 @@
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0", "globals": "^15.14.0",
"lucide-svelte": "^0.474.0", "lucide-svelte": "^0.474.0",
"prettier": "^3.4.2", "prettier": "^3.5.2",
"svelte-radix": "^2.0.1", "svelte-radix": "^2.0.1",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.0", "tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"typescript-eslint": "^8.20.0", "typescript-eslint": "^8.25.0",
"vite": "^6.0.3" "vite": "^6.2.0"
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://inlang.com/schema/project-settings", "$schema": "https://inlang.com/schema/project-settings",
"sourceLanguageTag": "en", "sourceLanguageTag": "en",
"languageTags": ["en", "zh", "ru", "pt", "vi"], "languageTags": ["en", "zh", "ru", "pt", "vi", "de"],
"modules": [ "modules": [
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js", "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js", "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",

View File

@ -34,7 +34,7 @@ chrono = { workspace = true }
log = { workspace = true } log = { workspace = true }
urlencoding = "2.1.3" urlencoding = "2.1.3"
tauri-plugin-process = "2.2.0" tauri-plugin-process = "2.2.0"
tauri-plugin-shellx = "2.0.12" tauri-plugin-shellx = { workspace = true }
tauri-plugin-fs = { version = "2.2.0", features = ["watch"] } tauri-plugin-fs = { version = "2.2.0", features = ["watch"] }
tauri-plugin-dialog = "2.2.0" tauri-plugin-dialog = "2.2.0"
tauri-plugin-notification = "2.2.1" tauri-plugin-notification = "2.2.1"
@ -58,6 +58,7 @@ uuid = "1.14.0"
obfstr = { workspace = true } obfstr = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
tauri-plugin-stronghold = "2.2.0" tauri-plugin-stronghold = "2.2.0"
tauri-plugin-sql = "2"
[target."cfg(target_os = \"macos\")".dependencies] [target."cfg(target_os = \"macos\")".dependencies]
@ -65,10 +66,10 @@ cocoa = "0.24.1"
mac-security-rs = { workspace = true } mac-security-rs = { workspace = true }
objc = "0.2.7" objc = "0.2.7"
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-autostart = "2" tauri-plugin-autostart = "2"
tauri-plugin-cli = "2" tauri-plugin-cli = "2"
tauri-plugin-global-shortcut = "2.0.1" tauri-plugin-global-shortcut = "2.0.1"
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
tauri-plugin-updater = "2.0.2" tauri-plugin-updater = "2.0.2"
tauri-plugin-svelte = "2.1.1"

View File

@ -24,6 +24,7 @@
"core:event:default", "core:event:default",
"core:window:default", "core:window:default",
"core:window:allow-set-size", "core:window:allow-set-size",
"core:window:allow-set-enabled",
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
"core:window:allow-set-focus", "core:window:allow-set-focus",
"core:window:allow-toggle-maximize", "core:window:allow-toggle-maximize",
@ -60,6 +61,7 @@
"shellx:allow-execute", "shellx:allow-execute",
"shellx:allow-open", "shellx:allow-open",
"shellx:allow-kill", "shellx:allow-kill",
"shellx:allow-kill-pid",
"shellx:allow-spawn", "shellx:allow-spawn",
"shellx:allow-stdin-write", "shellx:allow-stdin-write",
"shellx:allow-fix-path-env", "shellx:allow-fix-path-env",

View File

@ -0,0 +1,5 @@
{
"identifier": "svelte",
"windows": ["*"],
"permissions": ["svelte:default", "core:event:default"]
}

View File

@ -27,7 +27,7 @@ use utils::server::tauri_file_server;
pub fn run() { pub fn run() {
let context = tauri::generate_context!(); let context = tauri::generate_context!();
let mut builder = tauri::Builder::default(); let mut builder = tauri::Builder::default();
// let app_data_path = tauri::path::PathResolver::app_data_dir().unwrap();
// let db_key = if cfg!(debug_assertions) { // let db_key = if cfg!(debug_assertions) {
// None // None
// } else { // } else {
@ -108,10 +108,16 @@ pub fn run() {
.build(), .build(),
) )
.plugin(tauri_plugin_cli::init()) .plugin(tauri_plugin_cli::init())
.plugin(
tauri_plugin_sql::Builder::default()
// .add_migrations("sqlite:mydatabase.db", migrations)
.build(),
)
.plugin(tauri_plugin_user_input::init()) .plugin(tauri_plugin_user_input::init())
.plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_svelte::init())
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_autostart::init( .plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent, MacosLauncher::LaunchAgent,

View File

@ -5,7 +5,7 @@
"identifier": "sh.kunkun.desktop", "identifier": "sh.kunkun.desktop",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1566",
"beforeBuildCommand": "pnpm build", "beforeBuildCommand": "pnpm build",
"frontendDist": "../build" "frontendDist": "../build"
}, },
@ -20,6 +20,7 @@
"url": "/app", "url": "/app",
"title": "Kunkun", "title": "Kunkun",
"width": 800, "width": 800,
"label": "main",
"visible": false, "visible": false,
"height": 600, "height": 600,
"decorations": true, "decorations": true,

View File

@ -11,6 +11,7 @@ import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
import { exit } from "@tauri-apps/plugin-process" import { exit } from "@tauri-apps/plugin-process"
import { dev } from "$app/environment" import { dev } from "$app/environment"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import Fuse from "fuse.js"
import { toast } from "svelte-sonner" import { toast } from "svelte-sonner"
import { derived } from "svelte/store" import { derived } from "svelte/store"
import * as clipboard from "tauri-plugin-clipboard-api" import * as clipboard from "tauri-plugin-clipboard-api"
@ -241,6 +242,23 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
}, },
keywords: ["extension", "troubleshooter"] keywords: ["extension", "troubleshooter"]
}, },
{
name: "ORM Troubleshooter",
icon: {
type: IconEnum.Iconify,
value: "material-symbols:database"
},
description: "",
flags: {
developer: true,
dev: true
},
function: async () => {
appState.clearSearchTerm()
goto(i18n.resolveRoute("/app/troubleshooters/orm"))
},
keywords: ["extension", "troubleshooter", "database", "orm"]
},
{ {
name: "Create Quicklink", name: "Create Quicklink",
icon: { icon: {
@ -410,7 +428,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
visible: false visible: false
}) })
setTimeout(() => { setTimeout(() => {
window.show() window.show().then(() => window.setFocus())
}, 2_000) }, 2_000)
} }
}, },
@ -475,11 +493,19 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
} }
].map((cmd) => ({ ...cmd, id: uuidv4() })) ].map((cmd) => ({ ...cmd, id: uuidv4() }))
export const builtinCmds = derived([appConfig, appState], ([$appConfig, $appState]) => { export const fuse = new Fuse<BuiltinCmd>(rawBuiltinCmds, {
return rawBuiltinCmds.filter((cmd) => { includeScore: true,
const passDeveloper = cmd.flags?.developer ? $appConfig.developerMode : true threshold: 0.2,
const passDev = cmd.flags?.dev ? dev : true keys: ["name"]
return passDeveloper && passDev })
})
// .filter((cmd) => commandScore(cmd.name, $appState.searchTerm, cmd.keywords) > 0.5) export const builtinCmds = derived([appConfig, appState], ([$appConfig, $appState]) => {
return $appState.searchTerm
? fuse
.search($appState.searchTerm)
.map((result) => result.item)
.filter(
(cmd) => (!cmd.flags?.developer || $appConfig.developerMode) && (!cmd.flags?.dev || dev)
)
: rawBuiltinCmds
}) })

View File

@ -2,17 +2,23 @@ import { i18n } from "@/i18n"
import { appState } from "@/stores" import { appState } from "@/stores"
import { winExtMap } from "@/stores/winExtMap" import { winExtMap } from "@/stores/winExtMap"
import { helperAPI } from "@/utils/helper" import { helperAPI } from "@/utils/helper"
import { paste } from "@/utils/hotkey"
import { decideKkrpcSerialization } from "@/utils/kkrpc"
import { sleep } from "@/utils/time"
import { trimSlash } from "@/utils/url" import { trimSlash } from "@/utils/url"
import { constructExtensionSupportDir } from "@kksh/api" import { constructExtensionSupportDir } from "@kksh/api"
import { db, spawnExtensionFileServer } from "@kksh/api/commands" import { spawnExtensionFileServer } from "@kksh/api/commands"
import type { HeadlessCommand } from "@kksh/api/headless" import type { HeadlessCommand } from "@kksh/api/headless"
import { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models" import { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models"
import { constructJarvisServerAPIWithPermissions, type IApp } from "@kksh/api/ui" import { constructJarvisServerAPIWithPermissions, type IApp } from "@kksh/api/ui"
import { db } from "@kksh/drizzle"
import { launchNewExtWindow, loadExtensionManifestFromDisk } from "@kksh/extension" import { launchNewExtWindow, loadExtensionManifestFromDisk } from "@kksh/extension"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server" import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
import { convertFileSrc } from "@tauri-apps/api/core" import { convertFileSrc } from "@tauri-apps/api/core"
import * as path from "@tauri-apps/api/path" import * as path from "@tauri-apps/api/path"
import { getCurrentWindow } from "@tauri-apps/api/window"
import * as fs from "@tauri-apps/plugin-fs" import * as fs from "@tauri-apps/plugin-fs"
import { info } from "@tauri-apps/plugin-log"
import { platform } from "@tauri-apps/plugin-os" import { platform } from "@tauri-apps/plugin-os"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import { RPCChannel, WorkerParentIO } from "kkrpc/browser" import { RPCChannel, WorkerParentIO } from "kkrpc/browser"
@ -20,10 +26,12 @@ import * as v from "valibot"
export const KunkunIframeExtParams = v.object({ export const KunkunIframeExtParams = v.object({
url: v.string(), url: v.string(),
cmdName: v.optional(v.string()),
extPath: v.string() extPath: v.string()
}) })
export type KunkunIframeExtParams = v.InferOutput<typeof KunkunIframeExtParams> export type KunkunIframeExtParams = v.InferOutput<typeof KunkunIframeExtParams>
export const KunkunTemplateExtParams = v.object({ export const KunkunTemplateExtParams = v.object({
url: v.optional(v.string()),
extPath: v.string(), extPath: v.string(),
cmdName: v.string() cmdName: v.string()
}) })
@ -36,10 +44,10 @@ export async function createExtSupportDir(extPath: string) {
} }
} }
function setTemplateExtParams(extPath: string, cmdName: string) { function setTemplateExtParams(extPath: string, cmdName: string, url?: string) {
localStorage.setItem( localStorage.setItem(
"kunkun-template-ext-params", "kunkun-template-ext-params",
JSON.stringify({ extPath, cmdName } satisfies KunkunTemplateExtParams) JSON.stringify({ extPath, cmdName, url } satisfies KunkunTemplateExtParams)
) )
} }
@ -50,13 +58,15 @@ export async function onTemplateUiCmdSelect(
) { ) {
await createExtSupportDir(ext.extPath) await createExtSupportDir(ext.extPath)
const url = `/app/extension/ui-worker?extPath=${encodeURIComponent(ext.extPath)}&cmdName=${encodeURIComponent(cmd.name)}` const url = `/app/extension/ui-worker?extPath=${encodeURIComponent(ext.extPath)}&cmdName=${encodeURIComponent(cmd.name)}`
setTemplateExtParams(ext.extPath, cmd.name) setTemplateExtParams(ext.extPath, cmd.name, url)
if (cmd.window) { if (cmd.window) {
const winLabel = await winExtMap.registerExtensionWithWindow({ extPath: ext.extPath }) const winLabel = await winExtMap.registerExtensionWithWindow({ extPath: ext.extPath })
localStorage.setItem( const paramsStr = JSON.stringify({
"kunkun-template-ext-params", url,
JSON.stringify({ url, extPath: ext.extPath } satisfies KunkunIframeExtParams) extPath: ext.extPath,
) cmdName: cmd.name
} satisfies KunkunIframeExtParams)
localStorage.setItem("kunkun-template-ext-params", paramsStr)
const window = launchNewExtWindow(winLabel, url, cmd.window) const window = launchNewExtWindow(winLabel, url, cmd.window)
window.onCloseRequested(async (event) => { window.onCloseRequested(async (event) => {
await winExtMap.unregisterExtensionFromWindow(winLabel) await winExtMap.unregisterExtensionFromWindow(winLabel)
@ -78,6 +88,7 @@ export async function onHeadlessCmdSelect(
const loadedExt = await loadExtensionManifestFromDisk( const loadedExt = await loadExtensionManifestFromDisk(
await path.join(ext.extPath, "package.json") await path.join(ext.extPath, "package.json")
) )
const scriptPath = await path.join(loadedExt.extPath, cmd.main) const scriptPath = await path.join(loadedExt.extPath, cmd.main)
const workerScript = await fs.readTextFile(scriptPath) const workerScript = await fs.readTextFile(scriptPath)
const blob = new Blob([workerScript], { type: "application/javascript" }) const blob = new Blob([workerScript], { type: "application/javascript" })
@ -89,7 +100,21 @@ export async function onHeadlessCmdSelect(
} }
const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions( const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions(
loadedExt.kunkun.permissions, loadedExt.kunkun.permissions,
loadedExt.extPath loadedExt.extPath,
{
recordSpawnedProcess: async (pid: number) => {
console.log("recordSpawnedProcess pid", pid)
},
getSpawnedProcesses: async () => {
console.log("getSpawnedProcesses")
return []
},
paste: async () => {
await getCurrentWindow().hide()
await sleep(200)
return paste()
}
}
) )
const serverAPI2 = { const serverAPI2 = {
...serverAPI, ...serverAPI,
@ -103,8 +128,15 @@ export async function onHeadlessCmdSelect(
} satisfies IApp } satisfies IApp
} }
const io = new WorkerParentIO(worker) const io = new WorkerParentIO(worker)
const kkrpcSerialization = decideKkrpcSerialization(loadedExt)
info(
`Establishing kkrpc connection for ${loadedExt.kunkun.identifier} with serialization: ${kkrpcSerialization}`
)
const rpc = new RPCChannel<typeof serverAPI2, HeadlessCommand>(io, { const rpc = new RPCChannel<typeof serverAPI2, HeadlessCommand>(io, {
expose: serverAPI2 expose: serverAPI2,
serialization: {
version: kkrpcSerialization
}
}) })
const workerAPI = rpc.getAPI() const workerAPI = rpc.getAPI()
await workerAPI.load() await workerAPI.load()

View File

@ -12,7 +12,7 @@ import { onQuickLinkSelect } from "./quick-links"
const onExtCmdSelect: OnExtCmdSelect = ( const onExtCmdSelect: OnExtCmdSelect = (
ext: ExtPackageJsonExtra, ext: ExtPackageJsonExtra,
cmd: CustomUiCmd | TemplateUiCmd, cmd: CustomUiCmd | TemplateUiCmd | HeadlessCmd,
{ isDev, hmr }: { isDev: boolean; hmr: boolean } { isDev, hmr }: { isDev: boolean; hmr: boolean }
) => { ) => {
switch (cmd.type) { switch (cmd.type) {

View File

@ -1,10 +1,23 @@
import { getSystemCommands } from "@kksh/api/commands" import { getSystemCommands } from "@kksh/api/commands"
import type { SysCommand } from "@kksh/api/models" import type { SysCommand } from "@kksh/api/models"
import { commandScore } from "@kksh/ui/utils" import { commandScore } from "@kksh/ui/utils"
import Fuse from "fuse.js"
import { derived, readable } from "svelte/store" import { derived, readable } from "svelte/store"
import { appState } from "../stores/appState" import { appState } from "../stores/appState"
export const systemCommands = readable(getSystemCommands()) export const systemCommands = getSystemCommands()
export const fuse = new Fuse<SysCommand>(systemCommands, {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
export const systemCommandsFiltered = derived(appState, ($appState) => {
return $appState.searchTerm
? fuse.search($appState.searchTerm).map((result) => result.item)
: systemCommands
})
// export const systemCommandsFiltered = derived( // export const systemCommandsFiltered = derived(
// [systemCommands, appState], // [systemCommands, appState],

View File

@ -0,0 +1,37 @@
<script lang="ts">
import { appConfig, appState } from "@/stores"
import { cn } from "@/utils"
import { Button } from "@kksh/svelte5"
import { BorderBeam, Constants, Layouts, TauriLink } from "@kksh/ui"
import { goto } from "$app/navigation"
import { ArrowLeftIcon, LoaderCircleIcon } from "lucide-svelte"
import Dance from "../dance/dance.svelte"
let { class: className }: { class?: string } = $props()
function goHome() {
appState.setFullScreenLoading(false)
goto("/app")
}
</script>
<Layouts.Center class={cn("flex h-screen flex-col items-center justify-center", className)}>
<Button
variant="outline"
size="icon"
onclick={goHome}
class={cn(Constants.CLASSNAMES.BACK_BUTTON, "absolute left-4 top-4")}
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
>
<ArrowLeftIcon class="size-4" />
</Button>
{#if $appConfig.loadingAnimation === "kunkun-dancing"}
<!-- <DanceTransition delay={300} autoHide={false} show={!uiControl.iframeLoaded} /> -->
<Dance class="absolute z-50 h-screen opacity-20" />
{:else}
<!-- <LoadingAnimation delay={300} autoHide={false} show={!uiControl.iframeLoaded} /> -->
<LoaderCircleIcon class="h-24 w-24 animate-spin" />
<span class="font-mono">Loading</span>
{/if}
<BorderBeam size={150} duration={12} />
</Layouts.Center>

View File

@ -75,7 +75,7 @@
<div class={cn("flex items-center gap-2", className)}> <div class={cn("flex items-center gap-2", className)}>
<Shiki class={cn("w-full overflow-x-scroll rounded-md p-1 px-2")} {code} {lang} /> <Shiki class={cn("w-full overflow-x-scroll rounded-md p-1 px-2")} {code} {lang} />
<Button class="" size="sm" variant="secondary" onclick={copy}>Copy</Button> <Button class="" size="sm" variant="secondary" onclick={copy}>Copy</Button>
<Button class="" size="sm" variant="secondary" onclick={autoInstall} disabled={!autoInstallable}> <!-- <Button class="" size="sm" variant="secondary" onclick={autoInstall} disabled={!autoInstallable}>
Auto Install Auto Install
</Button> </Button> -->
</div> </div>

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { setAppConfigContext } from "@/context" import { setAppConfigContext } from "@/context"
import { setAppStateContext } from "@/context/appState" import { setAppStateContext } from "@/context/appState"
import type { AppConfig, AppState } from "@kksh/types" import type { AppConfigState, AppState } from "@kksh/types"
import type { Snippet } from "svelte" import { type Snippet } from "svelte"
import type { Writable } from "svelte/store" import type { Writable } from "svelte/store"
const { const {
@ -10,7 +10,7 @@
appState, appState,
children children
}: { }: {
appConfig: Writable<AppConfig> appConfig: Writable<AppConfigState>
appState: Writable<AppState> appState: Writable<AppState>
children: Snippet<[]> children: Snippet<[]>
} = $props() } = $props()

View File

@ -1,9 +1,11 @@
<script lang="ts"> <script lang="ts">
import { appState } from "@/stores"
import { IconEnum, type AppInfo } from "@kksh/api/models" import { IconEnum, type AppInfo } from "@kksh/api/models"
import { Command } from "@kksh/svelte5" import { Command } from "@kksh/svelte5"
import { IconMultiplexer } from "@kksh/ui" import { IconMultiplexer } from "@kksh/ui"
import { DraggableCommandGroup } from "@kksh/ui/custom" import { DraggableCommandGroup } from "@kksh/ui/custom"
import { convertFileSrc } from "@tauri-apps/api/core" import { convertFileSrc } from "@tauri-apps/api/core"
import { getCurrentWindow } from "@tauri-apps/api/window"
import * as os from "@tauri-apps/plugin-os" import * as os from "@tauri-apps/plugin-os"
import { toast } from "svelte-sonner" import { toast } from "svelte-sonner"
import { executeBashScript, open } from "tauri-plugin-shellx-api" import { executeBashScript, open } from "tauri-plugin-shellx-api"
@ -13,10 +15,11 @@
</script> </script>
<DraggableCommandGroup heading="Apps"> <DraggableCommandGroup heading="Apps">
{#each apps as app} {#each apps.filter((app) => app.name) as app, idx}
{@const iconPath = platform === "windows" ? (app.icon_path ?? app.app_path_exe) : app.icon_path}
<Command.Item <Command.Item
class="flex justify-between" class="flex justify-between"
onSelect={() => { onSelect={async () => {
if (platform === "windows") { if (platform === "windows") {
if (app.app_path_exe) { if (app.app_path_exe) {
open(app.app_path_exe) open(app.app_path_exe)
@ -34,15 +37,17 @@
} else { } else {
toast.error("Unsupported platform") toast.error("Unsupported platform")
} }
await getCurrentWindow().hide()
appState.clearSearchTerm()
}} }}
value={app.name} value={`app:${idx}:${app.app_desktop_path}`}
> >
<span class="flex gap-2"> <span class="flex gap-2">
<IconMultiplexer <IconMultiplexer
icon={app.icon_path icon={iconPath
? { ? {
type: IconEnum.RemoteUrl, type: IconEnum.RemoteUrl,
value: convertFileSrc(app.icon_path, "appicon") value: convertFileSrc(iconPath, "appicon")
} }
: { : {
type: IconEnum.Iconify, type: IconEnum.Iconify,
@ -51,6 +56,7 @@
class="!h-5 !w-5 shrink-0" class="!h-5 !w-5 shrink-0"
/> />
<span>{app.name}</span> <span>{app.name}</span>
<!-- <span>{app.app_path_exe}</span> -->
</span> </span>
</Command.Item> </Command.Item>
{/each} {/each}

View File

@ -11,20 +11,24 @@
} from "@/paraglide/runtime" } from "@/paraglide/runtime"
import { appConfig } from "@/stores" import { appConfig } from "@/stores"
import { Select, Switch } from "@kksh/svelte5" import { Select, Switch } from "@kksh/svelte5"
import type { LoadingAnimation } from "@kksh/types"
import * as autoStart from "@tauri-apps/plugin-autostart" import * as autoStart from "@tauri-apps/plugin-autostart"
import { onMount } from "svelte" import { onMount } from "svelte"
import { toast } from "svelte-sonner" import { toast } from "svelte-sonner"
const languages = availableLanguageTags.map((lang) => ({ const languages = availableLanguageTags.map((lang) => ({
value: lang, value: lang,
label: LanguageMap[lang] ?? lang label: LanguageMap[lang as keyof typeof LanguageMap] ?? lang
})) }))
let loadingAnimation = $state<LoadingAnimation>("spinning-circle")
const loadingAnimations = ["spinning-circle", "kunkun-dancing"] as const
let launchAtLogin = $state(false) let launchAtLogin = $state(false)
let language = $state(languageTag()) let language = $state(languageTag())
onMount(() => { onMount(() => {
autoStart.isEnabled().then((enabled) => { autoStart.isEnabled().then((enabled) => {
launchAtLogin = enabled launchAtLogin = enabled
}) })
loadingAnimation = $appConfig.loadingAnimation
}) })
const triggerContent = $derived(languages.find((f) => f.value === language)?.label ?? "Language") const triggerContent = $derived(languages.find((f) => f.value === language)?.label ?? "Language")
</script> </script>
@ -101,6 +105,31 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</li> </li>
<li>
<span>{m.settings_general_loading_animation()}</span>
<Select.Root type="single" name="loadingAnimation" bind:value={loadingAnimation}>
<Select.Trigger class="w-fit">
{loadingAnimation}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.GroupHeading>Loading Animation</Select.GroupHeading>
{#each loadingAnimations as anim}
<Select.Item
onclick={() => {
appConfig.setLoadingAnimation(anim)
}}
value={anim}
label={anim}
>
{anim}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</li>
</ul> </ul>
<style scoped> <style scoped>

View File

@ -51,18 +51,18 @@
runtime environment for executing extension code safely. It is optional but recommended. runtime environment for executing extension code safely. It is optional but recommended.
</p> </p>
<p class="font-mono text-sm">Choose any installation method below.</p> <p class="font-mono text-sm">Choose any installation method below.</p>
<p class="font-mono text-sm"> <!-- <p class="font-mono text-sm">
If you are unsure, you can use <strong class="text-lg">Auto Install</strong>. If you are unsure, you can use <strong class="text-lg">Auto Install</strong>.
</p> </p> -->
<p class="font-mono text-sm text-red-400"> <p class="font-mono text-sm text-red-400">
After installation, ensure the `deno` command is accessible from your system's PATH. After installation, ensure the `deno` command is accessible from your system's PATH.
</p> </p>
{#if _platform === "macos" || _platform === "linux"} <!-- {#if _platform === "macos" || _platform === "linux"}
<p class="font-mono text-sm text-red-400"> <p class="font-mono text-sm text-red-400">
Installation with <span class="font-bold text-green-500">curl</span> command likely requires manual Installation with <span class="font-bold text-green-500">curl</span> command likely requires manual
configuration. So auto install is disabled. Please copy the command and run it in a terminal. configuration. So auto install is disabled. Please copy the command and run it in a terminal.
</p> </p>
{/if} {/if} -->
{#if denoPath} {#if denoPath}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span></span> <span></span>

View File

@ -3,7 +3,7 @@
import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte" import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte"
import { i18n } from "@/i18n" import { i18n } from "@/i18n"
import * as m from "@/paraglide/messages" import * as m from "@/paraglide/messages"
import { appConfig, extensions } from "@/stores" import { appConfig, appState, extensions } from "@/stores"
import { goBackOnEscape } from "@/utils/key" import { goBackOnEscape } from "@/utils/key"
import { goBack } from "@/utils/route" import { goBack } from "@/utils/route"
import { IconEnum } from "@kksh/api/models" import { IconEnum } from "@kksh/api/models"
@ -65,10 +65,12 @@
} }
async function pickExtFolders() { async function pickExtFolders() {
appState.setLockHideOnBlur(true)
const selected = await openFileSelector({ const selected = await openFileSelector({
directory: true, directory: true,
multiple: true // allow install multiple extensions at once multiple: true // allow install multiple extensions at once
}) })
appState.setLockHideOnBlur(false)
if (!selected) { if (!selected) {
return toast.warning("No File Selected") return toast.warning("No File Selected")
} }
@ -91,6 +93,7 @@
toast.warning("Please set the dev extension path in the settings") toast.warning("Please set the dev extension path in the settings")
return goto(i18n.resolveRoute("/app/settings/set-dev-ext-path")) return goto(i18n.resolveRoute("/app/settings/set-dev-ext-path"))
} }
appState.setLockHideOnBlur(true)
const selected = await openFileSelector({ const selected = await openFileSelector({
directory: false, directory: false,
multiple: true, // allow install multiple extensions at once multiple: true, // allow install multiple extensions at once
@ -101,6 +104,7 @@
} }
] ]
}) })
appState.setLockHideOnBlur(false)
if (!selected) { if (!selected) {
return toast.warning("No File Selected") return toast.warning("No File Selected")
} }

View File

@ -3,16 +3,16 @@
* It's designed to allow all components to access a shared state. * It's designed to allow all components to access a shared state.
* With context, we can avoid prop drilling, and avoid using stores which makes components hard to encapsulate. * With context, we can avoid prop drilling, and avoid using stores which makes components hard to encapsulate.
*/ */
import type { AppConfig } from "@/types/appConfig" import type { AppConfigState } from "@kksh/types"
import { getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import type { Writable } from "svelte/store" import type { Writable } from "svelte/store"
export const APP_CONFIG_CONTEXT_KEY = Symbol("appConfig") export const APP_CONFIG_CONTEXT_KEY = Symbol("appConfig")
export function getAppConfigContext(): Writable<AppConfig> { export function getAppConfigContext(): Writable<AppConfigState> {
return getContext(APP_CONFIG_CONTEXT_KEY) return getContext(APP_CONFIG_CONTEXT_KEY)
} }
export function setAppConfigContext(appConfig: Writable<AppConfig>) { export function setAppConfigContext(appConfig: Writable<AppConfigState>) {
setContext(APP_CONFIG_CONTEXT_KEY, appConfig) setContext(APP_CONFIG_CONTEXT_KEY, appConfig)
} }

View File

@ -1,4 +1,4 @@
import type { AppState } from "@/types/appState" import type { AppState } from "@kksh/types"
import { getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import type { Writable } from "svelte/store" import type { Writable } from "svelte/store"

View File

@ -0,0 +1,60 @@
// /* eslint-disable @typescript-eslint/no-explicit-any */
// import { db as dbCmd } from "@kksh/api/commands"
// import * as schema from "@kksh/drizzle/schema"
// import { error } from "@tauri-apps/plugin-log"
// import { drizzle } from "drizzle-orm/sqlite-proxy"
// /**
// * Loads the sqlite database via the Tauri Proxy.
// */
// // export const sqlite = await Database.load("sqlite:test.db");
// /**
// * The drizzle database instance.
// */
// export const db = drizzle<typeof schema>(
// async (sql, params, method) => {
// let rows: any = []
// let results = []
// console.log({
// sql,
// params,
// method
// })
// console.log(sql)
// // If the query is a SELECT, use the select method
// if (isSelectQuery(sql)) {
// rows = await dbCmd.select(sql, params).catch((e) => {
// error("SQL Error:", e)
// return []
// })
// } else {
// // Otherwise, use the execute method
// rows = await dbCmd.execute(sql, params).catch((e) => {
// 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]
// return { rows: results }
// },
// // Pass the schema to the drizzle instance
// { schema: schema, logger: true }
// )
// /**
// * Checks if the given SQL query is a SELECT query.
// * @param sql The SQL query to check.
// * @returns True if the query is a SELECT query, false otherwise.
// */
// function isSelectQuery(sql: string): boolean {
// const selectRegex = /^\s*SELECT\b/i
// return selectRegex.test(sql)
// }

View File

@ -1,15 +1,16 @@
import { getExtensionsFolder } from "@/constants" import { getExtensionsFolder } from "@/constants"
import { createTauriSyncStore, type WithSyncStore } from "@/utils/sync-store" import type { SearchPath } from "@kksh/api/models"
import { updateTheme, type ThemeConfig } from "@kksh/svelte5" import { updateTheme, type ThemeConfig } from "@kksh/svelte5"
import { PersistedAppConfig, type AppConfig } from "@kksh/types" import { LoadingAnimation, PersistedAppConfig, type AppConfigState } from "@kksh/types"
import { debug, error } from "@tauri-apps/plugin-log" import { debug, error, info } from "@tauri-apps/plugin-log"
import * as os from "@tauri-apps/plugin-os" import * as os from "@tauri-apps/plugin-os"
import { load } from "@tauri-apps/plugin-store" import { load } from "@tauri-apps/plugin-store"
import { Store } from "@tauri-store/svelte"
import { toast } from "svelte-sonner"
import { get, writable } from "svelte/store" import { get, writable } from "svelte/store"
import * as v from "valibot" import * as v from "valibot"
import { setLanguageTag } from "../paraglide/runtime"
export const defaultAppConfig: AppConfig = { export const defaultAppConfig: AppConfigState = {
isInitialized: false, isInitialized: false,
platform: "macos", platform: "macos",
language: "en", language: "en",
@ -27,75 +28,82 @@ export const defaultAppConfig: AppConfig = {
extensionAutoUpgrade: true, extensionAutoUpgrade: true,
joinBetaProgram: false, joinBetaProgram: false,
onBoarded: false, onBoarded: false,
developerMode: false developerMode: false,
appSearchPaths: [],
loadingAnimation: "kunkun-dancing"
} }
export const appConfigLoaded = writable(false) export const appConfigLoaded = writable(false)
interface AppConfigAPI { interface AppConfigAPI {
init: () => Promise<void> init: () => Promise<void>
get: () => AppConfig get: () => AppConfigState
setTheme: (theme: ThemeConfig) => void setTheme: (theme: ThemeConfig) => void
setDevExtensionPath: (devExtensionPath: string | null) => void setDevExtensionPath: (devExtensionPath: string | null) => void
setTriggerHotkey: (triggerHotkey: string[]) => void setTriggerHotkey: (triggerHotkey: string[]) => void
setOnBoarded: (onBoarded: boolean) => void setOnBoarded: (onBoarded: boolean) => void
setLanguage: (language: string) => void setLanguage: (language: string) => void
addAppSearchPath: (appSearchPath: SearchPath) => void
removeAppSearchPath: (appSearchPath: SearchPath) => void
} }
function createAppConfig(): WithSyncStore<AppConfig & { language: string }> & AppConfigAPI { class AppConfigStore extends Store<AppConfigState> implements AppConfigAPI {
const store = createTauriSyncStore("app-config", defaultAppConfig) constructor() {
super("app-config", defaultAppConfig, {
async function init() { saveOnChange: true
debug("Initializing app config") })
const persistStore = await load("kk-config.json", { autoSave: true }) this.start().catch((err) => {
let loadedConfig = await persistStore.get("config") error("Failed to start app config store", err)
if (typeof loadedConfig === "object") { toast.error("Failed to start app config store", { description: err.message })
loadedConfig = { ...defaultAppConfig, ...loadedConfig }
}
const parseRes = v.safeParse(PersistedAppConfig, loadedConfig)
if (parseRes.success) {
console.log("Parse Persisted App Config Success", parseRes.output)
const extensionsInstallDir = await getExtensionsFolder()
store.update((config) => ({
...config,
...parseRes.output,
isInitialized: true,
extensionsInstallDir,
platform: os.platform()
}))
} else {
error("Failed to parse app config, going to remove it and reinitialize")
console.error(v.flatten<typeof PersistedAppConfig>(parseRes.issues))
await persistStore.clear()
await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig))
}
appConfigLoaded.set(true)
store.subscribe(async (config) => {
console.log("Saving app config", config)
await persistStore.set("config", config)
updateTheme(config.theme)
}) })
} }
async init() {
debug("Initializing app config")
const extensionsInstallDir = await getExtensionsFolder()
this.update((config) => ({
...config,
isInitialized: true,
platform: os.platform(),
extensionsInstallDir
}))
appConfigLoaded.set(true)
}
return { get() {
...store, return get(this)
get: () => get(store), }
setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })), setTheme(theme: ThemeConfig) {
setDevExtensionPath: (devExtensionPath: string | null) => { this.update((config) => ({ ...config, theme }))
console.log("setDevExtensionPath", devExtensionPath) }
store.update((config) => ({ ...config, devExtensionPath })) setDevExtensionPath(devExtensionPath: string | null) {
}, info(`setDevExtensionPath ${devExtensionPath}`)
setTriggerHotkey: (triggerHotkey: string[]) => { this.update((config) => ({ ...config, devExtensionPath }))
store.update((config) => ({ ...config, triggerHotkey })) }
}, setTriggerHotkey(triggerHotkey: string[]) {
setOnBoarded: (onBoarded: boolean) => { this.update((config) => ({ ...config, triggerHotkey }))
store.update((config) => ({ ...config, onBoarded })) }
}, setOnBoarded(onBoarded: boolean) {
setLanguage: (language: string) => { this.update((config) => ({ ...config, onBoarded }))
store.update((config) => ({ ...config, language })) }
}, setLanguage(language: string) {
init this.update((config) => ({ ...config, language }))
}
addAppSearchPath(appSearchPath: SearchPath) {
this.update((config) => ({
...config,
appSearchPaths: [...config.appSearchPaths, appSearchPath]
}))
}
removeAppSearchPath(appSearchPath: SearchPath) {
this.update((config) => ({
...config,
appSearchPaths: config.appSearchPaths.filter((path) => path.path !== appSearchPath.path)
}))
}
setLoadingAnimation(loadingAnimation: LoadingAnimation) {
this.update((config) => ({ ...config, loadingAnimation }))
} }
} }
export const appConfig = createAppConfig() // export const appConfig = createAppConfig()
export const appConfig = new AppConfigStore()

View File

@ -7,7 +7,9 @@ export const defaultAppState: AppState = {
highlightedCmd: "", highlightedCmd: "",
loadingBar: false, loadingBar: false,
defaultAction: "", defaultAction: "",
actionPanel: undefined actionPanel: undefined,
lockHideOnBlur: false, // when dialog is open, we don't hide the app, we lock the hide on blur and unlock when dialog is closed
fullScreenLoading: false
} }
interface AppStateAPI { interface AppStateAPI {
@ -16,6 +18,8 @@ interface AppStateAPI {
setLoadingBar: (loadingBar: boolean) => void setLoadingBar: (loadingBar: boolean) => void
setDefaultAction: (defaultAction: string | null) => void setDefaultAction: (defaultAction: string | null) => void
setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => void setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => void
setLockHideOnBlur: (lockHideOnBlur: boolean) => void
setFullScreenLoading: (fullScreenLoading: boolean) => void
} }
function createAppState(): Writable<AppState> & AppStateAPI { function createAppState(): Writable<AppState> & AppStateAPI {
@ -35,6 +39,12 @@ function createAppState(): Writable<AppState> & AppStateAPI {
}, },
setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => { setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => {
store.update((state) => ({ ...state, actionPanel })) store.update((state) => ({ ...state, actionPanel }))
},
setLockHideOnBlur: (lockHideOnBlur: boolean) => {
store.update((state) => ({ ...state, lockHideOnBlur }))
},
setFullScreenLoading: (fullScreenLoading: boolean) => {
store.update((state) => ({ ...state, fullScreenLoading }))
} }
} }
} }

View File

@ -3,9 +3,16 @@ import { AppInfo } from "@kksh/api/models"
import { commandScore } from "@kksh/ui/utils" import { commandScore } from "@kksh/ui/utils"
import * as fs from "@tauri-apps/plugin-fs" import * as fs from "@tauri-apps/plugin-fs"
import { platform } from "@tauri-apps/plugin-os" import { platform } from "@tauri-apps/plugin-os"
import Fuse from "fuse.js"
import { derived, get, writable } from "svelte/store" import { derived, get, writable } from "svelte/store"
import { appState } from "./appState" import { appState } from "./appState"
const fuse = new Fuse<AppInfo>([], {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
export function createAppsLoaderStore() { export function createAppsLoaderStore() {
const store = writable<AppInfo[]>([]) const store = writable<AppInfo[]>([])
@ -28,24 +35,14 @@ export function createAppsLoaderStore() {
// console.log("filteredApps", apps) // console.log("filteredApps", apps)
// fs.writeTextFile("/Users/hk/Desktop/apps.json", JSON.stringify(apps)) // fs.writeTextFile("/Users/hk/Desktop/apps.json", JSON.stringify(apps))
store.set(apps) store.set(apps)
fuse.setCollection(apps)
} }
} }
} }
export const appsLoader = createAppsLoaderStore() export const appsLoader = createAppsLoaderStore()
export const appsFiltered = derived([appsLoader, appState], ([$apps, $appState]) => {
// export const appsFiltered = derived([appsLoader, appState], ([$apps, $appState]) => { return $appState.searchTerm.length > 0
// return [] ? fuse.search($appState.searchTerm).map((result) => result.item)
// return $apps.filter((app) => { : $apps.slice(0, 20)
// if ($appState.searchTerm.length === 0) { })
// return false
// }
// return (
// commandScore(
// app.name,
// $appState.searchTerm
// // []
// ) > 0.8
// )
// })
// })

View File

@ -1,11 +1,9 @@
import { getExtensionsFolder } from "@/constants" import type { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models"
import { db } from "@kksh/api/commands" import { db } from "@kksh/drizzle"
import type { ExtPackageJson, ExtPackageJsonExtra } from "@kksh/api/models"
import * as extAPI from "@kksh/extension" import * as extAPI from "@kksh/extension"
import { commandScore } from "@kksh/ui/utils"
import * as path from "@tauri-apps/api/path" import * as path from "@tauri-apps/api/path"
import * as fs from "@tauri-apps/plugin-fs" import Fuse from "fuse.js"
import { derived, get, writable, type Readable, type Writable } from "svelte/store" import { derived, get, writable, type Writable } from "svelte/store"
import { appConfig } from "./appConfig" import { appConfig } from "./appConfig"
import { appState } from "./appState" import { appState } from "./appState"
@ -227,44 +225,62 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
} }
export const extensions = createExtensionsStore() export const extensions = createExtensionsStore()
export const installedStoreExts = derived(extensions, ($extensions) => {
const extContainerPath = get(appConfig).extensionsInstallDir
if (!extContainerPath) return []
return $extensions.filter((ext) => !extAPI.isExtPathInDev(extContainerPath, ext.extPath))
})
export const devStoreExts = derived(extensions, ($extensions) => {
const extContainerPath = get(appConfig).extensionsInstallDir
if (!extContainerPath) return []
return $extensions.filter((ext) => extAPI.isExtPathInDev(extContainerPath, ext.extPath))
})
export const installedStoreExts: Readable<ExtPackageJsonExtra[]> = derived( export type StoreExtCmd = (CustomUiCmd | TemplateUiCmd | HeadlessCmd) & {
extensions, ext: ExtPackageJsonExtra
($extensionsStore) => { }
const extContainerPath = get(appConfig).extensionsInstallDir
if (!extContainerPath) return []
return $extensionsStore.filter((ext) => !extAPI.isExtPathInDev(extContainerPath, ext.extPath))
}
)
export const devStoreExts: Readable<ExtPackageJsonExtra[]> = derived(
extensions,
($extensionsStore) => {
const extContainerPath = get(appConfig).extensionsInstallDir
if (!extContainerPath) return []
return $extensionsStore.filter((ext) => extAPI.isExtPathInDev(extContainerPath, ext.extPath))
}
)
// export const installedStoreExtsFiltered = derived( export const cmdsFuse = new Fuse<StoreExtCmd>([], {
// [installedStoreExts, appState], includeScore: true,
// ([$installedStoreExts, $appState]) => { threshold: 0.2,
// return $installedStoreExts.filter( keys: ["name"]
// (ext) => commandScore(ext.kunkun.name, $appState.searchTerm) > 0.5 })
// ) export const devCmdsFuse = new Fuse<StoreExtCmd>([], {
// } includeScore: true,
// ) threshold: 0.2,
keys: ["name"]
})
// export const devStoreExtsFiltered = derived( export const storeExtCmds = derived(installedStoreExts, ($exts) => {
// [devStoreExts, appState], const cmds = $exts.flatMap((ext) => {
// ([$devStoreExts, $appState]) => { return [
// return $devStoreExts.filter((ext) => { ...(ext.kunkun.customUiCmds ?? []),
// console.log( ...(ext.kunkun.templateUiCmds ?? []),
// "commandScore", ...(ext.kunkun.headlessCmds ?? [])
// ext.kunkun.name, ].map((cmd) => ({ ...cmd, ext }))
// $appState.searchTerm, })
// commandScore(ext.kunkun.name, $appState.searchTerm) cmdsFuse.setCollection(cmds)
// ) return cmds
// return commandScore(ext.kunkun.name, $appState.searchTerm) > 0.1 })
// }) export const devStoreExtCmds = derived(devStoreExts, ($exts) => {
// } const cmds = $exts.flatMap((ext) => {
// ) return [
...(ext.kunkun.customUiCmds ?? []),
...(ext.kunkun.templateUiCmds ?? []),
...(ext.kunkun.headlessCmds ?? [])
].map((cmd) => ({ ...cmd, ext }))
})
devCmdsFuse.setCollection(cmds)
return cmds
})
export const storeSearchExtCmds = derived([storeExtCmds, appState], ([$extCmds, $appState]) => {
return $appState.searchTerm
? cmdsFuse.search($appState.searchTerm).map((result) => result.item)
: $extCmds
})
export const devSearchExtCmds = derived([devStoreExtCmds, appState], ([$extCmds, $appState]) => {
return $appState.searchTerm
? devCmdsFuse.search($appState.searchTerm).map((result) => result.item)
: $extCmds
})

View File

@ -2,9 +2,16 @@ import type { Icon } from "@kksh/api/models"
import { createQuickLinkCommand, getAllQuickLinkCommands } from "@kksh/extension/db" import { createQuickLinkCommand, getAllQuickLinkCommands } from "@kksh/extension/db"
import type { QuickLink } from "@kksh/ui/types" import type { QuickLink } from "@kksh/ui/types"
import { commandScore } from "@kksh/ui/utils" import { commandScore } from "@kksh/ui/utils"
import Fuse from "fuse.js"
import { derived, get, writable, type Writable } from "svelte/store" import { derived, get, writable, type Writable } from "svelte/store"
import { appState } from "./appState" import { appState } from "./appState"
const fuse = new Fuse<QuickLink>([], {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
export interface QuickLinkAPI { export interface QuickLinkAPI {
get: () => QuickLink[] get: () => QuickLink[]
init: () => Promise<void> init: () => Promise<void>
@ -21,7 +28,9 @@ function createQuickLinksStore(): Writable<QuickLink[]> & QuickLinkAPI {
async function refresh() { async function refresh() {
const cmds = await getAllQuickLinkCommands() const cmds = await getAllQuickLinkCommands()
store.set(cmds.map((cmd) => ({ link: cmd.data.link, name: cmd.name, icon: cmd.data.icon }))) const items = cmds.map((cmd) => ({ link: cmd.data.link, name: cmd.name, icon: cmd.data.icon }))
store.set(items)
fuse.setCollection(items)
} }
async function createQuickLink(name: string, link: string, icon: Icon) { async function createQuickLink(name: string, link: string, icon: Icon) {
@ -39,7 +48,11 @@ function createQuickLinksStore(): Writable<QuickLink[]> & QuickLinkAPI {
} }
export const quickLinks = createQuickLinksStore() export const quickLinks = createQuickLinksStore()
export const quickLinksFiltered = derived([quickLinks, appState], ([$quickLinks, $appState]) => {
return $appState.searchTerm
? fuse.search($appState.searchTerm).map((result) => result.item)
: $quickLinks
})
// export const quickLinksFiltered = derived([quickLinks, appState], ([$quicklinks, $appState]) => { // export const quickLinksFiltered = derived([quickLinks, appState], ([$quicklinks, $appState]) => {
// return $quicklinks.filter((lnk) => { // return $quicklinks.filter((lnk) => {
// if ($appState.searchTerm.length === 0) { // if ($appState.searchTerm.length === 0) {

View File

@ -104,6 +104,7 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
} }
}, },
registerProcess: async (windowLabel: string, pid: number) => { registerProcess: async (windowLabel: string, pid: number) => {
console.log("registerProcess", windowLabel, pid)
const winExtMap = get(store) const winExtMap = get(store)
await registerExtensionSpawnedProcess(windowLabel, pid) await registerExtensionSpawnedProcess(windowLabel, pid)
if (!winExtMap[windowLabel]) { if (!winExtMap[windowLabel]) {
@ -116,6 +117,7 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
const winExtMap = get(store) const winExtMap = get(store)
const found = Object.entries(winExtMap).find(([windowLabel, ext]) => ext.pids.includes(pid)) const found = Object.entries(winExtMap).find(([windowLabel, ext]) => ext.pids.includes(pid))
if (!found) { if (!found) {
warn(`Process ${pid} does not have an extension registered, thus will not be killed`)
return return
} }
const [windowLabel, ext] = found const [windowLabel, ext] = found

View File

@ -1,19 +1,13 @@
import { SupabaseAPI } from "@kksh/supabase/api"
import type { Database } from "@kksh/supabase/types"
import * as sb from "@supabase/supabase-js" import * as sb from "@supabase/supabase-js"
import { SUPABASE_ANON_KEY, SUPABASE_URL } from "./constants" import { SUPABASE_ANON_KEY, SUPABASE_URL } from "./constants"
// export const supabase = createSB(SUPABASE_URL, SUPABASE_ANON_KEY) // export const supabase = createSB(SUPABASE_URL, SUPABASE_ANON_KEY)
export const supabase: sb.SupabaseClient<Database> = sb.createClient<Database>( export const supabase: sb.SupabaseClient = sb.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
SUPABASE_URL, auth: {
SUPABASE_ANON_KEY, flowType: "pkce"
{
auth: {
flowType: "pkce"
}
} }
) })
export const storage = supabase.storage export const storage = supabase.storage
export const supabaseExtensionsStorage = supabase.storage.from("extensions") export const supabaseExtensionsStorage = supabase.storage.from("extensions")
export const supabaseAPI = new SupabaseAPI(supabase) // export const supabaseAPI = new SupabaseAPI(supabase)

View File

@ -0,0 +1,44 @@
import { proxyDB, schema } from "@kksh/drizzle"
import { getExtClipboard } from "@kksh/drizzle/api"
import { error, info } from "@tauri-apps/plugin-log"
import * as orm from "drizzle-orm"
/**
* For now, simply delete all clipboard data older than 10 days
*/
export async function cleanClipboard() {
const nDays = 10
const clipboardExt = await getExtClipboard()
const nDaysAgo = new Date()
nDaysAgo.setDate(nDaysAgo.getDate() - nDays)
try {
// Select data older than 10 days to check what will be deleted
const oldClipboardData = await proxyDB
.select({ count: orm.count() })
.from(schema.extensionData)
.where(
orm.and(
orm.eq(schema.extensionData.extId, clipboardExt.extId),
orm.lt(schema.extensionData.createdAt, nDaysAgo.toISOString())
)
)
const nLinesToDelete = oldClipboardData.at(0)?.count ?? 0
console.info(`Found ${nLinesToDelete} clipboard entries older than ${nDays} days to clean up`)
info(`Found ${nLinesToDelete} clipboard entries older than ${nDays} days to clean up`)
// Now delete the old data
const deleted = await proxyDB
.delete(schema.extensionData)
.where(
orm.and(
orm.eq(schema.extensionData.extId, clipboardExt.extId),
orm.lt(schema.extensionData.createdAt, nDaysAgo.toISOString())
)
)
console.log("deleted", deleted)
} catch (e) {
error(`Error during clipboard cleanup: ${e}`)
}
}

View File

@ -0,0 +1,13 @@
import { proxyDB } from "@kksh/drizzle"
import { error, info } from "@tauri-apps/plugin-log"
import { sql } from "drizzle-orm"
export async function vacuumSqlite() {
const statement = sql`VACUUM`
try {
await proxyDB.run(statement)
info("Vacuumed sqlite")
} catch (error) {
console.error(`Failed to vacuum sqlite: ${error}`)
}
}

View File

@ -1,4 +1,5 @@
import { getAllWindows } from "@tauri-apps/api/window" import { app } from "@tauri-apps/api"
import { getAllWindows, getCurrentWindow, type Window } from "@tauri-apps/api/window"
import { isRegistered, register, unregister } from "@tauri-apps/plugin-global-shortcut" import { isRegistered, register, unregister } from "@tauri-apps/plugin-global-shortcut"
import { debug, info, warn } from "@tauri-apps/plugin-log" import { debug, info, warn } from "@tauri-apps/plugin-log"
import * as os from "@tauri-apps/plugin-os" import * as os from "@tauri-apps/plugin-os"
@ -47,8 +48,7 @@ export async function registerAppHotkey(hotkeyStr: string) {
mainWin.setFocus() mainWin.setFocus()
} }
} else { } else {
mainWin.show() mainWin.show().then(() => mainWin.setFocus())
mainWin.setFocus()
} }
} }
}) })
@ -78,12 +78,12 @@ export async function applyKeyComb(keys: userInput.Key[]) {
// await Promise.all(keys.map((key) => userInput.key("KeyPress", key))) // await Promise.all(keys.map((key) => userInput.key("KeyPress", key)))
for (const key of keys) { for (const key of keys) {
await userInput.key("KeyPress", key) await userInput.key("KeyPress", key)
await sleep(20) await sleep(100)
} }
await sleep(100) await sleep(150)
for (const key of keys) { for (const key of keys) {
await userInput.key("KeyRelease", key) await userInput.key("KeyRelease", key)
await sleep(20) await sleep(100)
} }
} }
@ -101,3 +101,12 @@ export async function paste() {
console.error("Unsupported platform: " + _platform) console.error("Unsupported platform: " + _platform)
} }
} }
export async function hideAndPaste(win?: Window) {
return app
.hide()
.then(() => sleep(60))
.then(() => (win ?? getCurrentWindow()).hide())
.then(() => sleep(60))
.then(() => paste())
}

View File

@ -1,14 +1,16 @@
import { appConfig, extensions } from "@/stores" import { appConfig, extensions } from "@/stores"
import { getCurrentWindow } from "@tauri-apps/api/window" import { getCurrentWindow } from "@tauri-apps/api/window"
import { info } from "@tauri-apps/plugin-log" import { error, info } from "@tauri-apps/plugin-log"
import { dev } from "$app/environment" import { dev } from "$app/environment"
import { cleanClipboard } from "./clipboard"
import { vacuumSqlite } from "./db"
import { mapKeyToTauriKey, registerAppHotkey } from "./hotkey" import { mapKeyToTauriKey, registerAppHotkey } from "./hotkey"
import { listenToReloadOneExtension } from "./tauri-events" import { listenToReloadOneExtension } from "./tauri-events"
/** /**
* Initialize the app * Initialize the app
*/ */
export function init() { export async function init() {
const window = getCurrentWindow() const window = getCurrentWindow()
if (window.label === "main") { if (window.label === "main") {
initMainWindow() initMainWindow()
@ -17,7 +19,14 @@ export function init() {
extensions.reloadExtension(extPath) extensions.reloadExtension(extPath)
}) })
} }
await cleanClipboard()
.then(() => {
info("Cleaned clipboard")
})
.catch((e) => {
error(`Failed to clean clipboard: ${e}`)
})
vacuumSqlite()
if (!dev) { if (!dev) {
// document.addEventListener("contextmenu", function (event) { // document.addEventListener("contextmenu", function (event) {
// event.preventDefault() // event.preventDefault()

View File

@ -97,7 +97,7 @@ export async function globalKeyDownHandler(e: KeyboardEvent) {
await appWin.hide() await appWin.hide()
location.reload() location.reload()
setTimeout(() => { setTimeout(() => {
appWin.show() appWin.show().then(() => appWin.setFocus())
}, 1_000) }, 1_000)
} }
} }

View File

@ -0,0 +1,19 @@
import { parseAPIVersion } from "@kksh/extension/load"
import type { ExtPackageJsonExtra } from "@kunkunapi/src/models/manifest"
import semver from "semver"
/**
* Decide the serialization method for kkrpc based on the api version
* apiVersion is populated in loadExtensionManifestFromDisk, but we parse it again
* @param apiVersion - The version of the @kksh/api
* @returns "superjson" or "json"
*/
export function decideKkrpcSerialization(ext: ExtPackageJsonExtra): "superjson" | "json" {
const apiVersion = parseAPIVersion(ext.dependencies || {})
if (apiVersion && semver.lte(apiVersion, "0.1.5")) {
// 0.1.6 is the first version that supports superjson and default to use superjson
return "json"
}
// fallback default to use superjson, some extensions might not install @kksh/api
return "superjson"
}

View File

@ -1,8 +1,7 @@
import { extensions } from "@/stores" import { extensions } from "@/stores"
import { supabaseAPI } from "@/supabase"
import { isCompatible } from "@kksh/api" import { isCompatible } from "@kksh/api"
import type { ExtPackageJsonExtra } from "@kksh/api/models" import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { greaterThan } from "@std/semver" import { getExtensionsLatestPublishByIdentifier } from "@kksh/sdk"
import { relaunch } from "@tauri-apps/plugin-process" import { relaunch } from "@tauri-apps/plugin-process"
import { check } from "@tauri-apps/plugin-updater" import { check } from "@tauri-apps/plugin-updater"
import { gt } from "semver" import { gt } from "semver"
@ -32,11 +31,22 @@ export async function checkSingleExtensionUpdate(
installedExt: ExtPackageJsonExtra, installedExt: ExtPackageJsonExtra,
autoupgrade: boolean autoupgrade: boolean
) { ) {
const { data: sbExt, error } = await supabaseAPI.getLatestExtPublish( const {
installedExt.kunkun.identifier data: sbExt,
) error,
response
} = await getExtensionsLatestPublishByIdentifier({
path: {
identifier: "RAG"
}
})
// const { data: sbExt, error } = await supabaseAPI.getLatestExtPublish(
// installedExt.kunkun.identifier
// )
if (error) { if (error) {
return toast.error(`Failed to check update for ${installedExt.kunkun.identifier}: ${error}`) return toast.error(
`Failed to check update for ${installedExt.kunkun.identifier}: ${error} (${response.status})`
)
} }
if (!sbExt) { if (!sbExt) {
@ -49,10 +59,7 @@ export async function checkSingleExtensionUpdate(
) { ) {
if (autoupgrade) { if (autoupgrade) {
await extensions await extensions
.upgradeStoreExtension( .upgradeStoreExtension(sbExt.identifier, sbExt.tarball_path)
sbExt.identifier,
supabaseAPI.translateExtensionFilePathToUrl(sbExt.tarball_path)
)
.then(() => { .then(() => {
toast.success(`${sbExt.name} upgraded`, { toast.success(`${sbExt.name} upgraded`, {
description: `From ${installedExt.version} to ${sbExt.version}` description: `From ${installedExt.version} to ${sbExt.version}`

View File

@ -12,6 +12,7 @@
<svelte:window on:keydown={handleKeyDown} /> <svelte:window on:keydown={handleKeyDown} />
<div class="fixed h-12 w-full" data-tauri-drag-region></div>
<Layouts.Center class="min-h-screen py-5"> <Layouts.Center class="min-h-screen py-5">
<Error.RawErrorJSONPreset <Error.RawErrorJSONPreset
title="Error" title="Error"

View File

@ -2,6 +2,8 @@
import { ParaglideJS } from "@inlang/paraglide-sveltekit" import { ParaglideJS } from "@inlang/paraglide-sveltekit"
import { i18n } from "$lib/i18n" import { i18n } from "$lib/i18n"
import "../app.css" import "../app.css"
import FullScreenLoading from "@/components/common/FullScreenLoading.svelte"
import { appState } from "@/stores/appState"
import { ModeWatcher, ThemeWrapper } from "@kksh/svelte5" import { ModeWatcher, ThemeWrapper } from "@kksh/svelte5"
import { Toaster } from "svelte-sonner" import { Toaster } from "svelte-sonner"
@ -12,6 +14,9 @@
<ModeWatcher /> <ModeWatcher />
<Toaster richColors closeButton /> <Toaster richColors closeButton />
<ThemeWrapper> <ThemeWrapper>
{#if $appState.fullScreenLoading}
<FullScreenLoading class="bg-background absolute inset-0 z-50" />
{/if}
{@render children()} {@render children()}
</ThemeWrapper> </ThemeWrapper>
</ParaglideJS> </ParaglideJS>

View File

@ -1,5 +1,15 @@
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { browser } from "$app/environment"
// Tauri doesn't have a Node.js server to do proper SSR // Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG) // so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info // See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true export const prerender = true
export const ssr = false export const ssr = false
export const load = () => {
if (browser) {
const win = getCurrentWebviewWindow()
return { win }
}
}

View File

@ -14,7 +14,7 @@
import { Constants, ViewTransition } from "@kksh/ui" import { Constants, ViewTransition } from "@kksh/ui"
import type { UnlistenFn } from "@tauri-apps/api/event" import type { UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { attachConsole, error, info } from "@tauri-apps/plugin-log" import { attachConsole, debug, error, info } from "@tauri-apps/plugin-log"
import { afterNavigate, beforeNavigate } from "$app/navigation" import { afterNavigate, beforeNavigate } from "$app/navigation"
import { gsap } from "gsap" import { gsap } from "gsap"
import { Flip } from "gsap/Flip" import { Flip } from "gsap/Flip"
@ -46,13 +46,14 @@
}) })
}) })
let { children } = $props() let { children, data } = $props()
const unlisteners: UnlistenFn[] = [] const unlisteners: UnlistenFn[] = []
onDestroy(() => { onDestroy(() => {
unlisteners.forEach((unlistener) => unlistener()) unlisteners.forEach((unlistener) => unlistener())
}) })
onMount(async () => { onMount(async () => {
attachConsole().then((unlistener) => unlisteners.push(unlistener)) await attachConsole().then((unlistener) => unlisteners.push(unlistener))
initDeeplink().then((unlistener) => unlisteners.push(unlistener)) initDeeplink().then((unlistener) => unlisteners.push(unlistener))
shellx shellx
.fixPathEnv() .fixPathEnv()
@ -79,7 +80,7 @@
// this extra is focused check may be needed because blur event got triggered somehow when window show() // this extra is focused check may be needed because blur event got triggered somehow when window show()
// for edge case: when settings page is opened and focused, switch to main window, the blur event is triggered for main window // for edge case: when settings page is opened and focused, switch to main window, the blur event is triggered for main window
if (!isFocused) { if (!isFocused) {
if ($appConfig.hideOnBlur) { if ($appConfig.hideOnBlur && !$appState.lockHideOnBlur) {
win.hide() win.hide()
} }
} }
@ -89,18 +90,18 @@
extensions.init() extensions.init()
unlisteners.push( unlisteners.push(
await listenToRecordExtensionProcessEvent(async (event) => { await listenToRecordExtensionProcessEvent(async (event) => {
console.log("record extension process event", event) debug(`record extension process event ${event.payload.pid}`)
winExtMap.registerProcess(event.payload.windowLabel, event.payload.pid) winExtMap.registerProcess(event.payload.windowLabel, event.payload.pid)
}) })
) )
unlisteners.push( unlisteners.push(
await listenToKillProcessEvent((event) => { await listenToKillProcessEvent((event) => {
console.log("kill process event", event) debug(`kill process event ${event.payload.pid}`)
winExtMap.unregisterProcess(event.payload.pid) winExtMap.unregisterProcess(event.payload.pid)
}) })
) )
} }
getCurrentWebviewWindow().show() data.win?.show().then(() => data.win?.setFocus())
}) })
</script> </script>

View File

@ -1,7 +1,11 @@
import { getExtensionsFolder, IS_IN_TAURI } from "@/constants" import { getExtensionsFolder, IS_IN_TAURI } from "@/constants"
import * as path from "@tauri-apps/api/path"
import { error } from "@tauri-apps/plugin-log" import { error } from "@tauri-apps/plugin-log"
import { setStoreCollectionPath } from "@tauri-store/svelte"
import type { LayoutLoad } from "./$types" import type { LayoutLoad } from "./$types"
export const load: LayoutLoad = async () => { export const load: LayoutLoad = async () => {
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "" } const appDataPath = await path.appDataDir()
await setStoreCollectionPath(await path.join(appDataPath, "kk-config"))
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "", appDataPath }
} }

View File

@ -2,22 +2,20 @@
<script lang="ts"> <script lang="ts">
import { commandLaunchers } from "@/cmds" import { commandLaunchers } from "@/cmds"
import { builtinCmds } from "@/cmds/builtin" import { builtinCmds } from "@/cmds/builtin"
import { systemCommands } from "@/cmds/system" import { systemCommands, systemCommandsFiltered } from "@/cmds/system"
import AppsCmds from "@/components/main/AppsCmds.svelte" import AppsCmds from "@/components/main/AppsCmds.svelte"
import { i18n } from "@/i18n" import { i18n } from "@/i18n"
import * as m from "@/paraglide/messages" import * as m from "@/paraglide/messages"
import { import {
appConfig, appConfig,
appConfigLoaded, appConfigLoaded,
// appsFiltered, appsFiltered,
appsLoader,
appState, appState,
devStoreExts, devSearchExtCmds,
// devStoreExtsFiltered, devStoreExtCmds,
// installedStoreExtsFiltered, quickLinksFiltered,
installedStoreExts, storeExtCmds,
quickLinks storeSearchExtCmds
// quickLinksFiltered
} from "@/stores" } from "@/stores"
import { cmdQueries } from "@/stores/cmdQuery" import { cmdQueries } from "@/stores/cmdQuery"
import { isKeyboardEventFromInputElement } from "@/utils/dom" import { isKeyboardEventFromInputElement } from "@/utils/dom"
@ -27,19 +25,28 @@
import { import {
BuiltinCmds, BuiltinCmds,
CustomCommandInput, CustomCommandInput,
ExtCmdsGroup, ExtCmds,
GlobalCommandPaletteFooter, GlobalCommandPaletteFooter,
QuickLinks, QuickLinks,
SystemCmds SystemCmds
} from "@kksh/ui/main" } from "@kksh/ui/main"
import { cn } from "@kksh/ui/utils" import { cn } from "@kksh/ui/utils"
import { Ext } from "@kunkunapi/src/models/extension"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { getCurrentWindow, Window } from "@tauri-apps/api/window" import { getCurrentWindow, Window } from "@tauri-apps/api/window"
import { platform } from "@tauri-apps/plugin-os" import { platform } from "@tauri-apps/plugin-os"
import { exit } from "@tauri-apps/plugin-process" import { exit } from "@tauri-apps/plugin-process"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import { ArrowBigUpIcon, CircleXIcon, EllipsisVerticalIcon, RefreshCcwIcon } from "lucide-svelte" import {
ArrowBigUpIcon,
CircleXIcon,
EllipsisVerticalIcon,
RefreshCcwIcon,
SettingsIcon
} from "lucide-svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import { Inspect } from "svelte-inspect-value"
import * as v from "valibot"
const win = getCurrentWindow() const win = getCurrentWindow()
let inputEle: HTMLInputElement | null = $state(null) let inputEle: HTMLInputElement | null = $state(null)
@ -59,12 +66,16 @@
if (splashscreenWin) { if (splashscreenWin) {
splashscreenWin.close() splashscreenWin.close()
} }
win.show() win.show().then(() => win.setFocus())
}) })
win.onFocusChanged(({ payload: focused }) => { win.onFocusChanged(({ payload: focused }) => {
if (focused) { if (focused) {
win.show() win
inputEle?.focus() .show()
.then(() => win.setFocus())
.finally(() => {
inputEle?.focus()
})
} }
}) })
inputEle?.focus() inputEle?.focus()
@ -72,6 +83,7 @@
// wait for appConfig store to be loaded, it's async and saved to disk when changed, so we use another store appConfigLoaded // wait for appConfig store to be loaded, it's async and saved to disk when changed, so we use another store appConfigLoaded
// to keep track of the loading status // to keep track of the loading status
if (loaded) { if (loaded) {
console.log("appConfig.get().onBoarded", appConfig.get().onBoarded)
if (!appConfig.get().onBoarded) { if (!appConfig.get().onBoarded) {
setTimeout(() => { setTimeout(() => {
goto(i18n.resolveRoute("/app/help/onboarding")) goto(i18n.resolveRoute("/app/help/onboarding"))
@ -94,10 +106,21 @@
} }
}} }}
/> />
<!--
<Inspect name="devStoreExts" value={$devStoreExts} />
<Inspect name="extensions" value={$extensions} />
<Inspect name="installedStoreExts" value={$installedStoreExts} />
<Inspect name="storeSearchExtCmds" value={$storeSearchExtCmds} />
<Inspect name="devSearchExtCmds" value={$devSearchExtCmds} />
<Inspect name="storeExtCmds" value={$storeExtCmds} />
<Inspect name="devStoreExtCmds" value={$devStoreExtCmds} />
<Inspect name="$appState.searchTerm" value={$appState.searchTerm} />
-->
<Command.Root <Command.Root
class={cn("h-screen rounded-lg shadow-md")} class={cn("h-screen rounded-lg shadow-md")}
bind:value={$appState.highlightedCmd} bind:value={$appState.highlightedCmd}
shouldFilter={true} shouldFilter={false}
loop loop
> >
<CustomCommandInput <CustomCommandInput
@ -169,8 +192,8 @@
<span class="flex items-center">⌃+<ArrowBigUpIcon class="h-4 w-4" />+R </span> <span class="flex items-center">⌃+<ArrowBigUpIcon class="h-4 w-4" />+R </span>
</DropdownMenu.Shortcut> </DropdownMenu.Shortcut>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item onclick={() => location.reload()}> <DropdownMenu.Item onclick={() => goto(i18n.resolveRoute("/app/settings"))}>
<RefreshCcwIcon class="mr-2 h-4 w-4 text-green-500" /> <SettingsIcon class="mr-2 h-4 w-4 text-green-500" />
{m.home_command_input_dropdown_open_preference()} {m.home_command_input_dropdown_open_preference()}
<DropdownMenu.Shortcut> <DropdownMenu.Shortcut>
{#if platform() === "macos"} {#if platform() === "macos"}
@ -198,45 +221,37 @@
</CustomCommandInput> </CustomCommandInput>
<Command.List class="max-h-screen grow"> <Command.List class="max-h-screen grow">
<Command.Empty data-tauri-drag-region>No results found.</Command.Empty> <Command.Empty data-tauri-drag-region>No results found.</Command.Empty>
{#if $appConfig.extensionsInstallDir && $devStoreExts.length > 0} {#if $devSearchExtCmds.length > 0}
<ExtCmdsGroup <ExtCmds
extensions={$devStoreExts}
heading={m.command_group_heading_dev_ext()} heading={m.command_group_heading_dev_ext()}
extCmds={$devSearchExtCmds}
hmr={$appConfig.hmr}
isDev={true} isDev={true}
onExtCmdSelect={commandLaunchers.onExtCmdSelect} onExtCmdSelect={commandLaunchers.onExtCmdSelect}
hmr={$appConfig.hmr}
/> />
{/if} {/if}
{#if $storeSearchExtCmds.length > 0}
{#if $appConfig.extensionsInstallDir && $installedStoreExts.length > 0} <ExtCmds
<ExtCmdsGroup
extensions={$installedStoreExts}
heading={m.command_group_heading_ext()} heading={m.command_group_heading_ext()}
isDev={false} extCmds={$storeSearchExtCmds}
hmr={false} hmr={false}
isDev={false}
onExtCmdSelect={commandLaunchers.onExtCmdSelect} onExtCmdSelect={commandLaunchers.onExtCmdSelect}
/> />
{/if} {/if}
<QuickLinks quickLinks={$quickLinks} />
<BuiltinCmds builtinCmds={$builtinCmds} />
<SystemCmds systemCommands={$systemCommands} />
<AppsCmds apps={$appsLoader} />
<!-- <AppsCmds apps={$appsFiltered} /> -->
<!-- {#if $quickLinksFiltered.length > 0}
<QuickLinks quickLinks={$quickLinksFiltered} />
{/if}
{#if $appsFiltered.length > 0}
<AppsCmds apps={$appsFiltered} />
{/if}
{#if $builtinCmds.length > 0} {#if $builtinCmds.length > 0}
<BuiltinCmds builtinCmds={$builtinCmds} /> <BuiltinCmds builtinCmds={$builtinCmds} />
{/if} {/if}
{#if $systemCommandsFiltered.length > 0} {#if $systemCommandsFiltered.length > 0}
<SystemCmds systemCommands={$systemCommandsFiltered} /> <SystemCmds systemCommands={$systemCommandsFiltered} />
{/if} --> {/if}
<!-- <AppsCmds apps={$appsLoader} /> --> {#if $appsFiltered.length > 0}
<!-- <AppsCmds apps={$appsFiltered} /> --> <AppsCmds apps={$appsFiltered} />
{/if}
{#if $quickLinksFiltered.length > 0}
<QuickLinks quickLinks={$quickLinksFiltered} />
{/if}
</Command.List> </Command.List>
<GlobalCommandPaletteFooter /> <GlobalCommandPaletteFooter />
</Command.Root> </Command.Root>

View File

@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import { paste } from "@/utils/hotkey" import { hideAndPaste } from "@/utils/hotkey"
import { goBack, goHome } from "@/utils/route" import { goHome } from "@/utils/route"
import { listenToNewClipboardItem } from "@/utils/tauri-events" import { listenToNewClipboardItem, listenToWindowFocus } from "@/utils/tauri-events"
import { sleep } from "@/utils/time"
import Icon from "@iconify/svelte" import Icon from "@iconify/svelte"
import { ClipboardContentType, db } from "@kksh/api/commands" import { ClipboardContentType } from "@kksh/api/commands"
import { SearchModeEnum, SQLSortOrderEnum, type ExtData } from "@kksh/api/models" import { SearchModeEnum, SQLSortOrderEnum, type ExtData } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { Button, Command, Resizable } from "@kksh/svelte5" import { Button, Command, Resizable } from "@kksh/svelte5"
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"
import { CustomCommandInput, GlobalCommandPaletteFooter } from "@kksh/ui/main" import { CustomCommandInput, GlobalCommandPaletteFooter } from "@kksh/ui/main"
import { app } from "@tauri-apps/api"
import type { UnlistenFn } from "@tauri-apps/api/event" import type { UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { platform } from "@tauri-apps/plugin-os" import { platform } from "@tauri-apps/plugin-os"
@ -20,12 +19,14 @@
import ContentPreview from "./content-preview.svelte" import ContentPreview from "./content-preview.svelte"
const _platform = platform() const _platform = platform()
let inputEle = $state<HTMLInputElement | null>(null)
const curWin = getCurrentWebviewWindow() const curWin = getCurrentWebviewWindow()
let searchTerm = $state("") let searchTerm = $state("")
let clipboardHistoryList = $state<ExtData[]>([]) let clipboardHistoryList = $state<ExtData[]>([])
let highlightedItemValue = $state<string>("") let highlightedItemValue = $state<string>("")
let highlighted = $state<ExtData | null>(null) let highlighted = $state<ExtData | null>(null)
let unlistenClipboard = $state<UnlistenFn | null>(null) let unlistenClipboard = $state<UnlistenFn | null>(null)
let unlistenFocusEvt = $state<UnlistenFn | null>(null)
let isScrolling = $state(false) let isScrolling = $state(false)
let page = $state(1) let page = $state(1)
@ -74,10 +75,19 @@
}).then((unlisten) => { }).then((unlisten) => {
unlistenClipboard = unlisten unlistenClipboard = unlisten
}) })
listenToWindowFocus(async () => {
if (inputEle) {
inputEle.focus()
}
}).then((unlisten) => {
unlistenFocusEvt = unlisten
})
}) })
onDestroy(() => { onDestroy(() => {
unlistenClipboard?.() unlistenClipboard?.()
unlistenFocusEvt?.()
}) })
$effect(() => { $effect(() => {
@ -191,11 +201,7 @@
return Promise.reject(new Error("No data found")) return Promise.reject(new Error("No data found"))
} }
return writeToClipboard(data).then(async () => { return writeToClipboard(data).then(async () => {
return app return hideAndPaste(curWin)
.hide()
.then(() => sleep(100))
.then(() => curWin.hide())
.then(() => paste())
}) })
}) })
.then(() => toast.success("Copied to clipboard")) .then(() => toast.success("Copied to clipboard"))
@ -242,6 +248,7 @@
autofocus autofocus
placeholder="Type a command or search..." placeholder="Type a command or search..."
leftSlot={leftSlot as Snippet} leftSlot={leftSlot as Snippet}
bind:ref={inputEle}
bind:value={searchTerm} bind:value={searchTerm}
/> />
<Resizable.PaneGroup direction="horizontal" class="w-full rounded-lg"> <Resizable.PaneGroup direction="horizontal" class="w-full rounded-lg">

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { cn } from "@/utils" import { cn } from "@/utils"
import { db } from "@kksh/api/commands"
import type { ExtData } from "@kksh/api/models" import type { ExtData } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { Resizable, Separator } from "@kksh/svelte5" import { Resizable, Separator } from "@kksh/svelte5"
import { convertFileSrc } from "@tauri-apps/api/core" import { convertFileSrc } from "@tauri-apps/api/core"
import DOMPurify from "dompurify" import DOMPurify from "dompurify"

View File

@ -8,6 +8,8 @@
import * as userInput from "tauri-plugin-user-input-api" import * as userInput from "tauri-plugin-user-input-api"
import { type InputEvent } from "tauri-plugin-user-input-api" import { type InputEvent } from "tauri-plugin-user-input-api"
let { data } = $props()
const SymbolMap = { const SymbolMap = {
Alt: "⎇", Alt: "⎇",
AltGr: "⌥", AltGr: "⌥",
@ -97,10 +99,7 @@
} }
$effect(() => { $effect(() => {
const win = getCurrentWebviewWindow() data.win?.show().then(() => data.win?.setFocus())
if (win) {
win.show()
}
userInput.setEventTypes([userInput.EventTypeEnum.KeyPress, userInput.EventTypeEnum.KeyRelease]) userInput.setEventTypes([userInput.EventTypeEnum.KeyPress, userInput.EventTypeEnum.KeyRelease])
userInput.startListening((evt: InputEvent) => { userInput.startListening((evt: InputEvent) => {

View File

@ -8,23 +8,24 @@
import * as clipboard from "tauri-plugin-clipboard-api" import * as clipboard from "tauri-plugin-clipboard-api"
let image = $state<string | null>(null) let image = $state<string | null>(null)
const appWin = getCurrentWebviewWindow() let { data } = $props()
let originalSize = $state<{ width: number; height: number } | null>(null) let originalSize = $state<{ width: number; height: number } | null>(null)
let originalScaleFactor = $state<number | null>(null) let originalScaleFactor = $state<number | null>(null)
let scale = $state<number>(1) let scale = $state<number>(1)
let currentSize = $derived( let currentSize = $derived(
originalSize ? { width: originalSize.width * scale, height: originalSize.height * scale } : null originalSize ? { width: originalSize.width * scale, height: originalSize.height * scale } : null
) )
const win = getCurrentWebviewWindow()
$effect(() => { $effect(() => {
if (currentSize) { if (currentSize) {
appWin.setSize(new LogicalSize(currentSize.width, currentSize.height)) win.setSize(new LogicalSize(currentSize.width, currentSize.height))
} }
}) })
async function getWindowSize() { async function getWindowSize() {
const size = await appWin.outerSize() const size = await win.outerSize()
const scaleFactor = originalScaleFactor ?? (await appWin.scaleFactor()) const scaleFactor = originalScaleFactor ?? (await win.scaleFactor())
const logicalSize = size.toLogical(scaleFactor) const logicalSize = size.toLogical(scaleFactor)
return { logicalSize, scaleFactor } return { logicalSize, scaleFactor }
} }
@ -36,7 +37,7 @@
image = b64 image = b64
}) })
.finally(() => { .finally(() => {
appWin.show() data.win?.show().then(() => data.win?.setFocus())
}) })
const { logicalSize, scaleFactor } = await getWindowSize() const { logicalSize, scaleFactor } = await getWindowSize()
originalSize = { width: logicalSize.width, height: logicalSize.height } originalSize = { width: logicalSize.width, height: logicalSize.height }
@ -67,13 +68,13 @@
<svelte:window <svelte:window
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === "Escape") { if (e.key === "Escape") {
appWin.close() win.close()
} }
}} }}
/> />
<Button size="icon" variant="ghost" class="fixed left-2 top-2" onclick={() => appWin.close()} <Button size="icon" variant="ghost" class="fixed left-2 top-2" onclick={() => win.close()}>
><CircleX /></Button <CircleX />
> </Button>
<main class="z-50 h-screen w-screen overflow-hidden" data-tauri-drag-region> <main class="z-50 h-screen w-screen overflow-hidden" data-tauri-drag-region>
{#if image} {#if image}
<img <img

View File

@ -2,14 +2,13 @@
import { getExtensionsFolder } from "@/constants" import { getExtensionsFolder } from "@/constants"
import { appState, extensions } from "@/stores" import { appState, extensions } from "@/stores"
import { keys } from "@/stores/keys" import { keys } from "@/stores/keys"
import { supabaseAPI } from "@/supabase"
import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key"
import { goBack, goHome } from "@/utils/route" import { goBack, goHome } from "@/utils/route"
import { Action as ActionSchema } from "@kksh/api/models" import { Action as ActionSchema, ExtensionStoreListItem, ExtPublish } from "@kksh/api/models"
import { Action } from "@kksh/api/ui" import { Action } from "@kksh/api/ui"
import { SBExt } from "@kksh/supabase/models" import {
import type { ExtPublishMetadata } from "@kksh/supabase/models" getExtensionsLatestPublishByIdentifier,
import { type Tables } from "@kksh/supabase/types" postExtensionsIncrementDownloads
} from "@kksh/sdk"
import { Button, Command } from "@kksh/svelte5" import { Button, Command } from "@kksh/svelte5"
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"
import { ExtListItem } from "@kksh/ui/extension" import { ExtListItem } from "@kksh/ui/extension"
@ -17,8 +16,9 @@
import { platform } from "@tauri-apps/plugin-os" import { platform } from "@tauri-apps/plugin-os"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import { ArrowLeft } from "lucide-svelte" import { ArrowLeft } from "lucide-svelte"
import type { Snippet } from "svelte" import { onMount, type Snippet } from "svelte"
import { toast } from "svelte-sonner" import { toast } from "svelte-sonner"
import type { Action as SvelteAction } from "svelte/action"
import { getInstallExtras } from "./[identifier]/helper.js" import { getInstallExtras } from "./[identifier]/helper.js"
let { data } = $props() let { data } = $props()
@ -38,51 +38,89 @@
// ) // )
// } // }
function onExtItemSelected(ext: SBExt) { onMount(() => {
// setTimeout(() => {
// console.log("focus", listviewInputRef)
// listviewInputRef?.focus()
// }, 3_000)
})
const inputAction: SvelteAction = (node) => {
// the node has been mounted in the DOM
$effect(() => {
// setup goes here
console.log("inputAction", node)
listviewInputRef?.focus()
return () => {
// teardown goes here
}
})
}
$effect(() => {
function docKeyDown(e: KeyboardEvent) {
if (e.key === "/") {
listviewInputRef?.focus()
}
}
document.addEventListener("keydown", docKeyDown)
return () => {
document.removeEventListener("keydown", docKeyDown)
}
})
function onExtItemSelected(ext: ExtensionStoreListItem) {
goto(`./store/${ext.identifier}`) goto(`./store/${ext.identifier}`)
} }
async function onExtItemUpgrade(ext: SBExt) { async function onExtItemUpgrade(ext: ExtensionStoreListItem) {
const res = await supabaseAPI.getLatestExtPublish(ext.identifier) const { data, error, response } = await getExtensionsLatestPublishByIdentifier({
if (res.error) path: {
identifier: ext.identifier
}
})
if (error)
return toast.error("Fail to get latest extension", { return toast.error("Fail to get latest extension", {
description: res.error.message description: error.error
}) })
const tarballUrl = res.data.tarball_path.startsWith("http") const installExtras = await getInstallExtras(data?.metadata)
? res.data.tarball_path
: supabaseAPI.translateExtensionFilePathToUrl(res.data.tarball_path)
const installExtras = await getInstallExtras(
res.data as Tables<"ext_publish"> & { metadata: ExtPublishMetadata }
)
return extensions return extensions
.upgradeStoreExtension(ext.identifier, tarballUrl, installExtras) .upgradeStoreExtension(ext.identifier, data.tarball_path, installExtras)
.then((newExt) => { .then((newExt) => {
toast.success(`${ext.name} Upgraded to ${newExt.version}`) toast.success(`${ext.name} Upgraded to ${newExt.version}`)
}) })
} }
async function onExtItemInstall(ext: SBExt) { async function onExtItemInstall(ext: ExtensionStoreListItem) {
const res = await supabaseAPI.getLatestExtPublish(ext.identifier) const { data, error, response } = await getExtensionsLatestPublishByIdentifier({
if (res.error) path: {
identifier: ext.identifier
}
})
if (error)
return toast.error("Fail to get latest extension", { return toast.error("Fail to get latest extension", {
description: res.error.message description: error.error
}) })
const tarballUrl = res.data.tarball_path.startsWith("http") const installExtras = await getInstallExtras(data?.metadata)
? res.data.tarball_path
: supabaseAPI.translateExtensionFilePathToUrl(res.data.tarball_path)
const installExtras = await getInstallExtras(
res.data as Tables<"ext_publish"> & { metadata: ExtPublishMetadata }
)
const installDir = await getExtensionsFolder() const installDir = await getExtensionsFolder()
return extensions return extensions
.installFromTarballUrl(tarballUrl, installDir, installExtras) .installFromTarballUrl(data.tarball_path, installDir, installExtras)
.then(() => toast.success(`Plugin ${ext.name} Installed`)) .then(() => toast.success(`Plugin ${ext.name} Installed`))
.then(() => .then(() =>
supabaseAPI.incrementDownloads({ postExtensionsIncrementDownloads({
identifier: ext.identifier, body: {
version: ext.version identifier: ext.identifier,
version: ext.version
}
}) })
.then(({ error }) => {
if (error) {
console.error(error)
}
})
.catch(console.error)
) )
} }
@ -126,7 +164,7 @@
}) })
</script> </script>
<svelte:window on:keydown={onkeydown} /> <svelte:window {onkeydown} />
{#snippet leftSlot()} {#snippet leftSlot()}
<Button <Button
variant="outline" variant="outline"
@ -142,7 +180,7 @@
<CustomCommandInput <CustomCommandInput
bind:ref={listviewInputRef} bind:ref={listviewInputRef}
autofocus autofocus
placeholder="Type a command or search..." placeholder="Type / to focus"
leftSlot={leftSlot as Snippet} leftSlot={leftSlot as Snippet}
bind:value={$appState.searchTerm} bind:value={$appState.searchTerm}
onkeydown={(e) => { onkeydown={(e) => {
@ -167,7 +205,9 @@
{ext} {ext}
installedVersion={$installedExtsMap[ext.identifier]} installedVersion={$installedExtsMap[ext.identifier]}
isUpgradable={!!$upgradableExpsMap[ext.identifier]} isUpgradable={!!$upgradableExpsMap[ext.identifier]}
onSelect={() => {}} onSelect={() => {
onExtItemSelected(ext)
}}
onUpgrade={() => onExtItemUpgrade(ext)} onUpgrade={() => onExtItemUpgrade(ext)}
onInstall={() => onExtItemInstall(ext)} onInstall={() => onExtItemInstall(ext)}
/> />

View File

@ -1,43 +1,47 @@
import { appConfig, extensions, installedStoreExts } from "@/stores" import { appConfig, appState, extensions, installedStoreExts } from "@/stores"
import { supabaseAPI } from "@/supabase" import { goHome } from "@/utils/route"
import type { ExtPackageJsonExtra } from "@kksh/api/models" // import { supabaseAPI } from "@/supabase"
import type { ExtensionStoreListItem, ExtPackageJsonExtra } from "@kksh/api/models"
import { isExtPathInDev, isUpgradable } from "@kksh/extension" import { isExtPathInDev, isUpgradable } from "@kksh/extension"
import { SBExt } from "@kksh/supabase/models" import { getExtensionsStoreList } from "@kksh/sdk"
import { error } from "@sveltejs/kit" import { toast } from "svelte-sonner"
import { derived, get, type Readable } from "svelte/store" import { derived, get, type Readable } from "svelte/store"
import type { PageLoad } from "./$types" import type { PageLoad } from "./$types"
export const load: PageLoad = async (): Promise<{ export const load: PageLoad = (): Promise<{
storeExtList: SBExt[] storeExtList: ExtensionStoreListItem[]
installedStoreExts: Readable<ExtPackageJsonExtra[]> installedStoreExts: Readable<ExtPackageJsonExtra[]>
installedExtsMap: Readable<Record<string, string>> installedExtsMap: Readable<Record<string, string>>
upgradableExpsMap: Readable<Record<string, boolean>> upgradableExpsMap: Readable<Record<string, boolean>>
}> => { }> => {
const storeExtList = await supabaseAPI.getExtList() appState.setFullScreenLoading(true)
// map identifier to extItem return getExtensionsStoreList()
const storeExtsMap = Object.fromEntries(storeExtList.map((ext) => [ext.identifier, ext])) .then(({ data: storeExtList, error, response }) => {
const _appConfig = get(appConfig) storeExtList = storeExtList ?? []
// const installedStoreExts = derived(extensions, ($extensions) => { if (error) {
// if (!_appConfig.extensionPath) return [] toast.error(`Failed to load extension store: ${error} (${response.status})`)
// return $extensions.filter((ext) => !isExtPathInDev(_appConfig.extensionPath!, ext.extPath)) goHome()
// }) }
// map installed extension identifier to version const storeExtsMap = Object.fromEntries(storeExtList.map((ext) => [ext.identifier, ext]))
const installedExtsMap = derived(installedStoreExts, ($exts) => const installedExtsMap = derived(installedStoreExts, ($exts) =>
Object.fromEntries($exts.map((ext) => [ext.kunkun.identifier, ext.version])) Object.fromEntries($exts.map((ext) => [ext.kunkun.identifier, ext.version]))
) )
const upgradableExpsMap = derived(installedStoreExts, ($exts) => const upgradableExpsMap = derived(installedStoreExts, ($exts) =>
Object.fromEntries( Object.fromEntries(
$exts.map((ext) => { $exts.map((ext) => {
const dbExt: SBExt | undefined = storeExtsMap[ext.kunkun.identifier] const dbExt: ExtensionStoreListItem | undefined = storeExtsMap[ext.kunkun.identifier]
return [ext.kunkun.identifier, dbExt ? isUpgradable(dbExt, ext.version) : false] return [ext.kunkun.identifier, dbExt ? isUpgradable(dbExt, ext.version) : false]
}) })
) )
) )
return {
return { storeExtList,
storeExtList, installedStoreExts,
installedStoreExts, installedExtsMap,
installedExtsMap, upgradableExpsMap
upgradableExpsMap }
} })
.finally(() => {
appState.setFullScreenLoading(false)
})
} }

View File

@ -2,11 +2,8 @@
import { getExtensionsFolder } from "@/constants.js" import { getExtensionsFolder } from "@/constants.js"
import { i18n } from "@/i18n.js" import { i18n } from "@/i18n.js"
import { extensions, installedStoreExts } from "@/stores/extensions.js" import { extensions, installedStoreExts } from "@/stores/extensions.js"
import { supabaseAPI } from "@/supabase" import { DBExtension, ExtensionStoreListItem, ExtPackageJson, ExtPublish } from "@kksh/api/models"
import { goBack } from "@/utils/route.js" import { postExtensionsIncrementDownloads } from "@kksh/sdk"
import { ExtPackageJson } from "@kksh/api/models"
import { ExtPublishMetadata } from "@kksh/supabase/models"
import type { Tables } from "@kksh/supabase/types"
import { Button } from "@kksh/svelte5" import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/svelte5/utils" import { cn } from "@kksh/svelte5/utils"
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"
@ -22,10 +19,8 @@
import { getInstallExtras } from "./helper" import { getInstallExtras } from "./helper"
const { data } = $props() const { data } = $props()
const extPublish: Tables<"ext_publish"> & { metadata: ExtPublishMetadata } = $derived( const extPublish = $derived(data.extPublish)
data.extPublish const ext = $derived(data.ext)
)
const ext: Tables<"extensions"> = $derived(data.ext)
const manifest = $derived(data.manifest) const manifest = $derived(data.manifest)
const installedExt = storeDerived(installedStoreExts, ($e) => { const installedExt = storeDerived(installedStoreExts, ($e) => {
return $e.find((e) => e.kunkun.identifier === extPublish.identifier) return $e.find((e) => e.kunkun.identifier === extPublish.identifier)
@ -77,26 +72,29 @@
}, 500) }, 500)
}) })
const demoImages = $derived( const demoImages = $derived(extPublish.demo_images)
extPublish.demo_images.map((src) => supabaseAPI.translateExtensionFilePathToUrl(src))
)
async function onInstallSelected() { async function onInstallSelected() {
loading.install = true loading.install = true
const tarballUrl = extPublish.tarball_path.startsWith("http") const installExtras = await getInstallExtras(extPublish.metadata)
? extPublish.tarball_path
: supabaseAPI.translateExtensionFilePathToUrl(extPublish.tarball_path)
const installExtras = await getInstallExtras(extPublish)
const installDir = await getExtensionsFolder() const installDir = await getExtensionsFolder()
return extensions return extensions
.installFromTarballUrl(tarballUrl, installDir, installExtras) .installFromTarballUrl(extPublish.tarball_path, installDir, installExtras)
.then(() => toast.success(`Plugin ${extPublish.name} Installed`)) .then(() => toast.success(`Plugin ${extPublish.name} Installed`))
.then((loadedExt) => { .then((loadedExt) => {
info(`Successfully installed ${extPublish.name}`) info(`Successfully installed ${extPublish.name}`)
supabaseAPI.incrementDownloads({ postExtensionsIncrementDownloads({
identifier: extPublish.identifier, body: {
version: extPublish.version identifier: extPublish.identifier,
version: extPublish.version
}
}) })
.then(({ error }) => {
if (error) {
console.error(error)
}
})
.catch(console.error)
showBtn.install = false showBtn.install = false
showBtn.uninstall = true showBtn.uninstall = true
}) })
@ -111,9 +109,8 @@
function onUpgradeSelected() { function onUpgradeSelected() {
loading.upgrade = true loading.upgrade = true
const tarballUrl = supabaseAPI.translateExtensionFilePathToUrl(extPublish.tarball_path)
return extensions return extensions
.upgradeStoreExtension(extPublish.identifier, tarballUrl) .upgradeStoreExtension(extPublish.identifier, extPublish.tarball_path)
.then((newExt) => { .then((newExt) => {
toast.success( toast.success(
`${extPublish.name} Upgraded from ${$installedExt?.version} to ${newExt.version}` `${extPublish.name} Upgraded from ${$installedExt?.version} to ${newExt.version}`

View File

@ -1,53 +1,57 @@
import { extensions } from "@/stores" import { appState } from "@/stores"
import { supabaseAPI } from "@/supabase" import { DBExtension, ExtPublish, KunkunExtManifest } from "@kksh/api/models"
import { KunkunExtManifest, type ExtPackageJsonExtra } from "@kksh/api/models" import { getExtensionsByIdentifier, getExtensionsLatestPublishByIdentifier } from "@kksh/sdk"
import { ExtPublishMetadata } from "@kksh/supabase/models"
import type { Tables } from "@kksh/supabase/types"
import { error } from "@sveltejs/kit" import { error } from "@sveltejs/kit"
import { toast } from "svelte-sonner"
import * as v from "valibot" import * as v from "valibot"
import type { PageLoad } from "./$types" import type { PageLoad } from "./$types"
export const load: PageLoad = async ({ export const load: PageLoad = ({
params params
}): Promise<{ }): Promise<{
extPublish: Tables<"ext_publish"> & { metadata: ExtPublishMetadata } extPublish: ExtPublish
ext: Tables<"extensions"> ext: DBExtension
manifest: KunkunExtManifest manifest: KunkunExtManifest
params: { params: {
identifier: string identifier: string
} }
}> => { }> => {
const { error: dbError, data: extPublish } = await supabaseAPI.getLatestExtPublish( appState.setFullScreenLoading(true)
params.identifier return getExtensionsLatestPublishByIdentifier({
) path: {
const metadataParse = v.safeParse(ExtPublishMetadata, extPublish?.metadata ?? {}) identifier: params.identifier
if (dbError) { }
return error(400, { })
message: dbError.message .then(async ({ data: extPublish, error: err, response }) => {
if (err || !extPublish) {
return error(response.status, {
message: "Failed to get extension publish"
})
}
const {
data: ext,
error: extError,
response: extRes
} = await getExtensionsByIdentifier({
path: {
identifier: params.identifier
}
})
if (extError || !ext) {
console.error(extError)
return error(extRes.status, {
message: extError.error || "Failed to get extension"
})
}
return {
extPublish: v.parse(ExtPublish, extPublish),
ext,
manifest: v.parse(KunkunExtManifest, extPublish.manifest),
params
}
}) })
} .finally(() => {
const metadata = metadataParse.success ? metadataParse.output : {} appState.setFullScreenLoading(false)
const parseManifest = v.safeParse(KunkunExtManifest, extPublish.manifest)
if (!parseManifest.success) {
const errMsg = "Invalid extension manifest, you may need to upgrade your app."
toast.error(errMsg)
throw error(400, errMsg)
}
const { data: ext, error: extError } = await supabaseAPI.getExtension(params.identifier)
if (extError) {
return error(400, {
message: extError.message
}) })
}
return {
extPublish: { ...extPublish, metadata },
ext,
params,
manifest: parseManifest.output
}
} }
export const csr = true export const csr = true

View File

@ -1,15 +1,15 @@
import type { ExtPublishMetadata } from "@kksh/supabase/models" import type { ExtPublishMetadata } from "@kunkunapi/src/models"
import type { Tables } from "@kksh/supabase/types"
export async function getInstallExtras( export async function getInstallExtras(extMetadata?: {
ext: Tables<"ext_publish"> & { metadata?: ExtPublishMetadata } sourceType?: string
): Promise<{ overwritePackageJson?: string }> { source?: string
}): Promise<{ overwritePackageJson?: string }> {
const extras: { overwritePackageJson?: string } = {} const extras: { overwritePackageJson?: string } = {}
if (ext.metadata?.sourceType) { if (extMetadata?.sourceType) {
if (ext.metadata?.sourceType === "jsr") { if (extMetadata?.sourceType === "jsr") {
if (ext.metadata?.source) { if (extMetadata?.source) {
try { try {
const res = await fetch(`${ext.metadata.source}/package.json`) const res = await fetch(`${extMetadata.source}/package.json`)
const pkgJsonContent = await res.text() const pkgJsonContent = await res.text()
extras.overwritePackageJson = pkgJsonContent extras.overwritePackageJson = pkgJsonContent
} catch (error) { } catch (error) {

View File

@ -1,13 +1,15 @@
<script lang="ts"> <script lang="ts">
import DanceTransition from "@/components/dance/dance-transition.svelte" import DanceTransition from "@/components/dance/dance-transition.svelte"
import { i18n } from "@/i18n" import { i18n } from "@/i18n"
import { appConfig, winExtMap } from "@/stores" import { appConfig, appState, winExtMap } from "@/stores"
import { helperAPI } from "@/utils/helper" import { helperAPI } from "@/utils/helper"
import { paste } from "@/utils/hotkey"
import { goBackOnEscape } from "@/utils/key" import { goBackOnEscape } from "@/utils/key"
import { decideKkrpcSerialization } from "@/utils/kkrpc"
import { goHome } from "@/utils/route" import { goHome } from "@/utils/route"
import { positionToCssStyleString, positionToTailwindClasses } from "@/utils/style" import { positionToCssStyleString, positionToTailwindClasses } from "@/utils/style"
import { sleep } from "@/utils/time"
import { isInMainWindow } from "@/utils/window" import { isInMainWindow } from "@/utils/window"
import { db } from "@kksh/api/commands"
import { CustomPosition, ThemeColor, type Position } from "@kksh/api/models" import { CustomPosition, ThemeColor, type Position } from "@kksh/api/models"
import { import {
constructJarvisServerAPIWithPermissions, constructJarvisServerAPIWithPermissions,
@ -16,10 +18,17 @@
type IUiCustom type IUiCustom
} from "@kksh/api/ui" } from "@kksh/api/ui"
import { toast, type IUiCustomServer1, type IUiCustomServer2 } from "@kksh/api/ui/custom" import { toast, type IUiCustomServer1, type IUiCustomServer2 } from "@kksh/api/ui/custom"
import { db } from "@kksh/drizzle"
import { Button } from "@kksh/svelte5" import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/ui/utils" import { cn } from "@kksh/ui/utils"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server" 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 { getCurrentWindow } from "@tauri-apps/api/window" import { getCurrentWindow } from "@tauri-apps/api/window"
import { info } from "@tauri-apps/plugin-log"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import { IframeParentIO, RPCChannel } from "kkrpc/browser" import { IframeParentIO, RPCChannel } from "kkrpc/browser"
import { ArrowLeftIcon, MoveIcon, RefreshCcwIcon, XIcon } from "lucide-svelte" import { ArrowLeftIcon, MoveIcon, RefreshCcwIcon, XIcon } from "lucide-svelte"
@ -28,7 +37,7 @@
let { data }: { data: PageData } = $props() let { data }: { data: PageData } = $props()
const { loadedExt, url, extPath, extInfoInDB } = data const { loadedExt, url, extPath, extInfoInDB } = data
const appWin = getCurrentWindow() let extSpawnedProcesses = $state<number[]>([])
let iframeRef: HTMLIFrameElement let iframeRef: HTMLIFrameElement
let uiControl = $state<{ let uiControl = $state<{
iframeLoaded: boolean iframeLoaded: boolean
@ -55,7 +64,7 @@
if (isInMainWindow()) { if (isInMainWindow()) {
goto(i18n.resolveRoute("/app/")) goto(i18n.resolveRoute("/app/"))
} else { } else {
appWin.close() data.win?.close()
} }
}, },
hideBackButton: async () => { hideBackButton: async () => {
@ -107,7 +116,25 @@
const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions( const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions(
loadedExt.kunkun.permissions, loadedExt.kunkun.permissions,
loadedExt.extPath 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 data.win?.hide()
await sleep(200)
return paste()
}
}
) )
const serverAPI2 = { const serverAPI2 = {
...serverAPI, ...serverAPI,
@ -127,7 +154,7 @@
if (isInMainWindow()) { if (isInMainWindow()) {
goHome() goHome()
} else { } else {
appWin.close() data.win?.close()
} }
} }
@ -135,16 +162,27 @@
setTimeout(() => { setTimeout(() => {
iframeRef.focus() iframeRef.focus()
uiControl.iframeLoaded = true uiControl.iframeLoaded = true
appState.setFullScreenLoading(false)
}, 300) }, 300)
} }
onMount(() => { onMount(() => {
appState.setFullScreenLoading(true)
setTimeout(() => { setTimeout(() => {
appWin.show() data.win?.setFocus()
}, 200) }, 200)
if (iframeRef?.contentWindow) { if (iframeRef?.contentWindow) {
const io = new IframeParentIO(iframeRef.contentWindow) const io = new IframeParentIO(iframeRef.contentWindow)
const rpc = new RPCChannel(io, { expose: serverAPI2 }) const kkrpcSerialization = decideKkrpcSerialization(loadedExt)
info(
`Establishing kkrpc connection for ${loadedExt.kunkun.identifier} with serialization: ${kkrpcSerialization}`
)
const rpc = new RPCChannel(io, {
expose: serverAPI2,
serialization: {
version: kkrpcSerialization
}
})
} else { } else {
toast.warning("iframeRef.contentWindow not available") toast.warning("iframeRef.contentWindow not available")
} }
@ -157,7 +195,7 @@
}) })
onDestroy(() => { onDestroy(() => {
winExtMap.unregisterExtensionFromWindow(appWin.label) winExtMap.unregisterExtensionFromWindow(data.win?.label ?? "")
}) })
</script> </script>
@ -170,7 +208,7 @@
onclick={onBackBtnClicked} onclick={onBackBtnClicked}
style={`${positionToCssStyleString(uiControl.backBtnPosition)}`} style={`${positionToCssStyleString(uiControl.backBtnPosition)}`}
> >
{#if appWin.label === "main"} {#if data.win?.label === "main"}
<ArrowLeftIcon class="w-4" /> <ArrowLeftIcon class="w-4" />
{:else} {:else}
<XIcon class="w-4" /> <XIcon class="w-4" />
@ -201,7 +239,6 @@
{/if} {/if}
<main class="h-screen"> <main class="h-screen">
<DanceTransition delay={300} autoHide={false} show={!uiControl.iframeLoaded} />
<iframe <iframe
bind:this={iframeRef} bind:this={iframeRef}
class={cn("h-full", { class={cn("h-full", {

View File

@ -1,7 +1,7 @@
import { KunkunIframeExtParams } from "@/cmds/ext" import { KunkunIframeExtParams } from "@/cmds/ext"
import { i18n } from "@/i18n" import { i18n } from "@/i18n"
import { db, unregisterExtensionWindow } from "@kksh/api/commands"
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models" import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension" import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { error as svError } from "@sveltejs/kit" import { error as svError } from "@sveltejs/kit"
import { join } from "@tauri-apps/api/path" import { join } from "@tauri-apps/api/path"

View File

@ -3,15 +3,16 @@
import { appState } from "@/stores/appState.js" import { appState } from "@/stores/appState.js"
import { keys } from "@/stores/keys" import { keys } from "@/stores/keys"
import { winExtMap } from "@/stores/winExtMap.js" import { winExtMap } from "@/stores/winExtMap.js"
import { WatchEvent } from "@/types/fs.js"
import { helperAPI } from "@/utils/helper.js" import { helperAPI } from "@/utils/helper.js"
import { paste } from "@/utils/hotkey"
import { decideKkrpcSerialization } from "@/utils/kkrpc.js"
import { import {
emitReloadOneExtension, emitReloadOneExtension,
listenToFileDrop, listenToFileDrop,
listenToRefreshDevExt listenToRefreshDevExt
} from "@/utils/tauri-events.js" } from "@/utils/tauri-events.js"
import { sleep } from "@/utils/time.js"
import { isInMainWindow } from "@/utils/window.js" import { isInMainWindow } from "@/utils/window.js"
import { db } from "@kksh/api/commands"
import { import {
constructJarvisServerAPIWithPermissions, constructJarvisServerAPIWithPermissions,
type IApp, type IApp,
@ -27,19 +28,29 @@
type IComponent, type IComponent,
type TemplateUiCommand type TemplateUiCommand
} from "@kksh/api/ui/template" } from "@kksh/api/ui/template"
import { db } from "@kksh/drizzle"
import { Button, Form } from "@kksh/svelte5"
import { LoadingBar } from "@kksh/ui" import { LoadingBar } from "@kksh/ui"
import { Templates } from "@kksh/ui/extension" import { Templates } from "@kksh/ui/extension"
import { GlobalCommandPaletteFooter } from "@kksh/ui/main" import { GlobalCommandPaletteFooter } from "@kksh/ui/main"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server" import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
import type { UnlistenFn } from "@tauri-apps/api/event" import {
RECORD_EXTENSION_PROCESS_EVENT,
type IRecordExtensionProcessEvent
} from "@kunkunapi/src/events.js"
import { Channel, invoke } from "@tauri-apps/api/core"
import { emitTo, type UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { getCurrentWindow } from "@tauri-apps/api/window"
import * as fs from "@tauri-apps/plugin-fs" import * as fs from "@tauri-apps/plugin-fs"
import { readTextFile } from "@tauri-apps/plugin-fs" import { readTextFile } from "@tauri-apps/plugin-fs"
import { debug } from "@tauri-apps/plugin-log" import { debug, info } from "@tauri-apps/plugin-log"
import { platform } from "@tauri-apps/plugin-os" import { platform } from "@tauri-apps/plugin-os"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import { RPCChannel, WorkerParentIO } from "kkrpc/browser" import { RPCChannel, WorkerParentIO } from "kkrpc/browser"
import { onDestroy, onMount, tick } from "svelte" import { onDestroy, onMount, tick } from "svelte"
import Inspect from "svelte-inspect-value"
import { type CommandEvent } from "tauri-plugin-shellx-api"
import * as v from "valibot" import * as v from "valibot"
const { data } = $props() const { data } = $props()
@ -50,21 +61,22 @@
let unlistenRefreshWorkerExt: UnlistenFn | undefined let unlistenRefreshWorkerExt: UnlistenFn | undefined
let unlistenFileDrop: UnlistenFn | undefined let unlistenFileDrop: UnlistenFn | undefined
let worker: Worker | undefined let worker: Worker | undefined
let listViewContent = $state<ListSchema.List>() let listViewContent = $state<ListSchema.List | null>(null)
let formViewContent = $state<FormSchema.Form>() let formViewContent = $state<FormSchema.Form | null>(null)
let markdownViewContent = $state<MarkdownSchema>() let markdownViewContent = $state<MarkdownSchema | null>(null)
let extensionLoadingBar = $state(false) // whether extension called showLoadingBar let extensionLoadingBar = $state(false) // whether extension called showLoadingBar
let pbar = $state<number | null>(null) let pbar = $state<number | null>(null)
let loading = $state(false) let loading = $state(false)
let searchTerm = $state("") let searchTerm = $state("")
let searchBarPlaceholder = $state("") let searchBarPlaceholder = $state("")
let extSpawnedProcesses = $state<number[]>([])
const appWin = getCurrentWebviewWindow() const appWin = getCurrentWebviewWindow()
const loadingBar = $derived($appState.loadingBar || extensionLoadingBar) const loadingBar = $derived($appState.loadingBar || extensionLoadingBar)
let loaded = $state(false) let loaded = $state(false)
let listview: Templates.ListView | undefined = $state(undefined) let listview: Templates.ListView | undefined = $state(undefined)
const _platform = platform() const _platform = platform()
let unlistenPkgJsonWatch: UnlistenFn | undefined let unlistenPkgJsonWatch: UnlistenFn | undefined
let curViewNodeName = $state<NodeNameEnum | FormNodeNameEnum | null>(null)
async function goBack() { async function goBack() {
if (isInMainWindow()) { if (isInMainWindow()) {
goto(i18n.resolveRoute("/app/")) goto(i18n.resolveRoute("/app/"))
@ -73,23 +85,28 @@
} }
} }
function clearViewContent(keep?: "list" | "form" | "markdown") { async function clearViewContent(keep?: "list" | "form" | "markdown") {
if (keep !== "list") { if (keep !== "list") {
listViewContent = undefined listViewContent = null
} }
if (keep !== "form") { if (keep !== "form") {
formViewContent = undefined formViewContent = null
} }
if (keep !== "markdown") { if (keep !== "markdown") {
markdownViewContent = undefined markdownViewContent = null
} }
await tick()
// await sleep(3000)
} }
const extUiAPI: IUiTemplate = { const extUiAPI: IUiTemplate = {
async render(view: IComponent<ListSchema.List | FormSchema.Form | MarkdownSchema>) { async render(_view: IComponent<ListSchema.List | FormSchema.Form | MarkdownSchema>) {
if (view.nodeName === NodeNameEnum.List) { // console.log("render nodeName", _view.nodeName)
clearViewContent("list") // console.log("render", _view)
const parsedListViewRes = v.safeParse(ListSchema.List, view) curViewNodeName = _view.nodeName
if (_view.nodeName === NodeNameEnum.List) {
await clearViewContent("list")
const parsedListViewRes = v.safeParse(ListSchema.List, _view)
if (!parsedListViewRes.success) { if (!parsedListViewRes.success) {
toast.error("Invalid List View", { toast.error("Invalid List View", {
description: "See console for details" description: "See console for details"
@ -169,20 +186,22 @@
// } else { // } else {
// listViewContent = parsedListView // listViewContent = parsedListView
// } // }
} else if (view.nodeName === FormNodeNameEnum.Form) { } else if (_view.nodeName === FormNodeNameEnum.Form) {
listViewContent = undefined listViewContent = null
clearViewContent("form") // await clearViewContent("form")
const parsedForm = v.parse(FormSchema.Form, view) // await tick()
const parsedForm = v.parse(FormSchema.Form, _view)
formViewContent = parsedForm formViewContent = parsedForm
// TODO: convert form to zod schema // TODO: convert form to zod schema
// const zodSchema = convertFormToZod(parsedForm) // const zodSchema = convertFormToZod(parsedForm)
// formViewZodSchema = zodSchema // formViewZodSchema = zodSchema
// formFieldConfig = buildFieldConfig(parsedForm) // formFieldConfig = buildFieldConfig(parsedForm)
} else if (view.nodeName === NodeNameEnum.Markdown) { } else if (_view.nodeName === NodeNameEnum.Markdown) {
clearViewContent("markdown") await clearViewContent("markdown")
markdownViewContent = v.parse(MarkdownSchema, view) await tick()
markdownViewContent = v.parse(MarkdownSchema, _view)
} else { } else {
toast.error(`Unsupported view type: ${view.nodeName}`) toast.error(`Unsupported view type: ${_view.nodeName}`)
} }
}, },
async showLoadingBar(loading: boolean) { async showLoadingBar(loading: boolean) {
@ -199,7 +218,6 @@
searchTerm = term searchTerm = term
}, },
async setSearchBarPlaceholder(placeholder: string) { async setSearchBarPlaceholder(placeholder: string) {
console.log("setSearchBarPlaceholder", placeholder)
searchBarPlaceholder = placeholder searchBarPlaceholder = placeholder
}, },
async goBack() { async goBack() {
@ -221,7 +239,25 @@
worker = new Worker(blobURL) worker = new Worker(blobURL)
const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions( const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions(
loadedExt.kunkun.permissions, loadedExt.kunkun.permissions,
loadedExt.extPath 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 = { const serverAPI2 = {
...serverAPI, ...serverAPI,
@ -234,10 +270,16 @@
language: () => Promise.resolve("en") language: () => Promise.resolve("en")
} satisfies IApp } satisfies IApp
} }
const io = new WorkerParentIO(worker) const io = new WorkerParentIO(worker)
const kkrpcSerialization = decideKkrpcSerialization(loadedExt)
info(
`Establishing kkrpc connection for ${loadedExt.kunkun.identifier} with serialization: ${kkrpcSerialization}`
)
const rpc = new RPCChannel<typeof serverAPI2, TemplateUiCommand>(io, { const rpc = new RPCChannel<typeof serverAPI2, TemplateUiCommand>(io, {
expose: serverAPI2 expose: serverAPI2,
serialization: {
version: kkrpcSerialization
}
}) })
workerAPI = rpc.getAPI() workerAPI = rpc.getAPI()
await workerAPI.load() await workerAPI.load()
@ -250,25 +292,25 @@
} }
}) })
function onPkgJsonChange(evt: fs.WatchEvent) { // function onPkgJsonChange(evt: fs.WatchEvent) {
const parsed = v.safeParse(WatchEvent, evt) // const parsed = v.safeParse(WatchEvent, evt)
if (parsed.success) { // if (parsed.success) {
if ( // if (
parsed.output.type.modify.kind === "data" && // parsed.output.type.modify.kind === "data" &&
parsed.output.type.modify.mode === "content" && // parsed.output.type.modify.mode === "content" &&
parsed.output.paths.includes(data.pkgJsonPath) // parsed.output.paths.includes(data.pkgJsonPath)
) { // ) {
console.log("pkgJson changed", parsed.output.paths) // console.log("pkgJson changed", parsed.output.paths)
// emit event to reload extension commands // // emit event to reload extension commands
emitReloadOneExtension(loadedExt.extPath) // emitReloadOneExtension(loadedExt.extPath)
} // }
} // }
} // }
onMount(async () => { onMount(async () => {
setTimeout(() => { setTimeout(() => {
appState.setLoadingBar(true) appState.setLoadingBar(true)
appWin.show() appWin.show().then(() => appWin.setFocus())
}, 100) }, 100)
unlistenRefreshWorkerExt = await listenToRefreshDevExt(() => { unlistenRefreshWorkerExt = await listenToRefreshDevExt(() => {
debug("Refreshing Worker Extension") debug("Refreshing Worker Extension")
@ -284,10 +326,9 @@
appState.setLoadingBar(false) appState.setLoadingBar(false)
loaded = true loaded = true
}, 500) }, 500)
console.log("watching", data.pkgJsonPath) // fs.watch(data.pkgJsonPath, onPkgJsonChange).then((unlisten) => {
fs.watch(data.pkgJsonPath, onPkgJsonChange).then((unlisten) => { // unlistenPkgJsonWatch = unlisten
unlistenPkgJsonWatch = unlisten // })
})
}) })
onDestroy(() => { onDestroy(() => {
@ -297,6 +338,8 @@
winExtMap.unregisterExtensionFromWindow(appWin.label) winExtMap.unregisterExtensionFromWindow(appWin.label)
extensionLoadingBar = false extensionLoadingBar = false
appState.setActionPanel(undefined) appState.setActionPanel(undefined)
appState.setDefaultAction(null)
appState.setActionPanel(undefined)
}) })
$effect(() => { $effect(() => {
@ -336,7 +379,8 @@
{#if loadingBar} {#if loadingBar}
<LoadingBar class="fixed left-0 top-0 w-full" color="white" /> <LoadingBar class="fixed left-0 top-0 w-full" color="white" />
{/if} {/if}
{#if loaded && listViewContent !== undefined}
{#if curViewNodeName === NodeNameEnum.List && listViewContent}
<Templates.ListView <Templates.ListView
bind:inputRef={listviewInputRef} bind:inputRef={listviewInputRef}
bind:searchTerm bind:searchTerm
@ -358,26 +402,18 @@
onSearchTermChange={(searchTerm: string) => { onSearchTermChange={(searchTerm: string) => {
workerAPI?.onSearchTermChange(searchTerm) workerAPI?.onSearchTermChange(searchTerm)
}} }}
onHighlightedItemChanged={(value: string) => { onHighlightedItemChanged={(item: ListSchema.Item) => {
// workerAPI?.onHighlightedListItemChanged(value) if (item.defaultAction) {
// if (listViewContent?.defaultAction) { appState.setDefaultAction(item.defaultAction)
// appState.setDefaultAction(listViewContent.defaultAction) } else if (listViewContent?.defaultAction) {
// } appState.setDefaultAction(listViewContent.defaultAction)
// if (listViewContent?.actions) {
// appState.setActionPanel(listViewContent.actions)
// }
try {
const parsedItem = v.parse(ListSchema.Item, JSON.parse(value))
if (parsedItem.defaultAction) {
appState.setDefaultAction(parsedItem.defaultAction)
}
if (parsedItem.actions) {
appState.setActionPanel(parsedItem.actions)
}
workerAPI?.onHighlightedListItemChanged(parsedItem.value)
} catch (error) {
console.error(error)
} }
if (item.actions) {
appState.setActionPanel(item.actions)
} else if (listViewContent?.actions) {
appState.setActionPanel(listViewContent.actions)
}
workerAPI?.onHighlightedListItemChanged(item.value)
}} }}
> >
{#snippet footer()} {#snippet footer()}
@ -395,7 +431,8 @@
/> />
{/snippet} {/snippet}
</Templates.ListView> </Templates.ListView>
{:else if loaded && formViewContent !== undefined} {/if}
{#if curViewNodeName === FormNodeNameEnum.Form && formViewContent}
<Templates.FormView <Templates.FormView
{formViewContent} {formViewContent}
{pbar} {pbar}
@ -405,6 +442,7 @@
workerAPI?.onFormSubmit(formData) workerAPI?.onFormSubmit(formData)
}} }}
/> />
{:else if loaded && markdownViewContent !== undefined} {/if}
{#if curViewNodeName === NodeNameEnum.Markdown && markdownViewContent}
<Templates.MarkdownView {markdownViewContent} onGoBack={goBack} /> <Templates.MarkdownView {markdownViewContent} onGoBack={goBack} />
{/if} {/if}

View File

@ -1,10 +1,11 @@
import { KunkunTemplateExtParams } from "@/cmds/ext" import { KunkunTemplateExtParams } from "@/cmds/ext"
import { i18n } from "@/i18n" import { i18n } from "@/i18n"
import { db, unregisterExtensionWindow } from "@kksh/api/commands" import type { ExtPackageJsonExtra } from "@kksh/api/models"
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models" import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension" import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { error as sbError, error as svError } from "@sveltejs/kit" import { error as sbError, error as svError } from "@sveltejs/kit"
import { join } from "@tauri-apps/api/path" import { join } from "@tauri-apps/api/path"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { exists, readTextFile } from "@tauri-apps/plugin-fs" import { exists, readTextFile } from "@tauri-apps/plugin-fs"
import { error } from "@tauri-apps/plugin-log" import { error } from "@tauri-apps/plugin-log"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
@ -14,15 +15,16 @@ import type { PageLoad } from "./$types"
export const load: PageLoad = async ({ url }) => { export const load: PageLoad = async ({ url }) => {
// both query parameter must exist // both query parameter must exist
const rawKunkunTemplateExtParams = localStorage.getItem("kunkun-template-ext-params") const rawKunkunTemplateExtParams = localStorage.getItem("kunkun-template-ext-params")
if (!rawKunkunTemplateExtParams) { if (!rawKunkunTemplateExtParams) {
toast.error("Invalid extension path or url") toast.error("Invalid extension path or url")
return svError(404, "Invalid extension path or url") return svError(404, "Invalid extension path or url")
} }
const json = JSON.parse(rawKunkunTemplateExtParams)
const parsed = v.safeParse(KunkunTemplateExtParams, JSON.parse(rawKunkunTemplateExtParams)) const parsed = v.safeParse(KunkunTemplateExtParams, json)
if (!parsed.success) { if (!parsed.success) {
getCurrentWindow().show()
console.error(v.flatten<typeof KunkunTemplateExtParams>(parsed.issues))
toast.error("Fail to parse extension params from local storage", { toast.error("Fail to parse extension params from local storage", {
description: `${v.flatten<typeof KunkunTemplateExtParams>(parsed.issues)}` description: `${v.flatten<typeof KunkunTemplateExtParams>(parsed.issues)}`
}) })

View File

@ -8,7 +8,6 @@
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import { ArrowRightIcon } from "lucide-svelte" import { ArrowRightIcon } from "lucide-svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { whereIsCommand } from "tauri-plugin-shellx-api" import { whereIsCommand } from "tauri-plugin-shellx-api"
import { Step } from "./steps" import { Step } from "./steps"
@ -26,18 +25,22 @@
} }
$effect(() => { $effect(() => {
if (step === Step.DenoInstall) { if (step > Step.GeneralSettings) {
if (denoPath) {
step++
}
} else if (step === Step.FFmpegInstall) {
if (ffmpegPath) {
step++
}
} else if (step > Step.FFmpegInstall) {
appConfig.setOnBoarded(true) appConfig.setOnBoarded(true)
goto(i18n.resolveRoute("/app")) goto(i18n.resolveRoute("/app"))
} }
// if (step === Step.DenoInstall) {
// if (denoPath) {
// step++
// }
// } else if (step === Step.FFmpegInstall) {
// if (ffmpegPath) {
// step++
// }
// } else if (step > Step.FFmpegInstall) {
// appConfig.setOnBoarded(true)
// goto(i18n.resolveRoute("/app"))
// }
}) })
</script> </script>

View File

@ -0,0 +1,137 @@
<script lang="ts">
import * as m from "@/paraglide/messages"
import { appsLoader } from "@/stores"
import { SearchPath } from "@kksh/api/models"
import { Button, Input, Table } from "@kksh/svelte5"
import { Form } from "@kksh/ui"
import * as dialog from "@tauri-apps/plugin-dialog"
import * as fs from "@tauri-apps/plugin-fs"
import { appConfig } from "$lib/stores/appConfig"
import { Inspect } from "svelte-inspect-value"
import { toast } from "svelte-sonner"
import SuperDebug, { defaults, superForm } from "sveltekit-superforms"
import { valibot, valibotClient } from "sveltekit-superforms/adapters"
import { open } from "tauri-plugin-shellx-api"
import * as v from "valibot"
export const SearchPathFormSchema = v.object({
path: v.pipe(v.string(), v.minLength(1)),
depth: v.optional(v.number(), 1)
})
const form = superForm(defaults(valibot(SearchPathFormSchema)), {
validators: valibotClient(SearchPathFormSchema),
SPA: true,
async onUpdate({ form, cancel }) {
if (!form.valid) {
return
}
const { path, depth } = form.data
if (!(await fs.exists(path))) {
return toast.error("Path does not exist")
}
appConfig.addAppSearchPath({ path, depth })
toast.success("Search Path Added")
appsLoader.init()
cancel()
}
})
const { form: formData, enhance, errors } = form
async function pickSearchPath() {
const result = await dialog.open({
directory: true
})
if (result) {
$formData.path = result
}
}
</script>
<main class="container flex flex-col space-y-2">
<h1 class="text-2xl font-bold">{m.settings_app_search_paths_title()}</h1>
{#if $appConfig.developerMode}
<Inspect name="Extra App Search Paths" value={$appConfig.appSearchPaths} />
{/if}
<form method="POST" use:enhance>
<Form.Field {form} name="path">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m.settings_app_search_paths_table_col_search_path()}</Form.Label>
<div class="flex items-center gap-1">
<Input
{...props}
disabled
bind:value={$formData.path}
placeholder={m.settings_app_search_paths_table_col_search_path()}
/>
<Form.Button class="my-1" onclick={pickSearchPath}>Pick</Form.Button>
</div>
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="depth">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m.settings_app_search_paths_table_col_depth()}</Form.Label>
<Input
{...props}
type="number"
bind:value={$formData.depth}
placeholder={m.settings_app_search_paths_table_col_depth()}
/>
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Button class="w-full" type="submit">
{m.settings_app_search_paths_add_app_search_path()}
</Button>
</form>
{#if $appConfig.developerMode}
<SuperDebug data={$formData} />
{/if}
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>{m.settings_app_search_paths_table_col_search_path()}</Table.Head>
<Table.Head class="text-center">
{m.settings_app_search_paths_table_col_depth()}
</Table.Head>
<Table.Head class="text-center">
{m.settings_app_search_paths_table_col_actions()}
</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each $appConfig.appSearchPaths as appSearchPath, i (i)}
<Table.Row>
<Table.Cell
class="cursor-pointer font-medium"
onclick={() => {
open(appSearchPath.path)
}}
>
<code>{appSearchPath.path}</code>
</Table.Cell>
<Table.Cell class="text-center">{appSearchPath.depth}</Table.Cell>
<Table.Cell class="text-center">
<Button
variant="destructive"
onclick={() => {
appConfig.removeAppSearchPath(appSearchPath)
toast.error("Search Path Removed")
appsLoader.init()
}}
>
Remove
</Button>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</main>

View File

@ -4,7 +4,7 @@
import { goHome } from "@/utils/route" import { goHome } from "@/utils/route"
import { Button, Sidebar } from "@kksh/svelte5" import { Button, Sidebar } from "@kksh/svelte5"
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"
import { ArrowLeftIcon } from "lucide-svelte" import { ArrowLeftIcon, FolderSearch } from "lucide-svelte"
import Blocks from "lucide-svelte/icons/blocks" import Blocks from "lucide-svelte/icons/blocks"
import Cog from "lucide-svelte/icons/cog" import Cog from "lucide-svelte/icons/cog"
import FileCode2 from "lucide-svelte/icons/file-code-2" import FileCode2 from "lucide-svelte/icons/file-code-2"
@ -19,10 +19,15 @@
icon: Cog icon: Cog
}, },
{ {
title: m.settings_menu_developer(), title: m.settings_menu_app_search_paths(),
url: i18n.resolveRoute("/app/settings/developer"), url: i18n.resolveRoute("/app/settings/app-search-paths"),
icon: SquareTerminal icon: FolderSearch
}, },
// {
// title: m.settings_menu_developer(),
// url: i18n.resolveRoute("/app/settings/developer"),
// icon: SquareTerminal
// },
{ {
title: m.settings_menu_extensions(), title: m.settings_menu_extensions(),
url: i18n.resolveRoute("/app/settings/extensions"), url: i18n.resolveRoute("/app/settings/extensions"),

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import * as m from "@/paraglide/messages" import * as m from "@/paraglide/messages"
import { db } from "@kksh/api/commands" import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension" import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { Button, Dialog, Table } from "@kksh/svelte5" import { Button, Dialog, Table } from "@kksh/svelte5"
import { join } from "@tauri-apps/api/path" import { join } from "@tauri-apps/api/path"

View File

@ -0,0 +1,124 @@
<script lang="ts">
import {
getAllCmds,
getAllExtensions,
getExtensionDataById,
getUniqueExtensionByIdentifier,
getUniqueExtensionByPath,
searchExtensionData,
updateCmdByID
} from "@kksh/drizzle/api"
import * as schema from "@kksh/drizzle/schema"
import { Button, Input } from "@kksh/svelte5"
import { CmdTypeEnum, Ext } from "@kunkunapi/src/models/extension"
import { SearchModeEnum, SQLSortOrderEnum } from "@kunkunapi/src/models/sql"
// import * as orm from "drizzle-orm"
import { Inspect } from "svelte-inspect-value"
import { toast } from "svelte-sonner"
import * as v from "valibot"
let searchText = $state("")
/* eslint-disable */
let data: any = $state(null)
let inspectTitle = $state("")
</script>
<main class="container space-y-2">
<Button
onclick={async () => {
getAllCmds()
.then((cmds) => {
console.log(cmds)
data = cmds
inspectTitle = "All Commands"
})
.catch((e) => {
console.error(e)
toast.error("Failed to get all commands", {
description: "See console for more details"
})
})
}}
>
Get All Commands
</Button>
<Button
onclick={() => {
getAllExtensions()
.then((exts) => {
data = exts
inspectTitle = "All Extensions"
})
.catch((e) => {
console.error(e)
toast.error("Failed to get all extensions", {
description: "See console for more details"
})
})
}}
>
Get All Extensions
</Button>
<Button
onclick={async () => {
// get all extensions with path not null
const exts = await getAllExtensions()
for (const ext of exts) {
if (ext.path === null) continue
const _ext = await getUniqueExtensionByIdentifier(ext.identifier)
console.log(_ext)
if (ext.path) {
const __ext = await getUniqueExtensionByPath(ext.path)
console.log(__ext)
}
}
// data = exts
}}
>
Get Unique Extension By Identifier and Path
</Button>
<!-- <Button
onclick={async () => {
updateCmdByID({
cmdId: 1,
name: "google",
cmdType: CmdTypeEnum.QuickLink,
data: `{"link":"https://google.com/search?query={argument}","icon":{"type":"remote-url","value":"https://google.com/favicon.ico","invert":false}}`,
enabled: true
})
}}
>
Update Command By ID
</Button> -->
<Button
onclick={async () => {
const _data = await getExtensionDataById(1, ["search_text", "data"])
data = _data
inspectTitle = "Extension Data"
}}
>
Get Extension Data By ID
</Button>
<form
class="flex gap-1"
onsubmit={async (e) => {
e.preventDefault()
const _data = await searchExtensionData({
extId: 1,
searchMode: SearchModeEnum.FTS,
searchText: searchText,
orderByCreatedAt: SQLSortOrderEnum.Desc,
limit: 10,
fields: ["search_text", "data"]
})
console.log(_data)
data = _data
inspectTitle = "Search Results"
}}
>
<Input class="" bind:value={searchText} placeholder="Search Text" />
<Button class="" type="submit">Search Extension Data</Button>
</form>
<Inspect name={inspectTitle} value={data} expandLevel={2} />
</main>

View File

@ -6,6 +6,7 @@
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"
import { ArrowLeftIcon } from "lucide-svelte" import { ArrowLeftIcon } from "lucide-svelte"
import AppWindow from "lucide-svelte/icons/app-window" import AppWindow from "lucide-svelte/icons/app-window"
import DB from "lucide-svelte/icons/database"
import Loader from "lucide-svelte/icons/loader" import Loader from "lucide-svelte/icons/loader"
import Network from "lucide-svelte/icons/network" import Network from "lucide-svelte/icons/network"
@ -25,6 +26,11 @@
title: m.troubleshooters_sidebar_mdns_debugger_title(), title: m.troubleshooters_sidebar_mdns_debugger_title(),
url: i18n.resolveRoute("/app/troubleshooters/mdns-debugger"), url: i18n.resolveRoute("/app/troubleshooters/mdns-debugger"),
icon: Network icon: Network
},
{
title: "ORM",
url: i18n.resolveRoute("/app/troubleshooters/orm"),
icon: DB
} }
] ]
let currentItem = $state(items.find((item) => window.location.pathname === item.url)) let currentItem = $state(items.find((item) => window.location.pathname === item.url))

View File

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Layouts } from "@kksh/ui" import { Layouts } from "@kksh/ui"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { onMount } from "svelte" import { onMount } from "svelte"
onMount(async () => { let { data } = $props()
const mainWin = await getCurrentWindow()
mainWin.show() onMount(() => {
data.win?.show().then(() => data.win?.setFocus())
}) })
</script> </script>

View File

@ -86,12 +86,18 @@ const config: Config = {
"caret-blink": { "caret-blink": {
"0%,70%,100%": { opacity: "1" }, "0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" } "20%,50%": { opacity: "0" }
},
"border-beam": {
"100%": {
"offset-distance": "100%"
}
} }
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite" "caret-blink": "caret-blink 1.25s ease-out infinite",
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear"
} }
} }
}, },

View File

@ -9,7 +9,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"moduleResolution": "bundler" "moduleResolution": "bundler",
"allowImportingTsExtensions": true
}, },
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files

View File

@ -14,7 +14,7 @@ export default defineConfig(async () => ({
clearScreen: false, clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available // 2. tauri expects a fixed port, fail if that port is not available
server: { server: {
port: 1420, port: 1566,
strictPort: true, strictPort: true,
host: host || false, host: host || false,
hmr: host hmr: host

2472
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,46 +11,47 @@
"format": "prettier --write \"**/*.{ts,tsx,md,svelte}\"" "format": "prettier --write \"**/*.{ts,tsx,md,svelte}\""
}, },
"devDependencies": { "devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.0", "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"prettier": "^3.4.2", "prettier": "^3.5.2",
"prettier-plugin-svelte": "^3.3.2", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.9", "prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.16.6", "svelte": "^5.20.5",
"svelte-check": "^4.1.1", "svelte-check": "^4.1.4",
"turbo": "^2.3.4", "turbo": "^2.4.4",
"typescript": "5.7.2", "typescript": "^5.0.0",
"verify-package-export": "^0.0.3" "verify-package-export": "^0.0.3"
}, },
"packageManager": "pnpm@9.15.4", "packageManager": "pnpm@10.7.0",
"engines": { "engines": {
"node": ">=22" "node": ">=22"
}, },
"dependencies": { "dependencies": {
"@changesets/cli": "^2.27.11", "@changesets/cli": "^2.28.1",
"@hey-api/client-fetch": "^0.8.3",
"@iconify/svelte": "^4.2.0", "@iconify/svelte": "^4.2.0",
"@supabase/supabase-js": "^2.48.0", "@kksh/sdk": "^0.0.3",
"@tauri-apps/api": "^2.2.0", "@supabase/supabase-js": "^2.49.1",
"@tauri-apps/cli": "^2.2.2", "@tauri-apps/api": "^2.3.0",
"@tauri-apps/cli": "^2.3.1",
"@tauri-apps/plugin-deep-link": "^2.2.0", "@tauri-apps/plugin-deep-link": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0", "@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0", "@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.0", "@tauri-apps/plugin-global-shortcut": "^2.2.0",
"@tauri-apps/plugin-http": "^2.2.0", "@tauri-apps/plugin-http": "^2.3.0",
"@tauri-apps/plugin-log": "^2.2.0", "@tauri-apps/plugin-log": "^2.2.3",
"@tauri-apps/plugin-notification": "^2.2.0", "@tauri-apps/plugin-notification": "^2.2.1",
"@tauri-apps/plugin-os": "^2.2.0", "@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-process": "2.2.0", "@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0", "@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-store": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.3.1", "@tauri-apps/plugin-updater": "^2.5.1",
"supabase": "^2.2.1", "supabase": "^2.15.8",
"tauri-plugin-keyring-api": "workspace:*", "tauri-plugin-keyring-api": "workspace:*",
"tauri-plugin-network-api": "workspace:*", "tauri-plugin-network-api": "workspace:*",
"tauri-plugin-shellx-api": "^2.0.14",
"tauri-plugin-system-info-api": "workspace:*", "tauri-plugin-system-info-api": "workspace:*",
"valibot": "^1.0.0-beta.11", "valibot": "^1.0.0",
"zod": "^3.24.1" "zod": "^3.24.2"
}, },
"workspaces": [ "workspaces": [
"apps/*", "apps/*",

View File

@ -1,5 +1,17 @@
# @kksh/api # @kksh/api
## 0.1.5
### Patch Changes
- Add clipboard.paste() API
## 0.1.4
### Patch Changes
- Add killPid extension API
## 0.1.2 ## 0.1.2
### Patch Changes ### Patch Changes
@ -41,3 +53,16 @@
### Patch Changes ### Patch Changes
- More Icon Options - More Icon Options
## 0.1.6
### Patch Changes
- Upgrade kkrpc to 0.2.1, which uses superjson for serialization
## 0.1.7
### Patch Changes
- Upgrade kkrpc to 0.2.2, supports both json and superjson serialization, for backward compatibility
- The previous version breaks extension compatibility.

View File

@ -1,6 +1,6 @@
{ {
"name": "@kksh/api", "name": "@kksh/api",
"version": "0.1.3", "version": "0.1.7",
"type": "module", "type": "module",
"repository": { "repository": {
"type": "git", "type": "git",
@ -65,17 +65,17 @@
"@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-store": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.3.0", "@tauri-apps/plugin-updater": "^2.3.0",
"@tauri-apps/plugin-upload": "^2.2.1", "@tauri-apps/plugin-upload": "^2.2.1",
"kkrpc": "^0.1.1", "kkrpc": "^0.2.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"semver": "^7.6.3", "semver": "^7.6.3",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"tauri-api-adapter": "^0.3.20", "tauri-api-adapter": "^0.3.27",
"tauri-plugin-network-api": "2.0.5", "tauri-plugin-network-api": "2.0.5",
"tauri-plugin-shellx-api": "^2.0.14", "tauri-plugin-shellx-api": "^2.0.16",
"tauri-plugin-system-info-api": "2.0.8", "tauri-plugin-system-info-api": "2.0.8",
"valibot": "^1.0.0-beta.10" "valibot": "^1.0.0"
}, },
"files": [ "files": [
"src", "src",

View File

@ -14,26 +14,23 @@ import type {
writeFile, writeFile,
writeTextFile writeTextFile
} from "@tauri-apps/plugin-fs" } from "@tauri-apps/plugin-fs"
import type { IShell as IShell1, IPath as ITauriPath } from "tauri-api-adapter"
import type { import type {
Child, IClipboard as _IClipboard,
ChildProcess, IShell as IShell1,
CommandEvents, IPath as ITauriPath
hasCommand, } from "tauri-api-adapter"
InternalSpawnOptions,
IOPayload,
likelyOnWindows,
OutputEvents,
SpawnOptions
} from "tauri-plugin-shellx-api"
import { EventEmitter, open as shellxOpen } from "tauri-plugin-shellx-api"
import * as v from "valibot" import * as v from "valibot"
import { KV, type JarvisExtDB } from "../commands/db"
import type { fileSearch } from "../commands/fileSearch" import type { fileSearch } from "../commands/fileSearch"
import { type AppInfo } from "../models/apps" import { type AppInfo } from "../models/apps"
import { type ExtData } from "../models/extension"
import { ExtDataField, SearchMode, SQLSortOrder } from "../models/sql"
import type { LightMode, Position, Radius, ThemeColor } from "../models/styles" import type { LightMode, Position, Radius, ThemeColor } from "../models/styles"
import type { DenoSysOptions } from "../permissions/schema" import type { DenoSysOptions } from "../permissions/schema"
export type IClipboard = _IClipboard & {
paste: (options?: {}) => Promise<void>
}
type PromiseWrap<T extends (...args: any[]) => any> = ( type PromiseWrap<T extends (...args: any[]) => any> = (
...args: Parameters<T> ...args: Parameters<T>
) => Promise<ReturnType<T>> ) => Promise<ReturnType<T>>
@ -158,23 +155,34 @@ export interface IUiCustom {
} }
export interface IDb { export interface IDb {
add: typeof JarvisExtDB.prototype.add add: (data: { data: string; dataType?: string; searchText?: string }) => Promise<void>
delete: typeof JarvisExtDB.prototype.delete delete: (dataId: number) => Promise<void>
search: typeof JarvisExtDB.prototype.search search: (searchParams: {
retrieveAll: typeof JarvisExtDB.prototype.retrieveAll dataId?: number
retrieveAllByType: typeof JarvisExtDB.prototype.retrieveAllByType searchMode?: SearchMode
deleteAll: typeof JarvisExtDB.prototype.deleteAll dataType?: string
update: typeof JarvisExtDB.prototype.update searchText?: string
afterCreatedAt?: Date
beforeCreatedAt?: Date
limit?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}) => Promise<ExtData[]>
retrieveAll: (options: { fields?: ExtDataField[] }) => Promise<ExtData[]>
retrieveAllByType: (dataType: string) => Promise<ExtData[]>
deleteAll: () => Promise<void>
update: (data: { dataId: number; data: string; searchText?: string }) => Promise<void>
} }
/** /**
* A key-value store built on top of the Database API (based on sqlite) * A key-value store built on top of the Database API (based on sqlite)
*/ */
export interface IKV { export interface IKV {
get: typeof KV.prototype.get get: <T = string>(key: string) => Promise<T | null | undefined>
set: typeof KV.prototype.set set: (key: string, value: string) => Promise<void>
exists: typeof KV.prototype.exists exists: (key: string) => Promise<boolean>
delete: typeof KV.prototype.delete delete: (key: string) => Promise<void>
} }
export interface IFs { export interface IFs {

View File

@ -0,0 +1,18 @@
import { constructClipboardApi as _constructClipboardApi } from "tauri-api-adapter"
import { type ClipboardPermission as _ClipboardPermission } from "tauri-api-adapter/permissions"
import type { ClipboardPermission } from "../../permissions/schema"
import { checkPermission } from "../../utils/permission-check"
import type { IClipboard } from "../client"
export function constructClipboardApi(
permissions: ClipboardPermission[],
paste: (options?: {}) => Promise<void>
): IClipboard {
return {
..._constructClipboardApi(permissions.filter((p) => p !== "clipboard:paste")), // this constructor has no paste API
paste: (options?: {}) => {
checkPermission(permissions, ["clipboard:paste"])
return paste(options)
}
}
}

View File

@ -1,5 +1,4 @@
import { import {
constructClipboardApi,
constructDialogApi, constructDialogApi,
constructFetchApi, constructFetchApi,
// constructFsApi, // a local constructFsApi is defined // constructFsApi, // a local constructFsApi is defined
@ -11,7 +10,7 @@ import {
// constructShellApi, // a local custom constructShellApi is defined // constructShellApi, // a local custom constructShellApi is defined
constructSystemInfoApi, constructSystemInfoApi,
constructUpdownloadApi, constructUpdownloadApi,
type IClipboard, // type IClipboard,
type IDialog, type IDialog,
type IFetchInternal, type IFetchInternal,
type ILogger, type ILogger,
@ -37,7 +36,6 @@ import {
type SystemInfoPermission, type SystemInfoPermission,
type UpdownloadPermission type UpdownloadPermission
} from "tauri-api-adapter/permissions" } from "tauri-api-adapter/permissions"
import type { IEvent, IFs, IOpen, ISecurity, ISystem, IToast, IUtils } from "../../api/client"
import type { IUiCustomServer1 } from "../../api/server-types" import type { IUiCustomServer1 } from "../../api/server-types"
import { import {
AllKunkunPermission, AllKunkunPermission,
@ -50,6 +48,8 @@ import {
type ShellPermissionScoped, type ShellPermissionScoped,
type SystemPermission type SystemPermission
} from "../../permissions" } from "../../permissions"
import type { IClipboard, IEvent, IFs, IOpen, ISecurity, ISystem, IToast, IUtils } from "../client"
import { constructClipboardApi } from "./clipboard"
// import type { IDbServer } from "./db" // import type { IDbServer } from "./db"
import { constructEventApi } from "./event" import { constructEventApi } from "./event"
import { constructFsApi } from "./fs" import { constructFsApi } from "./fs"
@ -126,13 +126,19 @@ export type IKunkunFullServerAPI = {
*/ */
export function constructJarvisServerAPIWithPermissions( export function constructJarvisServerAPIWithPermissions(
permissions: AllPermissions[], permissions: AllPermissions[],
extPath: string extPath: string,
customFunctions: {
recordSpawnedProcess: (pid: number) => Promise<void>
getSpawnedProcesses: () => Promise<number[]>
paste: (options?: {}) => Promise<void>
}
): IKunkunFullServerAPI { ): IKunkunFullServerAPI {
return { return {
clipboard: constructClipboardApi( clipboard: constructClipboardApi(
getStringPermissions(permissions).filter((p) => getStringPermissions(permissions).filter((p) =>
p.startsWith("clipboard:") p.startsWith("clipboard:")
) as ClipboardPermission[] ) as ClipboardPermission[],
customFunctions.paste
), ),
fetch: constructFetchApi( fetch: constructFetchApi(
getStringPermissions(permissions).filter((p) => p.startsWith("fetch:")) as FetchPermission[] getStringPermissions(permissions).filter((p) => p.startsWith("fetch:")) as FetchPermission[]
@ -193,7 +199,9 @@ export function constructJarvisServerAPIWithPermissions(
p.permission.startsWith("shell:") p.permission.startsWith("shell:")
) )
], ],
extPath extPath,
customFunctions.recordSpawnedProcess,
customFunctions.getSpawnedProcesses
), ),
iframeUi: constructIframeUiApi(), iframeUi: constructIframeUiApi(),
utils: constructUtilsApi(), utils: constructUtilsApi(),

View File

@ -2,6 +2,7 @@ import { emitKillProcessEvent } from "@kksh/api/events"
import { Channel, invoke } from "@tauri-apps/api/core" import { Channel, invoke } from "@tauri-apps/api/core"
import { emitTo } from "@tauri-apps/api/event" import { emitTo } from "@tauri-apps/api/event"
import { getCurrentWindow } from "@tauri-apps/api/window" import { getCurrentWindow } from "@tauri-apps/api/window"
import { toast } from "svelte-sonner"
import { import {
hasCommand, hasCommand,
whereIsCommand, whereIsCommand,
@ -10,6 +11,7 @@ import {
type InternalSpawnOptions, type InternalSpawnOptions,
type IOPayload type IOPayload
} from "tauri-plugin-shellx-api" } from "tauri-plugin-shellx-api"
import * as shell from "tauri-plugin-shellx-api"
import type { DenoRunConfig } from "../../api/client" import type { DenoRunConfig } from "../../api/client"
import type { IShellServer } from "../../api/server-types" import type { IShellServer } from "../../api/server-types"
import { RECORD_EXTENSION_PROCESS_EVENT, type IRecordExtensionProcessEvent } from "../../events" import { RECORD_EXTENSION_PROCESS_EVENT, type IRecordExtensionProcessEvent } from "../../events"
@ -73,13 +75,14 @@ async function verifyShellCmdPermission(
*/ */
export function constructShellApi( export function constructShellApi(
permissions: (ShellPermissionScoped | ShellPermission)[], permissions: (ShellPermissionScoped | ShellPermission)[],
extPath: string extPath: string,
recordSpawnedProcess: (pid: number) => Promise<void>,
getSpawnedProcesses: () => Promise<number[]>
): IShellServer { ): IShellServer {
const stringPermissiongs = permissions.filter((p) => typeof p === "string") as ShellPermission[] const stringPermissiongs = permissions.filter((p) => typeof p === "string") as ShellPermission[]
const objectPermissions = permissions.filter( const objectPermissions = permissions.filter(
(p) => typeof p !== "string" (p) => typeof p !== "string"
) as ShellPermissionScoped[] ) as ShellPermissionScoped[]
async function execute( async function execute(
program: string, program: string,
args: string[], args: string[],
@ -99,14 +102,19 @@ export function constructShellApi(
options: options options: options
}) })
} }
function kill(pid: number) { async function kill(pid: number) {
if (!stringPermissiongs.some((p) => ShellPermissionMap.kill.includes(p))) if (!stringPermissiongs.some((p) => ShellPermissionMap.kill.includes(p))) {
return Promise.reject( return Promise.reject(
new Error(`Permission denied. Requires one of ${ShellPermissionMap.kill}`) new Error(`Permission denied. Requires one of ${ShellPermissionMap.kill}`)
) )
}
const pids = await getSpawnedProcesses()
if (!pids.includes(pid)) {
return Promise.reject(new Error(`Process ${pid} not spawned by this extension`))
}
return invoke<void>("plugin:shellx|kill", { return invoke<void>("plugin:shellx|kill", {
cmd: "killChild", cmd: "killChild",
pid: pid pid
}).then(() => { }).then(() => {
emitKillProcessEvent(pid) emitKillProcessEvent(pid)
}) })
@ -146,13 +154,24 @@ export function constructShellApi(
options: InternalSpawnOptions, options: InternalSpawnOptions,
cb: (evt: CommandEvent<O>) => void cb: (evt: CommandEvent<O>) => void
) { ) {
await verifyShellCmdPermission(ShellPermissionMap.rawSpawn, objectPermissions, program, args) await verifyShellCmdPermission(
ShellPermissionMap.rawSpawn,
objectPermissions,
program,
args
).catch((err) => {
toast.error("Permission denied", {
description: err.message
})
console.error("rawSpawn permission denied", err)
throw err
})
const onEvent = new Channel<CommandEvent<O>>() const onEvent = new Channel<CommandEvent<O>>()
onEvent.onmessage = cb onEvent.onmessage = cb
return invoke<number>("plugin:shellx|spawn", { return invoke<number>("plugin:shellx|spawn", {
program: program, program,
args: args, args,
options: options, options,
onEvent onEvent
}) })
} }
@ -210,6 +229,14 @@ export function constructShellApi(
return likelyOnWindows() return likelyOnWindows()
} }
function killPid(pid: number) {
if (!stringPermissiongs.some((p) => ShellPermissionMap.killPid.includes(p)))
return Promise.reject(
new Error(`Permission denied. Requires one of ${ShellPermissionMap.killPid}`)
)
return shell.killPid(pid)
}
return { return {
whereIsCommand(command: string): Promise<string | null> { whereIsCommand(command: string): Promise<string | null> {
const cleanedCommand = command.trim().split(" ")[0] const cleanedCommand = command.trim().split(" ")[0]
@ -218,17 +245,7 @@ export function constructShellApi(
} }
return whereIsCommand(cleanedCommand).then((res) => (res === "" ? null : res)) return whereIsCommand(cleanedCommand).then((res) => (res === "" ? null : res))
}, },
async recordSpawnedProcess(pid: number): Promise<void> { recordSpawnedProcess,
// get window label
const curWin = await getCurrentWindow()
console.log("recordSpawnedProcess", pid, curWin.label)
await emitTo("main", RECORD_EXTENSION_PROCESS_EVENT, {
windowLabel: curWin.label,
pid
} satisfies IRecordExtensionProcessEvent)
// TODO: record process in a store
return Promise.resolve()
},
async denoExecute( async denoExecute(
scriptPath: string, scriptPath: string,
config: DenoRunConfig, config: DenoRunConfig,
@ -247,7 +264,6 @@ export function constructShellApi(
args1, args1,
extPath extPath
) )
console.log("denoExecute", program, args, options)
return invoke<ChildProcess<IOPayload>>("plugin:shellx|execute", { return invoke<ChildProcess<IOPayload>>("plugin:shellx|execute", {
program, program,
args, args,
@ -286,6 +302,7 @@ export function constructShellApi(
}, },
execute, execute,
kill, kill,
killPid,
stdinWrite, stdinWrite,
open, open,
rawSpawn, rawSpawn,

View File

@ -59,8 +59,8 @@ export class Child {
* *
* @since 2.0.0 * @since 2.0.0
*/ */
async kill(): Promise<void> { kill(): Promise<void> {
this.api.kill(this.pid) return this.api.kill(this.pid)
// await invoke("plugin:shellx|kill", { // await invoke("plugin:shellx|kill", {
// cmd: "killChild", // cmd: "killChild",
// pid: this.pid // pid: this.pid
@ -184,6 +184,7 @@ export class DenoCommand<O extends IOPayload> extends BaseShellCommand<O> {
} }
}) })
.then(async (pid) => { .then(async (pid) => {
console.log("spawned deno process", pid)
await this.api.recordSpawnedProcess(pid) await this.api.recordSpawnedProcess(pid)
return new Child(pid, this.api) return new Child(pid, this.api)
}) })
@ -232,6 +233,7 @@ export type IShell = {
}> }>
RPCChannel: typeof RPCChannel RPCChannel: typeof RPCChannel
whereIsCommand: (command: string) => Promise<string | null> whereIsCommand: (command: string) => Promise<string | null>
killPid: (pid: number) => Promise<void>
} }
export class TauriShellStdio implements IoInterface { export class TauriShellStdio implements IoInterface {
@ -347,6 +349,7 @@ export function constructShellAPI(api: IShellServer): IShell {
return { return {
open: api.open, open: api.open,
killPid: api.killPid,
makeBashScript, makeBashScript,
makePowershellScript, makePowershellScript,
makeAppleScript, makeAppleScript,

View File

@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core" import { invoke } from "@tauri-apps/api/core"
import { AppInfo } from "../models" import { AppInfo, SearchPath } from "../models"
import { generateJarvisPluginCommand } from "./common" import { generateJarvisPluginCommand } from "./common"
export function getAllApps(): Promise<AppInfo[]> { export function getAllApps(): Promise<AppInfo[]> {
@ -14,6 +14,10 @@ export function refreshApplicationsListInBg(): Promise<void> {
return invoke(generateJarvisPluginCommand("refresh_applications_list_in_bg")) return invoke(generateJarvisPluginCommand("refresh_applications_list_in_bg"))
} }
export function setExtraAppSearchPaths(paths: SearchPath[]): Promise<void> {
return invoke(generateJarvisPluginCommand("set_extra_app_search_paths"), { paths })
}
// export function convertAppToTListItem(app: AppInfo): TListItem { // export function convertAppToTListItem(app: AppInfo): TListItem {
// return { // return {
// title: app.name, // title: app.name,

View File

@ -1,437 +0,0 @@
import { invoke } from "@tauri-apps/api/core"
import { array, literal, optional, parse, safeParse, union, type InferOutput } from "valibot"
import { KUNKUN_EXT_IDENTIFIER } from "../constants"
import { CmdType, Ext, ExtCmd, ExtData } from "../models/extension"
import { convertDateToSqliteString, SearchMode, SearchModeEnum, SQLSortOrder } from "../models/sql"
import { generateJarvisPluginCommand } from "./common"
/* -------------------------------------------------------------------------- */
/* Extension CRUD */
/* -------------------------------------------------------------------------- */
export function createExtension(ext: {
identifier: string
version: string
enabled?: boolean
path?: string
data?: any
}) {
return invoke<void>(generateJarvisPluginCommand("create_extension"), ext)
}
export function getAllExtensions() {
return invoke<Ext[]>(generateJarvisPluginCommand("get_all_extensions"))
}
export function getUniqueExtensionByIdentifier(identifier: string) {
return invoke<Ext | undefined>(
generateJarvisPluginCommand("get_unique_extension_by_identifier"),
{
identifier
}
)
}
export function getUniqueExtensionByPath(path: string) {
return invoke<Ext | undefined>(generateJarvisPluginCommand("get_unique_extension_by_path"), {
path
})
}
export function getAllExtensionsByIdentifier(identifier: string) {
return invoke<Ext[]>(generateJarvisPluginCommand("get_all_extensions_by_identifier"), {
identifier
})
}
/**
* Use this function when you expect the extension to exist. Such as builtin extensions.
* @param identifier
* @returns
*/
export function getExtensionByIdentifierExpectExists(identifier: string): Promise<Ext> {
return getUniqueExtensionByIdentifier(identifier).then((ext) => {
if (!ext) {
throw new Error(`Unexpexted Error: Extension ${identifier} not found`)
}
return ext
})
}
// TODO: clean this up
// export function deleteExtensionByIdentifier(identifier: string) {
// return invoke<void>(generateJarvisPluginCommand("delete_extension_by_identifier"), { identifier })
// }
export function deleteExtensionByPath(path: string) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_by_path"), {
path
})
}
export function deleteExtensionByExtId(extId: string) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_by_ext_id"), { extId })
}
/* -------------------------------------------------------------------------- */
/* Extension Command CRUD */
/* -------------------------------------------------------------------------- */
export function createCommand(data: {
extId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled?: boolean
}) {
return invoke<void>(generateJarvisPluginCommand("create_command"), {
...data,
enabled: data.enabled ?? false
})
}
export function getCommandById(cmdId: number) {
return invoke<ExtCmd | undefined>(generateJarvisPluginCommand("get_command_by_id"), { cmdId })
}
export function getCommandsByExtId(extId: number) {
return invoke<ExtCmd[]>(generateJarvisPluginCommand("get_commands_by_ext_id"), { extId })
}
export function deleteCommandById(cmdId: number) {
return invoke<void>(generateJarvisPluginCommand("delete_command_by_id"), {
cmdId
})
}
export function updateCommandById(data: {
cmdId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled: boolean
}) {
return invoke<void>(generateJarvisPluginCommand("update_command_by_id"), data)
}
/* -------------------------------------------------------------------------- */
/* Extension Data CRUD */
/* -------------------------------------------------------------------------- */
export const ExtDataField = union([literal("data"), literal("search_text")])
export type ExtDataField = InferOutput<typeof ExtDataField>
function convertRawExtDataToExtData(rawData?: {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
}): ExtData | undefined {
if (!rawData) {
return rawData
}
const parsedRes = safeParse(ExtData, {
...rawData,
createdAt: new Date(rawData.createdAt),
updatedAt: new Date(rawData.updatedAt),
data: rawData.data ?? undefined,
searchText: rawData.searchText ?? undefined
})
if (parsedRes.success) {
return parsedRes.output
} else {
console.error("Extension Data Parse Failure", parsedRes.issues)
throw new Error("Fail to parse extension data")
}
}
export function createExtensionData(data: {
extId: number
dataType: string
data: string
searchText?: string
}) {
return invoke<void>(generateJarvisPluginCommand("create_extension_data"), data)
}
export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) {
return invoke<
| (ExtData & {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
})
| undefined
>(generateJarvisPluginCommand("get_extension_data_by_id"), {
dataId,
fields
}).then(convertRawExtDataToExtData)
}
/**
* Fields option can be used to select optional fields. By default, if left empty, data and searchText are not returned.
* This is because data and searchText can be large and we don't want to return them by default.
* If you just want to get data ids in order to delete them, retrieving all data is not necessary.
* @param searchParams
*/
export async function searchExtensionData(searchParams: {
extId: number
searchMode: SearchMode
dataId?: number
dataType?: string
searchText?: string
afterCreatedAt?: string
beforeCreatedAt?: string
limit?: number
offset?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}): Promise<ExtData[]> {
const fields = parse(optional(array(ExtDataField), []), searchParams.fields)
let items = await invoke<
(ExtData & {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
})[]
>(generateJarvisPluginCommand("search_extension_data"), {
searchQuery: {
...searchParams,
fields
}
})
return items.map(convertRawExtDataToExtData).filter((item) => item) as ExtData[]
}
export function deleteExtensionDataById(dataId: number) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_data_by_id"), { dataId })
}
export function updateExtensionDataById(data: {
dataId: number
data: string
searchText?: string
}) {
return invoke<void>(generateJarvisPluginCommand("update_extension_data_by_id"), data)
}
/* -------------------------------------------------------------------------- */
/* Built-in Extensions */
/* -------------------------------------------------------------------------- */
export function getExtClipboard() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_CLIPBOARD_EXT_IDENTIFIER)
}
export function getExtQuickLinks() {
return getExtensionByIdentifierExpectExists(
KUNKUN_EXT_IDENTIFIER.KUNKUN_QUICK_LINKS_EXT_IDENTIFIER
)
}
export function getExtRemote() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_REMOTE_EXT_IDENTIFIER)
}
export function getExtScriptCmd() {
return getExtensionByIdentifierExpectExists(
KUNKUN_EXT_IDENTIFIER.KUNKUN_SCRIPT_CMD_EXT_IDENTIFIER
)
}
export function getExtDev() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_DEV_EXT_IDENTIFIER)
}
/**
* Database API for extensions.
* Extensions shouldn't have full access to the database, they can only access their own data.
* When an extension is loaded, the main thread will create an instance of this class and
* expose it to the extension.
*/
export class JarvisExtDB {
extId: number
constructor(extId: number) {
this.extId = extId
}
async add(data: { data: string; dataType?: string; searchText?: string }) {
return createExtensionData({
data: data.data,
dataType: data.dataType ?? "default",
searchText: data.searchText,
extId: this.extId
})
}
async delete(dataId: number): Promise<void> {
// Verify if this data belongs to this extension
const d = await getExtensionDataById(dataId)
if (!d || d.extId !== this.extId) {
throw new Error("Extension Data not found")
}
return await deleteExtensionDataById(dataId)
}
async search(searchParams: {
dataId?: number
searchMode?: SearchMode
dataType?: string
searchText?: string
afterCreatedAt?: Date
beforeCreatedAt?: Date
limit?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}): Promise<ExtData[]> {
const beforeCreatedAt = searchParams.beforeCreatedAt
? convertDateToSqliteString(searchParams.beforeCreatedAt)
: undefined
const afterCreatedAt = searchParams.afterCreatedAt
? convertDateToSqliteString(searchParams.afterCreatedAt)
: undefined
return searchExtensionData({
...searchParams,
searchMode: searchParams.searchMode ?? SearchModeEnum.FTS,
extId: this.extId,
beforeCreatedAt,
afterCreatedAt
})
}
/**
* Retrieve all data of this extension.
* Use `search()` method for more advanced search.
* @param options optional fields to retrieve. By default, data and searchText are not returned.
* @returns
*/
retrieveAll(options: { fields?: ExtDataField[] }): Promise<ExtData[]> {
return this.search({ fields: options.fields })
}
/**
* Retrieve all data of this extension by type.
* Use `search()` method for more advanced search.
* @param dataType
* @returns
*/
retrieveAllByType(dataType: string): Promise<ExtData[]> {
return this.search({ dataType })
}
/**
* Delete all data of this extension.
*/
deleteAll(): Promise<void> {
return this.search({})
.then((items) => {
return Promise.all(items.map((item) => this.delete(item.dataId)))
})
.then(() => {})
}
/**
* Update data and searchText of this extension.
* @param dataId unique id of the data
* @param data
* @param searchText
* @returns
*/
async update(data: { dataId: number; data: string; searchText?: string }): Promise<void> {
const d = await getExtensionDataById(data.dataId)
if (!d || d.extId !== this.extId) {
throw new Error("Extension Data not found")
}
return updateExtensionDataById(data)
}
}
export class KV {
extId: number
db: JarvisExtDB
private DataType: string = "kunkun_kv"
constructor(extId: number) {
this.extId = extId
this.db = new JarvisExtDB(extId)
}
get<T = string>(key: string): Promise<T | null | undefined> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch,
fields: ["search_text", "data"]
})
.then((items) => {
if (items.length === 0) {
return null
} else if (items.length > 1) {
throw new Error("Multiple KVs with the same key")
}
return items[0].data ? (JSON.parse(items[0].data).value as T) : null
})
.catch((err) => {
console.warn(err)
return null
})
}
set(key: string, value: string): Promise<void> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch
})
.then((items) => {
if (items.length === 0) {
return this.db.add({
data: JSON.stringify({ value: value }),
dataType: this.DataType,
searchText: key
})
} else if (items.length === 1) {
return this.db.update({
dataId: items[0].dataId,
data: JSON.stringify({ value: value }),
searchText: key
})
} else {
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
Promise.resolve()
)
}
})
}
delete(key: string): Promise<void> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch
})
.then((items) => {
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
Promise.resolve()
)
})
}
exists(key: string): Promise<boolean> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch,
fields: []
})
.then((items) => {
return items.length > 0
})
}
}

View File

@ -5,8 +5,7 @@ export * from "./tools"
export * from "./extension" export * from "./extension"
export * from "./system" export * from "./system"
export * from "./store" export * from "./store"
export * as db from "./db" export * as sql from "./sql"
export { JarvisExtDB } from "./db"
export * from "./clipboard" export * from "./clipboard"
export * from "./fileSearch" export * from "./fileSearch"
export * from "./utils" export * from "./utils"

View File

@ -0,0 +1,30 @@
import { invoke } from "@tauri-apps/api/core"
import { generateJarvisPluginCommand } from "./common"
export interface QueryResult {
/** The number of rows affected by the query. */
rowsAffected: number
/**
* The last inserted `id`.
*
* This value is not set for Postgres databases. If the
* last inserted id is required on Postgres, the `select` function
* must be used, with a `RETURNING` clause
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
*/
lastInsertId?: number
}
export function select(query: string, values: any[]) {
return invoke<any[]>(generateJarvisPluginCommand("select"), {
query,
values
})
}
export function execute(query: string, values: any[]) {
return invoke<QueryResult>(generateJarvisPluginCommand("execute"), {
query,
values
})
}

Some files were not shown because too many files have changed in this diff Show More