nastech-tui 0.0.1
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/.prettierrc +11 -0
- package/README.md +346 -0
- package/eslint.config.mjs +111 -0
- package/package.json +51 -0
- package/packages/nastech-ink/ambient.d.ts +83 -0
- package/packages/nastech-ink/index.d.ts +40 -0
- package/packages/nastech-ink/index.js +1 -0
- package/packages/nastech-ink/nastech-ink/ambient.d.ts +83 -0
- package/packages/nastech-ink/nastech-ink/index.d.ts +40 -0
- package/packages/nastech-ink/nastech-ink/index.js +1 -0
- package/packages/nastech-ink/nastech-ink/package.json +54 -0
- package/packages/nastech-ink/nastech-ink/src/bootstrap/state.ts +9 -0
- package/packages/nastech-ink/nastech-ink/src/entry-exports.ts +32 -0
- package/packages/nastech-ink/nastech-ink/src/hooks/use-stderr.ts +15 -0
- package/packages/nastech-ink/nastech-ink/src/hooks/use-stdout.ts +15 -0
- package/packages/nastech-ink/nastech-ink/src/ink/Ansi.tsx +435 -0
- package/packages/nastech-ink/nastech-ink/src/ink/app-mouse.test.ts +123 -0
- package/packages/nastech-ink/nastech-ink/src/ink/bidi.ts +145 -0
- package/packages/nastech-ink/nastech-ink/src/ink/cache-eviction.ts +45 -0
- package/packages/nastech-ink/nastech-ink/src/ink/clearTerminal.ts +68 -0
- package/packages/nastech-ink/nastech-ink/src/ink/colorize.test.ts +60 -0
- package/packages/nastech-ink/nastech-ink/src/ink/colorize.ts +277 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/App.tsx +830 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/AppContext.ts +20 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Box.tsx +294 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Button.tsx +236 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Link.tsx +38 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Newline.tsx +43 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Spacer.tsx +23 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/StdinContext.ts +25 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Text.test.ts +38 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Text.tsx +336 -0
- package/packages/nastech-ink/nastech-ink/src/ink/constants.ts +6 -0
- package/packages/nastech-ink/nastech-ink/src/ink/cursor.ts +5 -0
- package/packages/nastech-ink/nastech-ink/src/ink/devtools.ts +2 -0
- package/packages/nastech-ink/nastech-ink/src/ink/dom.ts +495 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/click-event.ts +38 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/dispatcher.ts +242 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/emitter.ts +40 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/event-handlers.ts +84 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/event.ts +11 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/focus-event.ts +18 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/input-event.ts +176 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/mouse-event.ts +18 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/paste-event.ts +10 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/resize-event.ts +12 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-event.ts +107 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
- package/packages/nastech-ink/nastech-ink/src/ink/focus.ts +219 -0
- package/packages/nastech-ink/nastech-ink/src/ink/frame.ts +124 -0
- package/packages/nastech-ink/nastech-ink/src/ink/get-max-width.ts +27 -0
- package/packages/nastech-ink/nastech-ink/src/ink/global.d.ts +1 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hit-test.test.ts +38 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hit-test.ts +224 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-app.ts +9 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-input.ts +95 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
- package/packages/nastech-ink/package.json +57 -0
- package/packages/nastech-ink/src/bootstrap/state.ts +9 -0
- package/packages/nastech-ink/src/entry-exports.ts +32 -0
- package/packages/nastech-ink/src/hooks/use-stderr.ts +15 -0
- package/packages/nastech-ink/src/hooks/use-stdout.ts +15 -0
- package/packages/nastech-ink/src/ink/Ansi.tsx +435 -0
- package/packages/nastech-ink/src/ink/app-mouse.test.ts +123 -0
- package/packages/nastech-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/packages/nastech-ink/src/ink/bidi.ts +145 -0
- package/packages/nastech-ink/src/ink/cache-eviction.ts +45 -0
- package/packages/nastech-ink/src/ink/clearTerminal.ts +68 -0
- package/packages/nastech-ink/src/ink/colorize.test.ts +60 -0
- package/packages/nastech-ink/src/ink/colorize.ts +277 -0
- package/packages/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
- package/packages/nastech-ink/src/ink/components/App.tsx +855 -0
- package/packages/nastech-ink/src/ink/components/AppContext.ts +20 -0
- package/packages/nastech-ink/src/ink/components/Box.tsx +294 -0
- package/packages/nastech-ink/src/ink/components/Button.tsx +236 -0
- package/packages/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
- package/packages/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
- package/packages/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
- package/packages/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
- package/packages/nastech-ink/src/ink/components/Link.tsx +38 -0
- package/packages/nastech-ink/src/ink/components/Newline.tsx +43 -0
- package/packages/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
- package/packages/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
- package/packages/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
- package/packages/nastech-ink/src/ink/components/Spacer.tsx +23 -0
- package/packages/nastech-ink/src/ink/components/StdinContext.ts +25 -0
- package/packages/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
- package/packages/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
- package/packages/nastech-ink/src/ink/components/Text.test.ts +38 -0
- package/packages/nastech-ink/src/ink/components/Text.tsx +336 -0
- package/packages/nastech-ink/src/ink/constants.ts +6 -0
- package/packages/nastech-ink/src/ink/cursor.ts +5 -0
- package/packages/nastech-ink/src/ink/devtools.ts +2 -0
- package/packages/nastech-ink/src/ink/dom.ts +495 -0
- package/packages/nastech-ink/src/ink/events/click-event.ts +38 -0
- package/packages/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
- package/packages/nastech-ink/src/ink/events/dispatcher.ts +242 -0
- package/packages/nastech-ink/src/ink/events/emitter.ts +40 -0
- package/packages/nastech-ink/src/ink/events/event-handlers.ts +84 -0
- package/packages/nastech-ink/src/ink/events/event.ts +11 -0
- package/packages/nastech-ink/src/ink/events/focus-event.ts +18 -0
- package/packages/nastech-ink/src/ink/events/input-event.ts +176 -0
- package/packages/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
- package/packages/nastech-ink/src/ink/events/mouse-event.ts +18 -0
- package/packages/nastech-ink/src/ink/events/paste-event.ts +10 -0
- package/packages/nastech-ink/src/ink/events/resize-event.ts +12 -0
- package/packages/nastech-ink/src/ink/events/terminal-event.ts +107 -0
- package/packages/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
- package/packages/nastech-ink/src/ink/focus.ts +219 -0
- package/packages/nastech-ink/src/ink/frame.ts +124 -0
- package/packages/nastech-ink/src/ink/get-max-width.ts +27 -0
- package/packages/nastech-ink/src/ink/global.d.ts +1 -0
- package/packages/nastech-ink/src/ink/hit-test.test.ts +38 -0
- package/packages/nastech-ink/src/ink/hit-test.ts +224 -0
- package/packages/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
- package/packages/nastech-ink/src/ink/hooks/use-app.ts +9 -0
- package/packages/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
- package/packages/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
- package/packages/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
- package/packages/nastech-ink/src/ink/hooks/use-input.ts +95 -0
- package/packages/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
- package/packages/nastech-ink/src/ink/hooks/use-search-highlight.ts +56 -0
- package/packages/nastech-ink/src/ink/hooks/use-selection.ts +101 -0
- package/packages/nastech-ink/src/ink/hooks/use-stdin.ts +9 -0
- package/packages/nastech-ink/src/ink/hooks/use-tab-status.ts +71 -0
- package/packages/nastech-ink/src/ink/hooks/use-terminal-focus.ts +18 -0
- package/packages/nastech-ink/src/ink/hooks/use-terminal-title.ts +34 -0
- package/packages/nastech-ink/src/ink/hooks/use-terminal-viewport.ts +100 -0
- package/packages/nastech-ink/src/ink/hyperlinkHover.ts +52 -0
- package/packages/nastech-ink/src/ink/ink-cursor-advance.test.ts +234 -0
- package/packages/nastech-ink/src/ink/ink-resize.test.ts +50 -0
- package/packages/nastech-ink/src/ink/ink.tsx +2705 -0
- package/packages/nastech-ink/src/ink/instances.ts +10 -0
- package/packages/nastech-ink/src/ink/layout/engine.ts +6 -0
- package/packages/nastech-ink/src/ink/layout/geometry.ts +98 -0
- package/packages/nastech-ink/src/ink/layout/node.ts +145 -0
- package/packages/nastech-ink/src/ink/layout/yoga.ts +313 -0
- package/packages/nastech-ink/src/ink/line-width-cache.ts +38 -0
- package/packages/nastech-ink/src/ink/log-update.test.ts +223 -0
- package/packages/nastech-ink/src/ink/log-update.ts +752 -0
- package/packages/nastech-ink/src/ink/lru.ts +14 -0
- package/packages/nastech-ink/src/ink/measure-element.ts +23 -0
- package/packages/nastech-ink/src/ink/measure-text.ts +50 -0
- package/packages/nastech-ink/src/ink/node-cache.ts +53 -0
- package/packages/nastech-ink/src/ink/optimizer.ts +99 -0
- package/packages/nastech-ink/src/ink/output.ts +845 -0
- package/packages/nastech-ink/src/ink/parse-keypress.test.ts +133 -0
- package/packages/nastech-ink/src/ink/parse-keypress.ts +848 -0
- package/packages/nastech-ink/src/ink/reconciler.ts +382 -0
- package/packages/nastech-ink/src/ink/render-border.ts +206 -0
- package/packages/nastech-ink/src/ink/render-node-to-output.ts +1582 -0
- package/packages/nastech-ink/src/ink/render-to-screen.ts +236 -0
- package/packages/nastech-ink/src/ink/renderer.ts +169 -0
- package/packages/nastech-ink/src/ink/root.ts +204 -0
- package/packages/nastech-ink/src/ink/screen.ts +1590 -0
- package/packages/nastech-ink/src/ink/searchHighlight.ts +91 -0
- package/packages/nastech-ink/src/ink/selection.test.ts +82 -0
- package/packages/nastech-ink/src/ink/selection.ts +1143 -0
- package/packages/nastech-ink/src/ink/squash-text-nodes.ts +74 -0
- package/packages/nastech-ink/src/ink/stringWidth.ts +341 -0
- package/packages/nastech-ink/src/ink/styles.ts +750 -0
- package/packages/nastech-ink/src/ink/supports-hyperlinks.ts +51 -0
- package/packages/nastech-ink/src/ink/tabstops.ts +44 -0
- package/packages/nastech-ink/src/ink/terminal-focus-state.ts +52 -0
- package/packages/nastech-ink/src/ink/terminal-querier.ts +222 -0
- package/packages/nastech-ink/src/ink/terminal.test.ts +15 -0
- package/packages/nastech-ink/src/ink/terminal.ts +299 -0
- package/packages/nastech-ink/src/ink/termio/ansi.ts +75 -0
- package/packages/nastech-ink/src/ink/termio/csi.ts +334 -0
- package/packages/nastech-ink/src/ink/termio/dec.ts +99 -0
- package/packages/nastech-ink/src/ink/termio/esc.ts +69 -0
- package/packages/nastech-ink/src/ink/termio/osc.test.ts +191 -0
- package/packages/nastech-ink/src/ink/termio/osc.ts +724 -0
- package/packages/nastech-ink/src/ink/termio/parser.ts +467 -0
- package/packages/nastech-ink/src/ink/termio/sgr.ts +362 -0
- package/packages/nastech-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/packages/nastech-ink/src/ink/termio/tokenize.ts +350 -0
- package/packages/nastech-ink/src/ink/termio/types.ts +230 -0
- package/packages/nastech-ink/src/ink/termio.ts +42 -0
- package/packages/nastech-ink/src/ink/useTerminalNotification.ts +110 -0
- package/packages/nastech-ink/src/ink/warn.ts +15 -0
- package/packages/nastech-ink/src/ink/widest-line.ts +22 -0
- package/packages/nastech-ink/src/ink/wrap-text.test.ts +17 -0
- package/packages/nastech-ink/src/ink/wrap-text.ts +144 -0
- package/packages/nastech-ink/src/ink/wrapAnsi.ts +13 -0
- package/packages/nastech-ink/src/native-ts/yoga-layout/enums.ts +112 -0
- package/packages/nastech-ink/src/native-ts/yoga-layout/index.ts +2326 -0
- package/packages/nastech-ink/src/utils/debug.ts +6 -0
- package/packages/nastech-ink/src/utils/earlyInput.ts +131 -0
- package/packages/nastech-ink/src/utils/env.ts +66 -0
- package/packages/nastech-ink/src/utils/envUtils.ts +13 -0
- package/packages/nastech-ink/src/utils/execFileNoThrow.test.ts +146 -0
- package/packages/nastech-ink/src/utils/execFileNoThrow.ts +115 -0
- package/packages/nastech-ink/src/utils/fullscreen.ts +3 -0
- package/packages/nastech-ink/src/utils/intl.ts +87 -0
- package/packages/nastech-ink/src/utils/log.ts +7 -0
- package/packages/nastech-ink/src/utils/semver.ts +57 -0
- package/packages/nastech-ink/src/utils/sliceAnsi.ts +106 -0
- package/packages/nastech-ink/text-input.d.ts +2 -0
- package/packages/nastech-ink/text-input.js +1 -0
- package/scripts/build.mjs +61 -0
- package/scripts/profile-tui.mjs +121 -0
- package/src/__tests__/activeSessionSwitcher.test.ts +157 -0
- package/src/__tests__/appChromeStatusRule.test.tsx +84 -0
- package/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/src/__tests__/approvalAction.test.ts +50 -0
- package/src/__tests__/asCommandDispatch.test.ts +27 -0
- package/src/__tests__/blockLayout.test.ts +122 -0
- package/src/__tests__/clipboard.test.ts +369 -0
- package/src/__tests__/constants.test.ts +53 -0
- package/src/__tests__/createGatewayEventHandler.test.ts +1091 -0
- package/src/__tests__/createSlashHandler.test.ts +822 -0
- package/src/__tests__/creditsCommand.test.ts +144 -0
- package/src/__tests__/cursorDriftRegression.test.ts +114 -0
- package/src/__tests__/details.test.ts +115 -0
- package/src/__tests__/emoji.test.ts +64 -0
- package/src/__tests__/externalLink.test.ts +144 -0
- package/src/__tests__/forceTruecolor.test.ts +191 -0
- package/src/__tests__/gatewayClient.test.ts +394 -0
- package/src/__tests__/gatewayRecovery.test.ts +47 -0
- package/src/__tests__/markdown.test.ts +331 -0
- package/src/__tests__/mathUnicode.test.ts +293 -0
- package/src/__tests__/memoryMonitor.test.ts +102 -0
- package/src/__tests__/messageLine.test.ts +19 -0
- package/src/__tests__/messages.test.ts +92 -0
- package/src/__tests__/orchestratorPromptSession.test.ts +64 -0
- package/src/__tests__/osc52.test.ts +67 -0
- package/src/__tests__/parentLog.test.ts +75 -0
- package/src/__tests__/paths.test.ts +70 -0
- package/src/__tests__/platform.test.ts +556 -0
- package/src/__tests__/precisionWheel.test.ts +44 -0
- package/src/__tests__/prompt.test.ts +31 -0
- package/src/__tests__/providers.test.ts +65 -0
- package/src/__tests__/reasoning.test.ts +76 -0
- package/src/__tests__/rpc.test.ts +27 -0
- package/src/__tests__/scroll.test.ts +99 -0
- package/src/__tests__/slashParity.test.ts +123 -0
- package/src/__tests__/spawnHistoryStore.test.ts +46 -0
- package/src/__tests__/stateIsolation.test.ts +46 -0
- package/src/__tests__/statusBarTicker.test.ts +18 -0
- package/src/__tests__/statusRule.test.ts +32 -0
- package/src/__tests__/streamingMarkdown.test.ts +121 -0
- package/src/__tests__/subagentTree.test.ts +407 -0
- package/src/__tests__/syntax.test.ts +45 -0
- package/src/__tests__/terminalModes.test.ts +39 -0
- package/src/__tests__/terminalParity.test.ts +77 -0
- package/src/__tests__/terminalSetup.test.ts +386 -0
- package/src/__tests__/termux.test.ts +35 -0
- package/src/__tests__/termuxComposerLayout.test.ts +40 -0
- package/src/__tests__/text.test.ts +233 -0
- package/src/__tests__/textInputBurstInput.test.ts +40 -0
- package/src/__tests__/textInputCursorSourceOfTruth.test.ts +50 -0
- package/src/__tests__/textInputFastEcho.test.ts +200 -0
- package/src/__tests__/textInputLineNav.test.ts +55 -0
- package/src/__tests__/textInputPassThrough.test.ts +59 -0
- package/src/__tests__/textInputRightClick.test.ts +48 -0
- package/src/__tests__/textInputWrap.test.ts +151 -0
- package/src/__tests__/theme.test.ts +311 -0
- package/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/src/__tests__/turnStore.test.ts +66 -0
- package/src/__tests__/useCompletion.test.ts +35 -0
- package/src/__tests__/useComposerState.test.ts +59 -0
- package/src/__tests__/useConfigSync.test.ts +460 -0
- package/src/__tests__/useInputHandlers.test.ts +77 -0
- package/src/__tests__/useQueue.test.ts +28 -0
- package/src/__tests__/useSessionLifecycle.test.ts +60 -0
- package/src/__tests__/useVirtualHistoryHeights.test.ts +39 -0
- package/src/__tests__/viewport.test.ts +58 -0
- package/src/__tests__/viewportStore.test.ts +85 -0
- package/src/__tests__/virtualHeights.test.ts +96 -0
- package/src/__tests__/virtualHistoryClamp.test.ts +19 -0
- package/src/__tests__/virtualHistoryOffsetCache.test.ts +282 -0
- package/src/__tests__/wheelAccel.test.ts +138 -0
- package/src/app/createGatewayEventHandler.ts +833 -0
- package/src/app/createSlashHandler.ts +130 -0
- package/src/app/delegationStore.ts +77 -0
- package/src/app/gatewayContext.tsx +19 -0
- package/src/app/gatewayRecovery.ts +35 -0
- package/src/app/inputSelectionStore.ts +15 -0
- package/src/app/interfaces.ts +394 -0
- package/src/app/overlayStore.ts +53 -0
- package/src/app/scroll.ts +71 -0
- package/src/app/setupHandoff.ts +54 -0
- package/src/app/slash/commands/core.ts +648 -0
- package/src/app/slash/commands/credits.ts +57 -0
- package/src/app/slash/commands/debug.ts +48 -0
- package/src/app/slash/commands/ops.ts +717 -0
- package/src/app/slash/commands/session.ts +554 -0
- package/src/app/slash/commands/setup.ts +20 -0
- package/src/app/slash/registry.ts +20 -0
- package/src/app/slash/types.ts +21 -0
- package/src/app/spawnHistoryStore.ts +159 -0
- package/src/app/turnController.ts +866 -0
- package/src/app/turnStore.ts +85 -0
- package/src/app/uiStore.ts +44 -0
- package/src/app/useComposerState.ts +367 -0
- package/src/app/useConfigSync.ts +288 -0
- package/src/app/useInputHandlers.ts +576 -0
- package/src/app/useLongRunToolCharms.ts +69 -0
- package/src/app/useMainApp.ts +1039 -0
- package/src/app/useSessionLifecycle.ts +366 -0
- package/src/app/useSubmission.ts +429 -0
- package/src/app.tsx +25 -0
- package/src/banner.ts +93 -0
- package/src/components/activeSessionSwitcher.tsx +635 -0
- package/src/components/agentsOverlay.tsx +1073 -0
- package/src/components/appChrome.tsx +554 -0
- package/src/components/appLayout.tsx +444 -0
- package/src/components/appOverlays.tsx +254 -0
- package/src/components/branding.tsx +466 -0
- package/src/components/fpsOverlay.tsx +30 -0
- package/src/components/helpHint.tsx +73 -0
- package/src/components/markdown.tsx +1119 -0
- package/src/components/maskedPrompt.tsx +34 -0
- package/src/components/messageLine.tsx +237 -0
- package/src/components/modelPicker.tsx +527 -0
- package/src/components/overlayControls.tsx +50 -0
- package/src/components/pluginsHub.tsx +238 -0
- package/src/components/prompts.tsx +276 -0
- package/src/components/queuedMessages.tsx +64 -0
- package/src/components/sessionPicker.tsx +227 -0
- package/src/components/skillsHub.tsx +308 -0
- package/src/components/streamingAssistant.tsx +110 -0
- package/src/components/streamingMarkdown.tsx +174 -0
- package/src/components/textInput.tsx +1340 -0
- package/src/components/themed.tsx +30 -0
- package/src/components/thinking.tsx +1224 -0
- package/src/components/todoPanel.tsx +93 -0
- package/src/config/env.ts +64 -0
- package/src/config/limits.ts +13 -0
- package/src/config/timing.ts +6 -0
- package/src/content/charms.ts +1 -0
- package/src/content/faces.ts +17 -0
- package/src/content/fortunes.ts +30 -0
- package/src/content/hotkeys.ts +37 -0
- package/src/content/placeholders.ts +13 -0
- package/src/content/setup.ts +17 -0
- package/src/content/verbs.ts +38 -0
- package/src/domain/blockLayout.ts +146 -0
- package/src/domain/details.ts +76 -0
- package/src/domain/messages.ts +91 -0
- package/src/domain/paths.ts +16 -0
- package/src/domain/providers.ts +11 -0
- package/src/domain/roles.ts +9 -0
- package/src/domain/slash.ts +10 -0
- package/src/domain/usage.ts +3 -0
- package/src/domain/viewport.ts +51 -0
- package/src/entry.tsx +104 -0
- package/src/gatewayClient.ts +730 -0
- package/src/gatewayTypes.ts +568 -0
- package/src/hooks/useCompletion.ts +112 -0
- package/src/hooks/useGitBranch.ts +72 -0
- package/src/hooks/useInputHistory.ts +11 -0
- package/src/hooks/useQueue.ts +76 -0
- package/src/hooks/useVirtualHistory.ts +554 -0
- package/src/lib/circularBuffer.ts +48 -0
- package/src/lib/clipboard.ts +182 -0
- package/src/lib/editor.test.ts +74 -0
- package/src/lib/editor.ts +47 -0
- package/src/lib/emoji.ts +55 -0
- package/src/lib/externalCli.ts +16 -0
- package/src/lib/externalLink.ts +435 -0
- package/src/lib/forceTruecolor.ts +60 -0
- package/src/lib/fpsStore.ts +51 -0
- package/src/lib/fuzzy.test.ts +109 -0
- package/src/lib/fuzzy.ts +177 -0
- package/src/lib/gracefulExit.ts +47 -0
- package/src/lib/history.ts +82 -0
- package/src/lib/inputMetrics.ts +203 -0
- package/src/lib/liveProgress.test.ts +116 -0
- package/src/lib/liveProgress.ts +79 -0
- package/src/lib/mathUnicode.ts +770 -0
- package/src/lib/memory.test.ts +155 -0
- package/src/lib/memory.ts +188 -0
- package/src/lib/memoryMonitor.ts +109 -0
- package/src/lib/messages.test.ts +29 -0
- package/src/lib/messages.ts +8 -0
- package/src/lib/openExternalUrl.test.ts +217 -0
- package/src/lib/openExternalUrl.ts +158 -0
- package/src/lib/osc52.ts +73 -0
- package/src/lib/parentLog.ts +57 -0
- package/src/lib/perfPane.tsx +107 -0
- package/src/lib/platform.ts +409 -0
- package/src/lib/precisionWheel.ts +48 -0
- package/src/lib/prompt.ts +35 -0
- package/src/lib/reasoning.ts +55 -0
- package/src/lib/rpc.ts +41 -0
- package/src/lib/subagentTree.ts +355 -0
- package/src/lib/syntax.ts +117 -0
- package/src/lib/terminalModes.ts +51 -0
- package/src/lib/terminalParity.ts +78 -0
- package/src/lib/terminalSetup.ts +444 -0
- package/src/lib/termux.ts +29 -0
- package/src/lib/text.test.ts +18 -0
- package/src/lib/text.ts +339 -0
- package/src/lib/todo.test.ts +21 -0
- package/src/lib/todo.ts +9 -0
- package/src/lib/viewportStore.ts +124 -0
- package/src/lib/virtualHeights.ts +145 -0
- package/src/lib/wheelAccel.ts +190 -0
- package/src/protocol/interpolation.ts +3 -0
- package/src/protocol/paste.ts +1 -0
- package/src/theme.ts +589 -0
- package/src/types/nastech-ink.d.ts +176 -0
- package/src/types.ts +212 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
import type { InputEvent, Key } from '@nastechai/ink'
|
|
2
|
+
import * as Ink from '@nastechai/ink'
|
|
3
|
+
import { type MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { setInputSelection } from '../app/inputSelectionStore.js'
|
|
6
|
+
import { readClipboardText, writeClipboardText } from '../lib/clipboard.js'
|
|
7
|
+
import { cursorLayout, offsetFromPosition } from '../lib/inputMetrics.js'
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_VOICE_RECORD_KEY,
|
|
10
|
+
isActionMod,
|
|
11
|
+
isMac,
|
|
12
|
+
isMacActionFallback,
|
|
13
|
+
isVoiceToggleKey,
|
|
14
|
+
type ParsedVoiceRecordKey
|
|
15
|
+
} from '../lib/platform.js'
|
|
16
|
+
import { isTermuxTuiMode } from '../lib/termux.js'
|
|
17
|
+
|
|
18
|
+
type InkExt = typeof Ink & {
|
|
19
|
+
stringWidth: (s: string) => number
|
|
20
|
+
useCursorAdvance: () => (dx: number, dy?: number) => void
|
|
21
|
+
useDeclaredCursor: (a: { line: number; column: number; active: boolean }) => (el: any) => void
|
|
22
|
+
useStdout: () => { stdout?: NodeJS.WriteStream }
|
|
23
|
+
useTerminalFocus: () => boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ink = Ink as unknown as InkExt
|
|
27
|
+
const { Box, Text, useStdin, useInput, useStdout, stringWidth, useCursorAdvance, useDeclaredCursor, useTerminalFocus } = ink
|
|
28
|
+
|
|
29
|
+
const ESC = '\x1b'
|
|
30
|
+
const INV = `${ESC}[7m`
|
|
31
|
+
const INV_OFF = `${ESC}[27m`
|
|
32
|
+
const DIM = `${ESC}[2m`
|
|
33
|
+
const DIM_OFF = `${ESC}[22m`
|
|
34
|
+
const FWD_DEL_RE = new RegExp(`${ESC}\\[3(?:[~$^]|;)`)
|
|
35
|
+
const PRINTABLE = /^[ -~\u00a0-\uffff]+$/
|
|
36
|
+
const BRACKET_PASTE = new RegExp(`${ESC}?\\[20[01]~`, 'g')
|
|
37
|
+
const FRAME_BATCH_MS = 16
|
|
38
|
+
const MULTI_CLICK_MS = 500
|
|
39
|
+
type MinimalEnv = Record<string, string | undefined>
|
|
40
|
+
|
|
41
|
+
const invert = (s: string) => INV + s + INV_OFF
|
|
42
|
+
const dim = (s: string) => DIM + s + DIM_OFF
|
|
43
|
+
|
|
44
|
+
let _seg: Intl.Segmenter | null = null
|
|
45
|
+
const seg = () => (_seg ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' }))
|
|
46
|
+
const STOP_CACHE_MAX = 32
|
|
47
|
+
const stopCache = new Map<string, number[]>()
|
|
48
|
+
|
|
49
|
+
function graphemeStops(s: string) {
|
|
50
|
+
const hit = stopCache.get(s)
|
|
51
|
+
|
|
52
|
+
if (hit) {
|
|
53
|
+
return hit
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stops = [0]
|
|
57
|
+
|
|
58
|
+
for (const { index } of seg().segment(s)) {
|
|
59
|
+
if (index > 0) {
|
|
60
|
+
stops.push(index)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (stops.at(-1) !== s.length) {
|
|
65
|
+
stops.push(s.length)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
stopCache.set(s, stops)
|
|
69
|
+
|
|
70
|
+
if (stopCache.size > STOP_CACHE_MAX) {
|
|
71
|
+
const oldest = stopCache.keys().next().value
|
|
72
|
+
|
|
73
|
+
if (oldest !== undefined) {
|
|
74
|
+
stopCache.delete(oldest)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return stops
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function snapPos(s: string, p: number) {
|
|
82
|
+
const pos = Math.max(0, Math.min(p, s.length))
|
|
83
|
+
let last = 0
|
|
84
|
+
|
|
85
|
+
for (const stop of graphemeStops(s)) {
|
|
86
|
+
if (stop > pos) {
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
last = stop
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return last
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface TextInsertResult {
|
|
97
|
+
cursor: number
|
|
98
|
+
value: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function applyPrintableInsert(
|
|
102
|
+
value: string,
|
|
103
|
+
cursor: number,
|
|
104
|
+
text: string,
|
|
105
|
+
range?: { end: number; start: number } | null
|
|
106
|
+
): null | TextInsertResult {
|
|
107
|
+
if (!PRINTABLE.test(text)) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (range) {
|
|
112
|
+
return {
|
|
113
|
+
cursor: range.start + text.length,
|
|
114
|
+
value: value.slice(0, range.start) + text + value.slice(range.end)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
cursor: cursor + text.length,
|
|
120
|
+
value: value.slice(0, cursor) + text + value.slice(cursor)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const shouldRouteMultiCharInputAsPaste = (text: string): boolean => text.includes('\n')
|
|
125
|
+
|
|
126
|
+
export function shouldPreserveCtrlJNewline(env: MinimalEnv = process.env): boolean {
|
|
127
|
+
if (env.WT_SESSION) {
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) {
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (env.GHOSTTY_RESOURCES_DIR || env.GHOSTTY_BIN_DIR) {
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if ((env.TERM ?? '').toLowerCase() === 'xterm-ghostty') {
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if ((env.TERM_PROGRAM ?? '').toLowerCase() === 'ghostty') {
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (env.WSL_DISTRO_NAME ?? '').toLowerCase().includes('microsoft')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function prevPos(s: string, p: number) {
|
|
151
|
+
const pos = snapPos(s, p)
|
|
152
|
+
let prev = 0
|
|
153
|
+
|
|
154
|
+
for (const stop of graphemeStops(s)) {
|
|
155
|
+
if (stop >= pos) {
|
|
156
|
+
return prev
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
prev = stop
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return prev
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function nextPos(s: string, p: number) {
|
|
166
|
+
const pos = snapPos(s, p)
|
|
167
|
+
|
|
168
|
+
for (const stop of graphemeStops(s)) {
|
|
169
|
+
if (stop > pos) {
|
|
170
|
+
return stop
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return s.length
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function wordLeft(s: string, p: number) {
|
|
178
|
+
let i = snapPos(s, p) - 1
|
|
179
|
+
|
|
180
|
+
while (i > 0 && /\s/.test(s[i]!)) {
|
|
181
|
+
i--
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
while (i > 0 && !/\s/.test(s[i - 1]!)) {
|
|
185
|
+
i--
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return Math.max(0, i)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function wordRight(s: string, p: number) {
|
|
192
|
+
let i = snapPos(s, p)
|
|
193
|
+
|
|
194
|
+
while (i < s.length && !/\s/.test(s[i]!)) {
|
|
195
|
+
i++
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
while (i < s.length && /\s/.test(s[i]!)) {
|
|
199
|
+
i++
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return i
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Move cursor one logical line up or down inside `s` while preserving the
|
|
207
|
+
* column offset from the current line's start. Returns `null` when the cursor
|
|
208
|
+
* is already on the first line (up) or last line (down) — callers use that
|
|
209
|
+
* signal to fall through to history cycling instead of eating the arrow key.
|
|
210
|
+
*/
|
|
211
|
+
export function lineNav(s: string, p: number, dir: -1 | 1): null | number {
|
|
212
|
+
const pos = snapPos(s, p)
|
|
213
|
+
const curStart = s.lastIndexOf('\n', pos - 1) + 1
|
|
214
|
+
const col = pos - curStart
|
|
215
|
+
|
|
216
|
+
if (dir < 0) {
|
|
217
|
+
if (curStart === 0) {
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const prevStart = s.lastIndexOf('\n', curStart - 2) + 1
|
|
222
|
+
|
|
223
|
+
return snapPos(s, Math.min(prevStart + col, curStart - 1))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const nextBreak = s.indexOf('\n', pos)
|
|
227
|
+
|
|
228
|
+
if (nextBreak < 0) {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const nextEnd = s.indexOf('\n', nextBreak + 1)
|
|
233
|
+
const lineEnd = nextEnd < 0 ? s.length : nextEnd
|
|
234
|
+
|
|
235
|
+
return snapPos(s, Math.min(nextBreak + 1 + col, lineEnd))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { offsetFromPosition }
|
|
239
|
+
|
|
240
|
+
const ASCII_PRINTABLE_RE = /^[\x20-\x7e]+$/
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Pure shape-only precondition for the fast-echo append path.
|
|
244
|
+
*
|
|
245
|
+
* The fast-echo path bypasses Ink's renderer and writes text directly to
|
|
246
|
+
* stdout, so the stored value, the rendered terminal cells, and the cursor
|
|
247
|
+
* column must all stay in sync without any layout work. We only allow it
|
|
248
|
+
* when the inserted text is pure printable ASCII so that:
|
|
249
|
+
*
|
|
250
|
+
* - `text.length` matches the number of grapheme clusters (no combining
|
|
251
|
+
* marks, no surrogate pairs, no precomposed CJK / Latin-Extended
|
|
252
|
+
* letters that an IME might still be holding open as a composition),
|
|
253
|
+
* - terminal width is exactly 1 cell per character (no East-Asian wide,
|
|
254
|
+
* no zero-width, no ambiguous-width fonts),
|
|
255
|
+
* - input methods (Vietnamese Telex, IME, dead-keys) cannot leak
|
|
256
|
+
* intermediate composition bytes through the bypass before the final
|
|
257
|
+
* commit arrives — those always go through the normal Ink render path
|
|
258
|
+
* and stay layout-accurate (closes #5221, #7443, #17602/#17603).
|
|
259
|
+
*
|
|
260
|
+
* We deliberately do NOT just check `stringWidth(text) === text.length`:
|
|
261
|
+
* Vietnamese precomposed letters like "ề" (U+1EC1) report width 1 and
|
|
262
|
+
* length 1 but are still produced by IME compositions and must not be
|
|
263
|
+
* fast-echoed.
|
|
264
|
+
*/
|
|
265
|
+
export function canFastAppendShape(
|
|
266
|
+
current: string,
|
|
267
|
+
cursor: number,
|
|
268
|
+
text: string,
|
|
269
|
+
columns: number,
|
|
270
|
+
currentLineWidth: number
|
|
271
|
+
): boolean {
|
|
272
|
+
if (cursor !== current.length) {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (current.length === 0) {
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (current.includes('\n')) {
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!ASCII_PRINTABLE_RE.test(text)) {
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return currentLineWidth + text.length < Math.max(1, columns)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Pure shape-only precondition for the fast-echo backspace path.
|
|
293
|
+
*
|
|
294
|
+
* Same reasoning as canFastAppendShape — only allow the direct
|
|
295
|
+
* "\b \b" stdout shortcut when the deleted grapheme is pure printable
|
|
296
|
+
* ASCII. Anything else (combining marks, IME compositions, wide chars,
|
|
297
|
+
* tabs, ANSI fragments) goes through the normal render path so Ink can
|
|
298
|
+
* recompute cell widths.
|
|
299
|
+
*
|
|
300
|
+
* When `columns` is supplied, ALSO rejects when the physical cursor
|
|
301
|
+
* sits at visual column 0 — i.e., right after a soft-wrap boundary.
|
|
302
|
+
* The "\b \b" sequence cannot move the cursor onto the previous visual
|
|
303
|
+
* row (terminals don't back-step across line wraps), so the physical
|
|
304
|
+
* cursor would stay put while the logical caret moves to the end of
|
|
305
|
+
* the previous visual line, desyncing both Ink's `displayCursor` model
|
|
306
|
+
* and the user-visible position.
|
|
307
|
+
*
|
|
308
|
+
* When `columns` is OMITTED, the wrap-boundary check is skipped
|
|
309
|
+
* entirely and the function reverts to the legacy non-wrap-aware
|
|
310
|
+
* contract — values like `'hello '` will return `true` even though
|
|
311
|
+
* they would be unsafe at a width of 6. Production callers (the
|
|
312
|
+
* composer's `canFastBackspace` helper) always pass `columns`;
|
|
313
|
+
* `columns` is optional only so unit tests of the pre-wrap shape
|
|
314
|
+
* contract can keep calling the helper without threading width
|
|
315
|
+
* through. Do NOT omit it from any new caller that relies on the
|
|
316
|
+
* wrap-boundary protection.
|
|
317
|
+
*/
|
|
318
|
+
export function canFastBackspaceShape(current: string, cursor: number, columns?: number): boolean {
|
|
319
|
+
if (cursor !== current.length) {
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (cursor <= 0) {
|
|
324
|
+
return false
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (current.includes('\n')) {
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// If we know the wrap width, reject at the soft-wrap boundary: the
|
|
332
|
+
// caret's physical column would be at (or past) the terminal's right
|
|
333
|
+
// edge, so the terminal has already auto-wrapped to the next row.
|
|
334
|
+
// "\b \b" can't represent the physical move back across that wrap.
|
|
335
|
+
//
|
|
336
|
+
// We check `column === 0` for the "wrap-ansi broke onto a new line"
|
|
337
|
+
// case AND `column >= columns` for the "exact-fill, terminal auto-wraps"
|
|
338
|
+
// case. Both manifest as the same physical state (cursor parked at
|
|
339
|
+
// col 0 of the next row) but cursorLayout reports them differently
|
|
340
|
+
// because it now mirrors wrap-ansi's break points exactly (see the
|
|
341
|
+
// cursor-drift-multiline fix in lib/inputMetrics.ts).
|
|
342
|
+
if (columns !== undefined) {
|
|
343
|
+
const layout = cursorLayout(current, cursor, columns)
|
|
344
|
+
|
|
345
|
+
if (layout.column === 0 || layout.column >= columns) {
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const removed = current.slice(prevPos(current, cursor), cursor)
|
|
351
|
+
|
|
352
|
+
return ASCII_PRINTABLE_RE.test(removed)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function supportsFastEchoTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
356
|
+
// Terminal.app still shows paint/cursor artifacts under the fast-echo
|
|
357
|
+
// bypass path. Fall back to the normal Ink render path there.
|
|
358
|
+
if ((env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal') {
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Termux terminals are especially sensitive to bypass-path cursor drift and
|
|
363
|
+
// stale paints at soft-wrap boundaries on tall/narrow viewports. Keep this
|
|
364
|
+
// off by default in Termux mode; allow explicit opt-in for local debugging.
|
|
365
|
+
if (isTermuxTuiMode(env)) {
|
|
366
|
+
const override = String(env.NASTECH_TUI_TERMUX_FAST_ECHO ?? '').trim().toLowerCase()
|
|
367
|
+
|
|
368
|
+
if (override) {
|
|
369
|
+
return /^(?:1|true|yes|on)$/i.test(override)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function renderWithCursor(value: string, cursor: number) {
|
|
379
|
+
const pos = Math.max(0, Math.min(cursor, value.length))
|
|
380
|
+
|
|
381
|
+
let out = '',
|
|
382
|
+
done = false
|
|
383
|
+
|
|
384
|
+
for (const { segment, index } of seg().segment(value)) {
|
|
385
|
+
if (!done && index >= pos) {
|
|
386
|
+
out += invert(index === pos && segment !== '\n' ? segment : ' ')
|
|
387
|
+
done = true
|
|
388
|
+
|
|
389
|
+
if (index === pos && segment !== '\n') {
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
out += segment
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return done ? out : out + invert(' ')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function renderWithSelection(value: string, start: number, end: number) {
|
|
401
|
+
if (start >= end) {
|
|
402
|
+
return value
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return value.slice(0, start) + invert(value.slice(start, end) || ' ') + value.slice(end)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function useFwdDelete(active: boolean) {
|
|
409
|
+
const ref = useRef(false)
|
|
410
|
+
const { inputEmitter: ee } = useStdin()
|
|
411
|
+
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
if (!active) {
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const h = (d: string) => {
|
|
418
|
+
ref.current = FWD_DEL_RE.test(d)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
ee.prependListener('input', h)
|
|
422
|
+
|
|
423
|
+
return () => {
|
|
424
|
+
ee.removeListener('input', h)
|
|
425
|
+
}
|
|
426
|
+
}, [active, ee])
|
|
427
|
+
|
|
428
|
+
return ref
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
type PasteResult = { cursor: number; value: string } | null
|
|
432
|
+
|
|
433
|
+
const isPasteResultPromise = (
|
|
434
|
+
value: PasteResult | Promise<PasteResult> | null | undefined
|
|
435
|
+
): value is Promise<PasteResult> => !!value && typeof (value as PromiseLike<PasteResult>).then === 'function'
|
|
436
|
+
|
|
437
|
+
export function TextInput({
|
|
438
|
+
columns = 80,
|
|
439
|
+
value,
|
|
440
|
+
onChange,
|
|
441
|
+
onPaste,
|
|
442
|
+
onSubmit,
|
|
443
|
+
mask,
|
|
444
|
+
mouseApiRef,
|
|
445
|
+
voiceRecordKey = DEFAULT_VOICE_RECORD_KEY,
|
|
446
|
+
placeholder = '',
|
|
447
|
+
focus = true
|
|
448
|
+
}: TextInputProps) {
|
|
449
|
+
const [cur, setCur] = useState(value.length)
|
|
450
|
+
const [sel, setSel] = useState<null | { end: number; start: number }>(null)
|
|
451
|
+
const fwdDel = useFwdDelete(focus)
|
|
452
|
+
const termFocus = useTerminalFocus()
|
|
453
|
+
const { stdout } = useStdout()
|
|
454
|
+
const noteCursorAdvance = useCursorAdvance()
|
|
455
|
+
|
|
456
|
+
const curRef = useRef(cur)
|
|
457
|
+
const selRef = useRef<null | { end: number; start: number }>(null)
|
|
458
|
+
const vRef = useRef(value)
|
|
459
|
+
const self = useRef(false)
|
|
460
|
+
const keyBurstTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
461
|
+
const editVersionRef = useRef(0)
|
|
462
|
+
const parentChangeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
463
|
+
const pendingParentValue = useRef<string | null>(null)
|
|
464
|
+
const localRenderTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
465
|
+
const lineWidthRef = useRef(stringWidth(value.includes('\n') ? value.slice(value.lastIndexOf('\n') + 1) : value))
|
|
466
|
+
const mouseAnchorRef = useRef<null | number>(null)
|
|
467
|
+
const lastClickRef = useRef<{ at: number; offset: number }>({ at: 0, offset: -1 })
|
|
468
|
+
const undo = useRef<{ cursor: number; value: string }[]>([])
|
|
469
|
+
const redo = useRef<{ cursor: number; value: string }[]>([])
|
|
470
|
+
|
|
471
|
+
const cbChange = useRef(onChange)
|
|
472
|
+
const cbSubmit = useRef(onSubmit)
|
|
473
|
+
const cbPaste = useRef(onPaste)
|
|
474
|
+
cbChange.current = onChange
|
|
475
|
+
cbSubmit.current = onSubmit
|
|
476
|
+
cbPaste.current = onPaste
|
|
477
|
+
|
|
478
|
+
const raw = self.current ? vRef.current : value
|
|
479
|
+
const display = mask ? raw.replace(/[^\n]/g, mask[0] ?? '*') : raw
|
|
480
|
+
|
|
481
|
+
const selected = useMemo(
|
|
482
|
+
() =>
|
|
483
|
+
sel && sel.start !== sel.end ? { end: Math.max(sel.start, sel.end), start: Math.min(sel.start, sel.end) } : null,
|
|
484
|
+
[sel]
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
// Read `curRef.current` (always up-to-date) rather than the `cur`
|
|
488
|
+
// React state. The fast-echo path defers the React `setCur` by 16ms
|
|
489
|
+
// to batch re-renders during heavy typing; if an unrelated render
|
|
490
|
+
// flushes this component during that window and we used the stale
|
|
491
|
+
// `cur` state here, the layout effect inside `useDeclaredCursor`
|
|
492
|
+
// would publish a stale cursor declaration and clobber the Ink-level
|
|
493
|
+
// bump from `noteCursorAdvance(...)`. `cur` is still in scope and
|
|
494
|
+
// referenced by setSel/setCur paths below, so React tracks the
|
|
495
|
+
// dependency naturally — we just don't use it as the source of truth
|
|
496
|
+
// for layout. The cursorLayout call is cheap (one wrap-text pass
|
|
497
|
+
// over a single-line string in the common case), so dropping useMemo
|
|
498
|
+
// is fine.
|
|
499
|
+
const layout = cursorLayout(display, curRef.current, columns)
|
|
500
|
+
|
|
501
|
+
const boxRef = useDeclaredCursor({
|
|
502
|
+
line: layout.line,
|
|
503
|
+
column: layout.column,
|
|
504
|
+
active: focus && termFocus && !selected
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
// Hide the hardware cursor while a selection is active (prevents
|
|
508
|
+
// auto-wrap onto the next row when inverted text fills the column
|
|
509
|
+
// exactly) or when the terminal loses focus (suppresses the hollow-rect
|
|
510
|
+
// ghost most terminals draw at the parked position).
|
|
511
|
+
const hideHardwareCursor = focus && !!stdout?.isTTY && (!!selected || !termFocus)
|
|
512
|
+
|
|
513
|
+
useEffect(() => {
|
|
514
|
+
if (!hideHardwareCursor || !stdout) {
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
stdout.write('\x1b[?25l')
|
|
519
|
+
|
|
520
|
+
return () => {
|
|
521
|
+
stdout.write('\x1b[?25h')
|
|
522
|
+
}
|
|
523
|
+
}, [hideHardwareCursor, stdout])
|
|
524
|
+
|
|
525
|
+
const nativeCursor = focus && termFocus && !selected && !!stdout?.isTTY
|
|
526
|
+
|
|
527
|
+
// Placeholder text is just a hint, not a selection — render it dim
|
|
528
|
+
// without inverse styling. In a TTY the hardware cursor parks at column
|
|
529
|
+
// 0 and visually marks the input start. Non-TTY surfaces still need the
|
|
530
|
+
// synthetic inverse first-char to draw a cursor at all.
|
|
531
|
+
const rendered = useMemo(() => {
|
|
532
|
+
if (!focus) {
|
|
533
|
+
return display || dim(placeholder)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!display && placeholder) {
|
|
537
|
+
return nativeCursor ? dim(placeholder) : invert(placeholder[0] ?? ' ') + dim(placeholder.slice(1))
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (selected) {
|
|
541
|
+
return renderWithSelection(display, selected.start, selected.end)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return nativeCursor ? display || ' ' : renderWithCursor(display, cur)
|
|
545
|
+
}, [cur, display, focus, nativeCursor, placeholder, selected])
|
|
546
|
+
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
if (self.current) {
|
|
549
|
+
self.current = false
|
|
550
|
+
} else {
|
|
551
|
+
setCur(value.length)
|
|
552
|
+
setSel(null)
|
|
553
|
+
curRef.current = value.length
|
|
554
|
+
selRef.current = null
|
|
555
|
+
vRef.current = value
|
|
556
|
+
lineWidthRef.current = stringWidth(value.includes('\n') ? value.slice(value.lastIndexOf('\n') + 1) : value)
|
|
557
|
+
undo.current = []
|
|
558
|
+
redo.current = []
|
|
559
|
+
}
|
|
560
|
+
}, [value])
|
|
561
|
+
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
if (!focus) {
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const dropSel = () => {
|
|
568
|
+
if (!selRef.current) {
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
selRef.current = null
|
|
573
|
+
setSel(null)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
setInputSelection({
|
|
577
|
+
clear: dropSel,
|
|
578
|
+
collapseToEnd: () => {
|
|
579
|
+
dropSel()
|
|
580
|
+
setCur(vRef.current.length)
|
|
581
|
+
curRef.current = vRef.current.length
|
|
582
|
+
},
|
|
583
|
+
end: selected?.end ?? curRef.current,
|
|
584
|
+
start: selected?.start ?? curRef.current,
|
|
585
|
+
value: vRef.current
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
return () => setInputSelection(null)
|
|
589
|
+
}, [cur, focus, selected])
|
|
590
|
+
|
|
591
|
+
useEffect(
|
|
592
|
+
() => () => {
|
|
593
|
+
if (keyBurstTimer.current) {
|
|
594
|
+
clearTimeout(keyBurstTimer.current)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (parentChangeTimer.current) {
|
|
598
|
+
clearTimeout(parentChangeTimer.current)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (localRenderTimer.current) {
|
|
602
|
+
clearTimeout(localRenderTimer.current)
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
[]
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
const flushParentChange = () => {
|
|
609
|
+
if (parentChangeTimer.current) {
|
|
610
|
+
clearTimeout(parentChangeTimer.current)
|
|
611
|
+
parentChangeTimer.current = null
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const next = pendingParentValue.current
|
|
615
|
+
pendingParentValue.current = null
|
|
616
|
+
|
|
617
|
+
if (next !== null) {
|
|
618
|
+
self.current = true
|
|
619
|
+
cbChange.current(next)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const scheduleParentChange = (next: string) => {
|
|
624
|
+
pendingParentValue.current = next
|
|
625
|
+
|
|
626
|
+
if (parentChangeTimer.current) {
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
parentChangeTimer.current = setTimeout(flushParentChange, FRAME_BATCH_MS)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const cancelLocalRender = () => {
|
|
634
|
+
if (localRenderTimer.current) {
|
|
635
|
+
clearTimeout(localRenderTimer.current)
|
|
636
|
+
localRenderTimer.current = null
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const scheduleLocalRender = () => {
|
|
641
|
+
if (localRenderTimer.current) {
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
localRenderTimer.current = setTimeout(() => {
|
|
646
|
+
localRenderTimer.current = null
|
|
647
|
+
setCur(curRef.current)
|
|
648
|
+
}, FRAME_BATCH_MS)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const canFastEchoBase = () => supportsFastEchoTerminal() && focus && termFocus && !selected && !mask && !!stdout?.isTTY
|
|
652
|
+
|
|
653
|
+
const canFastAppend = (current: string, cursor: number, text: string) =>
|
|
654
|
+
canFastEchoBase() && canFastAppendShape(current, cursor, text, columns, lineWidthRef.current)
|
|
655
|
+
|
|
656
|
+
const canFastBackspace = (current: string, cursor: number) =>
|
|
657
|
+
canFastEchoBase() && canFastBackspaceShape(current, cursor, columns)
|
|
658
|
+
|
|
659
|
+
const commit = (
|
|
660
|
+
next: string,
|
|
661
|
+
nextCur: number,
|
|
662
|
+
track = true,
|
|
663
|
+
syncParent = true,
|
|
664
|
+
syncLocal = true,
|
|
665
|
+
nextLineWidth?: number
|
|
666
|
+
) => {
|
|
667
|
+
const prev = vRef.current
|
|
668
|
+
const c = snapPos(next, nextCur)
|
|
669
|
+
editVersionRef.current += 1
|
|
670
|
+
|
|
671
|
+
if (selRef.current) {
|
|
672
|
+
selRef.current = null
|
|
673
|
+
setSel(null)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (track && next !== prev) {
|
|
677
|
+
undo.current.push({ cursor: curRef.current, value: prev })
|
|
678
|
+
|
|
679
|
+
if (undo.current.length > 200) {
|
|
680
|
+
undo.current.shift()
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
redo.current = []
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (syncLocal) {
|
|
687
|
+
cancelLocalRender()
|
|
688
|
+
setCur(c)
|
|
689
|
+
} else {
|
|
690
|
+
scheduleLocalRender()
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
curRef.current = c
|
|
694
|
+
vRef.current = next
|
|
695
|
+
lineWidthRef.current =
|
|
696
|
+
nextLineWidth ?? stringWidth(next.includes('\n') ? next.slice(next.lastIndexOf('\n') + 1) : next)
|
|
697
|
+
|
|
698
|
+
if (next !== prev) {
|
|
699
|
+
if (syncParent) {
|
|
700
|
+
flushParentChange()
|
|
701
|
+
self.current = true
|
|
702
|
+
cbChange.current(next)
|
|
703
|
+
} else {
|
|
704
|
+
self.current = true
|
|
705
|
+
scheduleParentChange(next)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const swap = (from: typeof undo, to: typeof redo) => {
|
|
711
|
+
const entry = from.current.pop()
|
|
712
|
+
|
|
713
|
+
if (!entry) {
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
to.current.push({ cursor: curRef.current, value: vRef.current })
|
|
718
|
+
commit(entry.value, entry.cursor, false)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const emitPaste = (e: PasteEvent) => {
|
|
722
|
+
const startVersion = editVersionRef.current
|
|
723
|
+
const h = cbPaste.current?.(e)
|
|
724
|
+
|
|
725
|
+
if (isPasteResultPromise(h)) {
|
|
726
|
+
const fallbackText = e.text
|
|
727
|
+
|
|
728
|
+
void h
|
|
729
|
+
.then(result => {
|
|
730
|
+
if (result && editVersionRef.current === startVersion) {
|
|
731
|
+
commit(result.value, result.cursor)
|
|
732
|
+
} else if (result && fallbackText && PRINTABLE.test(fallbackText)) {
|
|
733
|
+
// User typed while async paste was in-flight — fall back to raw text insert
|
|
734
|
+
// so the pasted content is not silently lost.
|
|
735
|
+
const cur = curRef.current
|
|
736
|
+
const v = vRef.current
|
|
737
|
+
commit(v.slice(0, cur) + fallbackText + v.slice(cur), cur + fallbackText.length)
|
|
738
|
+
}
|
|
739
|
+
})
|
|
740
|
+
.catch(() => {})
|
|
741
|
+
|
|
742
|
+
return true
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (h) {
|
|
746
|
+
commit(h.value, h.cursor)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return !!h
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const flushKeyBurst = () => {
|
|
753
|
+
if (keyBurstTimer.current) {
|
|
754
|
+
clearTimeout(keyBurstTimer.current)
|
|
755
|
+
keyBurstTimer.current = null
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
flushParentChange()
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const scheduleKeyBurstCommit = (next: string, nextCur: number) => {
|
|
762
|
+
commit(next, nextCur, true, false, false)
|
|
763
|
+
|
|
764
|
+
if (keyBurstTimer.current) {
|
|
765
|
+
return
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
keyBurstTimer.current = setTimeout(() => {
|
|
769
|
+
keyBurstTimer.current = null
|
|
770
|
+
flushParentChange()
|
|
771
|
+
}, FRAME_BATCH_MS)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const clearSel = () => {
|
|
775
|
+
if (!selRef.current) {
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
selRef.current = null
|
|
780
|
+
setSel(null)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const selectAll = () => {
|
|
784
|
+
const end = vRef.current.length
|
|
785
|
+
|
|
786
|
+
if (!end) {
|
|
787
|
+
return
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const next = { end, start: 0 }
|
|
791
|
+
selRef.current = next
|
|
792
|
+
setSel(next)
|
|
793
|
+
setCur(end)
|
|
794
|
+
curRef.current = end
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const moveCursor = (next: number, extend = false) => {
|
|
798
|
+
const c = snapPos(vRef.current, next)
|
|
799
|
+
const anchor = selRef.current?.start ?? curRef.current
|
|
800
|
+
|
|
801
|
+
if (!extend || anchor === c) {
|
|
802
|
+
clearSel()
|
|
803
|
+
} else {
|
|
804
|
+
const nextSel = { end: c, start: anchor }
|
|
805
|
+
selRef.current = nextSel
|
|
806
|
+
setSel(nextSel)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
setCur(c)
|
|
810
|
+
curRef.current = c
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const selRange = () => {
|
|
814
|
+
const range = selRef.current
|
|
815
|
+
|
|
816
|
+
return range && range.start !== range.end
|
|
817
|
+
? { end: Math.max(range.start, range.end), start: Math.min(range.start, range.end) }
|
|
818
|
+
: null
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const ins = (v: string, c: number, s: string) => v.slice(0, c) + s + v.slice(c)
|
|
822
|
+
|
|
823
|
+
const pastePlainText = (text: string) => {
|
|
824
|
+
const cleaned = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
825
|
+
|
|
826
|
+
if (!cleaned) {
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const range = selRange()
|
|
831
|
+
|
|
832
|
+
const nextValue = range
|
|
833
|
+
? vRef.current.slice(0, range.start) + cleaned + vRef.current.slice(range.end)
|
|
834
|
+
: vRef.current.slice(0, curRef.current) + cleaned + vRef.current.slice(curRef.current)
|
|
835
|
+
|
|
836
|
+
const nextCursor = range ? range.start + cleaned.length : curRef.current + cleaned.length
|
|
837
|
+
|
|
838
|
+
commit(nextValue, nextCursor)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const startMouseSelection = (next: number) => {
|
|
842
|
+
const c = snapPos(vRef.current, next)
|
|
843
|
+
|
|
844
|
+
mouseAnchorRef.current = c
|
|
845
|
+
selRef.current = { end: c, start: c }
|
|
846
|
+
setSel(null)
|
|
847
|
+
setCur(c)
|
|
848
|
+
curRef.current = c
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const dragMouseSelection = (next: number) => {
|
|
852
|
+
if (mouseAnchorRef.current === null) {
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const c = snapPos(vRef.current, next)
|
|
857
|
+
const range = { end: c, start: mouseAnchorRef.current }
|
|
858
|
+
selRef.current = range
|
|
859
|
+
setSel(range.start === range.end ? null : range)
|
|
860
|
+
setCur(c)
|
|
861
|
+
curRef.current = c
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const endMouseSelection = () => {
|
|
865
|
+
mouseAnchorRef.current = null
|
|
866
|
+
|
|
867
|
+
const range = selRef.current
|
|
868
|
+
|
|
869
|
+
if (range && range.start === range.end) {
|
|
870
|
+
selRef.current = null
|
|
871
|
+
setSel(null)
|
|
872
|
+
|
|
873
|
+
return
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const normalized = selRange()
|
|
877
|
+
|
|
878
|
+
if (isMac && normalized) {
|
|
879
|
+
void writeClipboardText(vRef.current.slice(normalized.start, normalized.end))
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const offsetAt = (e: { localCol?: number; localRow?: number }) =>
|
|
884
|
+
offsetFromPosition(display, e.localRow ?? 0, e.localCol ?? 0, columns)
|
|
885
|
+
|
|
886
|
+
const isMultiClickAt = (offset: number) => {
|
|
887
|
+
const now = Date.now()
|
|
888
|
+
const last = lastClickRef.current
|
|
889
|
+
lastClickRef.current = { at: now, offset }
|
|
890
|
+
|
|
891
|
+
return now - last.at < MULTI_CLICK_MS && offset === last.offset
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (mouseApiRef) {
|
|
895
|
+
mouseApiRef.current = {
|
|
896
|
+
dragAt: (row, col) => dragMouseSelection(offsetFromPosition(display, row, col, columns)),
|
|
897
|
+
end: endMouseSelection,
|
|
898
|
+
startAtBeginning: () => startMouseSelection(0)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
useInput(
|
|
903
|
+
(inp: string, k: Key, event: InputEvent) => {
|
|
904
|
+
const eventRaw = event.keypress.raw
|
|
905
|
+
|
|
906
|
+
// Configured voice shortcut wins over composer-level defaults like
|
|
907
|
+
// paste/copy so users who bind voice to ctrl+v / alt+v / cmd+v
|
|
908
|
+
// actually get voice toggled instead of a paste (Copilot round-7
|
|
909
|
+
// follow-up on #19835). The pass-through predicate is a no-op for
|
|
910
|
+
// ordinary typing and plain paste when voice is unbound to 'v'.
|
|
911
|
+
if (shouldPassThroughToGlobalHandler(inp, k, voiceRecordKey)) {
|
|
912
|
+
flushKeyBurst()
|
|
913
|
+
|
|
914
|
+
return
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (
|
|
918
|
+
eventRaw === '\x1bv' ||
|
|
919
|
+
eventRaw === '\x1bV' ||
|
|
920
|
+
eventRaw === '\x16' ||
|
|
921
|
+
(isMac && isActionMod(k) && inp.toLowerCase() === 'v')
|
|
922
|
+
) {
|
|
923
|
+
flushKeyBurst()
|
|
924
|
+
|
|
925
|
+
if (cbPaste.current) {
|
|
926
|
+
return void emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current })
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (isMac) {
|
|
930
|
+
void readClipboardText().then(text => {
|
|
931
|
+
if (text) {
|
|
932
|
+
pastePlainText(text)
|
|
933
|
+
}
|
|
934
|
+
})
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (isMac && isActionMod(k) && inp.toLowerCase() === 'c') {
|
|
941
|
+
flushKeyBurst()
|
|
942
|
+
|
|
943
|
+
const range = selRange()
|
|
944
|
+
|
|
945
|
+
if (range) {
|
|
946
|
+
const text = vRef.current.slice(range.start, range.end)
|
|
947
|
+
|
|
948
|
+
void writeClipboardText(text)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (k.upArrow || k.downArrow) {
|
|
955
|
+
flushKeyBurst()
|
|
956
|
+
|
|
957
|
+
const next = lineNav(vRef.current, curRef.current, k.upArrow ? -1 : 1)
|
|
958
|
+
|
|
959
|
+
if (next !== null) {
|
|
960
|
+
moveCursor(next, k.shift)
|
|
961
|
+
|
|
962
|
+
return
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (k.return) {
|
|
969
|
+
flushKeyBurst()
|
|
970
|
+
|
|
971
|
+
const sequence = (event.keypress as { sequence?: string }).sequence
|
|
972
|
+
const preserveBareLineFeed = shouldPreserveCtrlJNewline() && sequence === '\n'
|
|
973
|
+
|
|
974
|
+
if (k.shift || k.ctrl || preserveBareLineFeed || (isMac ? isActionMod(k) : k.meta)) {
|
|
975
|
+
commit(ins(vRef.current, curRef.current, '\n'), curRef.current + 1)
|
|
976
|
+
} else {
|
|
977
|
+
cbSubmit.current?.(vRef.current)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
let c = curRef.current
|
|
984
|
+
let v = vRef.current
|
|
985
|
+
const mod = isActionMod(k)
|
|
986
|
+
const wordMod = mod || k.meta
|
|
987
|
+
const actionHome = k.home || (!isMac && mod && inp === 'a') || isMacActionFallback(k, inp, 'a')
|
|
988
|
+
const actionEnd = k.end || (mod && inp === 'e') || isMacActionFallback(k, inp, 'e')
|
|
989
|
+
const actionDeleteToStart = (mod && inp === 'u') || isMacActionFallback(k, inp, 'u')
|
|
990
|
+
const actionKillToEnd = (mod && inp === 'k') || isMacActionFallback(k, inp, 'k')
|
|
991
|
+
const actionDeleteWord = (mod && inp === 'w') || isMacActionFallback(k, inp, 'w')
|
|
992
|
+
const range = selRange()
|
|
993
|
+
const delFwd = k.delete || fwdDel.current
|
|
994
|
+
const isPrintableInput = (event.keypress.isPasted || inp.length > 0) && PRINTABLE.test(inp.replace(BRACKET_PASTE, ''))
|
|
995
|
+
|
|
996
|
+
if (!isPrintableInput) {
|
|
997
|
+
flushKeyBurst()
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (mod && inp === 'z') {
|
|
1001
|
+
return swap(undo, redo)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if ((mod && inp === 'y') || (mod && k.shift && inp === 'z')) {
|
|
1005
|
+
return swap(redo, undo)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (isMac && mod && inp === 'a') {
|
|
1009
|
+
return selectAll()
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (actionHome) {
|
|
1013
|
+
c = 0
|
|
1014
|
+
moveCursor(c, k.shift)
|
|
1015
|
+
|
|
1016
|
+
return
|
|
1017
|
+
} else if (actionEnd) {
|
|
1018
|
+
c = v.length
|
|
1019
|
+
moveCursor(c, k.shift)
|
|
1020
|
+
|
|
1021
|
+
return
|
|
1022
|
+
} else if (k.leftArrow) {
|
|
1023
|
+
if (range && !wordMod && !k.shift) {
|
|
1024
|
+
clearSel()
|
|
1025
|
+
c = range.start
|
|
1026
|
+
} else {
|
|
1027
|
+
c = wordMod ? wordLeft(v, c) : prevPos(v, c)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
moveCursor(c, k.shift)
|
|
1031
|
+
|
|
1032
|
+
return
|
|
1033
|
+
} else if (k.rightArrow) {
|
|
1034
|
+
if (range && !wordMod && !k.shift) {
|
|
1035
|
+
clearSel()
|
|
1036
|
+
c = range.end
|
|
1037
|
+
} else {
|
|
1038
|
+
c = wordMod ? wordRight(v, c) : nextPos(v, c)
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
moveCursor(c, k.shift)
|
|
1042
|
+
|
|
1043
|
+
return
|
|
1044
|
+
} else if (wordMod && inp === 'b') {
|
|
1045
|
+
clearSel()
|
|
1046
|
+
c = wordLeft(v, c)
|
|
1047
|
+
} else if (wordMod && inp === 'f') {
|
|
1048
|
+
clearSel()
|
|
1049
|
+
c = wordRight(v, c)
|
|
1050
|
+
} else if (range && (k.backspace || delFwd)) {
|
|
1051
|
+
v = v.slice(0, range.start) + v.slice(range.end)
|
|
1052
|
+
c = range.start
|
|
1053
|
+
} else if (k.backspace && c > 0) {
|
|
1054
|
+
if (wordMod) {
|
|
1055
|
+
const t = wordLeft(v, c)
|
|
1056
|
+
v = v.slice(0, t) + v.slice(c)
|
|
1057
|
+
c = t
|
|
1058
|
+
} else if (canFastBackspace(v, c)) {
|
|
1059
|
+
const t = prevPos(v, c)
|
|
1060
|
+
v = v.slice(0, t) + v.slice(c)
|
|
1061
|
+
c = t
|
|
1062
|
+
stdout!.write('\b \b')
|
|
1063
|
+
// The "\b \b" sequence ends with the cursor one column to the
|
|
1064
|
+
// LEFT of where Ink last parked it. Tell Ink so its `displayCursor`
|
|
1065
|
+
// (and log-update's relative-move basis on the next frame) stays
|
|
1066
|
+
// in sync — otherwise the cursor parks one cell to the right of
|
|
1067
|
+
// the caret on the next unrelated re-render.
|
|
1068
|
+
noteCursorAdvance(-1)
|
|
1069
|
+
commit(v, c, true, false, false, Math.max(0, lineWidthRef.current - 1))
|
|
1070
|
+
|
|
1071
|
+
return
|
|
1072
|
+
} else {
|
|
1073
|
+
const t = prevPos(v, c)
|
|
1074
|
+
v = v.slice(0, t) + v.slice(c)
|
|
1075
|
+
c = t
|
|
1076
|
+
}
|
|
1077
|
+
} else if (delFwd && c < v.length) {
|
|
1078
|
+
if (wordMod) {
|
|
1079
|
+
const t = wordRight(v, c)
|
|
1080
|
+
v = v.slice(0, c) + v.slice(t)
|
|
1081
|
+
} else {
|
|
1082
|
+
v = v.slice(0, c) + v.slice(nextPos(v, c))
|
|
1083
|
+
}
|
|
1084
|
+
} else if (actionDeleteWord) {
|
|
1085
|
+
if (range) {
|
|
1086
|
+
v = v.slice(0, range.start) + v.slice(range.end)
|
|
1087
|
+
c = range.start
|
|
1088
|
+
} else if (c > 0) {
|
|
1089
|
+
clearSel()
|
|
1090
|
+
const t = wordLeft(v, c)
|
|
1091
|
+
v = v.slice(0, t) + v.slice(c)
|
|
1092
|
+
c = t
|
|
1093
|
+
} else {
|
|
1094
|
+
return
|
|
1095
|
+
}
|
|
1096
|
+
} else if (actionDeleteToStart) {
|
|
1097
|
+
if (range) {
|
|
1098
|
+
v = v.slice(0, range.start) + v.slice(range.end)
|
|
1099
|
+
c = range.start
|
|
1100
|
+
} else {
|
|
1101
|
+
v = v.slice(c)
|
|
1102
|
+
c = 0
|
|
1103
|
+
}
|
|
1104
|
+
} else if (actionKillToEnd) {
|
|
1105
|
+
if (range) {
|
|
1106
|
+
v = v.slice(0, range.start) + v.slice(range.end)
|
|
1107
|
+
c = range.start
|
|
1108
|
+
} else {
|
|
1109
|
+
v = v.slice(0, c)
|
|
1110
|
+
}
|
|
1111
|
+
} else if (event.keypress.isPasted || inp.length > 0) {
|
|
1112
|
+
const bracketed = event.keypress.isPasted || inp.includes('[200~')
|
|
1113
|
+
const text = inp.replace(BRACKET_PASTE, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
1114
|
+
|
|
1115
|
+
if (bracketed && emitPaste({ bracketed: true, cursor: c, text, value: v })) {
|
|
1116
|
+
return
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (!text) {
|
|
1120
|
+
return
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (text === '\n') {
|
|
1124
|
+
return commit(ins(v, c, '\n'), c + 1)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (text.length > 1 || text.includes('\n')) {
|
|
1128
|
+
if (shouldRouteMultiCharInputAsPaste(text)) {
|
|
1129
|
+
flushKeyBurst()
|
|
1130
|
+
|
|
1131
|
+
if (!emitPaste({ cursor: c, text, value: v })) {
|
|
1132
|
+
commit(ins(v, c, text), c + text.length)
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const inserted = applyPrintableInsert(v, c, text, range)
|
|
1139
|
+
|
|
1140
|
+
if (!inserted) {
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
v = inserted.value
|
|
1145
|
+
c = inserted.cursor
|
|
1146
|
+
scheduleKeyBurstCommit(v, c)
|
|
1147
|
+
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
{
|
|
1152
|
+
const inserted = applyPrintableInsert(v, c, text, range)
|
|
1153
|
+
|
|
1154
|
+
if (!inserted) {
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (range) {
|
|
1159
|
+
v = inserted.value
|
|
1160
|
+
c = inserted.cursor
|
|
1161
|
+
} else {
|
|
1162
|
+
const simpleAppend = canFastAppend(v, c, text)
|
|
1163
|
+
|
|
1164
|
+
v = inserted.value
|
|
1165
|
+
c = inserted.cursor
|
|
1166
|
+
|
|
1167
|
+
if (simpleAppend) {
|
|
1168
|
+
stdout!.write(text)
|
|
1169
|
+
// ASCII-printable text advances the physical cursor by exactly
|
|
1170
|
+
// text.length cells (canFastAppendShape rejects non-ASCII,
|
|
1171
|
+
// wide chars, newlines). Notify Ink so the cached displayCursor
|
|
1172
|
+
// / log-update relative-move basis advances with it; otherwise
|
|
1173
|
+
// any unrelated re-render that happens before the 16ms
|
|
1174
|
+
// setCur/setParent flush parks the cursor text.length cells
|
|
1175
|
+
// too far right (#cursor-drift).
|
|
1176
|
+
noteCursorAdvance(text.length)
|
|
1177
|
+
commit(v, c, true, false, false, lineWidthRef.current + stringWidth(text))
|
|
1178
|
+
|
|
1179
|
+
return
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
return
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
commit(v, c)
|
|
1188
|
+
},
|
|
1189
|
+
{ isActive: focus }
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
return (
|
|
1193
|
+
<Box
|
|
1194
|
+
onClick={(e: MouseEventLite) => {
|
|
1195
|
+
if (!focus) {
|
|
1196
|
+
return
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
e.stopImmediatePropagation?.()
|
|
1200
|
+
clearSel()
|
|
1201
|
+
const next = offsetAt(e)
|
|
1202
|
+
setCur(next)
|
|
1203
|
+
curRef.current = next
|
|
1204
|
+
}}
|
|
1205
|
+
onMouseDown={(e: MouseEventLite) => {
|
|
1206
|
+
if (!focus) {
|
|
1207
|
+
return
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Right-click → copy active selection if any, otherwise paste.
|
|
1211
|
+
if (e.button === 2) {
|
|
1212
|
+
e.stopImmediatePropagation?.()
|
|
1213
|
+
const decision = decideRightClickAction(vRef.current, selRange())
|
|
1214
|
+
|
|
1215
|
+
if (decision.action === 'copy') {
|
|
1216
|
+
void writeClipboardText(decision.text)
|
|
1217
|
+
|
|
1218
|
+
return
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current })
|
|
1222
|
+
|
|
1223
|
+
return
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (e.button !== 0) {
|
|
1227
|
+
return
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
e.stopImmediatePropagation?.()
|
|
1231
|
+
const offset = offsetAt(e)
|
|
1232
|
+
|
|
1233
|
+
if (isMultiClickAt(offset)) {
|
|
1234
|
+
mouseAnchorRef.current = null
|
|
1235
|
+
selectAll()
|
|
1236
|
+
|
|
1237
|
+
return
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
startMouseSelection(offset)
|
|
1241
|
+
}}
|
|
1242
|
+
onMouseDrag={(e: MouseEventLite) => {
|
|
1243
|
+
if (!focus || e.button !== 0 || mouseAnchorRef.current === null) {
|
|
1244
|
+
return
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
e.stopImmediatePropagation?.()
|
|
1248
|
+
dragMouseSelection(offsetAt(e))
|
|
1249
|
+
}}
|
|
1250
|
+
onMouseUp={(e: MouseEventLite) => {
|
|
1251
|
+
e.stopImmediatePropagation?.()
|
|
1252
|
+
endMouseSelection()
|
|
1253
|
+
}}
|
|
1254
|
+
ref={boxRef}
|
|
1255
|
+
width={columns}
|
|
1256
|
+
>
|
|
1257
|
+
<Text wrap="wrap">{rendered}</Text>
|
|
1258
|
+
</Box>
|
|
1259
|
+
)
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
type MouseEventLite = {
|
|
1263
|
+
button?: number
|
|
1264
|
+
localCol?: number
|
|
1265
|
+
localRow?: number
|
|
1266
|
+
stopImmediatePropagation?: () => void
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export interface PasteEvent {
|
|
1270
|
+
bracketed?: boolean
|
|
1271
|
+
cursor: number
|
|
1272
|
+
hotkey?: boolean
|
|
1273
|
+
text: string
|
|
1274
|
+
value: string
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
interface TextInputProps {
|
|
1278
|
+
columns?: number
|
|
1279
|
+
focus?: boolean
|
|
1280
|
+
mask?: string
|
|
1281
|
+
mouseApiRef?: MutableRefObject<null | TextInputMouseApi>
|
|
1282
|
+
onChange: (v: string) => void
|
|
1283
|
+
onPaste?: (
|
|
1284
|
+
e: PasteEvent
|
|
1285
|
+
) => { cursor: number; value: string } | Promise<{ cursor: number; value: string } | null> | null
|
|
1286
|
+
onSubmit?: (v: string) => void
|
|
1287
|
+
placeholder?: string
|
|
1288
|
+
value: string
|
|
1289
|
+
voiceRecordKey?: ParsedVoiceRecordKey
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
export type RightClickDecision =
|
|
1293
|
+
| { action: 'copy'; text: string }
|
|
1294
|
+
| { action: 'paste' }
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Decide what right-click should do on the composer:
|
|
1298
|
+
* - non-empty selection → copy that text to the clipboard
|
|
1299
|
+
* - no selection (or empty/collapsed range) → fall through to paste
|
|
1300
|
+
*
|
|
1301
|
+
* Mirrors terminal-native behavior (xterm, iTerm, gnome-terminal) where
|
|
1302
|
+
* right-click pastes only when there is nothing selected to copy.
|
|
1303
|
+
*
|
|
1304
|
+
* Callers pass the already-normalized range from `selRange()` (start <= end,
|
|
1305
|
+
* or null when collapsed), so this helper does not need to re-normalize.
|
|
1306
|
+
*/
|
|
1307
|
+
export function decideRightClickAction(
|
|
1308
|
+
value: string,
|
|
1309
|
+
range: { end: number; start: number } | null
|
|
1310
|
+
): RightClickDecision {
|
|
1311
|
+
if (range && range.end > range.start) {
|
|
1312
|
+
const text = value.slice(range.start, range.end)
|
|
1313
|
+
|
|
1314
|
+
if (text) {
|
|
1315
|
+
return { action: 'copy', text }
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
return { action: 'paste' }
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export const shouldPassThroughToGlobalHandler = (
|
|
1323
|
+
input: string,
|
|
1324
|
+
key: Key,
|
|
1325
|
+
voiceRecordKey: ParsedVoiceRecordKey = DEFAULT_VOICE_RECORD_KEY
|
|
1326
|
+
): boolean =>
|
|
1327
|
+
(key.ctrl && input === 'c') ||
|
|
1328
|
+
(key.ctrl && input === 'x') ||
|
|
1329
|
+
key.tab ||
|
|
1330
|
+
(key.shift && key.tab) ||
|
|
1331
|
+
key.pageUp ||
|
|
1332
|
+
key.pageDown ||
|
|
1333
|
+
key.escape ||
|
|
1334
|
+
isVoiceToggleKey(key, input, voiceRecordKey)
|
|
1335
|
+
|
|
1336
|
+
export interface TextInputMouseApi {
|
|
1337
|
+
dragAt: (row: number, col: number) => void
|
|
1338
|
+
end: () => void
|
|
1339
|
+
startAtBeginning: () => void
|
|
1340
|
+
}
|