ink-prompt 0.1.5 → 0.1.6

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 (34) hide show
  1. package/dist/components/MultilineInput/KeyHandler.d.ts +3 -1
  2. package/dist/components/MultilineInput/KeyHandler.js +120 -2
  3. package/dist/components/MultilineInput/__tests__/KeyHandler.test.js +173 -0
  4. package/dist/components/MultilineInput/types.d.ts +4 -0
  5. package/dist/index.d.ts +1 -0
  6. package/package.json +1 -1
  7. package/dist/examples/examples/basic.d.ts +0 -1
  8. package/dist/examples/examples/basic.js +0 -9
  9. package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +0 -15
  10. package/dist/examples/src/components/MultilineInput/KeyHandler.js +0 -97
  11. package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +0 -34
  12. package/dist/examples/src/components/MultilineInput/TextBuffer.js +0 -127
  13. package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +0 -24
  14. package/dist/examples/src/components/MultilineInput/TextRenderer.js +0 -72
  15. package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts +0 -1
  16. package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +0 -115
  17. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +0 -1
  18. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +0 -254
  19. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +0 -1
  20. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +0 -176
  21. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +0 -1
  22. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +0 -71
  23. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +0 -1
  24. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +0 -65
  25. package/dist/examples/src/components/MultilineInput/index.d.ts +0 -39
  26. package/dist/examples/src/components/MultilineInput/index.js +0 -82
  27. package/dist/examples/src/components/MultilineInput/types.d.ts +0 -55
  28. package/dist/examples/src/components/MultilineInput/types.js +0 -1
  29. package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +0 -16
  30. package/dist/examples/src/components/MultilineInput/useTextInput.js +0 -82
  31. package/dist/examples/src/hello.test.d.ts +0 -1
  32. package/dist/examples/src/hello.test.js +0 -13
  33. package/dist/examples/src/index.d.ts +0 -2
  34. package/dist/examples/src/index.js +0 -2
@@ -2,6 +2,7 @@ import { type Key, type Buffer, type Cursor } from './types.js';
2
2
  import { type UseTextInputResult } from './useTextInput.js';
3
3
  export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset'> {
4
4
  submit: () => void;
5
+ onBoundaryArrow?: (direction: 'up' | 'down' | 'left' | 'right') => void;
5
6
  }
6
7
  /**
7
8
  * Handles keyboard input and maps it to text input actions.
@@ -12,5 +13,6 @@ export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'c
12
13
  * @param actions - The actions available to modify the state
13
14
  * @param cursor - The current cursor position (optional, but required for some logic like backslash check)
14
15
  * @param rawInput - The raw input sequence (optional, used for detecting Home/End keys)
16
+ * @param width - Terminal width for visual-aware boundary detection (optional)
15
17
  */
16
- export declare function handleKey(key: Partial<Key>, input: string, buffer: Buffer, actions: KeyHandlerActions, cursor?: Cursor, rawInput?: string): void;
18
+ export declare function handleKey(key: Partial<Key>, input: string, buffer: Buffer, actions: KeyHandlerActions, cursor?: Cursor, rawInput?: string, width?: number): void;
@@ -1,4 +1,105 @@
1
+ import { getVisualRows } from './TextBuffer.js';
1
2
  import { log } from '../../utils/logger.js';
