diff --git a/app/app.vue b/app/app.vue
index bc69bd9f03..1691e5d8cc 100644
--- a/app/app.vue
+++ b/app/app.vue
@@ -31,6 +31,8 @@ const colorScheme = computed(() => {
}[colorMode.preference]
})
+const commandBarRef = useTemplateRef('commandBarRef')
+
useHead({
htmlAttrs: {
'lang': () => locale.value,
@@ -77,6 +79,16 @@ onKeyDown(
{ dedupe: true },
)
+onKeyDown(
+ 'k',
+ e => {
+ if (!(e.metaKey || e.ctrlKey)) return
+ e.preventDefault()
+ commandBarRef.value?.toggle()
+ },
+ { dedupe: true },
+)
+
onKeyUp(
'?',
e => {
@@ -122,6 +134,7 @@ if (import.meta.client) {
{{ $t('common.skip_link') }}
+
@@ -153,6 +166,7 @@ if (import.meta.client) {
color: var(--bg);
text-decoration: underline;
}
+
.skip-link:focus {
top: 0;
}
diff --git a/app/components/CommandBar.vue b/app/components/CommandBar.vue
new file mode 100644
index 0000000000..993ffbf4d1
--- /dev/null
+++ b/app/components/CommandBar.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
{{ item.name }}
+
{{ item.description }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/Terminal/Install.vue b/app/components/Terminal/Install.vue
index b03a5d6fa5..bcb75c8fb1 100644
--- a/app/components/Terminal/Install.vue
+++ b/app/components/Terminal/Install.vue
@@ -2,6 +2,8 @@
import type { JsrPackageInfo } from '#shared/types/jsr'
import type { PackageManagerId } from '~/utils/install-command'
+const { t } = useI18n()
+
const props = defineProps<{
packageName: string
requestedVersion?: string | null
@@ -93,6 +95,37 @@ const copyRunCommand = (command?: string) => copyRun(getFullRunCommand(command))
const { copied: createCopied, copy: copyCreate } = useClipboard({ copiedDuring: 2000 })
const copyCreateCommand = () => copyCreate(getFullCreateCommand())
+
+registerScopedCommand({
+ id: 'package:install',
+ name: t('command.copy_install'),
+ description: t('command.copy_install_desc'),
+ handler: async () => {
+ copyInstallCommand()
+ },
+})
+
+if (props.executableInfo?.hasExecutable) {
+ registerScopedCommand({
+ id: 'packages:copy-run',
+ name: t('command.copy_run'),
+ description: t('command.copy_run_desc'),
+ handler: async () => {
+ copyRunCommand()
+ },
+ })
+}
+
+if (props.createPackageInfo) {
+ registerScopedCommand({
+ id: 'packages:copy-create',
+ name: t('command.copy_create'),
+ description: t('command.copy_create_desc'),
+ handler: async () => {
+ copyCreateCommand()
+ },
+ })
+}
diff --git a/app/composables/useCommandRegistry.ts b/app/composables/useCommandRegistry.ts
new file mode 100644
index 0000000000..37767d7b2f
--- /dev/null
+++ b/app/composables/useCommandRegistry.ts
@@ -0,0 +1,81 @@
+import { computed } from 'vue'
+
+export interface Command {
+ id: string
+ name: string
+ description: string | undefined
+ handler?: (ctx: CommandContext) => Promise
+}
+
+export interface CommandContext {
+ input: (options: CommandInputOptions) => Promise
+ select: (options: CommandSelectOptions) => Promise
+}
+
+export interface CommandInputOptions {
+ prompt: string
+}
+
+export interface CommandSelectOptions {
+ prompt: string
+ items: T[]
+}
+
+/**
+ * Composable for global command registry.
+ * @public
+ */
+export const useCommandRegistry = () => {
+ const commands = useState