u-foo 2.3.31 → 2.4.0
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/README.md +157 -213
- package/README.zh-CN.md +151 -197
- package/SKILLS/ufoo/SKILL.md +8 -8
- package/bin/uagy.js +69 -0
- package/bin/uclaude.js +2 -2
- package/bin/ucode.js +4 -4
- package/bin/ucodex.js +2 -2
- package/bin/ufoo.js +5 -23
- package/modules/AGENTS.template.md +1 -1
- package/modules/bus/SKILLS/ubus/SKILL.md +35 -10
- package/package.json +9 -5
- package/scripts/chat-app-smoke.js +30 -0
- package/scripts/global-chat-switch-benchmark.js +5 -5
- package/scripts/ink-demo.js +23 -0
- package/scripts/ink-smoke.js +30 -0
- package/scripts/ucode-app-smoke.js +36 -0
- package/src/{agent → agents/activity}/activityDetector.js +39 -2
- package/src/{agent → agents/activity}/activityStatePublisher.js +1 -1
- package/src/{agent → agents/activity}/activityStateWriter.js +2 -2
- package/src/{agent → agents/activity}/activityTracker.js +1 -1
- package/src/agents/activity/index.js +8 -0
- package/src/{agent → agents/controller}/controllerToolExecutor.js +4 -4
- package/src/agents/controller/index.js +8 -0
- package/src/{agent → agents/controller}/loopObservability.js +2 -2
- package/src/{agent → agents/controller}/loopRuntime.js +1 -1
- package/src/{agent → agents/controller}/ufooAgent.js +9 -9
- package/src/agents/index.js +10 -0
- package/src/agents/internal/index.js +3 -0
- package/src/{agent → agents/internal}/internalRunner.js +45 -22
- package/src/agents/launch/agyConversation.js +159 -0
- package/src/agents/launch/index.js +12 -0
- package/src/{agent → agents/launch}/launchEnvironment.js +2 -3
- package/src/{agent → agents/launch}/launcher.js +64 -21
- package/src/{agent → agents/launch}/notifier.js +23 -12
- package/src/{agent → agents/launch}/ptyRunner.js +44 -12
- package/src/{agent → agents/launch}/ptyWrapper.js +2 -2
- package/src/{agent → agents/launch}/publisherRouting.js +1 -1
- package/src/{agent → agents/launch}/readyDetector.js +23 -0
- package/src/{agent → agents/prompts}/defaultBootstrap.js +63 -4
- package/src/{group/bootstrap.js → agents/prompts/groupBootstrap.js} +41 -6
- package/src/agents/prompts/index.js +8 -0
- package/src/{code/prompts → agents/prompts/native}/index.js +1 -1
- package/src/{agent → agents/providers}/claudeThreadProvider.js +1 -1
- package/src/{agent → agents/providers}/codexThreadProvider.js +1 -1
- package/src/{agent → agents/providers}/directAuthStatus.js +184 -1
- package/src/agents/providers/index.js +13 -0
- package/src/{agent → agents/providers}/upstreamTransport.js +2 -2
- package/src/{chat → app/chat}/agentSockets.js +1 -1
- package/src/{chat → app/chat}/commandExecutor.js +56 -28
- package/src/{chat → app/chat}/commands.js +119 -5
- package/src/{chat → app/chat}/daemonConnection.js +1 -1
- package/src/{chat → app/chat}/daemonMessageRouter.js +54 -4
- package/src/{chat → app/chat}/daemonTransport.js +2 -1
- package/src/{chat → app/chat}/dashboardView.js +2 -21
- package/src/app/chat/index.js +6 -0
- package/src/{chat → app/chat}/inputSubmitHandler.js +38 -13
- package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
- package/src/app/chat/multiWindow/index.js +268 -0
- package/src/app/chat/multiWindow/paneLayout.js +84 -0
- package/src/app/chat/multiWindow/paneManager.js +299 -0
- package/src/app/chat/multiWindow/renderer.js +384 -0
- package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
- package/src/{chat → app/chat}/projectCloseController.js +1 -1
- package/src/app/chat/shellCommand.js +42 -0
- package/src/{chat → app/chat}/transport.js +16 -3
- package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
- package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
- package/src/{init/index.js → app/cli/features/init.js} +14 -32
- package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
- package/src/app/cli/index.js +9 -0
- package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
- package/src/{cli.js → app/cli/run.js} +62 -59
- package/src/app/index.js +6 -0
- package/src/code/agent.js +10 -9
- package/src/code/index.js +2 -0
- package/src/code/launcher/index.js +9 -0
- package/src/{agent → code/launcher}/ucode.js +7 -8
- package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
- package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
- package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
- package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
- package/src/code/nativeRunner.js +4 -4
- package/src/code/taskDecomposer.js +5 -4
- package/src/code/tui.js +39 -1997
- package/src/config.js +15 -2
- package/src/{bus → coordination/bus}/activate.js +2 -2
- package/src/{bus → coordination/bus}/daemon.js +15 -5
- package/src/coordination/bus/envelope.js +173 -0
- package/src/{bus → coordination/bus}/index.js +7 -3
- package/src/{bus → coordination/bus}/inject.js +11 -3
- package/src/{bus → coordination/bus}/message.js +1 -1
- package/src/coordination/bus/messageMeta.js +130 -0
- package/src/coordination/bus/promptEnvelope.js +65 -0
- package/src/{bus → coordination/bus}/shake.js +1 -1
- package/src/{bus → coordination/bus}/store.js +3 -3
- package/src/{bus → coordination/bus}/subscriber.js +2 -2
- package/src/{bus → coordination/bus}/utils.js +2 -2
- package/src/{history → coordination/history}/inputTimeline.js +5 -5
- package/src/coordination/index.js +10 -0
- package/src/{memory → coordination/memory}/historySearch.js +1 -1
- package/src/{memory → coordination/memory}/index.js +3 -3
- package/src/{report → coordination/report}/store.js +2 -2
- package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +43 -0
- package/src/{status → coordination/status}/index.js +3 -3
- package/src/online/bridge.js +2 -2
- package/src/{controller → orchestration/controller}/flags.js +1 -1
- package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
- package/src/orchestration/controller/index.js +10 -0
- package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
- package/src/orchestration/groups/bootstrap.js +3 -0
- package/src/orchestration/groups/index.js +10 -0
- package/src/orchestration/groups/promptProfiles.js +3 -0
- package/src/{group → orchestration/groups}/templates.js +1 -1
- package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
- package/src/orchestration/index.js +7 -0
- package/src/orchestration/solo/index.js +3 -0
- package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
- package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
- package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
- package/src/{daemon → runtime/daemon}/index.js +273 -79
- package/src/{daemon → runtime/daemon}/ipcServer.js +24 -2
- package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
- package/src/{daemon → runtime/daemon}/ops.js +48 -61
- package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
- package/src/{daemon → runtime/daemon}/promptRequest.js +13 -8
- package/src/runtime/daemon/providerSessions.js +230 -0
- package/src/{daemon → runtime/daemon}/reporting.js +4 -4
- package/src/{daemon → runtime/daemon}/run.js +12 -5
- package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
- package/src/{daemon → runtime/daemon}/status.js +5 -5
- package/src/runtime/index.js +10 -0
- package/src/runtime/process/nodeExecutable.js +26 -0
- package/src/{projects → runtime/projects}/registry.js +1 -1
- package/src/{projects → runtime/projects}/runtimes.js +1 -1
- package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
- package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
- package/src/tools/handlers/common.js +1 -1
- package/src/tools/handlers/listAgents.js +1 -1
- package/src/tools/handlers/memory.js +3 -3
- package/src/tools/handlers/readBusSummary.js +1 -1
- package/src/tools/handlers/readOpenDecisions.js +1 -1
- package/src/tools/handlers/readProjectRegistry.js +1 -1
- package/src/tools/handlers/readPromptHistory.js +2 -2
- package/src/tools/schemaFixtures.js +1 -1
- package/src/ui/MIGRATION.md +336 -0
- package/src/ui/format/index.js +974 -0
- package/src/ui/index.js +9 -0
- package/src/ui/ink/ChatApp.js +3674 -0
- package/src/ui/ink/DashboardBar.js +685 -0
- package/src/ui/ink/InkDemo.js +96 -0
- package/src/ui/ink/MultilineInput.js +612 -0
- package/src/ui/ink/UcodeApp.js +822 -0
- package/src/ui/ink/agentMirror.js +730 -0
- package/src/ui/ink/chatReducer.js +359 -0
- package/src/ui/runInk.js +57 -0
- package/src/bus/messageMeta.js +0 -52
- package/src/chat/agentViewController.js +0 -1072
- package/src/chat/chatLogController.js +0 -138
- package/src/chat/completionController.js +0 -533
- package/src/chat/dashboardKeyController.js +0 -573
- package/src/chat/index.js +0 -2214
- package/src/chat/inputHistoryController.js +0 -135
- package/src/chat/inputListenerController.js +0 -470
- package/src/chat/layout.js +0 -186
- package/src/chat/pasteController.js +0 -81
- package/src/chat/statusLineController.js +0 -223
- package/src/chat/streamTracker.js +0 -156
- package/src/code/config +0 -0
- package/src/daemon/providerSessions.js +0 -488
- package/src/terminal/adapters/internalPtyAdapter.js +0 -42
- /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
- /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
- /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
- /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
- /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
- /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
- /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
- /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
- /package/src/{agent → agents/providers}/credentials/index.js +0 -0
- /package/src/{chat → app/chat}/agentBar.js +0 -0
- /package/src/{chat → app/chat}/agentDirectory.js +0 -0
- /package/src/{chat → app/chat}/cronScheduler.js +0 -0
- /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
- /package/src/{chat → app/chat}/daemonReconnect.js +0 -0
- /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
- /package/src/{chat → app/chat}/inputMath.js +0 -0
- /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
- /package/src/{chat → app/chat}/settingsController.js +0 -0
- /package/src/{chat → app/chat}/text.js +0 -0
- /package/src/{chat → app/chat}/transientAgentState.js +0 -0
- /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
- /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
- /package/src/{bus → coordination/bus}/nickname.js +0 -0
- /package/src/{bus → coordination/bus}/queue.js +0 -0
- /package/src/{context → coordination/context}/decisions.js +0 -0
- /package/src/{context → coordination/context}/doctor.js +0 -0
- /package/src/{context → coordination/context}/index.js +0 -0
- /package/src/{context → coordination/context}/sync.js +0 -0
- /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
- /package/src/{ufoo → coordination/state}/paths.js +0 -0
- /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
- /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
- /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
- /package/src/{group → orchestration/groups}/diagram.js +0 -0
- /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
- /package/src/{solo → orchestration/solo}/commands.js +0 -0
- /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
- /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
- /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
- /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
- /package/src/{projects → runtime/projects}/identity.js +0 -0
- /package/src/{projects → runtime/projects}/index.js +0 -0
- /package/src/{projects → runtime/projects}/projectId.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/detect.js +0 -0
- /package/src/{terminal → runtime/terminal}/index.js +0 -0
- /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
- /package/src/{utils → ui/format}/banner.js +0 -0
- /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multiline text input for the ink-based ucode TUI.
|
|
5
|
+
*
|
|
6
|
+
* Built on ink's useInput. Cursor math is delegated to src/ui/format so
|
|
7
|
+
* jest can cover the editor behaviour without mounting ink.
|
|
8
|
+
*
|
|
9
|
+
* Props:
|
|
10
|
+
* value (string) text contents (controlled)
|
|
11
|
+
* onChange(nextValue) called when value changes
|
|
12
|
+
* onSubmit(value) Enter pressed without modifiers, no trailing `\`
|
|
13
|
+
* onCancel() Esc pressed; the parent decides what to do
|
|
14
|
+
* (e.g. abort an in-flight task). The component
|
|
15
|
+
* does NOT mutate `value` on cancel.
|
|
16
|
+
* onArrowUpAtTop(value) cursor on the first visual row, Up pressed
|
|
17
|
+
* onArrowDownAtBottom(value) cursor on the last visual row, Down pressed
|
|
18
|
+
* onArrowLeftAtEmpty(value) Left pressed while value is empty
|
|
19
|
+
* onArrowRightAtEmpty(value) Right pressed while value is empty
|
|
20
|
+
* width (number) wrap width in display cells
|
|
21
|
+
* interactive (boolean) gates useInput; pass false for non-TTY mounts
|
|
22
|
+
* interceptArrowsAndEnter (boolean)
|
|
23
|
+
* when true, Up/Down/Left/Right/Return are
|
|
24
|
+
* suppressed inside the editor so a parent
|
|
25
|
+
* component (e.g. completion popup) can
|
|
26
|
+
* handle them. Plain editing keys still work.
|
|
27
|
+
* placeholder (string) rendered in gray when value is empty
|
|
28
|
+
*
|
|
29
|
+
* Newlines: Enter submits. Use Alt+Enter (delivered as meta+Return) or end the
|
|
30
|
+
* line with `\` (the legacy continuation trick) to insert a literal newline.
|
|
31
|
+
* We do NOT rely on Shift+Enter — many terminals don't distinguish it from
|
|
32
|
+
* plain Enter, so it would silently submit.
|
|
33
|
+
*
|
|
34
|
+
* Bracketed paste arrives as a multi-byte `input` chunk in useInput; we route
|
|
35
|
+
* it through insertText, so multi-line paste already works without extra code.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const fmt = require("../format");
|
|
39
|
+
|
|
40
|
+
// IME cursor parking interacts with ink's frame rendering. Two facts about
|
|
41
|
+
// ink make the naive "move cursor up in useEffect" approach insufficient:
|
|
42
|
+
//
|
|
43
|
+
// 1. ink throttles onRender to 32ms (ink.js:39), so frame writes happen
|
|
44
|
+
// AFTER our useEffect, not before — anything we wrote in useEffect ends
|
|
45
|
+
// up getting overwritten by ink's parking cursor at the bottom of the
|
|
46
|
+
// next frame, which is what the user sees as "光标被结尾抢走".
|
|
47
|
+
//
|
|
48
|
+
// 2. ink's log-update emits ansi-escapes.eraseLines(N) before each frame:
|
|
49
|
+
// a sequence of `eraseLine + cursorUp` pairs starting from "wherever
|
|
50
|
+
// the cursor currently is". If our IME hack left the cursor mid-frame,
|
|
51
|
+
// ink's relative cursorUp walks past the top of the frame and tramples
|
|
52
|
+
// lines above it.
|
|
53
|
+
//
|
|
54
|
+
// Fix: wrap stdout.write once. Before any frame-shaped write (starts with
|
|
55
|
+
// ESC[2K from eraseLines, or ESC[2J from full-screen rerender), push the
|
|
56
|
+
// cursor back DOWN to the parking row so ink's math is restored. AFTER the
|
|
57
|
+
// frame write, if the IME park target is active, re-emit the cursor-up +
|
|
58
|
+
// CHA so the hardware cursor follows the inverse caret again. This way the
|
|
59
|
+
// caret stays parked at the IME-visible row even when ink's throttled write
|
|
60
|
+
// fires long after React commit.
|
|
61
|
+
const __imeStdoutState = new WeakSet();
|
|
62
|
+
const __imeCursor = {
|
|
63
|
+
active: false,
|
|
64
|
+
// Where to park the cursor: rowsUp above ink's "row after last frame line"
|
|
65
|
+
// anchor, and 0-based terminal column.
|
|
66
|
+
parkRowsUp: 0,
|
|
67
|
+
parkCol: 0,
|
|
68
|
+
// How many rows up we last actually moved the cursor — used to undo the
|
|
69
|
+
// move before ink runs its relative eraseLines. ALWAYS matches the move
|
|
70
|
+
// we last wrote, regardless of whether that was through the patched
|
|
71
|
+
// stdout write or the useEffect path.
|
|
72
|
+
movedUpRows: 0,
|
|
73
|
+
// Tracks whether the LAST frame ink wrote ended with '\n' (the log-update
|
|
74
|
+
// path) or not (the full-screen path). The anchor row is one row higher
|
|
75
|
+
// when there's no trailing newline, which shifts subsequent restore-down
|
|
76
|
+
// math by one. Without this flag, useEffect after a full-screen frame
|
|
77
|
+
// restores down too far and overshoots upward → a "ghost caret" sits one
|
|
78
|
+
// or more rows above the real caret.
|
|
79
|
+
lastFrameHadNewline: true,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function isFrameWrite(chunk) {
|
|
83
|
+
if (typeof chunk !== "string" && !(chunk instanceof String)) return false;
|
|
84
|
+
const str = String(chunk);
|
|
85
|
+
// eraseLines(N>0) starts with ESC[2K; full-screen clear starts with ESC[2J.
|
|
86
|
+
return str.startsWith("\x1b[2K") || str.startsWith("\x1b[2J");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Compute "rows up from the current anchor to the caret". The anchor sits
|
|
90
|
+
// one row below the last frame line when the frame ended with '\n', and AT
|
|
91
|
+
// the last frame line otherwise — so a frame with no trailing newline needs
|
|
92
|
+
// one fewer row up to land on the caret.
|
|
93
|
+
function rowsUpFromAnchor() {
|
|
94
|
+
const base = __imeCursor.parkRowsUp;
|
|
95
|
+
return __imeCursor.lastFrameHadNewline ? base : Math.max(0, base - 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function applyParkSequence(parkRowsUp) {
|
|
99
|
+
if (!__imeCursor.active) return "";
|
|
100
|
+
const up = parkRowsUp > 0 ? `\x1b[${parkRowsUp}A` : "";
|
|
101
|
+
const col = `\x1b[${__imeCursor.parkCol + 1}G`; // CHA is 1-based
|
|
102
|
+
__imeCursor.movedUpRows = parkRowsUp;
|
|
103
|
+
return `\x1b[?25h${up}${col}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function patchStdoutForIME(out) {
|
|
107
|
+
if (!out || typeof out.write !== "function" || __imeStdoutState.has(out)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
__imeStdoutState.add(out);
|
|
111
|
+
const originalWrite = out.write.bind(out);
|
|
112
|
+
out.write = function patchedWrite(chunk, encoding, callback) {
|
|
113
|
+
if (!isFrameWrite(chunk) || (typeof chunk !== "string" && !(chunk instanceof String))) {
|
|
114
|
+
return originalWrite(chunk, encoding, callback);
|
|
115
|
+
}
|
|
116
|
+
// Combine "hide cursor + restore-to-anchor + ink's frame + reposition +
|
|
117
|
+
// show cursor" into a SINGLE write so the terminal processes the whole
|
|
118
|
+
// transition atomically. With ink's eraseLines walking the cursor up
|
|
119
|
+
// through the frame mid-write, even one stray byte between escape
|
|
120
|
+
// sequences can leave the hardware cursor visible on an intermediate
|
|
121
|
+
// row for a frame — exactly the "faint cursor above the real one"
|
|
122
|
+
// ghost the user reports.
|
|
123
|
+
const str = String(chunk);
|
|
124
|
+
let prefix = "\x1b[?25l"; // hide cursor for the whole transition
|
|
125
|
+
if (__imeCursor.movedUpRows > 0) {
|
|
126
|
+
// Push the cursor back down to ink's "after last frame line" anchor
|
|
127
|
+
// so the relative cursorUp inside eraseLines walks the right rows.
|
|
128
|
+
prefix += `\x1b[${__imeCursor.movedUpRows}B`;
|
|
129
|
+
__imeCursor.movedUpRows = 0;
|
|
130
|
+
}
|
|
131
|
+
// Record which ink path this frame took so subsequent restores-down know
|
|
132
|
+
// where the anchor actually sits.
|
|
133
|
+
__imeCursor.lastFrameHadNewline = str.endsWith("\n");
|
|
134
|
+
const suffix = applyParkSequence(rowsUpFromAnchor()) || "\x1b[?25l";
|
|
135
|
+
return originalWrite(prefix + str + suffix, encoding, callback);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createMultilineInput({ React, ink }) {
|
|
140
|
+
const { useState, useCallback, useMemo, useEffect } = React;
|
|
141
|
+
const { Box, Text, useInput, useStdout } = ink;
|
|
142
|
+
const h = React.createElement;
|
|
143
|
+
|
|
144
|
+
return function MultilineInput({
|
|
145
|
+
value = "",
|
|
146
|
+
valueVersion = 0,
|
|
147
|
+
onChange = () => {},
|
|
148
|
+
onSubmit = () => {},
|
|
149
|
+
onCancel = () => {},
|
|
150
|
+
onArrowUpAtTop,
|
|
151
|
+
onArrowDownAtBottom,
|
|
152
|
+
onArrowLeftAtEmpty,
|
|
153
|
+
onArrowRightAtEmpty,
|
|
154
|
+
width = 80,
|
|
155
|
+
interactive = true,
|
|
156
|
+
interceptArrowsAndEnter = false,
|
|
157
|
+
placeholder = "",
|
|
158
|
+
promptPrefix = "› ",
|
|
159
|
+
promptColor = "magenta",
|
|
160
|
+
borderColor = "gray",
|
|
161
|
+
// How many terminal rows of UI sit *below* the bottom of this input box
|
|
162
|
+
// (status line, dashboard rows, etc.). The component uses this to compute
|
|
163
|
+
// how far up the hardware cursor needs to be moved after each render so
|
|
164
|
+
// the IME composition window pops up at the visible (inverse) cursor
|
|
165
|
+
// instead of at the bottom of the screen.
|
|
166
|
+
linesBelowInput = 0,
|
|
167
|
+
}) {
|
|
168
|
+
// Cursor is owned by this component. preferredCol tracks the visual
|
|
169
|
+
// column we want to keep when bouncing across lines of different widths
|
|
170
|
+
// via Up/Down.
|
|
171
|
+
const [cursorState, setCursorState] = useState(() => String(value || "").length);
|
|
172
|
+
const [preferredCol, setPreferredCol] = useState(null);
|
|
173
|
+
const cursorPos = fmt.clampCursorPos(cursorState, value);
|
|
174
|
+
|
|
175
|
+
// When the parent forces a new value via valueVersion (e.g. accepting
|
|
176
|
+
// a completion), park the cursor at the end of the freshly inserted
|
|
177
|
+
// text so the user can keep typing without arrow-keying back to the
|
|
178
|
+
// tail.
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
setCursorState(String(value || "").length);
|
|
181
|
+
setPreferredCol(null);
|
|
182
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
|
+
}, [valueVersion]);
|
|
184
|
+
|
|
185
|
+
const wrapWidth = Math.max(1, Math.floor(Number(width) || 80));
|
|
186
|
+
|
|
187
|
+
const setCursor = useCallback((next) => {
|
|
188
|
+
setCursorState(fmt.clampCursorPos(next, value));
|
|
189
|
+
}, [value]);
|
|
190
|
+
const resetPreferredCol = useCallback(() => setPreferredCol(null), []);
|
|
191
|
+
|
|
192
|
+
const change = useCallback((nextValue, nextCursor) => {
|
|
193
|
+
const clamped = fmt.clampCursorPos(nextCursor, nextValue);
|
|
194
|
+
setCursorState(clamped);
|
|
195
|
+
onChange(nextValue);
|
|
196
|
+
}, [onChange]);
|
|
197
|
+
|
|
198
|
+
const insertText = useCallback((text) => {
|
|
199
|
+
const before = value.slice(0, cursorPos);
|
|
200
|
+
const after = value.slice(cursorPos);
|
|
201
|
+
change(`${before}${text}${after}`, cursorPos + text.length);
|
|
202
|
+
setPreferredCol(null);
|
|
203
|
+
}, [value, cursorPos, change]);
|
|
204
|
+
|
|
205
|
+
const replaceRange = useCallback((start, end, text) => {
|
|
206
|
+
const safeStart = Math.max(0, Math.min(value.length, start));
|
|
207
|
+
const safeEnd = Math.max(safeStart, Math.min(value.length, end));
|
|
208
|
+
const next = `${value.slice(0, safeStart)}${text}${value.slice(safeEnd)}`;
|
|
209
|
+
change(next, safeStart + text.length);
|
|
210
|
+
setPreferredCol(null);
|
|
211
|
+
}, [value, change]);
|
|
212
|
+
|
|
213
|
+
const deleteBefore = useCallback(() => {
|
|
214
|
+
if (cursorPos <= 0) return;
|
|
215
|
+
replaceRange(cursorPos - 1, cursorPos, "");
|
|
216
|
+
}, [cursorPos, replaceRange]);
|
|
217
|
+
|
|
218
|
+
const deleteAt = useCallback(() => {
|
|
219
|
+
if (cursorPos >= value.length) return;
|
|
220
|
+
replaceRange(cursorPos, cursorPos + 1, "");
|
|
221
|
+
}, [cursorPos, value.length, replaceRange]);
|
|
222
|
+
|
|
223
|
+
const deleteToBoundary = useCallback((boundary) => {
|
|
224
|
+
const target = fmt.moveCursorToVisualLineBoundary({
|
|
225
|
+
cursorPos,
|
|
226
|
+
inputValue: value,
|
|
227
|
+
width: wrapWidth,
|
|
228
|
+
boundary,
|
|
229
|
+
});
|
|
230
|
+
const start = Math.min(cursorPos, target);
|
|
231
|
+
const end = Math.max(cursorPos, target);
|
|
232
|
+
if (start === end) return;
|
|
233
|
+
replaceRange(start, end, "");
|
|
234
|
+
}, [cursorPos, value, wrapWidth, replaceRange]);
|
|
235
|
+
|
|
236
|
+
const deleteWordBefore = useCallback(() => {
|
|
237
|
+
const next = fmt.deleteWordBeforeCursor(value, cursorPos);
|
|
238
|
+
change(next.value, next.cursorPos);
|
|
239
|
+
setPreferredCol(null);
|
|
240
|
+
}, [value, cursorPos, change]);
|
|
241
|
+
|
|
242
|
+
const deleteWordAfter = useCallback(() => {
|
|
243
|
+
const end = fmt.moveCursorByWord(value, cursorPos, "forward");
|
|
244
|
+
replaceRange(cursorPos, end, "");
|
|
245
|
+
}, [value, cursorPos, replaceRange]);
|
|
246
|
+
|
|
247
|
+
useInput((input, key) => {
|
|
248
|
+
// Let the parent absorb arrow keys + Enter when a popup (e.g. the
|
|
249
|
+
// completion list) is open. We still process plain text input so
|
|
250
|
+
// typing continues to filter the popup live.
|
|
251
|
+
if (interceptArrowsAndEnter && (
|
|
252
|
+
key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return
|
|
253
|
+
)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Submit: Enter without modifiers, except after a trailing backslash
|
|
257
|
+
// (which is the legacy "\\\n" continuation trick). Alt+Enter inserts
|
|
258
|
+
// a literal newline. Shift+Enter is intentionally NOT used: many
|
|
259
|
+
// terminals don't distinguish it from plain Enter.
|
|
260
|
+
if (key.return) {
|
|
261
|
+
if (key.meta) {
|
|
262
|
+
insertText("\n");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (cursorPos > 0 && value[cursorPos - 1] === "\\") {
|
|
266
|
+
replaceRange(cursorPos - 1, cursorPos, "\n");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
onSubmit(value);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (key.escape) {
|
|
273
|
+
onCancel();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (key.ctrl) {
|
|
277
|
+
if (input === "a") {
|
|
278
|
+
setCursor(fmt.moveCursorToVisualLineBoundary({
|
|
279
|
+
cursorPos, inputValue: value, width: wrapWidth, boundary: "start",
|
|
280
|
+
}));
|
|
281
|
+
resetPreferredCol();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (input === "e") {
|
|
285
|
+
setCursor(fmt.moveCursorToVisualLineBoundary({
|
|
286
|
+
cursorPos, inputValue: value, width: wrapWidth, boundary: "end",
|
|
287
|
+
}));
|
|
288
|
+
resetPreferredCol();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (input === "b") {
|
|
292
|
+
setCursor(fmt.moveCursorHorizontally(cursorPos, value, "left"));
|
|
293
|
+
resetPreferredCol();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (input === "f") {
|
|
297
|
+
setCursor(fmt.moveCursorHorizontally(cursorPos, value, "right"));
|
|
298
|
+
resetPreferredCol();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (input === "d") { deleteAt(); return; }
|
|
302
|
+
if (input === "h") { deleteBefore(); return; }
|
|
303
|
+
if (input === "k") { deleteToBoundary("end"); return; }
|
|
304
|
+
if (input === "u") { deleteToBoundary("start"); return; }
|
|
305
|
+
if (input === "w") { deleteWordBefore(); return; }
|
|
306
|
+
// Ctrl+C is parent's responsibility (typically exits the app).
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (key.meta) {
|
|
310
|
+
if (input === "b") {
|
|
311
|
+
setCursor(fmt.moveCursorByWord(value, cursorPos, "backward"));
|
|
312
|
+
resetPreferredCol();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (input === "f") {
|
|
316
|
+
setCursor(fmt.moveCursorByWord(value, cursorPos, "forward"));
|
|
317
|
+
resetPreferredCol();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (input === "d") { deleteWordAfter(); return; }
|
|
321
|
+
}
|
|
322
|
+
if (key.backspace) {
|
|
323
|
+
if (key.meta || key.ctrl) deleteWordBefore();
|
|
324
|
+
else deleteBefore();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (key.delete) {
|
|
328
|
+
// ink reports key.delete for the 0x7F byte that most terminals send
|
|
329
|
+
// when the user presses the top-left Delete key (a.k.a. Backspace on
|
|
330
|
+
// non-Mac keyboards). Treat it as "delete the character before the
|
|
331
|
+
// cursor" by default. Real forward-delete (Fn+Delete on macOS) sends
|
|
332
|
+
// an escape sequence and ink also sets key.delete with no leading
|
|
333
|
+
// input — we can't reliably tell them apart, so favour the much
|
|
334
|
+
// more common backspace semantics. Meta+Delete still maps to
|
|
335
|
+
// delete-to-line-end as before.
|
|
336
|
+
if (key.meta) deleteToBoundary("end");
|
|
337
|
+
else deleteBefore();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (key.leftArrow) {
|
|
341
|
+
if (!value && typeof onArrowLeftAtEmpty === "function") {
|
|
342
|
+
onArrowLeftAtEmpty(value);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
setCursor(fmt.moveCursorHorizontally(cursorPos, value, "left"));
|
|
346
|
+
resetPreferredCol();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (key.rightArrow) {
|
|
350
|
+
if (!value && typeof onArrowRightAtEmpty === "function") {
|
|
351
|
+
onArrowRightAtEmpty(value);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
setCursor(fmt.moveCursorHorizontally(cursorPos, value, "right"));
|
|
355
|
+
resetPreferredCol();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (key.upArrow) {
|
|
359
|
+
if (value) {
|
|
360
|
+
const move = fmt.moveCursorVertically({
|
|
361
|
+
cursorPos, inputValue: value, width: wrapWidth,
|
|
362
|
+
direction: "up", preferredCol,
|
|
363
|
+
});
|
|
364
|
+
setPreferredCol(move.preferredCol);
|
|
365
|
+
if (move.moved) {
|
|
366
|
+
setCursor(move.nextCursorPos);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (typeof onArrowUpAtTop === "function") onArrowUpAtTop(value);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (key.downArrow) {
|
|
374
|
+
if (value) {
|
|
375
|
+
const move = fmt.moveCursorVertically({
|
|
376
|
+
cursorPos, inputValue: value, width: wrapWidth,
|
|
377
|
+
direction: "down", preferredCol,
|
|
378
|
+
});
|
|
379
|
+
setPreferredCol(move.preferredCol);
|
|
380
|
+
if (move.moved) {
|
|
381
|
+
setCursor(move.nextCursorPos);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (typeof onArrowDownAtBottom === "function") onArrowDownAtBottom(value);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Plain character / paste. Filter control bytes.
|
|
390
|
+
if (input && !key.ctrl && !key.meta) {
|
|
391
|
+
const filtered = input.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, "");
|
|
392
|
+
if (filtered) insertText(filtered);
|
|
393
|
+
}
|
|
394
|
+
}, { isActive: interactive });
|
|
395
|
+
|
|
396
|
+
// Render: split into logical lines, then split each into visual rows by
|
|
397
|
+
// wrap width. Highlight one cell at the cursor location. With a
|
|
398
|
+
// placeholder, we still draw the cursor (visible at offset 0) and append
|
|
399
|
+
// the placeholder text in gray after it.
|
|
400
|
+
const showPlaceholder = !value && !!placeholder;
|
|
401
|
+
const visualRows = useMemo(
|
|
402
|
+
() => layoutRows(value, wrapWidth, cursorPos),
|
|
403
|
+
[value, wrapWidth, cursorPos]
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Hardware-cursor parking for IME support. ink hides the terminal cursor
|
|
407
|
+
// by default and parks it after the last frame line; macOS/Linux IMEs
|
|
408
|
+
// (Pinyin, kkc, etc.) anchor the candidate window to the *hardware*
|
|
409
|
+
// cursor, so without this hack Chinese input pops up at the bottom-right
|
|
410
|
+
// instead of next to the inverse-block caret. We compute the row offset
|
|
411
|
+
// of the inverse caret from the bottom of ink's rendered frame and emit
|
|
412
|
+
// ANSI cursor-position escapes after every render.
|
|
413
|
+
//
|
|
414
|
+
// ink frame layout (top→bottom)
|
|
415
|
+
// ... chat log ...
|
|
416
|
+
// ┌── input border top ──┐ <- visualRows[0]
|
|
417
|
+
// │ › row 0 │
|
|
418
|
+
// │ row 1 │
|
|
419
|
+
// └── input border bot ──┘
|
|
420
|
+
// status line <- linesBelowInput rows
|
|
421
|
+
// dashboard row(s)
|
|
422
|
+
// <ink parks cursor here>
|
|
423
|
+
const { stdout } = useStdout() || {};
|
|
424
|
+
// Find the visual (row, col) of the cursor inside the wrapped layout.
|
|
425
|
+
let cursorVisualRow = 0;
|
|
426
|
+
let cursorVisualCol = 0;
|
|
427
|
+
{
|
|
428
|
+
let placed = false;
|
|
429
|
+
for (let r = 0; r < visualRows.length && !placed; r += 1) {
|
|
430
|
+
const row = visualRows[r];
|
|
431
|
+
let col = 0;
|
|
432
|
+
for (const seg of row.segments) {
|
|
433
|
+
if (seg.cursor) {
|
|
434
|
+
cursorVisualRow = r;
|
|
435
|
+
cursorVisualCol = col;
|
|
436
|
+
placed = true;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
col += fmt.displayCellWidth(seg.text);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (!placed) {
|
|
443
|
+
// Cursor at end-of-input on a fresh row.
|
|
444
|
+
cursorVisualRow = Math.max(0, visualRows.length - 1);
|
|
445
|
+
cursorVisualCol = 0;
|
|
446
|
+
const lastRow = visualRows[cursorVisualRow];
|
|
447
|
+
if (lastRow) {
|
|
448
|
+
for (const seg of lastRow.segments) {
|
|
449
|
+
if (seg.cursor) break;
|
|
450
|
+
cursorVisualCol += fmt.displayCellWidth(seg.text);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const promptCols = cursorVisualRow === 0
|
|
456
|
+
? fmt.displayCellWidth(promptPrefix)
|
|
457
|
+
: 2; // " " indent on continuation rows
|
|
458
|
+
const cursorTermCol = promptCols + cursorVisualCol; // 0-based column
|
|
459
|
+
|
|
460
|
+
// Distance from the cursor's row to the parking row that ink will leave
|
|
461
|
+
// behind: bottom border (1) + linesBelowInput + the trailing newline ink
|
|
462
|
+
// appends to its frame string (1, see ink/log-update.js).
|
|
463
|
+
const rowsBelowCursor = (visualRows.length - 1 - cursorVisualRow)
|
|
464
|
+
+ 1 // bottom border row of the input box
|
|
465
|
+
+ Math.max(0, Math.floor(Number(linesBelowInput) || 0))
|
|
466
|
+
+ 1; // ink appends "\n" after the frame, so the cursor sits one extra
|
|
467
|
+
// line below the last printed row
|
|
468
|
+
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
const out = stdout || process.stdout;
|
|
471
|
+
if (!out || typeof out.write !== "function" || !out.isTTY) {
|
|
472
|
+
__imeCursor.active = false;
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
if (!interactive) {
|
|
476
|
+
// Hand the cursor back to ink and stop chasing the caret.
|
|
477
|
+
if (__imeCursor.movedUpRows > 0) {
|
|
478
|
+
out.write(`\x1b[${__imeCursor.movedUpRows}B`);
|
|
479
|
+
__imeCursor.movedUpRows = 0;
|
|
480
|
+
}
|
|
481
|
+
__imeCursor.active = false;
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
patchStdoutForIME(out);
|
|
485
|
+
// Publish the desired park target so the stdout monkey-patch can
|
|
486
|
+
// re-park after every throttled ink frame write.
|
|
487
|
+
__imeCursor.active = true;
|
|
488
|
+
__imeCursor.parkRowsUp = rowsBelowCursor;
|
|
489
|
+
__imeCursor.parkCol = cursorTermCol;
|
|
490
|
+
// Park immediately — covers cases where ink has nothing to render
|
|
491
|
+
// (output unchanged) and won't fire a frame write at all, and keeps
|
|
492
|
+
// the caret visible between frames. Combine hide + restore + park +
|
|
493
|
+
// show into a single write so the terminal never sees the cursor at
|
|
494
|
+
// an intermediate row.
|
|
495
|
+
//
|
|
496
|
+
// CRITICAL: the move-up amount must match the anchor that movedUpRows
|
|
497
|
+
// was measured against. If the last frame ended without '\n' (the
|
|
498
|
+
// full-screen path), the anchor is one row higher than the log-update
|
|
499
|
+
// case, so we use rowsUpFromAnchor() rather than parkRowsUp directly.
|
|
500
|
+
// Otherwise restoring down by movedUpRows then moving up parkRowsUp
|
|
501
|
+
// overshoots by one and leaves the hardware cursor one row above the
|
|
502
|
+
// inverse caret — the residual "ghost cursor" symptom.
|
|
503
|
+
let combined = "\x1b[?25l";
|
|
504
|
+
if (__imeCursor.movedUpRows > 0) {
|
|
505
|
+
combined += `\x1b[${__imeCursor.movedUpRows}B`;
|
|
506
|
+
__imeCursor.movedUpRows = 0;
|
|
507
|
+
}
|
|
508
|
+
combined += applyParkSequence(rowsUpFromAnchor());
|
|
509
|
+
out.write(combined);
|
|
510
|
+
return undefined;
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// On unmount, return the cursor to ink's expected parking row (so the
|
|
514
|
+
// next frame ink renders after us doesn't trample lines above its frame)
|
|
515
|
+
// and re-hide it so the rest of ink's lifetime behaves as before.
|
|
516
|
+
useEffect(() => () => {
|
|
517
|
+
const out = stdout || process.stdout;
|
|
518
|
+
__imeCursor.active = false;
|
|
519
|
+
if (out && typeof out.write === "function" && out.isTTY) {
|
|
520
|
+
const restore = __imeCursor.movedUpRows > 0
|
|
521
|
+
? `\x1b[${__imeCursor.movedUpRows}B`
|
|
522
|
+
: "";
|
|
523
|
+
__imeCursor.movedUpRows = 0;
|
|
524
|
+
out.write(`${restore}\x1b[?25l`);
|
|
525
|
+
}
|
|
526
|
+
}, [stdout]);
|
|
527
|
+
|
|
528
|
+
return h(Box, {
|
|
529
|
+
borderStyle: "single",
|
|
530
|
+
borderTop: true,
|
|
531
|
+
borderBottom: true,
|
|
532
|
+
borderLeft: false,
|
|
533
|
+
borderRight: false,
|
|
534
|
+
borderColor,
|
|
535
|
+
flexDirection: "column",
|
|
536
|
+
width: "100%",
|
|
537
|
+
},
|
|
538
|
+
...visualRows.map((row, idx) =>
|
|
539
|
+
h(Box, { key: `row-${idx}` },
|
|
540
|
+
idx === 0 ? h(Text, { color: promptColor }, promptPrefix) : h(Text, null, " "),
|
|
541
|
+
...row.segments.map((seg, segIdx) =>
|
|
542
|
+
h(Text, {
|
|
543
|
+
key: `s-${segIdx}`,
|
|
544
|
+
inverse: seg.cursor,
|
|
545
|
+
color: showPlaceholder && idx === 0 && segIdx === 0 ? "gray" : undefined,
|
|
546
|
+
}, seg.text)
|
|
547
|
+
),
|
|
548
|
+
showPlaceholder && idx === 0
|
|
549
|
+
? h(Text, { color: "gray" }, placeholder)
|
|
550
|
+
: null,
|
|
551
|
+
)
|
|
552
|
+
),
|
|
553
|
+
);
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Lay out `value` into visual rows respecting `width`, and mark the cell at
|
|
559
|
+
* `cursor` so the renderer can invert it. Returns:
|
|
560
|
+
* [{ segments: [{ text, cursor }] }, ...]
|
|
561
|
+
*
|
|
562
|
+
* Cursor at end-of-input is rendered as an inverted space appended to the
|
|
563
|
+
* final row. Newlines split rows but never appear in segments. Pass
|
|
564
|
+
* `cursor < 0` to suppress the cursor entirely (used in placeholder mode).
|
|
565
|
+
*/
|
|
566
|
+
function layoutRows(value, width, cursor) {
|
|
567
|
+
const text = String(value == null ? "" : value);
|
|
568
|
+
const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
|
|
569
|
+
const rawCursor = Number(cursor);
|
|
570
|
+
const showCursor = Number.isFinite(rawCursor) && rawCursor >= 0;
|
|
571
|
+
const cursorIdx = showCursor
|
|
572
|
+
? Math.min(text.length, Math.floor(rawCursor))
|
|
573
|
+
: -1;
|
|
574
|
+
|
|
575
|
+
const rows = [];
|
|
576
|
+
let row = { segments: [], cellsUsed: 0, cursorPlaced: false };
|
|
577
|
+
const pushRow = () => {
|
|
578
|
+
rows.push(row);
|
|
579
|
+
row = { segments: [], cellsUsed: 0, cursorPlaced: false };
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
583
|
+
const ch = text[i];
|
|
584
|
+
if (ch === "\n") {
|
|
585
|
+
if (cursorIdx === i && !row.cursorPlaced) {
|
|
586
|
+
row.segments.push({ text: " ", cursor: true });
|
|
587
|
+
row.cursorPlaced = true;
|
|
588
|
+
}
|
|
589
|
+
pushRow();
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const w = fmt.displayCellWidth(ch);
|
|
593
|
+
if (row.cellsUsed + w > safeWidth) pushRow();
|
|
594
|
+
if (cursorIdx === i) {
|
|
595
|
+
row.segments.push({ text: ch, cursor: true });
|
|
596
|
+
row.cursorPlaced = true;
|
|
597
|
+
} else {
|
|
598
|
+
row.segments.push({ text: ch, cursor: false });
|
|
599
|
+
}
|
|
600
|
+
row.cellsUsed += w;
|
|
601
|
+
}
|
|
602
|
+
if (cursorIdx === text.length && !row.cursorPlaced) {
|
|
603
|
+
row.segments.push({ text: " ", cursor: true });
|
|
604
|
+
}
|
|
605
|
+
rows.push(row);
|
|
606
|
+
return rows;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
module.exports = {
|
|
610
|
+
createMultilineInput,
|
|
611
|
+
layoutRows,
|
|
612
|
+
};
|