|
| 1 | +import type { RegisteredBinding } from "../../keybindings/index.js"; |
| 2 | +import type { |
| 3 | + BadgeProps, |
| 4 | + CalloutProps, |
| 5 | + EmptyProps, |
| 6 | + ErrorBoundaryProps, |
| 7 | + ErrorDisplayProps, |
| 8 | + GaugeProps, |
| 9 | + IconProps, |
| 10 | + KbdProps, |
| 11 | + LinkProps, |
| 12 | + ProgressProps, |
| 13 | + RichTextProps, |
| 14 | + RichTextSpan, |
| 15 | + SkeletonProps, |
| 16 | + SpinnerProps, |
| 17 | + StatusProps, |
| 18 | + TagProps, |
| 19 | + VNode, |
| 20 | +} from "../types.js"; |
| 21 | +import { column, divider, row, text } from "./basic.js"; |
| 22 | +import type { KeybindingHelpOptions } from "./helpers.js"; |
| 23 | + |
| 24 | +export function icon(iconPath: string, props: Omit<IconProps, "icon"> = {}): VNode { |
| 25 | + return { kind: "icon", props: { icon: iconPath, ...props } }; |
| 26 | +} |
| 27 | + |
| 28 | +export function spinner(props: SpinnerProps = {}): VNode { |
| 29 | + return { kind: "spinner", props }; |
| 30 | +} |
| 31 | + |
| 32 | +export function progress(value: number, props: Omit<ProgressProps, "value"> = {}): VNode { |
| 33 | + return { kind: "progress", props: { value, ...props } }; |
| 34 | +} |
| 35 | + |
| 36 | +export function skeleton(width: number, props: Omit<SkeletonProps, "width"> = {}): VNode { |
| 37 | + return { kind: "skeleton", props: { width, ...props } }; |
| 38 | +} |
| 39 | + |
| 40 | +export function richText( |
| 41 | + spans: readonly RichTextSpan[], |
| 42 | + props: Omit<RichTextProps, "spans"> = {}, |
| 43 | +): VNode { |
| 44 | + return { kind: "richText", props: { spans, ...props } }; |
| 45 | +} |
| 46 | + |
| 47 | +export function kbd(keys: string | readonly string[], props: Omit<KbdProps, "keys"> = {}): VNode { |
| 48 | + return { kind: "kbd", props: { keys, ...props } }; |
| 49 | +} |
| 50 | + |
| 51 | +function keybindingSequenceToKbdInput(sequence: string): string | readonly string[] { |
| 52 | + const normalized = sequence.trim(); |
| 53 | + if (normalized.length === 0) return ""; |
| 54 | + const parts = normalized.split(/\s+/); |
| 55 | + if (parts.length <= 1) return normalized; |
| 56 | + return Object.freeze(parts); |
| 57 | +} |
| 58 | + |
| 59 | +function keybindingComparator(a: RegisteredBinding, b: RegisteredBinding): number { |
| 60 | + const byMode = a.mode.localeCompare(b.mode); |
| 61 | + if (byMode !== 0) return byMode; |
| 62 | + return a.sequence.localeCompare(b.sequence); |
| 63 | +} |
| 64 | + |
| 65 | +export function keybindingHelp( |
| 66 | + bindings: readonly RegisteredBinding[], |
| 67 | + options: KeybindingHelpOptions = {}, |
| 68 | +): VNode { |
| 69 | + const title = options.title ?? "Keyboard Shortcuts"; |
| 70 | + const emptyText = options.emptyText ?? "No shortcuts registered."; |
| 71 | + const showMode = |
| 72 | + options.showMode ?? |
| 73 | + (() => { |
| 74 | + const firstMode = bindings[0]?.mode; |
| 75 | + if (firstMode === undefined) return false; |
| 76 | + return bindings.some((binding) => binding.mode !== firstMode); |
| 77 | + })(); |
| 78 | + |
| 79 | + const rows = options.sort === false ? [...bindings] : [...bindings].sort(keybindingComparator); |
| 80 | + |
| 81 | + return column( |
| 82 | + { |
| 83 | + gap: 1, |
| 84 | + ...(options.key === undefined ? {} : { key: options.key }), |
| 85 | + }, |
| 86 | + [ |
| 87 | + text(title, { style: { bold: true } }), |
| 88 | + divider({ char: "·" }), |
| 89 | + rows.length > 0 |
| 90 | + ? column( |
| 91 | + { gap: 0 }, |
| 92 | + rows.map((binding, i) => { |
| 93 | + const keyTokens = keybindingSequenceToKbdInput(binding.sequence); |
| 94 | + const sequenceNode = Array.isArray(keyTokens) |
| 95 | + ? kbd(keyTokens, { separator: " " }) |
| 96 | + : kbd(keyTokens); |
| 97 | + const descriptionNode = |
| 98 | + binding.description === undefined |
| 99 | + ? text("No description", { dim: true }) |
| 100 | + : text(binding.description); |
| 101 | + return row( |
| 102 | + { |
| 103 | + key: `keybinding-help-${binding.mode}-${binding.sequence}-${String(i)}`, |
| 104 | + gap: 1, |
| 105 | + items: "center", |
| 106 | + wrap: true, |
| 107 | + }, |
| 108 | + [ |
| 109 | + sequenceNode, |
| 110 | + showMode ? text(`[${binding.mode}]`, { dim: true }) : null, |
| 111 | + descriptionNode, |
| 112 | + ], |
| 113 | + ); |
| 114 | + }), |
| 115 | + ) |
| 116 | + : text(emptyText, { dim: true }), |
| 117 | + ], |
| 118 | + ); |
| 119 | +} |
| 120 | + |
| 121 | +export function badge(textValue: string, props: Omit<BadgeProps, "text"> = {}): VNode { |
| 122 | + return { kind: "badge", props: { text: textValue, ...props } }; |
| 123 | +} |
| 124 | + |
| 125 | +export function status( |
| 126 | + statusValue: StatusProps["status"], |
| 127 | + props: Omit<StatusProps, "status"> = {}, |
| 128 | +): VNode { |
| 129 | + return { kind: "status", props: { status: statusValue, ...props } }; |
| 130 | +} |
| 131 | + |
| 132 | +export function tag(textValue: string, props: Omit<TagProps, "text"> = {}): VNode { |
| 133 | + return { kind: "tag", props: { text: textValue, ...props } }; |
| 134 | +} |
| 135 | + |
| 136 | +export function gauge(value: number, props: Omit<GaugeProps, "value"> = {}): VNode { |
| 137 | + return { kind: "gauge", props: { value, ...props } }; |
| 138 | +} |
| 139 | + |
| 140 | +export function empty(title: string, props: Omit<EmptyProps, "title"> = {}): VNode { |
| 141 | + return { kind: "empty", props: { title, ...props } }; |
| 142 | +} |
| 143 | + |
| 144 | +export function errorDisplay( |
| 145 | + message: string, |
| 146 | + props: Omit<ErrorDisplayProps, "message"> = {}, |
| 147 | +): VNode { |
| 148 | + return { kind: "errorDisplay", props: { message, ...props } }; |
| 149 | +} |
| 150 | + |
| 151 | +export function errorBoundary(props: ErrorBoundaryProps): VNode { |
| 152 | + return { kind: "errorBoundary", props }; |
| 153 | +} |
| 154 | + |
| 155 | +export function callout(message: string, props: Omit<CalloutProps, "message"> = {}): VNode { |
| 156 | + return { kind: "callout", props: { message, ...props } }; |
| 157 | +} |
| 158 | + |
| 159 | +export function link(props: LinkProps): VNode { |
| 160 | + return { kind: "link", props }; |
| 161 | +} |
0 commit comments