Feature: app launcher (#92)

* feat: implement app loader (has performance problem)

* feat: enhance command filtering and search functionality

- Implement command score filtering for various command types
- Add filtered stores for quick links, system commands, and extensions
- Update command components to use new filtering mechanism
- Improve search experience by dynamically filtering results
- Refactor command value handling to use direct name matching
This commit is contained in:
Huakun Shen 2025-02-06 20:29:56 -05:00 committed by GitHub
parent f895594b62
commit 872bcfdfd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 738 additions and 343 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@kksh/desktop", "name": "@kksh/desktop",
"version": "0.1.19", "version": "0.1.20",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -41,28 +41,28 @@
"uuid": "^11.0.3" "uuid": "^11.0.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.18.0", "@eslint/js": "^9.19.0",
"@inlang/paraglide-js": "1.11.8", "@inlang/paraglide-js": "1.11.8",
"@kksh/types": "workspace:*", "@kksh/types": "workspace:*",
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.12.1", "@sveltejs/kit": "^2.17.1",
"@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.9", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.16",
"@tauri-apps/cli": "^2.1.0", "@tauri-apps/cli": "^2.2.7",
"@types/bun": "latest", "@types/bun": "latest",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.20.0", "@typescript-eslint/parser": "^8.23.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.72", "bits-ui": "1.0.0-next.86",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0", "globals": "^15.14.0",
"lucide-svelte": "^0.469.0", "lucide-svelte": "^0.474.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"svelte-radix": "^2.0.1", "svelte-radix": "^2.0.1",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",

View File

@ -4,6 +4,7 @@ import { checkUpdateAndInstall } from "@/utils/updater"
import { setTransparentTitlebar } from "@kksh/api/commands" import { setTransparentTitlebar } from "@kksh/api/commands"
import { IconEnum } from "@kksh/api/models" import { IconEnum } from "@kksh/api/models"
import type { BuiltinCmd } from "@kksh/ui/types" import type { BuiltinCmd } from "@kksh/ui/types"
import { commandScore } from "@kksh/ui/utils"
import { getVersion } from "@tauri-apps/api/app" import { getVersion } from "@tauri-apps/api/app"
import { appDataDir } from "@tauri-apps/api/path" import { appDataDir } from "@tauri-apps/api/path"
import { WebviewWindow } from "@tauri-apps/api/webviewWindow" import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
@ -474,10 +475,12 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
} }
].map((cmd) => ({ ...cmd, id: uuidv4() })) ].map((cmd) => ({ ...cmd, id: uuidv4() }))
export const builtinCmds = derived(appConfig, ($appConfig) => { export const builtinCmds = derived([appConfig, appState], ([$appConfig, $appState]) => {
return rawBuiltinCmds.filter((cmd) => { return rawBuiltinCmds
const passDeveloper = cmd.flags?.developer ? $appConfig.developerMode : true .filter((cmd) => {
const passDev = cmd.flags?.dev ? dev : true const passDeveloper = cmd.flags?.developer ? $appConfig.developerMode : true
return passDeveloper && passDev const passDev = cmd.flags?.dev ? dev : true
}) return passDeveloper && passDev
})
.filter((cmd) => commandScore(cmd.name, $appState.searchTerm, cmd.keywords) > 0.5)
}) })

View File

@ -1,4 +1,14 @@
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 { derived, readable } from "svelte/store"
import { appState } from "../stores/appState"
export const systemCommands: SysCommand[] = getSystemCommands() export const systemCommands = readable(getSystemCommands())
// export const systemCommandsFiltered = derived(
// [systemCommands, appState],
// ([$systemCommands, $appState]) => {
// return $systemCommands.filter((cmd) => commandScore(cmd.name, $appState.searchTerm) > 0.5)
// }
// )

View File

