ink-prompt 0.1.2

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.
Files changed (31) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/components/MultilineInput/KeyHandler.d.ts +16 -0
  4. package/dist/components/MultilineInput/KeyHandler.js +124 -0
  5. package/dist/components/MultilineInput/TextBuffer.d.ts +52 -0
  6. package/dist/components/MultilineInput/TextBuffer.js +312 -0
  7. package/dist/components/MultilineInput/TextRenderer.d.ts +24 -0
  8. package/dist/components/MultilineInput/TextRenderer.js +80 -0
  9. package/dist/components/MultilineInput/__tests__/KeyHandler.test.d.ts +1 -0
  10. package/dist/components/MultilineInput/__tests__/KeyHandler.test.js +124 -0
  11. package/dist/components/MultilineInput/__tests__/TextBuffer.test.d.ts +1 -0
  12. package/dist/components/MultilineInput/__tests__/TextBuffer.test.js +450 -0
  13. package/dist/components/MultilineInput/__tests__/TextRenderer.test.d.ts +1 -0
  14. package/dist/components/MultilineInput/__tests__/TextRenderer.test.js +185 -0
  15. package/dist/components/MultilineInput/__tests__/integration.test.d.ts +1 -0
  16. package/dist/components/MultilineInput/__tests__/integration.test.js +88 -0
  17. package/dist/components/MultilineInput/__tests__/useTextInput.test.d.ts +1 -0
  18. package/dist/components/MultilineInput/__tests__/useTextInput.test.js +172 -0
  19. package/dist/components/MultilineInput/index.d.ts +45 -0
  20. package/dist/components/MultilineInput/index.js +132 -0
  21. package/dist/components/MultilineInput/types.d.ts +55 -0
  22. package/dist/components/MultilineInput/types.js +1 -0
  23. package/dist/components/MultilineInput/useTextInput.d.ts +22 -0
  24. package/dist/components/MultilineInput/useTextInput.js +108 -0
  25. package/dist/hello.test.d.ts +1 -0
  26. package/dist/hello.test.js +13 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +2 -0
  29. package/dist/utils/logger.d.ts +15 -0
  30. package/dist/utils/logger.js +42 -0
  31. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Duc Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # ink-prompt
