diff --git a/README.md b/README.md index e1e524a..2a06799 100644 --- a/README.md +++ b/README.md @@ -15,22 +15,16 @@ ```shell # Register a widget with the name "_wk_widget", and bind it to the ^G. - eval "$(wk --init '^G')" + eval "$(wk init --bindkey '^G')" ``` 3. Restart zsh. > [!TIP] -> If you want to change the trigger key, change it as follows: -> -> ```shell -> eval "$(wk --init '^T')" -> ``` -> > If you want to register only the widget, change it as follows: > > ```shell -> eval "$(wk --init '')" +> eval "$(wk init)" > ``` ## :gear: Configuration diff --git a/deno.jsonc b/deno.jsonc index 64ac42b..c7b08c8 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -15,6 +15,7 @@ }, "imports": { "@cliffy/ansi": "jsr:@cliffy/ansi@1.0.0-rc.7", + "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.7", "@cliffy/keypress": "jsr:@cliffy/keypress@1.0.0-rc.7", "@cliffy/table": "jsr:@cliffy/table@1.0.0-rc.7", "@eta-dev/eta": "jsr:@eta-dev/eta@3.5.0", diff --git a/deno.lock b/deno.lock index 0b25622..c65d102 100644 --- a/deno.lock +++ b/deno.lock @@ -2,6 +2,8 @@ "version": "5", "specifiers": { "jsr:@cliffy/ansi@1.0.0-rc.7": "1.0.0-rc.7", + "jsr:@cliffy/command@1.0.0-rc.7": "1.0.0-rc.7", + "jsr:@cliffy/flags@1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/internal@1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/keycode@1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/keypress@1.0.0-rc.7": "1.0.0-rc.7", @@ -12,6 +14,7 @@ "jsr:@std/fmt@~1.0.2": "1.0.3", "jsr:@std/io@~0.224.9": "0.224.9", "jsr:@std/path@1.0.8": "1.0.8", + "jsr:@std/text@~1.0.7": "1.0.15", "jsr:@std/yaml@1.0.5": "1.0.5", "npm:@types/node@*": "22.13.8", "npm:xdg-basedir@5.1.0": "5.1.0" @@ -26,6 +29,22 @@ "jsr:@std/io" ] }, + "@cliffy/command@1.0.0-rc.7": { + "integrity": "1288808d7a3cd18b86c24c2f920e47a6d954b7e23cadc35c8cbd78f8be41f0cd", + "dependencies": [ + "jsr:@cliffy/flags", + "jsr:@cliffy/internal", + "jsr:@cliffy/table", + "jsr:@std/fmt@~1.0.2", + "jsr:@std/text" + ] + }, + "@cliffy/flags@1.0.0-rc.7": { + "integrity": "318d9be98f6a6417b108e03dec427dea96cdd41a15beb21d2554ae6da450a781", + "dependencies": [ + "jsr:@std/text" + ] + }, "@cliffy/internal@1.0.0-rc.7": { "integrity": "10412636ab3e67517d448be9eaab1b70c88eba9be22617b5d146257a11cc9b17" }, @@ -60,6 +79,9 @@ "@std/path@1.0.8": { "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" }, + "@std/text@1.0.15": { + "integrity": "91f5cc1e12779a3d95f1be34e763f9c28a75a078b7360e6fcaef0d8d9b1e3e7f" + }, "@std/yaml@1.0.5": { "integrity": "71ba3d334305ee2149391931508b2c293a8490f94a337eef3a09cade1a2a2742" } @@ -81,6 +103,7 @@ "workspace": { "dependencies": [ "jsr:@cliffy/ansi@1.0.0-rc.7", + "jsr:@cliffy/command@1.0.0-rc.7", "jsr:@cliffy/keypress@1.0.0-rc.7", "jsr:@cliffy/table@1.0.0-rc.7", "jsr:@eta-dev/eta@3.5.0", diff --git a/src/cli.ts b/src/cli.ts index a98ecc7..9d16047 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,139 +1,12 @@ -import { Eta } from '@eta-dev/eta' -import { join as joinPath } from '@std/path' -import { parse as parseYaml } from '@std/yaml' -import { parseArgs } from 'node:util' +import { Command } from '@cliffy/command' import VERSION from '../VERSION' with { type: 'text' } -import { XDG_CONFIG_HOME } from './const.ts' -import { AbortError, KeyParseError, UndefinedKeyError } from './errors.ts' -import { type Dependencies, main } from './main.ts' -import { TUI } from './tui.ts' -import { type Binding } from './types/Binding.ts' -import { defaultContext, mergeContext, PartialContext } from './types/Context.ts' -import { getKeySymbol, renderPrompt, renderTable } from './ui.ts' -import WIDGET_TEMPLATE from './widget.eta' with { type: 'text' } - -const { values: opts } = parseArgs({ - options: { - version: { - type: 'boolean', - short: 'v', - }, - init: { - type: 'string', - }, - 'up-one-line': { - type: 'string', - }, - }, -}) - -if (opts.version) { - console.log(VERSION.trim()) - Deno.exit(0) -} - -if (opts.init !== undefined) { - const eta = new Eta() - - const rendered = eta.renderString(WIDGET_TEMPLATE, { - wk_path: Deno.execPath(), - bindkey: opts.init, - }) - - console.log(rendered) - - Deno.exit(0) -} - -async function loadYaml(path: string) { - const text = await Deno.readTextFile(path) - return parseYaml(text) as T -} - -const fetchContextWaiting = (async () => { - const found = await loadYaml(joinPath(XDG_CONFIG_HOME, 'wk', 'config.yaml')).catch(() => undefined) - if (found === undefined) { - return defaultContext - } - return mergeContext(found) -})() - -const fetchBindingsWaiting = Promise.all([ - loadYaml(joinPath(XDG_CONFIG_HOME, 'wk', 'bindings.yaml')).catch(() => []).catch(() => []), - loadYaml(joinPath(Deno.cwd(), 'wk.bindings.yaml')).catch(() => []).catch(() => []), -]).then(([globalBindings, localBindings]) => [...globalBindings, ...localBindings]) - -const tty = await Deno.open('/dev/tty', { read: true, write: true }) -const tui = new TUI(tty, tty) - -try { - tui.init(opts['up-one-line'] === 'true' ? true : opts['up-one-line'] === 'false' ? false : 'auto') - - const [ctx, bindings] = await Promise.all([fetchContextWaiting, fetchBindingsWaiting]) - - let timeoutTimerId: number | undefined - const handleTimeout = () => { - tui.close() - Deno.exit(4) - } - - const deps: Dependencies = { - keypress: tui.keypress, - draw: (inputKeys, bindings) => tui.draw(renderPrompt(ctx, inputKeys), renderTable(ctx, bindings).toString()), - setTimeoutTimer: () => { - if (ctx.timeout > 0) { - timeoutTimerId = setTimeout(handleTimeout, ctx.timeout) - } - }, - clearTimeoutTimer: () => { - if (timeoutTimerId !== undefined) clearTimeout(timeoutTimerId) - }, - } - - const { - key: _, - desc: __, - icon: ___, - type: ____, - buffer, - delimiter: definedDelimiter, - ...rest - } = await main(deps, bindings) - - tui.close() - - const outputs = [buffer] - for (const [k, v] of Object.entries(rest)) { - switch (typeof v) { - case 'string': - outputs.push(`${k}:${v}`) - break - case 'boolean': - outputs.push(`${k}:${JSON.stringify(v)}`) - break - default: - break - } - } - - const delimiter = typeof definedDelimiter === 'string' ? definedDelimiter : ctx.outputDelimiter - - console.log(outputs.join(delimiter)) -} catch (e: unknown) { - if (e instanceof AbortError) { - tui.close() - Deno.exit(3) - } else if (e instanceof UndefinedKeyError) { - tui.close() - console.error(`"${e.getInputKeys().map((k) => getKeySymbol(defaultContext, k)).join(' ')}" is undefined`) - Deno.exit(5) - } else if (e instanceof KeyParseError) { - tui.close() - console.error('Failed to parse key', e.getKey()) - Deno.exit(6) - } else { - throw e - } -} finally { - tui.showCursor() -} +import { initCommand } from './init.ts' +import { runCommand } from './run.ts' + +await new Command() + .name('wk') + .description('which-key like menu for shell.') + .version(VERSION.trim()) + .command('init', initCommand) + .command('run', runCommand) + .parse() diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 0000000..c4c9608 --- /dev/null +++ b/src/init.ts @@ -0,0 +1,19 @@ +import WIDGET_TEMPLATE from './widget.eta' with { type: 'text' } +import { Eta } from '@eta-dev/eta' +import { Command } from '@cliffy/command' + +export const initCommand = new Command() + .description('Render the widget for zsh.') + .option('--bindkey ', 'The key to bind the widget to.') + .example('eval "$(wk init)"', 'Register the widget without binding it to a key.') + .example(`eval "$(wk init --bindkey '^G')"`, 'Register the widget and bind it to Ctrl-G.') + .action(({ bindkey }) => { + const eta = new Eta() + + const rendered = eta.renderString(WIDGET_TEMPLATE, { + wk_path: Deno.execPath(), + bindkey, + }) + + console.log(rendered) + }) diff --git a/src/run.ts b/src/run.ts new file mode 100644 index 0000000..410e59a --- /dev/null +++ b/src/run.ts @@ -0,0 +1,111 @@ +import { Command, EnumType } from '@cliffy/command' +import { join as joinPath } from '@std/path' +import { parse as parseYaml } from '@std/yaml' +import { Binding } from './types/Binding.ts' +import { XDG_CONFIG_HOME } from './const.ts' +import { TUI } from './tui.ts' +import { defaultContext, mergeContext, PartialContext } from './types/Context.ts' +import { Dependencies, main } from './main.ts' +import { getKeySymbol, renderPrompt, renderTable } from './ui.ts' +import { AbortError, KeyParseError, UndefinedKeyError } from './errors.ts' + +async function loadYaml(path: string) { + const text = await Deno.readTextFile(path) + return parseYaml(text) as T +} + +export const runCommand = new Command() + .description('Run.') + .type('boolOrAuto', new EnumType(['true', 'false', 'auto'])) + .option('--up-one-line [VALUE:boolOrAuto]', 'Whether to move the input up one line.', { default: 'auto' }) + .action(async ({ upOneLine }) => { + const fetchContextWaiting = (async () => { + const found = await loadYaml(joinPath(XDG_CONFIG_HOME, 'wk', 'config.yaml')).catch(() => + undefined + ) + if (found === undefined) { + return defaultContext + } + return mergeContext(found) + })() + + const fetchBindingsWaiting = Promise.all([ + loadYaml(joinPath(XDG_CONFIG_HOME, 'wk', 'bindings.yaml')).catch(() => []).catch(() => []), + loadYaml(joinPath(Deno.cwd(), 'wk.bindings.yaml')).catch(() => []).catch(() => []), + ]).then(([globalBindings, localBindings]) => [...globalBindings, ...localBindings]) + + const tty = await Deno.open('/dev/tty', { read: true, write: true }) + const tui = new TUI(tty, tty) + + try { + tui.init(upOneLine === true ? true : upOneLine === 'true' ? true : upOneLine === 'false' ? false : 'auto') + + const [ctx, bindings] = await Promise.all([fetchContextWaiting, fetchBindingsWaiting]) + + let timeoutTimerId: number | undefined + const handleTimeout = () => { + tui.close() + Deno.exit(4) + } + + const deps: Dependencies = { + keypress: tui.keypress, + draw: (inputKeys, bindings) => tui.draw(renderPrompt(ctx, inputKeys), renderTable(ctx, bindings).toString()), + setTimeoutTimer: () => { + if (ctx.timeout > 0) { + timeoutTimerId = setTimeout(handleTimeout, ctx.timeout) + } + }, + clearTimeoutTimer: () => { + if (timeoutTimerId !== undefined) clearTimeout(timeoutTimerId) + }, + } + + const { + key: _, + desc: __, + icon: ___, + type: ____, + buffer, + delimiter: definedDelimiter, + ...rest + } = await main(deps, bindings) + + tui.close() + + const outputs = [buffer] + for (const [k, v] of Object.entries(rest)) { + switch (typeof v) { + case 'string': + outputs.push(`${k}:${v}`) + break + case 'boolean': + outputs.push(`${k}:${JSON.stringify(v)}`) + break + default: + break + } + } + + const delimiter = typeof definedDelimiter === 'string' ? definedDelimiter : ctx.outputDelimiter + + console.log(outputs.join(delimiter)) + } catch (e: unknown) { + if (e instanceof AbortError) { + tui.close() + Deno.exit(3) + } else if (e instanceof UndefinedKeyError) { + tui.close() + console.error(`"${e.getInputKeys().map((k) => getKeySymbol(defaultContext, k)).join(' ')}" is undefined`) + Deno.exit(5) + } else if (e instanceof KeyParseError) { + tui.close() + console.error('Failed to parse key', e.getKey()) + Deno.exit(6) + } else { + throw e + } + } finally { + tui.showCursor() + } + }) diff --git a/src/widget.eta b/src/widget.eta index 5d17092..0816424 100644 --- a/src/widget.eta +++ b/src/widget.eta @@ -3,7 +3,7 @@ _wk_widget() { zstyle -a ':wk:*' options opts || opts=() local res='' - res=$(<%= it.wk_path %> ${=opts} < $TTY 2>&1) + res=$(<%= it.wk_path %> run ${=opts} < $TTY 2>&1) local wk_exit=$? zle redisplay