@ -1,8 +1,6 @@
import { findAllArgsInLink } from "@/cmds/quick-links" import { Action as ActionSchema } from "@kksh/api/models"
import { Action as ActionSchema, CmdTypeEnum } from "@kksh/api/models"
import type { AppState } from "@kksh/types" import type { AppState } from "@kksh/types"
import type { CmdValue } from "@kksh/ui/types" import { get, writable, type Writable } from "svelte/store"
import { derived, get, writable, type Writable } from "svelte/store"
export const defaultAppState: AppState = { export const defaultAppState: AppState = {
searchTerm: "", searchTerm: "",

View File

@ -0,0 +1,48 @@
import { getAllApps, refreshApplicationsList } from "@kksh/api/commands"
import { AppInfo } from "@kksh/api/models"
import { commandScore } from "@kksh/ui/utils"
import * as fs from "@tauri-apps/plugin-fs"
import { derived, get, writable } from "svelte/store"
import { appState } from "./appState"
export function createAppsLoaderStore() {
const store = writable<AppInfo[]>([])
return {
...store,
get: () => get(store),
init: async () => {
await refreshApplicationsList()
const apps = await getAllApps()
// fs.writeTextFile("/Users/hk/Desktop/apps.json", JSON.stringify(apps))
store.set(
apps.filter((app) => {
return (
!app.app_desktop_path.includes("Parallels") &&
!app.app_desktop_path.startsWith("/Library/Application Support") &&
!app.app_desktop_path.startsWith("/System/Library/CoreServices") &&
!app.app_desktop_path.startsWith("/System/Library/PrivateFrameworks")
)
})
)
}
}
}
export const appsLoader = createAppsLoaderStore()
// export const appsFiltered = derived([appsLoader, appState], ([$apps, $appState]) => {
// return []
// return $apps.filter((app) => {
// if ($appState.searchTerm.length === 0) {
// return false
// }
// return (
// commandScore(
// app.name,
// $appState.searchTerm
// // []
// ) > 0.8
// )
// })
// })

View File

@ -2,10 +2,12 @@ import { getExtensionsFolder } from "@/constants"
import { db } from "@kksh/api/commands" import { db } from "@kksh/api/commands"
import type { ExtPackageJson, ExtPackageJsonExtra } from "@kksh/api/models" 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 * as fs from "@tauri-apps/plugin-fs"
import { derived, get, writable, type Readable, type Writable } from "svelte/store" import { derived, get, writable, type Readable, type Writable } from "svelte/store"
import { appConfig } from "./appConfig" import { appConfig } from "./appConfig"
import { appState } from "./appState"
function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & { function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
init: () => Promise<void> init: () => Promise<void>
@ -224,3 +226,27 @@ export const devStoreExts: Readable<ExtPackageJsonExtra[]> = derived(
return $extensionsStore.filter((ext) => extAPI.isExtPathInDev(extContainerPath, ext.extPath)) return $extensionsStore.filter((ext) => extAPI.isExtPathInDev(extContainerPath, ext.extPath))
} }
) )
// export const installedStoreExtsFiltered = derived(
// [installedStoreExts, appState],
// ([$installedStoreExts, $appState]) => {
// return $installedStoreExts.filter(
// (ext) => commandScore(ext.kunkun.name, $appState.searchTerm) > 0.5
// )
// }
// )
// export const devStoreExtsFiltered = derived(
// [devStoreExts, appState],
// ([$devStoreExts, $appState]) => {
// return $devStoreExts.filter((ext) => {
// console.log(
// "commandScore",
// ext.kunkun.name,
// $appState.searchTerm,
// commandScore(ext.kunkun.name, $appState.searchTerm)
// )
// return commandScore(ext.kunkun.name, $appState.searchTerm) > 0.1
// })
// }
// )

View File

@ -4,3 +4,4 @@ export * from "./winExtMap"
export * from "./extensions" export * from "./extensions"
export * from "./auth" export * from "./auth"
export * from "./quick-links" export * from "./quick-links"
export * from "./apps"

View File