2
+
3
+ A React Ink component library focused on terminal-friendly prompts. The first
4
+ export is `MultilineInput`, an Ink component for collecting multi-line text in
5
+ CLIs.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install ink-prompt
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```tsx
16
+ import React from 'react';
17
+ import { render, Box, Text } from 'ink';
18
+ import { MultilineInput } from 'ink-prompt';
19
+
20
+ const App = () => {
21
+ return (
22
+ <Box flexDirection="column">
23
+ <Text>Describe your change (press Enter to submit):</Text>
24
+ <MultilineInput
25
+ onSubmit={(value) => console.log(value)}
26
+ width={80}
27
+ />
28
+ </Box>
29
+ );
30
+ };
31
+
32
+ render(<App />);
33
+ ```
34
+
35
+ `MultilineInput` supports typical editing controls:
36
+
37
+ - Arrow keys for navigation
38
+ - `Ctrl+J` or typing `\` before Enter to add a newline
39
+ - `Ctrl+Z`/`Ctrl+Y` for undo/redo
40
+ - Enter submits the current buffer
41
+
42
+ ## Development
43
+
44
+ ```bash
45
+ # Install dependencies
46
+ npm install
47
+
48
+ # Build the project
49
+ npm run build
50
+
51
+ # Watch for changes
52
+ npm run dev
53
+
54
+ # Type check
55
+ npm run type-check
56
+ ```
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,16 @@
1
+ import { type Key, type Buffer, type Cursor } from './types.js';
2
+ import { type UseTextInputResult } from './useTextInput.js';
3
+ export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset'> {
4
+ submit: () => void;
5
+ }
6
+ /**
7
+ * Handles keyboard input and maps it to text input actions.
8
+ *
9
+ * @param key - The Ink key object
10
+ * @param input - The input string (if any)
11
+ * @param buffer - The current text buffer
12
+ * @param actions - The actions available to modify the state
13
+ * @param cursor - The current cursor position (optional, but required for some logic like backslash check)
14
+ * @param rawInput - The raw input sequence (optional, used for detecting Home/End keys)
15
+ */
16
+ export declare function handleKey(key: Partial<Key>, input: string, buffer: Buffer, actions: KeyHandlerActions, cursor?: Cursor, rawInput?: string): void;
@@ -0,0 +1,124 @@
1
+ import { log } from '../../utils/logger.js';
2
+ /**
3
+ * Escape sequences for Home key (various terminal emulators)
4
+ */
5
+ const HOME_SEQUENCES = [
6
+ '\x1b[H', // CSI H (xterm)
7
+ '\x1b[1~', // CSI 1~ (linux console)
8
+ '\x1bOH', // SS3 H (xterm application mode)
9
+ '\x1b[7~', // CSI 7~ (rxvt)
10
+ ];
11
+ /**
12
+ * Escape sequences for End key (various terminal emulators)
13
+ */
14
+ const END_SEQUENCES = [
15
+ '\x1b[F', // CSI F (xterm)
16
+ '\x1b[4~', // CSI 4~ (linux console)
17
+ '\x1bOF', // SS3 F (xterm application mode)
18
+ '\x1b[8~', // CSI 8~ (rxvt)
19
+ ];
20
+ /**
21
+ * Handles keyboard input and maps it to text input actions.
22
+ *
23
+ * @param key - The Ink key object
24
+ * @param input - The input string (if any)
25
+ * @param buffer - The current text buffer
26
+ * @param actions - The actions available to modify the state
27
+ * @param cursor - The current cursor position (optional, but required for some logic like backslash check)
28
+ * @param rawInput - The raw input sequence (optional, used for detecting Home/End keys)
29
+ */
30
+ export function handleKey(key, input, buffer, actions, cursor, rawInput) {
31
+ // Navigation
32
+ if (key.upArrow) {
33
+ actions.moveCursor('up');
34
+ return;
35
+ }
36
+ if (key.downArrow) {
37
+ actions.moveCursor('down');
38
+ return;
39
+ }
40
+ if (key.leftArrow) {
41
+ actions.moveCursor('left');
42
+ return;
43
+ }
44
+ if (key.rightArrow) {
45
+ actions.moveCursor('right');
46
+ return;
47
+ }
48
+ // Home/End key detection
49
+ // Ink doesn't expose key.home/key.end, so we check:
50
+ // 1. Raw escape sequences if available
51
+ // 2. Ctrl+A (home) and Ctrl+E (end) - common terminal shortcuts
52
+ if (rawInput && HOME_SEQUENCES.includes(rawInput)) {
53
+ actions.moveCursor('lineStart');
54
+ return;
55
+ }
56
+ if (rawInput && END_SEQUENCES.includes(rawInput)) {
57
+ actions.moveCursor('lineEnd');
58
+ return;
59
+ }
60
+ // Ctrl+A for line start (common in bash/readline)
61
+ if (key.ctrl && input === 'a') {
62
+ actions.moveCursor('lineStart');
63
+ return;
64
+ }
65
+ // Ctrl+E for line end (common in bash/readline)
66
+ if (key.ctrl && input === 'e') {
67
+ actions.moveCursor('lineEnd');
68
+ return;
69
+ }
70
+ // History
71
+ if (key.ctrl) {
72
+ if (input === 'z') {
73
+ actions.undo();
74
+ return;
75
+ }
76
+ if (input === 'y') {
77
+ actions.redo();
78
+ return;
79
+ }
80
+ if (input === 'j') {
81
+ actions.newLine();
82
+ return;
83
+ }
84
+ }
85
+ // Editing
86
+ if (key.backspace) {
87
+ actions.delete();
88
+ return;
89
+ }
90
+ if (key.delete) {
91
+ // Delete key - forward delete (delete character after cursor)
92
+ actions.deleteForward();
93
+ return;
94
+ }
95
+ // Submission / New Line
96
+ if (key.return) {
97
+ log(`[KEYHANDLER] return key, cursor=${JSON.stringify(cursor)}, currentLine="${(cursor ? buffer.lines[cursor.line || 0] : 'no cursor').replace(/[\x00-\x1F\x7F-\uFFFF]/g, c => `\\x${c.charCodeAt(0).toString(16)}`)}" endsWithBackslash=${cursor ? buffer.lines[cursor.line || 0].endsWith('\\') : false}`);
98
+ if (cursor) {
99
+ const currentLine = buffer.lines[cursor.line]; // Check if line ends with backslash AND cursor is at the end (or we just check the line content?)
100
+ // Requirement: "Line ending with \ + Enter continues to next line"
101
+ // Usually this implies the user typed '\' then Enter.
102
+ // We should probably check if the character *before* the cursor is '\' if we want to be precise,
103
+ // or just if the line ends with '\'.
104
+ // Let's assume "line ends with \" means the last char of the line is '\'.
105
+ if (currentLine.endsWith('\\')) {
106
+ // Use combined action to ensure both operations happen with correct state
107
+ actions.deleteAndNewLine();
108
+ return;
109
+ }
110
+ }
111
+ log(`[KEYHANDLER] submit value lines=${buffer.lines.length} lastLine="${buffer.lines[buffer.lines.length - 1]?.replace(/[\x00-\x1F\x7F-\uFFFF]/g, c => `\\x${c.charCodeAt(0).toString(16)}`)}"`);
112
+ actions.submit();
113
+ return;
114
+ }
115
+ // Text Insertion
116
+ // Ignore control keys if they don't have a specific handler above
117
+ if (key.ctrl || key.meta) {
118
+ return;
119
+ }
120
+ if (input) {
121
+ log(`[KEYHANDLER] insert input="${input.replace(/[\x00-\x1F\x7F-\uFFFF]/g, c => `\\x${c.charCodeAt(0).toString(16)}`)}" len=${input.length} cursor=${JSON.stringify(cursor || {})} bufferLines=${buffer.lines.length}`);
122
+ actions.insert(input);
123
+ }
124
+ }
@@ -0,0 +1,52 @@
1
+ import type { Buffer, Cursor, Direction } from './types.js';
2
+ /**
3
+ * Create a new buffer from optional initial text
4
+ */
5
+ export declare function createBuffer(text?: string): Buffer;
6
+ /**
7
+ * Insert text at the cursor position.
8
+ * Handles both single-line and multi-line text (containing \n).
9
+ */
10
+ export declare function insertText(buffer: Buffer, cursor: Cursor, text: string): {
11
+ buffer: Buffer;
12
+ cursor: Cursor;
13
+ };
14
+ /**
15
+ * Delete character before cursor (backspace)
16
+ */
17
+ export declare function deleteChar(buffer: Buffer, cursor: Cursor): {
18
+ buffer: Buffer;
19
+ cursor: Cursor;
20
+ };
21
+ /**
22
+ * Delete character after cursor (forward delete / Delete key)
23
+ */
24
+ export declare function deleteCharForward(buffer: Buffer, cursor: Cursor): {
25
+ buffer: Buffer;
26
+ cursor: Cursor;
27
+ };
28
+ /**
29
+ * Insert a new line at cursor position (splits current line)
30
+ */
31
+ export declare function insertNewLine(buffer: Buffer, cursor: Cursor): {
32
+ buffer: Buffer;
33
+ cursor: Cursor;
34
+ };
35
+ /**
36
+ * Move cursor in specified direction with bounds checking.
37
+ * When width is provided, up/down movement is based on visual lines (accounting for wrapping).
38
+ * When width is not provided, up/down movement is based on buffer lines.
39
+ */
40
+ export declare function moveCursor(buffer: Buffer, cursor: Cursor, direction: Direction, width?: number): Cursor;
41
+ /**
42
+ * Get the full text content from buffer (lines joined with newlines)
43
+ */
44
+ export declare function getTextContent(buffer: Buffer): string;
45
+ /**
46
+ * Get the flat offset (index) from a cursor position.
47
+ */
48
+ export declare function getOffset(buffer: Buffer, cursor: Cursor): number;
49
+ /**
50
+ * Get the cursor position from a flat offset.
51
+ */
52
+ export declare function getCursor(buffer: Buffer, offset: number): Cursor;
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Create a new buffer from optional initial text
3
+ */
4
+ export function createBuffer(text) {
5
+ if (!text) {
6
+ return { lines: [''] };
7
+ }
8
+ return { lines: text.split('\n') };
9
+ }
10
+ /**
11
+ * Insert text at the cursor position.
12
+ * Handles both single-line and multi-line text (containing \n).
13
+ */
14
+ export function insertText(buffer, cursor, text) {
15
+ if (!text)
16
+ return { buffer, cursor };
17
+ const { line, column } = cursor;
18
+ const currentLine = buffer.lines[line];
19
+ // Insert text and handle newlines
20
+ const beforeCursor = currentLine.slice(0, column);
21
+ const afterCursor = currentLine.slice(column);
22
+ const fullText = beforeCursor + text + afterCursor;
23
+ // Split into lines
24
+ const newLines = fullText.split('\n');
25
+ // Calculate new cursor position
26
+ const textLines = text.split('\n');
27
+ const cursorLine = line + (textLines.length - 1);
28
+ const cursorColumn = textLines.length === 1
29
+ ? column + text.length
30
+ : textLines[textLines.length - 1].length;
31
+ // Rebuild buffer
32
+ const resultLines = [
33
+ ...buffer.lines.slice(0, line),
34
+ ...newLines,
35
+ ...buffer.lines.slice(line + 1),
36
+ ];
37
+ return {
38
+ buffer: { lines: resultLines },
39
+ cursor: { line: cursorLine, column: cursorColumn },
40
+ };
41
+ }
42
+ /**
43
+ * Delete character before cursor (backspace)
44
+ */
45
+ export function deleteChar(buffer, cursor) {
46
+ const { line, column } = cursor;
47
+ // At the very start of the buffer - nothing to delete
48
+ if (line === 0 && column === 0) {
49
+ return { buffer, cursor };
50
+ }
51
+ // At the start of a line - merge with previous line
52
+ if (column === 0) {
53
+ const previousLine = buffer.lines[line - 1];
54
+ const currentLine = buffer.lines[line];
55
+ const mergedLine = previousLine + currentLine;
56
+ const newLines = [...buffer.lines];
57
+ newLines[line - 1] = mergedLine;
58
+ newLines.splice(line, 1);
59
+ return {
60
+ buffer: { lines: newLines },
61
+ cursor: { line: line - 1, column: previousLine.length },
62
+ };
63
+ }
64
+ // Delete character within the line
65
+ const currentLine = buffer.lines[line];
66
+ const newLine = currentLine.slice(0, column - 1) + currentLine.slice(column);
67
+ const newLines = [...buffer.lines];
68
+ newLines[line] = newLine;
69
+ return {
70
+ buffer: { lines: newLines },
71
+ cursor: { line, column: column - 1 },
72
+ };
73
+ }
74
+ /**
75
+ * Delete character after cursor (forward delete / Delete key)
76
+ */
77
+ export function deleteCharForward(buffer, cursor) {
78
+ const { line, column } = cursor;
79
+ const currentLine = buffer.lines[line];
80
+ const lineCount = buffer.lines.length;
81
+ // At the very end of the buffer - nothing to delete
82
+ if (line === lineCount - 1 && column >= currentLine.length) {
83
+ return { buffer, cursor };
84
+ }
85
+ // At the end of a line - merge with next line
86
+ if (column >= currentLine.length) {
87
+ const nextLine = buffer.lines[line + 1];
88
+ const mergedLine = currentLine + nextLine;
89
+ const newLines = [...buffer.lines];
90
+ newLines[line] = mergedLine;
91
+ newLines.splice(line + 1, 1);
92
+ return {
93
+ buffer: { lines: newLines },
94
+ cursor, // Cursor stays in place
95
+ };
96
+ }
97
+ // Delete character after cursor within the line
98
+ const newLine = currentLine.slice(0, column) + currentLine.slice(column + 1);
99
+ const newLines = [...buffer.lines];
100
+ newLines[line] = newLine;
101
+ return {
102
+ buffer: { lines: newLines },
103
+ cursor, // Cursor stays in place
104
+ };
105
+ }
106
+ /**
107
+ * Insert a new line at cursor position (splits current line)
108
+ */
109
+ export function insertNewLine(buffer, cursor) {
110
+ const { line, column } = cursor;
111
+ const currentLine = buffer.lines[line];
112
+ const beforeCursor = currentLine.slice(0, column);
113
+ const afterCursor = currentLine.slice(column);
114
+ const newLines = [...buffer.lines];
115
+ newLines[line] = beforeCursor;
116
+ newLines.splice(line + 1, 0, afterCursor);
117
+ return {
118
+ buffer: { lines: newLines },
119
+ cursor: { line: line + 1, column: 0 },
120
+ };
121
+ }
122
+ /**
123
+ * Calculate which visual row (within a buffer line) the cursor is on,
124
+ * and the column within that visual row.
125
+ */
126
+ function getVisualPosition(bufferColumn, lineLength, width) {
127
+ if (lineLength <= width) {
128
+ return { visualRow: 0, visualCol: bufferColumn };
129
+ }
130
+ const visualRow = Math.floor(bufferColumn / width);
131
+ const visualCol = bufferColumn % width;
132
+ return { visualRow, visualCol };
133
+ }
134
+ /**
135
+ * Calculate how many visual rows a buffer line takes.
136
+ */
137
+ function getVisualRowCount(lineLength, width) {
138
+ if (lineLength === 0)
139
+ return 1;
140
+ return Math.ceil(lineLength / width);
141
+ }
142
+ /**
143
+ * Convert visual position back to buffer column.
144
+ */
145
+ function visualToBufferColumn(visualRow, visualCol, lineLength, width) {
146
+ const bufferColumn = visualRow * width + visualCol;
147
+ return Math.min(bufferColumn, lineLength);
148
+ }
149
+ /**
150
+ * Get the length of a specific visual row within a buffer line.
151
+ */
152
+ function getVisualRowLength(lineLength, visualRow, width) {
153
+ const totalVisualRows = getVisualRowCount(lineLength, width);
154
+ if (visualRow >= totalVisualRows)
155
+ return 0;
156
+ if (visualRow === totalVisualRows - 1) {
157
+ // Last visual row - might be shorter
158
+ const remaining = lineLength - visualRow * width;
159
+ return remaining;
160
+ }
161
+ return width;
162
+ }
163
+ /**
164
+ * Move cursor in specified direction with bounds checking.
165
+ * When width is provided, up/down movement is based on visual lines (accounting for wrapping).
166
+ * When width is not provided, up/down movement is based on buffer lines.
167
+ */
168
+ export function moveCursor(buffer, cursor, direction, width) {
169
+ const { line, column } = cursor;
170
+ const currentLine = buffer.lines[line];
171
+ const lineCount = buffer.lines.length;
172
+ switch (direction) {
173
+ case 'left':
174
+ if (column > 0) {
175
+ return { line, column: column - 1 };
176
+ }
177
+ // Wrap to end of previous line
178
+ if (line > 0) {
179
+ return { line: line - 1, column: buffer.lines[line - 1].length };
180
+ }
181
+ return cursor;
182
+ case 'right':
183
+ if (column < currentLine.length) {
184
+ return { line, column: column + 1 };
185
+ }
186
+ // Wrap to start of next line
187
+ if (line < lineCount - 1) {
188
+ return { line: line + 1, column: 0 };
189
+ }
190
+ return cursor;
191
+ case 'up':
192
+ if (width !== undefined) {
193
+ // Visual-aware movement
194
+ const { visualRow, visualCol } = getVisualPosition(column, currentLine.length, width);
195
+ if (visualRow > 0) {
196
+ // Move to previous visual row within the same buffer line
197
+ const targetVisualRow = visualRow - 1;
198
+ const targetVisualRowLength = getVisualRowLength(currentLine.length, targetVisualRow, width);
199
+ const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
200
+ return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine.length, width) };
201
+ }
202
+ // At first visual row of current line - move to last visual row of previous buffer line
203
+ if (line > 0) {
204
+ const prevLine = buffer.lines[line - 1];
205
+ const prevLineVisualRows = getVisualRowCount(prevLine.length, width);
206
+ const targetVisualRow = prevLineVisualRows - 1;
207
+ const targetVisualRowLength = getVisualRowLength(prevLine.length, targetVisualRow, width);
208
+ const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
209
+ return { line: line - 1, column: visualToBufferColumn(targetVisualRow, targetVisualCol, prevLine.length, width) };
210
+ }
211
+ return cursor;
212
+ }
213
+ // Buffer-line movement (no width provided)
214
+ if (line > 0) {
215
+ const targetLine = buffer.lines[line - 1];
216
+ return { line: line - 1, column: Math.min(column, targetLine.length) };
217
+ }
218
+ return cursor;
219
+ case 'down':
220
+ if (width !== undefined) {
221
+ // Visual-aware movement
222
+ const { visualRow, visualCol } = getVisualPosition(column, currentLine.length, width);
223
+ const currentLineVisualRows = getVisualRowCount(currentLine.length, width);
224
+ if (visualRow < currentLineVisualRows - 1) {
225
+ // Move to next visual row within the same buffer line
226
+ const targetVisualRow = visualRow + 1;
227
+ const targetVisualRowLength = getVisualRowLength(currentLine.length, targetVisualRow, width);
228
+ const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
229
+ return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine.length, width) };
230
+ }
231
+ // At last visual row of current line - move to first visual row of next buffer line
232
+ if (line < lineCount - 1) {
233
+ const nextLine = buffer.lines[line + 1];
234
+ const targetVisualRowLength = getVisualRowLength(nextLine.length, 0, width);
235
+ const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
236
+ return { line: line + 1, column: Math.min(targetVisualCol, nextLine.length) };
237
+ }
238
+ return cursor;
239
+ }
240
+ // Buffer-line movement (no width provided)
241
+ if (line < lineCount - 1) {
242
+ const targetLine = buffer.lines[line + 1];
243
+ return { line: line + 1, column: Math.min(column, targetLine.length) };
244
+ }
245
+ return cursor;
246
+ case 'lineStart':
247
+ return { line, column: 0 };
248
+ case 'lineEnd':
249
+ return { line, column: currentLine.length };
250
+ default:
251
+ return cursor;
252
+ }
253
+ }
254
+ /**
255
+ * Get the full text content from buffer (lines joined with newlines)
256
+ */
257
+ export function getTextContent(buffer) {
258
+ // Single empty line is considered empty buffer
259
+ if (buffer.lines.length === 1 && buffer.lines[0] === '') {
260
+ return '';
261
+ }
262
+ return buffer.lines.join('\n');
263
+ }
264
+ /**
265
+ * Get the flat offset (index) from a cursor position.
266
+ */
267
+ export function getOffset(buffer, cursor) {
268
+ let offset = 0;
269
+ for (let i = 0; i < cursor.line; i++) {
270
+ offset += buffer.lines[i].length + 1; // +1 for the newline
271
+ }
272
+ offset += cursor.column;
273
+ return offset;
274
+ }
275
+ /**
276
+ * Get the cursor position from a flat offset.
277
+ */
278
+ export function getCursor(buffer, offset) {
279
+ let currentOffset = 0;
280
+ for (let i = 0; i < buffer.lines.length; i++) {
281
+ const lineLength = buffer.lines[i].length;
282
+ // Check if the offset falls within this line (including the newline character unless it's the last line)
283
+ // We treat the position *after* the newline as the start of the next line.
284
+ // Position *at* the end of line (before newline) is valid cursor column.
285
+ // If we are on the last line, we accept up to lineLength
286
+ if (i === buffer.lines.length - 1) {
287
+ if (offset <= currentOffset + lineLength) {
288
+ return { line: i, column: Math.max(0, offset - currentOffset) };
289
+ }
290
+ // If offset is beyond the content, clamp to end
291
+ return { line: i, column: lineLength };
292
+ }
293
+ // For non-last lines, we account for newline character (+1)
294
+ if (offset <= currentOffset + lineLength) {
295
+ return { line: i, column: Math.max(0, offset - currentOffset) };
296
+ }
297
+ // If offset is exactly at the newline character, it depends on interpretation.
298
+ // Usually, cursor at the newline means it's at the end of the line.
299
+ // But if we are "past" the newline, we are on the start of next line.
300
+ // Logic:
301
+ // Line 0: "abc" (len 3). Newline at index 3.
302
+ // Index 0='a', 1='b', 2='c', 3='\n'.
303
+ // If target offset is 3: That's end of line 0.
304
+ // If target offset is 4: That's start of line 1.
305
+ // currentOffset + lineLength is index of newline.
306
+ // If offset == currentOffset + lineLength + 1, we move to next loop
307
+ currentOffset += lineLength + 1;
308
+ }
309
+ // Fallback (should have returned in loop)
310
+ const lastLineIdx = buffer.lines.length - 1;
311
+ return { line: lastLineIdx, column: buffer.lines[lastLineIdx].length };
312
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import type { Buffer, Cursor, WrapResult } from './types.js';
3
+ /**
4
+ * Props for the TextRenderer component
5
+ */
6
+ export interface TextRendererProps {
7
+ /** Text buffer to render */
8
+ buffer: Buffer;
9
+ /** Current cursor position */
10
+ cursor: Cursor;
11
+ /** Terminal width for word wrapping (defaults to 80) */
12
+ width?: number;
13
+ /** Whether to show the cursor (defaults to true) */
14
+ showCursor?: boolean;
15
+ }
16
+ /**
17
+ * Wrap buffer lines to fit within a given width.
18
+ * Returns visual lines and maps cursor position to visual coordinates.
19
+ */
20
+ export declare function wrapLines(buffer: Buffer, cursor: Cursor, width: number): WrapResult;
21
+ /**
22
+ * TextRenderer component for displaying buffer content with cursor
23
+ */
24
+ export declare function TextRenderer({ buffer, cursor, width, showCursor, }: TextRendererProps): React.ReactElement;