3
+ /**
4
+ * Check if cursor is at the left boundary (start of text).
5
+ */
6
+ function isAtLeftBoundary(cursor) {
7
+ return cursor.line === 0 && cursor.column === 0;
8
+ }
9
+ /**
10
+ * Check if cursor is at the right boundary (end of text).
11
+ */
12
+ function isAtRightBoundary(buffer, cursor) {
13
+ const lastLineIndex = buffer.lines.length - 1;
14
+ const lastLine = buffer.lines[lastLineIndex];
15
+ return cursor.line === lastLineIndex && cursor.column >= lastLine.length;
16
+ }
17
+ /**
18
+ * Check if cursor is at the top boundary (cannot move up).
19
+ * When width is provided, this considers visual line wrapping.
20
+ */
21
+ function isAtTopBoundary(buffer, cursor, width) {
22
+ if (cursor.line > 0) {
23
+ // Not on first buffer line, check visual wrapping
24
+ if (width !== undefined) {
25
+ const currentLine = buffer.lines[cursor.line];
26
+ const rows = getVisualRows(currentLine, width);
27
+ // Find which visual row the cursor is on
28
+ let visualRow = 0;
29
+ for (let i = 0; i < rows.length; i++) {
30
+ const row = rows[i];
31
+ const rowEnd = row.start + row.length;
32
+ if (cursor.column >= row.start && cursor.column <= rowEnd) {
33
+ visualRow = i;
34
+ break;
35
+ }
36
+ }
37
+ // If cursor is not on the first visual row of this line, it can move up
38
+ return false;
39
+ }
40
+ return false;
41
+ }
42
+ // Cursor is on first buffer line
43
+ if (width !== undefined) {
44
+ const currentLine = buffer.lines[0];
45
+ const rows = getVisualRows(currentLine, width);
46
+ // Find which visual row the cursor is on
47
+ for (let i = 0; i < rows.length; i++) {
48
+ const row = rows[i];
49
+ const rowEnd = row.start + row.length;
50
+ if (cursor.column >= row.start && cursor.column <= rowEnd) {
51
+ // At top boundary only if on first visual row
52
+ return i === 0;
53
+ }
54
+ }
55
+ }
56
+ // No width - buffer line-based: first line means at top
57
+ return true;
58
+ }
59
+ /**
60
+ * Check if cursor is at the bottom boundary (cannot move down).
61
+ * When width is provided, this considers visual line wrapping.
62
+ */
63
+ function isAtBottomBoundary(buffer, cursor, width) {
64
+ const lastLineIndex = buffer.lines.length - 1;
65
+ if (cursor.line < lastLineIndex) {
66
+ // Not on last buffer line, check visual wrapping
67
+ if (width !== undefined) {
68
+ const currentLine = buffer.lines[cursor.line];
69
+ const rows = getVisualRows(currentLine, width);
70
+ // Find which visual row the cursor is on
71
+ let visualRow = 0;
72
+ for (let i = 0; i < rows.length; i++) {
73
+ const row = rows[i];
74
+ const rowEnd = row.start + row.length;
75
+ if (cursor.column >= row.start && cursor.column <= rowEnd) {
76
+ visualRow = i;
77
+ break;
78
+ }
79
+ }
80
+ // If cursor is not on the last visual row of this line, it can move down
81
+ return false;
82
+ }
83
+ return false;
84
+ }
85
+ // Cursor is on last buffer line
86
+ if (width !== undefined) {
87
+ const currentLine = buffer.lines[lastLineIndex];
88
+ const rows = getVisualRows(currentLine, width);
89
+ const lastVisualRow = rows.length - 1;
90
+ // Find which visual row the cursor is on
91
+ for (let i = 0; i < rows.length; i++) {
92
+ const row = rows[i];
93
+ const rowEnd = row.start + row.length;
94
+ if (cursor.column >= row.start && cursor.column <= rowEnd) {
95
+ // At bottom boundary only if on last visual row
96
+ return i === lastVisualRow;
97
+ }
98
+ }
99
+ }
100
+ // No width - buffer line-based: last line means at bottom
101
+ return true;
102
+ }
2
103
  /**
3
104
  * Escape sequences for Home key (various terminal emulators)
4
105
  */
@@ -33,22 +134,39 @@ function isBackspaceSequence(seq) {
33
134
  * @param actions - The actions available to modify the state
34
135
  * @param cursor - The current cursor position (optional, but required for some logic like backslash check)
35
136
  * @param rawInput - The raw input sequence (optional, used for detecting Home/End keys)
137
+ * @param width - Terminal width for visual-aware boundary detection (optional)
36
138
  */
37
- export function handleKey(key, input, buffer, actions, cursor, rawInput) {
38
- // Navigation
139
+ export function handleKey(key, input, buffer, actions, cursor, rawInput, width) {
140
+ // Navigation with boundary detection
39
141
  if (key.upArrow) {
142
+ if (cursor && actions.onBoundaryArrow && isAtTopBoundary(buffer, cursor, width)) {
143
+ actions.onBoundaryArrow('up');
144
+ return;
145
+ }
40
146
  actions.moveCursor('up');
41
147
  return;
42
148
  }
43
149
  if (key.downArrow) {
150
+ if (cursor && actions.onBoundaryArrow && isAtBottomBoundary(buffer, cursor, width)) {
151
+ actions.onBoundaryArrow('down');
152
+ return;
153
+ }
44
154
  actions.moveCursor('down');
45
155
  return;
46
156
  }
47
157
  if (key.leftArrow) {
158
+ if (cursor && actions.onBoundaryArrow && isAtLeftBoundary(cursor)) {
159
+ actions.onBoundaryArrow('left');
160
+ return;
161
+ }
48
162
  actions.moveCursor('left');
49
163
  return;
50
164
  }
51
165
  if (key.rightArrow) {
166
+ if (cursor && actions.onBoundaryArrow && isAtRightBoundary(buffer, cursor)) {
167
+ actions.onBoundaryArrow('right');
168
+ return;
169
+ }
52
170
  actions.moveCursor('right');
53
171
  return;
54
172
  }
@@ -62,6 +62,179 @@ describe('KeyHandler', () => {
62
62
  expect(actions.moveCursor).toHaveBeenCalledWith('lineEnd');
63
63
  });
64
64
  });
65
+ describe('Boundary Arrow', () => {
66
+ describe('Left boundary', () => {
67
+ it('calls onBoundaryArrow("left") when cursor is at position 0', () => {
68
+ buffer = { lines: ['hello'] };
69
+ const cursor = { line: 0, column: 0 };
70
+ const onBoundaryArrow = vi.fn();
71
+ actions.onBoundaryArrow = onBoundaryArrow;
72
+ handleKey({ leftArrow: true }, '', buffer, actions, cursor);
73
+ expect(onBoundaryArrow).toHaveBeenCalledWith('left');
74
+ expect(actions.moveCursor).not.toHaveBeenCalled();
75
+ });
76
+ it('does not call onBoundaryArrow when cursor can move left', () => {
77
+ buffer = { lines: ['hello'] };
78
+ const cursor = { line: 0, column: 3 };
79
+ const onBoundaryArrow = vi.fn();
80
+ actions.onBoundaryArrow = onBoundaryArrow;
81
+ handleKey({ leftArrow: true }, '', buffer, actions, cursor);
82
+ expect(onBoundaryArrow).not.toHaveBeenCalled();
83
+ expect(actions.moveCursor).toHaveBeenCalledWith('left');
84
+ });
85
+ it('does not call onBoundaryArrow when at start of line but previous line exists', () => {
86
+ buffer = { lines: ['first', 'second'] };
87
+ const cursor = { line: 1, column: 0 };
88
+ const onBoundaryArrow = vi.fn();
89
+ actions.onBoundaryArrow = onBoundaryArrow;
90
+ handleKey({ leftArrow: true }, '', buffer, actions, cursor);
91
+ expect(onBoundaryArrow).not.toHaveBeenCalled();
92
+ expect(actions.moveCursor).toHaveBeenCalledWith('left');
93
+ });
94
+ });
95
+ describe('Right boundary', () => {
96
+ it('calls onBoundaryArrow("right") when cursor is at end of text', () => {
97
+ buffer = { lines: ['hello'] };
98
+ const cursor = { line: 0, column: 5 };
99
+ const onBoundaryArrow = vi.fn();
100
+ actions.onBoundaryArrow = onBoundaryArrow;
101
+ handleKey({ rightArrow: true }, '', buffer, actions, cursor);
102
+ expect(onBoundaryArrow).toHaveBeenCalledWith('right');
103
+ expect(actions.moveCursor).not.toHaveBeenCalled();
104
+ });
105
+ it('does not call onBoundaryArrow when cursor can move right', () => {
106
+ buffer = { lines: ['hello'] };
107
+ const cursor = { line: 0, column: 2 };
108
+ const onBoundaryArrow = vi.fn();
109
+ actions.onBoundaryArrow = onBoundaryArrow;
110
+ handleKey({ rightArrow: true }, '', buffer, actions, cursor);
111
+ expect(onBoundaryArrow).not.toHaveBeenCalled();
112
+ expect(actions.moveCursor).toHaveBeenCalledWith('right');
113
+ });
114
+ it('does not call onBoundaryArrow when at end of line but next line exists', () => {
115
+ buffer = { lines: ['first', 'second'] };
116
+ const cursor = { line: 0, column: 5 };
117
+ const onBoundaryArrow = vi.fn();
118
+ actions.onBoundaryArrow = onBoundaryArrow;
119
+ handleKey({ rightArrow: true }, '', buffer, actions, cursor);
120
+ expect(onBoundaryArrow).not.toHaveBeenCalled();
121
+ expect(actions.moveCursor).toHaveBeenCalledWith('right');
122
+ });
123
+ });
124
+ describe('Up boundary', () => {
125
+ it('calls onBoundaryArrow("up") when cursor is on first line', () => {
126
+ buffer = { lines: ['hello'] };
127
+ const cursor = { line: 0, column: 2 };
128
+ const onBoundaryArrow = vi.fn();
129
+ actions.onBoundaryArrow = onBoundaryArrow;
130
+ handleKey({ upArrow: true }, '', buffer, actions, cursor);
131
+ expect(onBoundaryArrow).toHaveBeenCalledWith('up');
132
+ expect(actions.moveCursor).not.toHaveBeenCalled();
133
+ });
134
+ it('does not call onBoundaryArrow when previous line exists', () => {
135
+ buffer = { lines: ['first', 'second'] };
136
+ const cursor = { line: 1, column: 2 };
137
+ const onBoundaryArrow = vi.fn();
138
+ actions.onBoundaryArrow = onBoundaryArrow;
139
+ handleKey({ upArrow: true }, '', buffer, actions, cursor);
140
+ expect(onBoundaryArrow).not.toHaveBeenCalled();
141
+ expect(actions.moveCursor).toHaveBeenCalledWith('up');
142
+ });
143
+ it('considers visual rows when width is provided - cursor on first visual row', () => {
144
+ // "hello world" with width 5 wraps to:
145
+ // "hello" (visual row 0)
146
+ // " worl" (visual row 1)
147
+ // "d" (visual row 2)
148
+ buffer = { lines: ['hello world'] };
149
+ const cursor = { line: 0, column: 2 }; // On first visual row
150
+ const onBoundaryArrow = vi.fn();
151
+ actions.onBoundaryArrow = onBoundaryArrow;
152
+ handleKey({ upArrow: true }, '', buffer, actions, cursor, undefined, 5);
153
+ expect(onBoundaryArrow).toHaveBeenCalledWith('up');
154
+ expect(actions.moveCursor).not.toHaveBeenCalled();
155
+ });
156
+ it('considers visual rows when width is provided - cursor on second visual row', () => {
157
+ // "hello world" with width 5 wraps to visual rows
158
+ buffer = { lines: ['hello world'] };
159
+ const cursor = { line: 0, column: 7 }; // On second visual row (after first wrap)
160
+ const onBoundaryArrow = vi.fn();
161
+ actions.onBoundaryArrow = onBoundaryArrow;
162
+ handleKey({ upArrow: true }, '', buffer, actions, cursor, undefined, 5);
163
+ // Can move up within the same buffer line (to previous visual row)
164
+ expect(onBoundaryArrow).not.toHaveBeenCalled();
165
+ expect(actions.moveCursor).toHaveBeenCalledWith('up');
166
+ });
167
+ });
168
+ describe('Down boundary', () => {
169
+ it('calls onBoundaryArrow("down") when cursor is on last line', () => {
170
+ buffer = { lines: ['hello'] };
171
+ const cursor = { line: 0, column: 2 };
172
+ const onBoundaryArrow = vi.fn();
173
+ actions.onBoundaryArrow = onBoundaryArrow;
174
+ handleKey({ downArrow: true }, '', buffer, actions, cursor);
175
+ expect(onBoundaryArrow).toHaveBeenCalledWith('down');
176
+ expect(actions.moveCursor).not.toHaveBeenCalled();
177
+ });
178
+ it('does not call onBoundaryArrow when next line exists', () => {
179
+ buffer = { lines: ['first', 'second'] };
180
+ const cursor = { line: 0, column: 2 };
181
+ const onBoundaryArrow = vi.fn();
182
+ actions.onBoundaryArrow = onBoundaryArrow;
183
+ handleKey({ downArrow: true }, '', buffer, actions, cursor);
184
+ expect(onBoundaryArrow).not.toHaveBeenCalled();
185
+ expect(actions.moveCursor).toHaveBeenCalledWith('down');
186
+ });
187
+ it('considers visual rows when width is provided - cursor on last visual row', () => {
188
+ // "hello world" with width 5 wraps to visual rows
189
+ buffer = { lines: ['hello world'] };
190
+ const cursor = { line: 0, column: 10 }; // On last visual row (at 'd')
191
+ const onBoundaryArrow = vi.fn();
192
+ actions.onBoundaryArrow = onBoundaryArrow;
193
+ handleKey({ downArrow: true }, '', buffer, actions, cursor, undefined, 5);
194
+ expect(onBoundaryArrow).toHaveBeenCalledWith('down');
195
+ expect(actions.moveCursor).not.toHaveBeenCalled();
196
+ });
197
+ it('considers visual rows when width is provided - cursor on first visual row', () => {
198
+ // "hello world" with width 5 wraps to visual rows
199
+ buffer = { lines: ['hello world'] };
200
+ const cursor = { line: 0, column: 2 }; // On first visual row
201
+ const onBoundaryArrow = vi.fn();
202
+ actions.onBoundaryArrow = onBoundaryArrow;
203
+ handleKey({ downArrow: true }, '', buffer, actions, cursor, undefined, 5);
204
+ // Can move down within the same buffer line (to next visual row)
205
+ expect(onBoundaryArrow).not.toHaveBeenCalled();
206
+ expect(actions.moveCursor).toHaveBeenCalledWith('down');
207
+ });
208
+ });
209
+ describe('No callback provided', () => {
210
+ it('moves cursor normally when onBoundaryArrow is not set', () => {
211
+ buffer = { lines: ['hello'] };
212
+ const cursor = { line: 0, column: 0 };
213
+ // actions.onBoundaryArrow is not set
214
+ handleKey({ leftArrow: true }, '', buffer, actions, cursor);
215
+ expect(actions.moveCursor).toHaveBeenCalledWith('left');
216
+ });
217
+ });
218
+ describe('Empty buffer', () => {
219
+ it('calls onBoundaryArrow for all directions in empty buffer', () => {
220
+ buffer = { lines: [''] };
221
+ const cursor = { line: 0, column: 0 };
222
+ const onBoundaryArrow = vi.fn();
223
+ actions.onBoundaryArrow = onBoundaryArrow;
224
+ handleKey({ upArrow: true }, '', buffer, actions, cursor);
225
+ expect(onBoundaryArrow).toHaveBeenCalledWith('up');
226
+ onBoundaryArrow.mockClear();
227
+ handleKey({ downArrow: true }, '', buffer, actions, cursor);
228
+ expect(onBoundaryArrow).toHaveBeenCalledWith('down');
229
+ onBoundaryArrow.mockClear();
230
+ handleKey({ leftArrow: true }, '', buffer, actions, cursor);
231
+ expect(onBoundaryArrow).toHaveBeenCalledWith('left');
232
+ onBoundaryArrow.mockClear();
233
+ handleKey({ rightArrow: true }, '', buffer, actions, cursor);
234
+ expect(onBoundaryArrow).toHaveBeenCalledWith('right');
235
+ });
236
+ });
237
+ });
65
238
  describe('Editing', () => {
66
239
  it('handles Backspace', () => {
67
240
  handleKey({ backspace: true }, 'backspace', buffer, actions);
@@ -18,6 +18,10 @@ export interface Buffer {
18
18
  * Cursor movement directions
19
19
  */
20
20
  export type Direction = 'up' | 'down' | 'left' | 'right' | 'lineStart' | 'lineEnd';
21
+ /**
22
+ * Boundary arrow directions (subset of Direction used for boundary detection)
23
+ */
24
+ export type BoundaryDirection = 'up' | 'down' | 'left' | 'right';
21
25
  /**
22
26
  * Result of wrapping buffer lines for visual display
23
27
  */
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { MultilineInput } from './components/MultilineInput/index.js';
2
2
  export type { MultilineInputProps } from './components/MultilineInput/index.js';
3
+ export type { BoundaryDirection } from './components/MultilineInput/types.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ink-prompt",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A React Ink component for prompts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- export {};
@@ -1,9 +0,0 @@
1
- import React from 'react';
2
- import { render } from 'ink';
3
- import { MultilineInput } from '../src/index.js';
4
- const App = () => {
5
- return (React.createElement(MultilineInput
6
- // Add props here
7
- , null));
8
- };
9
- render(React.createElement(App, null));
@@ -1,15 +0,0 @@
1
- import { type Key, type Buffer, type Cursor } from './types';
2
- import { type UseTextInputResult } from './useTextInput';
3
- export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor'> {
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
- */
15
- export declare function handleKey(key: Partial<Key>, input: string, buffer: Buffer, actions: KeyHandlerActions, cursor?: Cursor): void;
@@ -1,97 +0,0 @@
1
- /**
2
- * Handles keyboard input and maps it to text input actions.
3
- *
4
- * @param key - The Ink key object
5
- * @param input - The input string (if any)
6
- * @param buffer - The current text buffer
7
- * @param actions - The actions available to modify the state
8
- * @param cursor - The current cursor position (optional, but required for some logic like backslash check)
9
- */
10
- export function handleKey(key, input, buffer, actions, cursor) {
11
- // Navigation
12
- if (key.upArrow) {
13
- actions.moveCursor('up');
14
- return;
15
- }
16
- if (key.downArrow) {
17
- actions.moveCursor('down');
18
- return;
19
- }
20
- if (key.leftArrow) {
21
- actions.moveCursor('left');
22
- return;
23
- }
24
- if (key.rightArrow) {
25
- actions.moveCursor('right');
26
- return;
27
- }
28
- // Home/End (Ink might not provide these directly in all environments, but if it does)
29
- // We check for 'home' and 'end' properties if they exist on the key object,
30
- // or specific sequences if we were parsing raw input, but here we assume Ink's Key object.
31
- // Note: Ink's Key interface might not have home/end in all versions, but we'll assume it does or we extend it.
32
- // If not, we might need to check specific input sequences, but for now let's trust the test/types.
33
- if (key.home) {
34
- actions.moveCursor('lineStart');
35
- return;
36
- }
37
- if (key.end) {
38
- actions.moveCursor('lineEnd');
39
- return;
40
- }
41
- // History
42
- if (key.ctrl) {
43
- if (input === 'z') {
44
- actions.undo();
45
- return;
46
- }
47
- if (input === 'y') {
48
- actions.redo();
49
- return;
50
- }
51
- if (input === 'j') {
52
- actions.newLine();
53
- return;
54
- }
55
- }
56
- // Editing
57
- if (key.backspace) {
58
- actions.delete();
59
- return;
60
- }
61
- if (key.delete) {
62
- // Currently mapped to delete (backspace behavior) as per requirements/tests,
63
- // but usually delete is forward.
64
- // The plan said "Delete (delete at cursor)", which usually means forward.
65
- // But our useTextInput only has `delete` (which is backspace).
66
- // For now, we map it to `delete` as per the test "handles Delete".
67
- actions.delete();
68
- return;
69
- }
70
- // Submission / New Line
71
- if (key.return) {
72
- if (cursor) {
73
- const currentLine = buffer.lines[cursor.line];
74
- // Check if line ends with backslash AND cursor is at the end (or we just check the line content?)
75
- // Requirement: "Line ending with \ + Enter continues to next line"
76
- // Usually this implies the user typed '\' then Enter.
77
- // We should probably check if the character *before* the cursor is '\' if we want to be precise,
78
- // or just if the line ends with '\'.
79
- // Let's assume "line ends with \" means the last char of the line is '\'.
80
- if (currentLine.endsWith('\\')) {
81
- actions.delete(); // Remove the backslash
82
- actions.newLine(); // Insert newline
83
- return;
84
- }
85
- }
86
- actions.submit();
87
- return;
88
- }
89
- // Text Insertion
90
- // Ignore control keys if they don't have a specific handler above
91
- if (key.ctrl || key.meta) {
92
- return;
93
- }
94
- if (input) {
95
- actions.insert(input);
96
- }
97
- }
@@ -1,34 +0,0 @@
1
- import type { Buffer, Cursor, Direction } from './types';
2
- /**
3
- * Create a new buffer from optional initial text
4
- */
5
- export declare function createBuffer(text?: string): Buffer;
6
- /**
7
- * Insert a character at the cursor position
8
- */
9
- export declare function insertChar(buffer: Buffer, cursor: Cursor, char: string): {
10
- buffer: Buffer;
11
- cursor: Cursor;
12
- };
13
- /**
14
- * Delete character before cursor (backspace)
15
- */
16
- export declare function deleteChar(buffer: Buffer, cursor: Cursor): {
17
- buffer: Buffer;
18
- cursor: Cursor;
19
- };
20
- /**
21
- * Insert a new line at cursor position (splits current line)
22
- */
23
- export declare function insertNewLine(buffer: Buffer, cursor: Cursor): {
24
- buffer: Buffer;
25
- cursor: Cursor;
26
- };
27
- /**
28
- * Move cursor in specified direction with bounds checking
29
- */
30
- export declare function moveCursor(buffer: Buffer, cursor: Cursor, direction: Direction): Cursor;
31
- /**
32
- * Get the full text content from buffer (lines joined with newlines)
33
- */
34
- export declare function getTextContent(buffer: Buffer): string;
@@ -1,127 +0,0 @@
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 a character at the cursor position
12
- */
13
- export function insertChar(buffer, cursor, char) {
14
- const { line, column } = cursor;
15
- const currentLine = buffer.lines[line];
16
- const newLine = currentLine.slice(0, column) + char + currentLine.slice(column);
17
- const newLines = [...buffer.lines];
18
- newLines[line] = newLine;
19
- return {
20
- buffer: { lines: newLines },
21
- cursor: { line, column: column + 1 },
22
- };
23
- }
24
- /**
25
- * Delete character before cursor (backspace)
26
- */
27
- export function deleteChar(buffer, cursor) {
28
- const { line, column } = cursor;
29
- // At the very start of the buffer - nothing to delete
30
- if (line === 0 && column === 0) {
31
- return { buffer, cursor };
32
- }
33
- // At the start of a line - merge with previous line
34
- if (column === 0) {
35
- const previousLine = buffer.lines[line - 1];
36
- const currentLine = buffer.lines[line];
37
- const mergedLine = previousLine + currentLine;
38
- const newLines = [...buffer.lines];
39
- newLines[line - 1] = mergedLine;
40
- newLines.splice(line, 1);
41
- return {
42
- buffer: { lines: newLines },
43
- cursor: { line: line - 1, column: previousLine.length },
44
- };
45
- }
46
- // Delete character within the line
47
- const currentLine = buffer.lines[line];
48
- const newLine = currentLine.slice(0, column - 1) + currentLine.slice(column);
49
- const newLines = [...buffer.lines];
50
- newLines[line] = newLine;
51
- return {
52
- buffer: { lines: newLines },
53
- cursor: { line, column: column - 1 },
54
- };
55
- }
56
- /**
57
- * Insert a new line at cursor position (splits current line)
58
- */
59
- export function insertNewLine(buffer, cursor) {
60
- const { line, column } = cursor;
61
- const currentLine = buffer.lines[line];
62
- const beforeCursor = currentLine.slice(0, column);
63
- const afterCursor = currentLine.slice(column);
64
- const newLines = [...buffer.lines];
65
- newLines[line] = beforeCursor;
66
- newLines.splice(line + 1, 0, afterCursor);
67
- return {
68
- buffer: { lines: newLines },
69
- cursor: { line: line + 1, column: 0 },
70
- };
71
- }
72
- /**
73
- * Move cursor in specified direction with bounds checking
74
- */
75
- export function moveCursor(buffer, cursor, direction) {
76
- const { line, column } = cursor;
77
- const currentLine = buffer.lines[line];
78
- const lineCount = buffer.lines.length;
79
- switch (direction) {
80
- case 'left':
81
- if (column > 0) {
82
- return { line, column: column - 1 };
83
- }
84
- // Wrap to end of previous line
85
- if (line > 0) {
86
- return { line: line - 1, column: buffer.lines[line - 1].length };
87
- }
88
- return cursor;
89
- case 'right':
90
- if (column < currentLine.length) {
91
- return { line, column: column + 1 };
92
- }
93
- // Wrap to start of next line
94
- if (line < lineCount - 1) {
95
- return { line: line + 1, column: 0 };
96
- }
97
- return cursor;
98
- case 'up':
99
- if (line > 0) {
100
- const targetLine = buffer.lines[line - 1];
101
- return { line: line - 1, column: Math.min(column, targetLine.length) };
102
- }
103
- return cursor;
104
- case 'down':
105
- if (line < lineCount - 1) {
106
- const targetLine = buffer.lines[line + 1];
107
- return { line: line + 1, column: Math.min(column, targetLine.length) };
108
- }
109
- return cursor;
110
- case 'lineStart':
111
- return { line, column: 0 };
112
- case 'lineEnd':
113
- return { line, column: currentLine.length };
114
- default:
115
- return cursor;
116
- }
117
- }
118
- /**
119
- * Get the full text content from buffer (lines joined with newlines)
120
- */
121
- export function getTextContent(buffer) {
122
- // Single empty line is considered empty buffer
123
- if (buffer.lines.length === 1 && buffer.lines[0] === '') {
124
- return '';
125
- }
126
- return buffer.lines.join('\n');
127
- }