@ -1,7 +1,9 @@
import type { Icon } from "@kksh/api/models" import type { Icon } from "@kksh/api/models"
import { createQuickLinkCommand, getAllQuickLinkCommands } from "@kksh/extension/db" import { createQuickLinkCommand, getAllQuickLinkCommands } from "@kksh/extension/db"
import type { CmdQuery, QuickLink } from "@kksh/ui/types" import type { QuickLink } from "@kksh/ui/types"
import { get, writable, type Writable } from "svelte/store" import { commandScore } from "@kksh/ui/utils"
import { derived, get, writable, type Writable } from "svelte/store"
import { appState } from "./appState"
export interface QuickLinkAPI { export interface QuickLinkAPI {
get: () => QuickLink[] get: () => QuickLink[]
@ -37,3 +39,18 @@ function createQuickLinksStore(): Writable<QuickLink[]> & QuickLinkAPI {
} }
export const quickLinks = createQuickLinksStore() export const quickLinks = createQuickLinksStore()
// export const quickLinksFiltered = derived([quickLinks, appState], ([$quicklinks, $appState]) => {
// return $quicklinks.filter((lnk) => {
// if ($appState.searchTerm.length === 0) {
// return false
// }
// return (
// commandScore(
// lnk.name,
// $appState.searchTerm
// // []
// ) > 0.5
// )
// })
// })

View File

@ -3,6 +3,7 @@
import { i18n, switchToLanguage } from "@/i18n" import { i18n, switchToLanguage } from "@/i18n"
import { setLanguageTag, type AvailableLanguageTag } from "@/paraglide/runtime" import { setLanguageTag, type AvailableLanguageTag } from "@/paraglide/runtime"
import { appConfig, appState, extensions, quickLinks, winExtMap } from "@/stores" import { appConfig, appState, extensions, quickLinks, winExtMap } from "@/stores"
import { appsLoader } from "@/stores/apps"
import { initDeeplink } from "@/utils/deeplink" import { initDeeplink } from "@/utils/deeplink"
import { updateAppHotkey } from "@/utils/hotkey" import { updateAppHotkey } from "@/utils/hotkey"
import { globalKeyDownHandler, globalKeyUpHandler, goBackOrCloseOnEscape } from "@/utils/key" import { globalKeyDownHandler, globalKeyUpHandler, goBackOrCloseOnEscape } from "@/utils/key"
@ -60,13 +61,13 @@
info("fixed path env") info("fixed path env")
}) })
.catch(error) .catch(error)
quickLinks.init() quickLinks.init()
appConfig.init().then(() => { appConfig.init().then(() => {
console.log("appConfig.language", $appConfig.language) console.log("appConfig.language", $appConfig.language)
setLanguageTag($appConfig.language as AvailableLanguageTag) setLanguageTag($appConfig.language as AvailableLanguageTag)
switchToLanguage($appConfig.language as AvailableLanguageTag) switchToLanguage($appConfig.language as AvailableLanguageTag)
}) })
appsLoader.init()
if (isInMainWindow()) { if (isInMainWindow()) {
if ($appConfig.triggerHotkey) { if ($appConfig.triggerHotkey) {
updateAppHotkey($appConfig.triggerHotkey) updateAppHotkey($appConfig.triggerHotkey)

View File

@ -8,10 +8,15 @@
import { import {
appConfig, appConfig,
appConfigLoaded, appConfigLoaded,
// appsFiltered,
appsLoader,
appState, appState,
devStoreExts, devStoreExts,
// devStoreExtsFiltered,
// installedStoreExtsFiltered,
installedStoreExts, installedStoreExts,
quickLinks quickLinks
// 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"
@ -19,6 +24,7 @@
import { db, toggleDevTools } from "@kksh/api/commands" import { db, toggleDevTools } from "@kksh/api/commands"
import { Button, Command, DropdownMenu } from "@kksh/svelte5" import { Button, Command, DropdownMenu } from "@kksh/svelte5"
import { import {
AppsCmds,
BuiltinCmds, BuiltinCmds,
CustomCommandInput, CustomCommandInput,
ExtCmdsGroup, ExtCmdsGroup,
@ -87,16 +93,19 @@
} }
}} }}
/> />
<!-- <div>appsFiltered: {$appsFiltered.length}</div> -->
<!-- <div>appsLoader: {$appsLoader.length}</div> -->
<!-- filter={(value, search, keywords) => {
return commandScore(
value.startsWith("{") ? (JSON.parse(value) as CmdValue).cmdName : value,
search,
keywords
)
}} -->
<Command.Root <Command.Root
class={cn("h-screen rounded-lg border shadow-md")} class={cn("h-screen rounded-lg border shadow-md")}
bind:value={$appState.highlightedCmd} bind:value={$appState.highlightedCmd}
filter={(value, search, keywords) => { shouldFilter={true}
return commandScore(
value.startsWith("{") ? (JSON.parse(value) as CmdValue).cmdName : value,
search,
keywords
)
}}
loop loop
> >
<CustomCommandInput <CustomCommandInput
@ -206,6 +215,7 @@
hmr={$appConfig.hmr} hmr={$appConfig.hmr}
/> />
{/if} {/if}
{#if $appConfig.extensionsInstallDir && $installedStoreExts.length > 0} {#if $appConfig.extensionsInstallDir && $installedStoreExts.length > 0}
<ExtCmdsGroup <ExtCmdsGroup
extensions={$installedStoreExts} extensions={$installedStoreExts}
@ -215,9 +225,26 @@
onExtCmdSelect={commandLaunchers.onExtCmdSelect} onExtCmdSelect={commandLaunchers.onExtCmdSelect}
/> />
{/if} {/if}
<AppsCmds apps={$appsLoader} />
<QuickLinks quickLinks={$quickLinks} /> <QuickLinks quickLinks={$quickLinks} />
<BuiltinCmds builtinCmds={$builtinCmds} /> <BuiltinCmds builtinCmds={$builtinCmds} />
<SystemCmds {systemCommands} /> <SystemCmds systemCommands={$systemCommands} />
<!-- <AppsCmds apps={$appsFiltered} /> -->
<!-- {#if $quickLinksFiltered.length > 0}
<QuickLinks quickLinks={$quickLinksFiltered} />
{/if}
{#if $appsFiltered.length > 0}
<AppsCmds apps={$appsFiltered} />
{/if}
{#if $builtinCmds.length > 0}
<BuiltinCmds builtinCmds={$builtinCmds} />
{/if}
{#if $systemCommandsFiltered.length > 0}
<SystemCmds systemCommands={$systemCommandsFiltered} />
{/if} -->
<!-- <AppsCmds apps={$appsLoader} /> -->
<!-- <AppsCmds apps={$appsFiltered} /> -->
</Command.List> </Command.List>
<GlobalCommandPaletteFooter /> <GlobalCommandPaletteFooter />
</Command.Root> </Command.Root>

View File

@ -0,0 +1,50 @@
<script lang="ts">
import { IconEnum, type AppInfo } from "@kksh/api/models"
import { Command } from "@kksh/svelte5"
import { convertFileSrc } from "@tauri-apps/api/core"
import * as os from "@tauri-apps/plugin-os"
import { toast } from "svelte-sonner"
import { executeBashScript, open } from "tauri-plugin-shellx-api"
import IconMultiplexer from "../common/IconMultiplexer.svelte"
import { DraggableCommandGroup } from "../custom"
const platform = os.platform()
let { apps }: { apps: AppInfo[] } = $props()
// let appsDisplay = $derived(apps.length > 20 ? apps.slice(0, 20) : apps)
</script>
<DraggableCommandGroup heading="Apps">
{#each apps as app}
<Command.Item
class="flex justify-between"
onSelect={() => {
if (platform === "windows") {
if (app.app_path_exe) {
open(app.app_path_exe)
} else {
toast.error("No executable path found for this app")
}
} else {
open(app.app_desktop_path)
}
}}
value={app.name}
>
<span class="flex gap-2">
<IconMultiplexer
icon={app.icon_path
? {
type: IconEnum.RemoteUrl,
value: convertFileSrc(app.icon_path, "appicon")
}
: {
type: IconEnum.Iconify,
value: "mdi:application"
}}
class="!h-5 !w-5 shrink-0"
/>
<span>{app.name}</span>
</span>
</Command.Item>
{/each}
</DraggableCommandGroup>

View File

@ -9,18 +9,19 @@
</script> </script>
<DraggableCommandGroup heading="Builtin Commands"> <DraggableCommandGroup heading="Builtin Commands">
{#each builtinCmds as cmd (cmd.id)} {#each builtinCmds as cmd (`builtin-${cmd.name}`)}
<Command.Item <Command.Item
class="flex justify-between" class="flex justify-between"
onSelect={() => { onSelect={() => {
cmd.function() cmd.function()
}} }}
value={JSON.stringify({ keywords={cmd.keywords}
value={cmd.name}
>
<!-- value={JSON.stringify({
cmdName: cmd.name, cmdName: cmd.name,
cmdType: CmdTypeEnum.Builtin cmdType: CmdTypeEnum.Builtin
} satisfies CmdValue)} } satisfies CmdValue)} -->
keywords={cmd.keywords}
>
<span class="flex gap-2"> <span class="flex gap-2">
<IconMultiplexer icon={cmd.icon} class="!h-5 !w-5 shrink-0" /> <IconMultiplexer icon={cmd.icon} class="!h-5 !w-5 shrink-0" />
<span>{cmd.name}</span> <span>{cmd.name}</span>

View File

@ -34,12 +34,13 @@
onSelect={() => { onSelect={() => {
onExtCmdSelect(ext, cmd, { isDev, hmr }) onExtCmdSelect(ext, cmd, { isDev, hmr })
}} }}
value={JSON.stringify({ value={cmd.name}
>
<!-- value={JSON.stringify({
cmdName: cmd.name, cmdName: cmd.name,
cmdType: cmd.type, cmdType: cmd.type,
data: { isDev: heading === "Dev Extensions" } data: { isDev: heading === "Dev Extensions" }
} satisfies CmdValue)} } satisfies CmdValue)} -->
>
<span class="flex gap-2"> <span class="flex gap-2">
<IconMultiplexer icon={cmd.icon ?? ext.kunkun.icon} class="!h-5 !w-5 shrink-0" /> <IconMultiplexer icon={cmd.icon ?? ext.kunkun.icon} class="!h-5 !w-5 shrink-0" />
<span>{cmd.name}</span> <span>{cmd.name}</span>

View File

@ -9,19 +9,20 @@
</script> </script>
<DraggableCommandGroup heading="Quick Links"> <DraggableCommandGroup heading="Quick Links">
{#each quickLinks as cmd} {#each quickLinks as cmd (`quick-link-${cmd.name}`)}
<Command.Item <Command.Item
class="flex justify-between" class="flex justify-between"
onSelect={() => { onSelect={() => {
console.log(cmd) console.log(cmd)
}} }}
keywords={["quick", "link"]} keywords={["quick", "link"]}
value={JSON.stringify({ value={cmd.name}
>
<!-- value={JSON.stringify({
cmdName: cmd.name, cmdName: cmd.name,
cmdType: CmdTypeEnum.QuickLink, cmdType: CmdTypeEnum.QuickLink,
data: cmd.link data: cmd.link
} satisfies CmdValue)} } satisfies CmdValue)} -->
>
<span class="flex gap-2"> <span class="flex gap-2">
<IconMultiplexer icon={cmd.icon} class="!h-5 !w-5 shrink-0" /> <IconMultiplexer icon={cmd.icon} class="!h-5 !w-5 shrink-0" />
<span>{cmd.name}</span> <span>{cmd.name}</span>

View File

@ -12,7 +12,7 @@
</script> </script>
<DraggableCommandGroup heading="System Commands"> <DraggableCommandGroup heading="System Commands">
{#each systemCommands as cmd} {#each systemCommands as cmd (`system-cmds-${cmd.name}`)}
<Command.Item <Command.Item
class="flex justify-between" class="flex justify-between"
onSelect={async () => { onSelect={async () => {
@ -23,11 +23,12 @@
} }
} }
}} }}
value={JSON.stringify({ value={cmd.name}
>
<!-- value={JSON.stringify({
cmdName: cmd.name, cmdName: cmd.name,
cmdType: CmdTypeEnum.System cmdType: CmdTypeEnum.System
} satisfies CmdValue)} } satisfies CmdValue)} -->
>
<span class="flex gap-2"> <span class="flex gap-2">
{#if cmd.icon} {#if cmd.icon}
<IconMultiplexer icon={cmd.icon} class="!h-5 !w-5 shrink-0" /> <IconMultiplexer icon={cmd.icon} class="!h-5 !w-5 shrink-0" />

View File

@ -4,4 +4,5 @@ export { default as GlobalCommandPaletteFooter } from "./GlobalCommandPaletteFoo
export { default as ExtCmdsGroup } from "./ExtCmdsGroup.svelte" export { default as ExtCmdsGroup } from "./ExtCmdsGroup.svelte"
export { default as SystemCmds } from "./SystemCmds.svelte" export { default as SystemCmds } from "./SystemCmds.svelte"
export { default as QuickLinks } from "./QuickLinks.svelte" export { default as QuickLinks } from "./QuickLinks.svelte"
export { default as AppsCmds } from "./AppsCmds.svelte"
export * from "./types" export * from "./types"

797
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff