Files
nanobot/tui/src/command-menu.ts
T

79 lines
2.3 KiB
TypeScript
Raw Normal View History

2026-08-13 13:46:53 +09:00
import { type BoxRenderable, type CliRenderer } from "@opentui/core"
2026-08-13 12:55:26 +09:00
import type { SlashCommand } from "./protocol"
2026-08-13 13:46:53 +09:00
import { PickerMenu, type PickerMenuTheme } from "./picker-menu"
2026-08-13 12:55:26 +09:00
2026-08-13 13:46:53 +09:00
export type CommandMenuTheme = PickerMenuTheme
2026-08-13 12:55:26 +09:00
/** Retained slash-command discovery with one small completion interface. */
export class CommandMenu {
readonly root: BoxRenderable
2026-08-13 13:46:53 +09:00
private readonly picker: PickerMenu<SlashCommand>
2026-08-13 12:55:26 +09:00
private commands: SlashCommand[] = []
private query = ""
constructor(
2026-08-13 13:46:53 +09:00
renderer: CliRenderer,
theme: CommandMenuTheme,
2026-08-13 12:55:26 +09:00
) {
2026-08-13 13:46:53 +09:00
this.picker = new PickerMenu(renderer, theme, {
2026-08-13 12:55:26 +09:00
id: "nanobot-tui-command-menu",
2026-08-13 13:46:53 +09:00
searchText: (command) => `${command.command} ${command.title}`,
render: (command) => {
const hint = command.argHint ? ` ${command.argHint}` : ""
const detail = (command.description || command.title).replace(/\s+/gu, " ")
return `${command.command}${hint} ${detail}`
},
2026-08-13 12:55:26 +09:00
})
2026-08-13 13:46:53 +09:00
this.root = this.picker.root
2026-08-13 12:55:26 +09:00
}
get visible(): boolean {
2026-08-13 13:46:53 +09:00
return this.picker.visible
2026-08-13 12:55:26 +09:00
}
setCommands(commands: SlashCommand[]): void {
this.commands = [...commands].sort((left, right) => left.command.localeCompare(right.command))
this.update(this.query)
}
update(input: string, limit = 6): void {
const changed = input !== this.query
this.query = input
const token = /^\/[^\s]*$/u.test(input) ? input.toLocaleLowerCase() : ""
if (!token) {
this.hide()
return
}
2026-08-13 13:46:53 +09:00
if (changed || !this.picker.visible) this.picker.show(this.commands, token.slice(1), limit)
else this.picker.update(token.slice(1), limit)
2026-08-13 12:55:26 +09:00
}
move(direction: -1 | 1): boolean {
2026-08-13 13:46:53 +09:00
return this.picker.move(direction)
2026-08-13 12:55:26 +09:00
}
completion(input: string): string | null {
if (!this.visible) return null
2026-08-13 13:46:53 +09:00
const command = this.picker.current()
2026-08-13 12:55:26 +09:00
if (!command || input.trim() === command.command) return null
return `${command.command}${command.acceptsArgs ? " " : ""}`
}
complete(): string | null {
if (!this.visible) return null
2026-08-13 13:46:53 +09:00
const command = this.picker.current()
2026-08-13 12:55:26 +09:00
if (!command) return null
this.hide()
return `${command.command}${command.acceptsArgs ? " " : ""}`
}
hide(): void {
2026-08-13 13:46:53 +09:00
this.picker.hide()
2026-08-13 12:55:26 +09:00
}
setTheme(theme: CommandMenuTheme): void {
2026-08-13 13:46:53 +09:00
this.picker.setTheme(theme)
2026-08-13 12:55:26 +09:00
}
}