@robota-sdk/agent-ui-terminal 3.0.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1204 -0
- package/LICENSE +661 -0
- package/README.md +50 -0
- package/dist/node/index.d.ts +1224 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +33 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +88 -0
- package/src/App.tsx +157 -0
- package/src/AppPresentation.tsx +190 -0
- package/src/AppView.tsx +26 -0
- package/src/BackgroundTaskPanel.tsx +73 -0
- package/src/CjkTextInput.tsx +336 -0
- package/src/ConfirmPrompt.tsx +140 -0
- package/src/ContextWarningBanner.tsx +45 -0
- package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
- package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
- package/src/HistorySearchOverlay.tsx +150 -0
- package/src/InputArea.tsx +338 -0
- package/src/ListPicker.tsx +158 -0
- package/src/MenuSelect.tsx +159 -0
- package/src/MessageList.tsx +308 -0
- package/src/MultiSelectList.tsx +294 -0
- package/src/PendingActionPrompt.tsx +121 -0
- package/src/PermissionPrompt.tsx +236 -0
- package/src/PluginTUI.tsx +258 -0
- package/src/RoleLabel.tsx +72 -0
- package/src/SafeText.tsx +66 -0
- package/src/SessionEventNotices.tsx +27 -0
- package/src/SessionPicker.tsx +92 -0
- package/src/SessionStatusBar.tsx +80 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +308 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +872 -0
- package/src/TextPrompt.tsx +112 -0
- package/src/ThemePicker.tsx +366 -0
- package/src/ToolCommandOutput.tsx +48 -0
- package/src/ToolDiffBlock.tsx +44 -0
- package/src/TransportTUI.tsx +166 -0
- package/src/TuiInteractionChannel.ts +530 -0
- package/src/UpdateNotice.tsx +18 -0
- package/src/UsageSummaryEntry.tsx +49 -0
- package/src/WaveText.tsx +70 -0
- package/src/__tests__/ListPicker.test.tsx +188 -0
- package/src/__tests__/MenuSelect.test.tsx +127 -0
- package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
- package/src/__tests__/PluginTUI.test.tsx +167 -0
- package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
- package/src/__tests__/SupervisedSessionView-render.test.tsx +52 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +1112 -0
- package/src/__tests__/TextPrompt.test.tsx +112 -0
- package/src/__tests__/TransportTUI.test.tsx +124 -0
- package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +294 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +650 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/__fixtures__/korean-text.json +17 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/__tests__/attached-app-session-switch.test.tsx +317 -0
- package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
- package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
- package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
- package/src/__tests__/background-task-panel.test.tsx +98 -0
- package/src/__tests__/background-task-row-format.test.ts +152 -0
- package/src/__tests__/channel-factory-integration.test.ts +150 -0
- package/src/__tests__/checkpoint-store-per-channel.test.ts +81 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +68 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +89 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +187 -0
- package/src/__tests__/cjk-text-input.test.ts +196 -0
- package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
- package/src/__tests__/command-output-summary.test.ts +95 -0
- package/src/__tests__/command-result-handler.test.ts +171 -0
- package/src/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/__tests__/confirm-permission-flow.test.ts +149 -0
- package/src/__tests__/confirm-prompt.test.tsx +103 -0
- package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
- package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +47 -0
- package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
- package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
- package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
- package/src/__tests__/external-prompt-notice.test.tsx +100 -0
- package/src/__tests__/fixtures/command-handoff-driver.tsx +114 -0
- package/src/__tests__/fixtures/fake-editor.sh +7 -0
- package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
- package/src/__tests__/fork-attach.test.tsx +235 -0
- package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
- package/src/__tests__/helpers/terminal-profiles.ts +152 -0
- package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
- package/src/__tests__/history-clear-broadcast.test.ts +66 -0
- package/src/__tests__/history-search-overlay.test.tsx +329 -0
- package/src/__tests__/humanize-tool-name.test.ts +30 -0
- package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
- package/src/__tests__/input-area-flow.test.ts +199 -0
- package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
- package/src/__tests__/interaction-channel-charter.test.ts +13 -0
- package/src/__tests__/key-hint-consistency.test.tsx +249 -0
- package/src/__tests__/key-hint-footer.test.tsx +69 -0
- package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
- package/src/__tests__/message-list-rendering.test.tsx +474 -0
- package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
- package/src/__tests__/output-style-reaches-session.test.ts +35 -0
- package/src/__tests__/palette-consistency.test.ts +329 -0
- package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
- package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
- package/src/__tests__/product-display-name.test.tsx +79 -0
- package/src/__tests__/prompt-queue.test.tsx +260 -0
- package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
- package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +350 -0
- package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
- package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
- package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
- package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
- package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +320 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +211 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +187 -0
- package/src/__tests__/pty/isolated-home.ts +97 -0
- package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
- package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
- package/src/__tests__/pty/pty-driver.ts +155 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +135 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +140 -0
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
- package/src/__tests__/pty/session-attach.ptytest.ts +182 -0
- package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
- package/src/__tests__/pty/spawn-pty.test.ts +120 -0
- package/src/__tests__/pty/spawn-pty.ts +229 -0
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
- package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
- package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
- package/src/__tests__/pty-driver-dispose.test.ts +43 -0
- package/src/__tests__/real-cursor-positioning.test.tsx +264 -0
- package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
- package/src/__tests__/render-channel-options.test.ts +201 -0
- package/src/__tests__/render-lifecycle.test.ts +36 -0
- package/src/__tests__/render-markdown.test.ts +164 -0
- package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
- package/src/__tests__/safe-text-boundary.test.tsx +75 -0
- package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
- package/src/__tests__/screen-reader-input.test.tsx +106 -0
- package/src/__tests__/screen-reader-labels.test.ts +91 -0
- package/src/__tests__/screen-reader-menus.test.tsx +200 -0
- package/src/__tests__/screen-reader-pacing.test.ts +230 -0
- package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
- package/src/__tests__/screen-reader-render-options.test.ts +262 -0
- package/src/__tests__/screen-reader-stdout.test.ts +322 -0
- package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
- package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
- package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
- package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
- package/src/__tests__/selection-flow.test.ts +61 -0
- package/src/__tests__/session-event-notices.test.tsx +31 -0
- package/src/__tests__/session-init-poller.test.ts +102 -0
- package/src/__tests__/session-picker.test.tsx +93 -0
- package/src/__tests__/session-switch-channel.test.tsx +470 -0
- package/src/__tests__/shutdown-signal.test.ts +34 -0
- package/src/__tests__/status-activity.test.ts +71 -0
- package/src/__tests__/status-bar.test.tsx +290 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +178 -0
- package/src/__tests__/terminal-capabilities.test.ts +153 -0
- package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
- package/src/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/__tests__/theme-picker.test.tsx +345 -0
- package/src/__tests__/transport-boundary.test.ts +11 -0
- package/src/__tests__/tui-app-channel-port.test.ts +31 -0
- package/src/__tests__/tui-channel-init-failure.test.ts +70 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +149 -0
- package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
- package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
- package/src/__tests__/tui-state-manager.test.ts +482 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/__tests__/wire-tui-channel.test.ts +1457 -0
- package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +140 -0
- package/src/attached-session-connection.ts +14 -0
- package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
- package/src/attention/__tests__/attention-tracker.test.ts +103 -0
- package/src/attention/__tests__/countdown.test.ts +15 -0
- package/src/attention/__tests__/focus-input-filter.test.ts +93 -0
- package/src/attention/__tests__/interval-recap.test.ts +49 -0
- package/src/attention/attention-coordinator.ts +71 -0
- package/src/attention/attention-tracker.ts +124 -0
- package/src/attention/countdown.ts +24 -0
- package/src/attention/focus-input-filter.ts +131 -0
- package/src/attention/interval-recap.ts +148 -0
- package/src/attention/time-units.ts +6 -0
- package/src/attention-bell.ts +79 -0
- package/src/attributed-user-echo.ts +30 -0
- package/src/background-task-row-format.ts +99 -0
- package/src/command-interaction.ts +8 -0
- package/src/command-output-summary.ts +129 -0
- package/src/create-default-tui-cli-adapter.ts +45 -0
- package/src/execution-workspace-switcher-selection.ts +118 -0
- package/src/execution-workspace-view-model.ts +139 -0
- package/src/external-prompt-notice.tsx +38 -0
- package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
- package/src/flows/__tests__/defer-submit.test.ts +88 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +139 -0
- package/src/flows/background-focus-flow.ts +44 -0
- package/src/flows/cjk-cursor-motion.ts +75 -0
- package/src/flows/cjk-text-deletion.ts +53 -0
- package/src/flows/cjk-text-input-flow.ts +248 -0
- package/src/flows/cjk-text-input-types.ts +24 -0
- package/src/flows/confirm-prompt-flow.ts +90 -0
- package/src/flows/defer-submit.ts +67 -0
- package/src/flows/fork-attach-flow.ts +62 -0
- package/src/flows/input-area-flow.ts +188 -0
- package/src/flows/permission-prompt-flow.ts +108 -0
- package/src/flows/real-cursor-flow.ts +83 -0
- package/src/flows/selection-flow.ts +205 -0
- package/src/flows/session-init-poller.ts +76 -0
- package/src/flows/text-prompt-flow.ts +93 -0
- package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
- package/src/history-search/history-search-flow.ts +94 -0
- package/src/history-search/useHistorySearch.ts +336 -0
- package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +87 -0
- package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
- package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
- package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
- package/src/hooks/command-result-handler.ts +44 -0
- package/src/hooks/side-effects-types.ts +45 -0
- package/src/hooks/useAppController.ts +271 -0
- package/src/hooks/useAppInputBindings.ts +163 -0
- package/src/hooks/useAppInteractionState.ts +84 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +90 -0
- package/src/hooks/useAppScreenState.ts +81 -0
- package/src/hooks/useAppSubmissionState.ts +45 -0
- package/src/hooks/useAppThemeState.ts +157 -0
- package/src/hooks/useAppWorkspaceState.ts +149 -0
- package/src/hooks/useAppearanceSettings.ts +30 -0
- package/src/hooks/useAutocomplete.ts +101 -0
- package/src/hooks/useBackgroundPanel.ts +58 -0
- package/src/hooks/useCountdownTick.ts +24 -0
- package/src/hooks/useExecutionDetailPage.ts +69 -0
- package/src/hooks/useForkAttach.ts +60 -0
- package/src/hooks/useInputAreaHistorySearch.ts +72 -0
- package/src/hooks/useInputAreaKeys.ts +114 -0
- package/src/hooks/useNumberedSelection.ts +93 -0
- package/src/hooks/usePluginCallbacks.ts +38 -0
- package/src/hooks/usePluginScreenData.ts +85 -0
- package/src/hooks/useRealCursorPosition.ts +125 -0
- package/src/hooks/useRenderMarkdown.ts +38 -0
- package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
- package/src/hooks/useSideEffects.ts +144 -0
- package/src/hooks/useStatusLineSettings.ts +50 -0
- package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
- package/src/hooks/useTuiChannel.ts +141 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +72 -0
- package/src/input-area-rules.tsx +71 -0
- package/src/json-value.ts +18 -0
- package/src/key-hint-footer.tsx +54 -0
- package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +118 -0
- package/src/keybindings/keybinding-catalogue.ts +171 -0
- package/src/keybindings/keybinding-registry.ts +303 -0
- package/src/keybindings/keybinding-syntax.ts +155 -0
- package/src/keybindings/keybindings-context.tsx +124 -0
- package/src/keybindings/node-keybindings-source.ts +189 -0
- package/src/numbered-list.tsx +113 -0
- package/src/plugin-tui-handlers.ts +163 -0
- package/src/product-display-name-context.tsx +30 -0
- package/src/render-markdown.ts +209 -0
- package/src/render.tsx +746 -0
- package/src/sanitize-terminal-text.ts +171 -0
- package/src/screen-reader-announcement.ts +52 -0
- package/src/screen-reader-context.tsx +32 -0
- package/src/screen-reader-labels.ts +64 -0
- package/src/screen-reader-pacing-context.tsx +47 -0
- package/src/screen-reader-pacing.ts +178 -0
- package/src/screen-reader-stdout.ts +328 -0
- package/src/short-session-id.ts +9 -0
- package/src/shutdown-signal.ts +34 -0
- package/src/slash-command-input.ts +11 -0
- package/src/status-activity.ts +70 -0
- package/src/status-glyph.ts +78 -0
- package/src/terminal-capabilities-context.tsx +27 -0
- package/src/terminal-capabilities.ts +77 -0
- package/src/terminal-focus-reporting.ts +50 -0
- package/src/terminal-handoff-controller.ts +98 -0
- package/src/terminal-marks.ts +63 -0
- package/src/theme/__tests__/built-in-themes.test.ts +117 -0
- package/src/theme/__tests__/color-vision.test.ts +82 -0
- package/src/theme/__tests__/theme-context.test.tsx +146 -0
- package/src/theme/__tests__/theme-document.test.ts +222 -0
- package/src/theme/__tests__/theme-registry.test.ts +138 -0
- package/src/theme/__tests__/theme-styles.test.ts +233 -0
- package/src/theme/built-in-themes.ts +342 -0
- package/src/theme/color-vision.ts +171 -0
- package/src/theme/index.ts +45 -0
- package/src/theme/theme-context.tsx +81 -0
- package/src/theme/theme-contracts.ts +139 -0
- package/src/theme/theme-document.ts +278 -0
- package/src/theme/theme-registry.ts +127 -0
- package/src/theme/theme-styles.ts +177 -0
- package/src/tool-summary-status.ts +59 -0
- package/src/tui-app-channel-port.ts +111 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +184 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +171 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +149 -0
- package/src/tui-session-events.ts +89 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +134 -0
- package/src/tui-state-manager.ts +347 -0
- package/src/types.ts +21 -0
- package/src/use-terminal-title.ts +29 -0
- package/src/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/utils/edit-diff.ts +151 -0
- package/src/utils/input-top-border.ts +19 -0
- package/src/utils/paste-labels.ts +9 -0
- package/src/utils/tool-call-extractor.ts +92 -0
- package/src/utils/tool-diff-summary.ts +75 -0
- package/src/waiting-loop-stop-notice.ts +23 -0
- package/src/wire-history-sync.ts +159 -0
- package/src/wire-tui-channel.ts +963 -0
- package/src/wire-tui-client-commands.ts +142 -0
- package/src/wire-tui-projection.ts +136 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { isJsonRecord } from '../json-value.js';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_KEYBINDINGS,
|
|
4
|
+
KEYBINDING_ACTIONS,
|
|
5
|
+
TEXT_ENTRY_CONTEXTS,
|
|
6
|
+
type TKeybindingAction,
|
|
7
|
+
type TKeybindingContext,
|
|
8
|
+
} from './keybinding-catalogue.js';
|
|
9
|
+
import {
|
|
10
|
+
displayBinding,
|
|
11
|
+
keybindingDeliverySignature,
|
|
12
|
+
keyStrokesFromInput,
|
|
13
|
+
normalizeBinding,
|
|
14
|
+
type IKeyInput,
|
|
15
|
+
} from './keybinding-syntax.js';
|
|
16
|
+
|
|
17
|
+
import type { TJsonValue } from '../json-value.js';
|
|
18
|
+
|
|
19
|
+
export { DEFAULT_KEYBINDINGS } from './keybinding-catalogue.js';
|
|
20
|
+
export type { IKeyInput } from './keybinding-syntax.js';
|
|
21
|
+
export type { TKeybindingAction, TKeybindingContext } from './keybinding-catalogue.js';
|
|
22
|
+
|
|
23
|
+
export interface IKeybindingWarning {
|
|
24
|
+
readonly code: 'multiplexer-conflict' | 'modifier-delivery';
|
|
25
|
+
readonly path: string;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IKeybindingDiagnostic {
|
|
30
|
+
readonly file: string;
|
|
31
|
+
readonly path: string;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IKeybindingSnapshot {
|
|
36
|
+
readonly generation: number;
|
|
37
|
+
readonly sourcePath: string;
|
|
38
|
+
readonly bindings: Readonly<
|
|
39
|
+
Record<TKeybindingContext, Readonly<Record<string, readonly string[]>>>
|
|
40
|
+
>;
|
|
41
|
+
readonly warnings: readonly IKeybindingWarning[];
|
|
42
|
+
readonly diagnostic?: IKeybindingDiagnostic;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type TParseKeybindingsResult =
|
|
46
|
+
| { readonly ok: true; readonly snapshot: IKeybindingSnapshot }
|
|
47
|
+
| { readonly ok: false; readonly diagnostic: IKeybindingDiagnostic };
|
|
48
|
+
|
|
49
|
+
const CHORD_TIMEOUT_MS = 1_000;
|
|
50
|
+
|
|
51
|
+
function diagnostic(file: string, path: string, message: string): TParseKeybindingsResult {
|
|
52
|
+
return { ok: false, diagnostic: { file, path, message } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cloneDefaults(): Record<TKeybindingContext, Record<string, string[]>> {
|
|
56
|
+
return Object.fromEntries(
|
|
57
|
+
Object.entries(DEFAULT_KEYBINDINGS).map(([context, actions]) => [
|
|
58
|
+
context,
|
|
59
|
+
Object.fromEntries(
|
|
60
|
+
Object.entries(actions).map(([action, bindings]) => [action, [...bindings]]),
|
|
61
|
+
),
|
|
62
|
+
]),
|
|
63
|
+
) as Record<TKeybindingContext, Record<string, string[]>>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function bindingValues(value: TJsonValue): readonly string[] | null | undefined {
|
|
67
|
+
if (value === null) return null;
|
|
68
|
+
if (typeof value === 'string') return [value];
|
|
69
|
+
if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'string')) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateContextBindings(
|
|
76
|
+
context: TKeybindingContext,
|
|
77
|
+
bindings: Record<string, string[]>,
|
|
78
|
+
file: string,
|
|
79
|
+
): TParseKeybindingsResult | undefined {
|
|
80
|
+
const owners = new Map<string, string>();
|
|
81
|
+
const prefixes = new Map<string, string>();
|
|
82
|
+
for (const [action, values] of Object.entries(bindings)) {
|
|
83
|
+
for (const binding of values) {
|
|
84
|
+
const deliveryBinding = keybindingDeliverySignature(binding);
|
|
85
|
+
const owner = owners.get(deliveryBinding);
|
|
86
|
+
const actionPath = `$.bindings.${context}.${action}`;
|
|
87
|
+
if (owner !== undefined && owner !== action) {
|
|
88
|
+
return diagnostic(file, actionPath, `Binding duplicates action ${owner}.`);
|
|
89
|
+
}
|
|
90
|
+
const strokes = deliveryBinding.split(' ');
|
|
91
|
+
if (strokes.length > 1 && TEXT_ENTRY_CONTEXTS.has(context) && isPrintable(strokes[0] ?? '')) {
|
|
92
|
+
return diagnostic(
|
|
93
|
+
file,
|
|
94
|
+
actionPath,
|
|
95
|
+
'Printable text cannot begin a chord in a text context.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
owners.set(deliveryBinding, action);
|
|
99
|
+
if (strokes.length > 1) prefixes.set(strokes[0] ?? '', action);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const [binding, action] of owners) {
|
|
103
|
+
const prefixOwner = prefixes.get(binding);
|
|
104
|
+
if (prefixOwner !== undefined) {
|
|
105
|
+
return diagnostic(
|
|
106
|
+
file,
|
|
107
|
+
`$.bindings.${context}.${prefixOwner}`,
|
|
108
|
+
`Chord prefix collides with action ${action}.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isPrintable(stroke: string): boolean {
|
|
116
|
+
return !stroke.includes('+') && [...stroke].length === 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function warningFor(binding: string, path: string): IKeybindingWarning[] {
|
|
120
|
+
const warnings: IKeybindingWarning[] = [];
|
|
121
|
+
if (binding.split(' ').includes('ctrl+b')) {
|
|
122
|
+
warnings.push({
|
|
123
|
+
code: 'multiplexer-conflict',
|
|
124
|
+
path,
|
|
125
|
+
message: 'Ctrl+B may be captured by a terminal multiplexer.',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (/ctrl\+shift\+[a-z]/u.test(binding)) {
|
|
129
|
+
warnings.push({
|
|
130
|
+
code: 'modifier-delivery',
|
|
131
|
+
path,
|
|
132
|
+
message: 'This terminal may not distinguish Ctrl+Shift from Ctrl.',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return warnings;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function parseKeybindingsDocument(text: string, file: string): TParseKeybindingsResult {
|
|
139
|
+
let value: TJsonValue;
|
|
140
|
+
try {
|
|
141
|
+
value = JSON.parse(text) as TJsonValue;
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
return diagnostic(file, '$', cause instanceof Error ? cause.message : 'Invalid JSON.');
|
|
144
|
+
}
|
|
145
|
+
if (!isJsonRecord(value)) return diagnostic(file, '$', 'Expected an object.');
|
|
146
|
+
if (value.version !== 1) return diagnostic(file, '$.version', 'Expected version 1.');
|
|
147
|
+
if (!isJsonRecord(value.bindings)) return diagnostic(file, '$.bindings', 'Expected an object.');
|
|
148
|
+
|
|
149
|
+
const effective = cloneDefaults();
|
|
150
|
+
const warnings: IKeybindingWarning[] = [];
|
|
151
|
+
for (const [rawContext, rawActions] of Object.entries(value.bindings)) {
|
|
152
|
+
if (!(rawContext in KEYBINDING_ACTIONS)) {
|
|
153
|
+
return diagnostic(file, `$.bindings.${rawContext}`, 'Unknown keybinding context.');
|
|
154
|
+
}
|
|
155
|
+
const context = rawContext as TKeybindingContext;
|
|
156
|
+
if (!isJsonRecord(rawActions)) {
|
|
157
|
+
return diagnostic(file, `$.bindings.${context}`, 'Expected an action object.');
|
|
158
|
+
}
|
|
159
|
+
for (const [action, rawBindings] of Object.entries(rawActions)) {
|
|
160
|
+
if (!(KEYBINDING_ACTIONS[context] as readonly string[]).includes(action)) {
|
|
161
|
+
return diagnostic(file, `$.bindings.${context}.${action}`, 'Unknown action.');
|
|
162
|
+
}
|
|
163
|
+
const values = bindingValues(rawBindings);
|
|
164
|
+
const actionPath = `$.bindings.${context}.${action}`;
|
|
165
|
+
if (values === undefined) {
|
|
166
|
+
return diagnostic(file, actionPath, 'Expected a binding, binding list, or null.');
|
|
167
|
+
}
|
|
168
|
+
if (values === null) {
|
|
169
|
+
effective[context][action] = [];
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const normalized: string[] = [];
|
|
173
|
+
for (const rawBinding of values) {
|
|
174
|
+
const binding = normalizeBinding(rawBinding);
|
|
175
|
+
if (binding === undefined) return diagnostic(file, actionPath, 'Invalid binding syntax.');
|
|
176
|
+
if (binding.split(' ').includes('ctrl+c')) {
|
|
177
|
+
return diagnostic(file, actionPath, 'Ctrl+C is reserved for two-stage shutdown.');
|
|
178
|
+
}
|
|
179
|
+
normalized.push(binding);
|
|
180
|
+
warnings.push(...warningFor(binding, actionPath));
|
|
181
|
+
}
|
|
182
|
+
effective[context][action] = normalized;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const context of Object.keys(effective) as TKeybindingContext[]) {
|
|
186
|
+
const invalid = validateContextBindings(context, effective[context], file);
|
|
187
|
+
if (invalid !== undefined) return invalid;
|
|
188
|
+
}
|
|
189
|
+
const bindings = Object.freeze(
|
|
190
|
+
Object.fromEntries(
|
|
191
|
+
Object.entries(effective).map(([context, actions]) => [
|
|
192
|
+
context,
|
|
193
|
+
Object.freeze(
|
|
194
|
+
Object.fromEntries(
|
|
195
|
+
Object.entries(actions).map(([action, bindings]) => [action, Object.freeze(bindings)]),
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
]),
|
|
199
|
+
),
|
|
200
|
+
) as IKeybindingSnapshot['bindings'];
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
snapshot: Object.freeze({
|
|
204
|
+
generation: 0,
|
|
205
|
+
sourcePath: file,
|
|
206
|
+
bindings,
|
|
207
|
+
warnings: Object.freeze(warnings),
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface IChordState {
|
|
213
|
+
readonly context?: TKeybindingContext;
|
|
214
|
+
readonly generation?: number;
|
|
215
|
+
readonly strokes: readonly string[];
|
|
216
|
+
readonly deadline?: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function createEmptyChordState(): IChordState {
|
|
220
|
+
return { strokes: [] };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function actionBindings(
|
|
224
|
+
snapshot: IKeybindingSnapshot,
|
|
225
|
+
context: TKeybindingContext,
|
|
226
|
+
): [TKeybindingAction, readonly string[]][] {
|
|
227
|
+
return Object.entries(snapshot.bindings[context]) as [TKeybindingAction, readonly string[]][];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function resolveKeybindingInput(options: {
|
|
231
|
+
readonly snapshot: IKeybindingSnapshot;
|
|
232
|
+
readonly state: IChordState;
|
|
233
|
+
readonly context: TKeybindingContext;
|
|
234
|
+
readonly input: string;
|
|
235
|
+
readonly key: IKeyInput;
|
|
236
|
+
readonly now: number;
|
|
237
|
+
}): {
|
|
238
|
+
readonly state: IChordState;
|
|
239
|
+
readonly actions: readonly TKeybindingAction[];
|
|
240
|
+
readonly consumed: boolean;
|
|
241
|
+
} {
|
|
242
|
+
const strokes = keyStrokesFromInput(options.input, options.key);
|
|
243
|
+
if (strokes.length === 0) {
|
|
244
|
+
return { state: createEmptyChordState(), actions: [], consumed: false };
|
|
245
|
+
}
|
|
246
|
+
const stateValid =
|
|
247
|
+
options.state.context === options.context &&
|
|
248
|
+
options.state.generation === options.snapshot.generation &&
|
|
249
|
+
(options.state.deadline === undefined || options.now <= options.state.deadline) &&
|
|
250
|
+
!options.key.escape;
|
|
251
|
+
const prior = stateValid ? options.state.strokes : [];
|
|
252
|
+
const entries = actionBindings(options.snapshot, options.context);
|
|
253
|
+
for (const stroke of strokes) {
|
|
254
|
+
const sequence = [...prior, stroke].join(' ');
|
|
255
|
+
const exact = entries
|
|
256
|
+
.filter(([, bindings]) => bindings.includes(sequence))
|
|
257
|
+
.map(([action]) => action);
|
|
258
|
+
if (exact.length > 0) {
|
|
259
|
+
return { state: createEmptyChordState(), actions: exact, consumed: true };
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
entries.some(([, bindings]) => bindings.some((binding) => binding.startsWith(`${sequence} `)))
|
|
263
|
+
) {
|
|
264
|
+
return {
|
|
265
|
+
state: {
|
|
266
|
+
context: options.context,
|
|
267
|
+
generation: options.snapshot.generation,
|
|
268
|
+
strokes: [...prior, stroke],
|
|
269
|
+
deadline: options.now + CHORD_TIMEOUT_MS,
|
|
270
|
+
},
|
|
271
|
+
actions: [],
|
|
272
|
+
consumed: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (prior.length > 0) {
|
|
277
|
+
return resolveKeybindingInput({ ...options, state: createEmptyChordState() });
|
|
278
|
+
}
|
|
279
|
+
return { state: createEmptyChordState(), actions: [], consumed: false };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function keybindingHints<TContext extends TKeybindingContext>(
|
|
283
|
+
snapshot: IKeybindingSnapshot,
|
|
284
|
+
context: TContext,
|
|
285
|
+
actions: readonly (readonly [
|
|
286
|
+
TKeybindingAction<TContext> | readonly TKeybindingAction<TContext>[],
|
|
287
|
+
string,
|
|
288
|
+
])[],
|
|
289
|
+
): { keys: string; label: string }[] {
|
|
290
|
+
return actions.flatMap(([actionOrActions, label]) => {
|
|
291
|
+
const grouped = Array.isArray(actionOrActions);
|
|
292
|
+
const actionList = grouped ? actionOrActions : [actionOrActions];
|
|
293
|
+
const bindings = actionList.flatMap((action) => {
|
|
294
|
+
const values = snapshot.bindings[context][action] ?? [];
|
|
295
|
+
return grouped ? values.slice(0, 1) : values;
|
|
296
|
+
});
|
|
297
|
+
const displayed = bindings.map(displayBinding);
|
|
298
|
+
const keys = displayed.every((binding) => /^[↑↓←→]$/u.test(binding))
|
|
299
|
+
? displayed.join('')
|
|
300
|
+
: displayed.join('/');
|
|
301
|
+
return bindings.length === 0 ? [] : [{ keys, label }];
|
|
302
|
+
});
|
|
303
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export interface IKeyInput {
|
|
2
|
+
readonly ctrl?: boolean;
|
|
3
|
+
readonly shift?: boolean;
|
|
4
|
+
readonly meta?: boolean;
|
|
5
|
+
readonly return?: boolean;
|
|
6
|
+
readonly escape?: boolean;
|
|
7
|
+
readonly tab?: boolean;
|
|
8
|
+
readonly upArrow?: boolean;
|
|
9
|
+
readonly downArrow?: boolean;
|
|
10
|
+
readonly leftArrow?: boolean;
|
|
11
|
+
readonly rightArrow?: boolean;
|
|
12
|
+
readonly backspace?: boolean;
|
|
13
|
+
readonly delete?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MODIFIER_ALIASES: Readonly<Record<string, 'ctrl' | 'alt' | 'meta' | 'shift'>> = {
|
|
17
|
+
ctrl: 'ctrl',
|
|
18
|
+
control: 'ctrl',
|
|
19
|
+
alt: 'alt',
|
|
20
|
+
option: 'alt',
|
|
21
|
+
meta: 'meta',
|
|
22
|
+
cmd: 'meta',
|
|
23
|
+
command: 'meta',
|
|
24
|
+
shift: 'shift',
|
|
25
|
+
};
|
|
26
|
+
const MODIFIER_ORDER = ['ctrl', 'alt', 'meta', 'shift'] as const;
|
|
27
|
+
const KEY_ALIASES: Readonly<Record<string, string>> = {
|
|
28
|
+
return: 'enter',
|
|
29
|
+
enter: 'enter',
|
|
30
|
+
esc: 'escape',
|
|
31
|
+
escape: 'escape',
|
|
32
|
+
tab: 'tab',
|
|
33
|
+
space: 'space',
|
|
34
|
+
spacebar: 'space',
|
|
35
|
+
up: 'up',
|
|
36
|
+
down: 'down',
|
|
37
|
+
left: 'left',
|
|
38
|
+
right: 'right',
|
|
39
|
+
backspace: 'backspace',
|
|
40
|
+
delete: 'delete',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function normalizeBinding(raw: string): string | undefined {
|
|
44
|
+
const strokes = raw.trim().split(/\s+/u);
|
|
45
|
+
if (strokes.length < 1 || strokes.length > 2) return undefined;
|
|
46
|
+
const normalized = strokes.map(normalizeStroke);
|
|
47
|
+
return normalized.every((stroke): stroke is string => stroke !== undefined)
|
|
48
|
+
? normalized.join(' ')
|
|
49
|
+
: undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeStroke(raw: string): string | undefined {
|
|
53
|
+
if (!raw || raw.includes(' ')) return undefined;
|
|
54
|
+
const parts = raw.split('+');
|
|
55
|
+
if (parts.some((part) => part.length === 0)) return undefined;
|
|
56
|
+
const rawKey = parts.at(-1);
|
|
57
|
+
if (rawKey === undefined) return undefined;
|
|
58
|
+
const modifiers = new Set<'ctrl' | 'alt' | 'meta' | 'shift'>();
|
|
59
|
+
for (const rawModifier of parts.slice(0, -1)) {
|
|
60
|
+
const modifier = MODIFIER_ALIASES[rawModifier.toLowerCase()];
|
|
61
|
+
if (modifier === undefined || modifiers.has(modifier)) return undefined;
|
|
62
|
+
modifiers.add(modifier);
|
|
63
|
+
}
|
|
64
|
+
let key = KEY_ALIASES[rawKey.toLowerCase()];
|
|
65
|
+
if (key === undefined) {
|
|
66
|
+
if ([...rawKey].length !== 1 || /\s/u.test(rawKey)) return undefined;
|
|
67
|
+
key = rawKey.toLowerCase();
|
|
68
|
+
if (parts.length === 1 && rawKey !== key) modifiers.add('shift');
|
|
69
|
+
}
|
|
70
|
+
return [...MODIFIER_ORDER.filter((modifier) => modifiers.has(modifier)), key].join('+');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function keyStrokeFromInput(input: string, key: IKeyInput): string | undefined {
|
|
74
|
+
// Ink preserves LF as a one-character input string instead of setting `key.return`. In terminal
|
|
75
|
+
// raw mode that byte is Ctrl+J; normalize it before printable-text handling can treat it as paste.
|
|
76
|
+
if (/^[\r\n]*\n[\r\n]*$/u.test(input)) return 'ctrl+j';
|
|
77
|
+
const special = key.return
|
|
78
|
+
? 'enter'
|
|
79
|
+
: key.escape
|
|
80
|
+
? 'escape'
|
|
81
|
+
: key.tab
|
|
82
|
+
? 'tab'
|
|
83
|
+
: key.upArrow
|
|
84
|
+
? 'up'
|
|
85
|
+
: key.downArrow
|
|
86
|
+
? 'down'
|
|
87
|
+
: key.leftArrow
|
|
88
|
+
? 'left'
|
|
89
|
+
: key.rightArrow
|
|
90
|
+
? 'right'
|
|
91
|
+
: key.backspace
|
|
92
|
+
? 'backspace'
|
|
93
|
+
: key.delete
|
|
94
|
+
? 'delete'
|
|
95
|
+
: input === ' '
|
|
96
|
+
? 'space'
|
|
97
|
+
: [...input].length === 1
|
|
98
|
+
? input.toLowerCase()
|
|
99
|
+
: undefined;
|
|
100
|
+
if (special === undefined) return undefined;
|
|
101
|
+
const modifiers = [
|
|
102
|
+
...(key.ctrl ? ['ctrl'] : []),
|
|
103
|
+
...(key.meta ? ['meta'] : []),
|
|
104
|
+
...(key.shift ? ['shift'] : []),
|
|
105
|
+
];
|
|
106
|
+
return [...modifiers, special].join('+');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Terminal protocols collapse several control characters onto named keys. Keep the configured
|
|
111
|
+
* spelling for hints, but offer every spelling that the same received byte can represent.
|
|
112
|
+
*/
|
|
113
|
+
export function keyStrokesFromInput(input: string, key: IKeyInput): readonly string[] {
|
|
114
|
+
const primary = keyStrokeFromInput(input, key);
|
|
115
|
+
if (primary === undefined) return [];
|
|
116
|
+
if (primary === 'enter') return ['enter', 'ctrl+m'];
|
|
117
|
+
if (primary === 'tab') return ['tab', 'ctrl+i'];
|
|
118
|
+
if (primary === 'escape') return ['escape', 'ctrl+['];
|
|
119
|
+
return [primary];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function keybindingDeliverySignature(binding: string): string {
|
|
123
|
+
return binding
|
|
124
|
+
.split(' ')
|
|
125
|
+
.map((stroke) => {
|
|
126
|
+
if (stroke === 'ctrl+m') return 'enter';
|
|
127
|
+
if (stroke === 'ctrl+i') return 'tab';
|
|
128
|
+
if (stroke === 'ctrl+[') return 'escape';
|
|
129
|
+
return stroke;
|
|
130
|
+
})
|
|
131
|
+
.join(' ');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function displayBinding(binding: string): string {
|
|
135
|
+
const displayStroke = (stroke: string): string =>
|
|
136
|
+
stroke
|
|
137
|
+
.split('+')
|
|
138
|
+
.map(
|
|
139
|
+
(part) =>
|
|
140
|
+
({
|
|
141
|
+
ctrl: 'Ctrl',
|
|
142
|
+
alt: 'Alt',
|
|
143
|
+
meta: 'Meta',
|
|
144
|
+
shift: 'Shift',
|
|
145
|
+
escape: 'Esc',
|
|
146
|
+
up: '↑',
|
|
147
|
+
down: '↓',
|
|
148
|
+
left: '←',
|
|
149
|
+
right: '→',
|
|
150
|
+
})[part] ??
|
|
151
|
+
(part.length === 1 ? part.toUpperCase() : `${part[0]?.toUpperCase()}${part.slice(1)}`),
|
|
152
|
+
)
|
|
153
|
+
.join('+');
|
|
154
|
+
return binding.split(' ').map(displayStroke).join(' ');
|
|
155
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useInput } from 'ink';
|
|
2
|
+
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createEmptyChordState,
|
|
6
|
+
keybindingHints,
|
|
7
|
+
parseKeybindingsDocument,
|
|
8
|
+
resolveKeybindingInput,
|
|
9
|
+
type IChordState,
|
|
10
|
+
type IKeybindingSnapshot,
|
|
11
|
+
type IKeyInput,
|
|
12
|
+
type TKeybindingAction,
|
|
13
|
+
type TKeybindingContext,
|
|
14
|
+
} from './keybinding-registry.js';
|
|
15
|
+
|
|
16
|
+
import type { IKeybindingsSource } from './node-keybindings-source.js';
|
|
17
|
+
|
|
18
|
+
const parsedDefault = parseKeybindingsDocument('{"version":1,"bindings":{}}', '<defaults>');
|
|
19
|
+
if (!parsedDefault.ok) throw new Error(parsedDefault.diagnostic.message);
|
|
20
|
+
const DEFAULT_SNAPSHOT = parsedDefault.snapshot;
|
|
21
|
+
|
|
22
|
+
const KeybindingsContext = createContext<IKeybindingSnapshot>(DEFAULT_SNAPSHOT);
|
|
23
|
+
|
|
24
|
+
export function KeybindingsProvider({
|
|
25
|
+
source,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
source?: IKeybindingsSource;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
}): React.ReactElement {
|
|
31
|
+
const [snapshot, setSnapshot] = useState(() => source?.getSnapshot() ?? DEFAULT_SNAPSHOT);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (source === undefined) return;
|
|
34
|
+
setSnapshot(source.getSnapshot());
|
|
35
|
+
return source.subscribe(setSnapshot);
|
|
36
|
+
}, [source]);
|
|
37
|
+
return <KeybindingsContext.Provider value={snapshot}>{children}</KeybindingsContext.Provider>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useKeybindings(): IKeybindingSnapshot {
|
|
41
|
+
return useContext(KeybindingsContext);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useKeybindingHints<TContext extends TKeybindingContext>(
|
|
45
|
+
context: TContext,
|
|
46
|
+
actions: readonly (readonly [
|
|
47
|
+
TKeybindingAction<TContext> | readonly TKeybindingAction<TContext>[],
|
|
48
|
+
string,
|
|
49
|
+
])[],
|
|
50
|
+
): { keys: string; label: string }[] {
|
|
51
|
+
return keybindingHints(useKeybindings(), context, actions);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useKeybindingActions<
|
|
55
|
+
TContext extends TKeybindingContext,
|
|
56
|
+
TFallback extends TKeybindingContext = TContext,
|
|
57
|
+
>(
|
|
58
|
+
context: TContext,
|
|
59
|
+
handler: (
|
|
60
|
+
actions: readonly (TKeybindingAction<TContext> | TKeybindingAction<TFallback>)[],
|
|
61
|
+
input: string,
|
|
62
|
+
key: IKeyInput,
|
|
63
|
+
consumed: boolean,
|
|
64
|
+
resolvedContext: TContext | TFallback,
|
|
65
|
+
) => void,
|
|
66
|
+
options: {
|
|
67
|
+
readonly isActive?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* A key `context` does not bind is resolved again here, so a surface layered over an editor (the
|
|
70
|
+
* autocomplete menu over the chat input) leaves every key it does not use to the editor's bindings.
|
|
71
|
+
*/
|
|
72
|
+
readonly fallbackContext?: TFallback;
|
|
73
|
+
} = {},
|
|
74
|
+
): void {
|
|
75
|
+
const snapshot = useKeybindings();
|
|
76
|
+
const stateRef = useRef<IChordState>(createEmptyChordState());
|
|
77
|
+
const fallbackStateRef = useRef<IChordState>(createEmptyChordState());
|
|
78
|
+
const { fallbackContext } = options;
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
stateRef.current = createEmptyChordState();
|
|
81
|
+
fallbackStateRef.current = createEmptyChordState();
|
|
82
|
+
}, [context, fallbackContext, snapshot.generation]);
|
|
83
|
+
useInput(
|
|
84
|
+
(input, key) => {
|
|
85
|
+
const result = resolveKeybindingInput({
|
|
86
|
+
snapshot,
|
|
87
|
+
state: stateRef.current,
|
|
88
|
+
context,
|
|
89
|
+
input,
|
|
90
|
+
key,
|
|
91
|
+
now: Date.now(),
|
|
92
|
+
});
|
|
93
|
+
stateRef.current = result.state;
|
|
94
|
+
if (fallbackContext === undefined || result.consumed) {
|
|
95
|
+
fallbackStateRef.current = createEmptyChordState();
|
|
96
|
+
handler(
|
|
97
|
+
result.actions as readonly TKeybindingAction<TContext>[],
|
|
98
|
+
input,
|
|
99
|
+
key,
|
|
100
|
+
result.consumed,
|
|
101
|
+
context,
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const fallback = resolveKeybindingInput({
|
|
106
|
+
snapshot,
|
|
107
|
+
state: fallbackStateRef.current,
|
|
108
|
+
context: fallbackContext,
|
|
109
|
+
input,
|
|
110
|
+
key,
|
|
111
|
+
now: Date.now(),
|
|
112
|
+
});
|
|
113
|
+
fallbackStateRef.current = fallback.state;
|
|
114
|
+
handler(
|
|
115
|
+
fallback.actions as readonly TKeybindingAction<TFallback>[],
|
|
116
|
+
input,
|
|
117
|
+
key,
|
|
118
|
+
fallback.consumed,
|
|
119
|
+
fallbackContext,
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
{ isActive: options.isActive ?? true },
|
|
123
|
+
);
|
|
124
|
+
}
|