Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

149 changes: 11 additions & 138 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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<T>(path: string) {
const text = await Deno.readTextFile(path)
return parseYaml(text) as T
}

const fetchContextWaiting = (async () => {
const found = await loadYaml<PartialContext>(joinPath(XDG_CONFIG_HOME, 'wk', 'config.yaml')).catch(() => undefined)
if (found === undefined) {
return defaultContext
}
return mergeContext(found)
})()

const fetchBindingsWaiting = Promise.all([
loadYaml<Binding[]>(joinPath(XDG_CONFIG_HOME, 'wk', 'bindings.yaml')).catch(() => []).catch(() => []),
loadYaml<Binding[]>(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()
19 changes: 19 additions & 0 deletions src/init.ts
Original file line number Diff line number Diff line change
@@ -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 <KEY:string>', '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)
})
111 changes: 111 additions & 0 deletions src/run.ts
Original file line number Diff line number Diff line change
@@ -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<T>(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<PartialContext>(joinPath(XDG_CONFIG_HOME, 'wk', 'config.yaml')).catch(() =>
undefined
)
if (found === undefined) {
return defaultContext
}
return mergeContext(found)
})()

const fetchBindingsWaiting = Promise.all([
loadYaml<Binding[]>(joinPath(XDG_CONFIG_HOME, 'wk', 'bindings.yaml')).catch(() => []).catch(() => []),
loadYaml<Binding[]>(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()
}
})
2 changes: 1 addition & 1 deletion src/widget.eta
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down