@denizokcu/haze 0.1.1 → 0.2.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.
@@ -50,7 +50,59 @@ function compactPasteBlocksForDisplay(value, blocks) {
50
50
  displayValue += value.slice(offset);
51
51
  return displayValue;
52
52
  }
53
- export function TextInput({ placeholder, disabled, mask, historyItems = [], recordHistory = true, suggestions = [], suggestionMode = 'slash', submitOnEmpty = false, onHistoryAdd, onCancel, onEscape, onSubmit }) {
53
+ function valueCursorForDisplayCursor(blocks, displayCursor) {
54
+ let valueCursor = displayCursor;
55
+ let displayOffset = 0;
56
+ for (const block of [...blocks].sort((a, b) => a.start - b.start)) {
57
+ const placeholderLength = pastePlaceholder(block).length;
58
+ const displayStart = block.start - displayOffset;
59
+ const displayEnd = displayStart + placeholderLength;
60
+ if (displayCursor <= displayStart)
61
+ break;
62
+ if (displayCursor <= displayEnd)
63
+ return block.end;
64
+ const compactedLength = block.end - block.start - placeholderLength;
65
+ valueCursor += compactedLength;
66
+ displayOffset += compactedLength;
67
+ }
68
+ return valueCursor;
69
+ }
70
+ function wrapDisplayValue(displayValue, width) {
71
+ const wrapWidth = Math.max(1, width);
72
+ const lines = [];
73
+ let start = 0;
74
+ let text = '';
75
+ for (let index = 0; index < displayValue.length; index += 1) {
76
+ const char = displayValue[index];
77
+ if (char === '\n') {
78
+ lines.push({ text, start, end: index });
79
+ start = index + 1;
80
+ text = '';
81
+ continue;
82
+ }
83
+ if (text.length >= wrapWidth) {
84
+ lines.push({ text, start, end: index });
85
+ start = index;
86
+ text = '';
87
+ }
88
+ text += char;
89
+ }
90
+ lines.push({ text, start, end: displayValue.length });
91
+ return lines;
92
+ }
93
+ function cursorPosition(lines, displayCursor) {
94
+ const foundIndex = lines.findIndex((line, index) => {
95
+ const isLast = index === lines.length - 1;
96
+ if (line.start === line.end)
97
+ return displayCursor === line.start;
98
+ const nextLineStartsAfterNewline = lines[index + 1]?.start === line.end + 1;
99
+ return displayCursor >= line.start && (displayCursor < line.end || (nextLineStartsAfterNewline && displayCursor === line.end) || (isLast && displayCursor <= line.end));
100
+ });
101
+ const lineIndex = Math.max(0, foundIndex);
102
+ const line = lines[lineIndex] ?? lines[0] ?? { start: 0, end: 0 };
103
+ return { lineIndex, column: Math.max(0, Math.min(displayCursor - line.start, line.end - line.start)) };
104
+ }
105
+ export function TextInput({ placeholder, disabled, mask, historyItems = [], recordHistory = true, suggestions = [], suggestionMode = 'slash', submitOnEmpty = false, width = 80, onHistoryAdd, onCancel, onEscape, onSubmit }) {
54
106
  const [value, setValue] = useState('');
55
107
  const [cursor, setCursor] = useState(0);
56
108
  const [pasteBlocks, setPasteBlocks] = useState([]);
@@ -59,6 +111,7 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
59
111
  const historyIndex = useRef(null);
60
112
  const draft = useRef('');
61
113
  const nextPasteId = useRef(1);
114
+ const preferredColumn = useRef(null);
62
115
  useEffect(() => {
63
116
  history.current = historyItems;
64
117
  }, [historyItems]);
@@ -74,6 +127,7 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
74
127
  }
75
128
  }, [disabled]);
76
129
  function setInput(next, nextCursor = next.length, nextPasteBlocks = []) {
130
+ preferredColumn.current = null;
77
131
  setValue(next);
78
132
  setCursor(Math.max(0, Math.min(nextCursor, next.length)));
79
133
  setPasteBlocks(nextPasteBlocks);
@@ -105,6 +159,24 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
105
159
  .slice(0, 8);
106
160
  const activeSuggestionIndex = Math.min(selectedSuggestionIndex, Math.max(0, filteredSuggestions.length - 1));
107
161
  const activeSuggestion = filteredSuggestions[activeSuggestionIndex];
162
+ const displayValue = mask ? '•'.repeat(value.length) : compactPasteBlocksForDisplay(value, pasteBlocks);
163
+ const displayCursor = mask ? cursor : displayCursorForValueCursor(pasteBlocks, cursor);
164
+ const inputWidth = Math.max(1, width - 2);
165
+ const wrappedLines = wrapDisplayValue(displayValue, inputWidth);
166
+ const currentCursorPosition = cursorPosition(wrappedLines, displayCursor);
167
+ function moveCursorToDisplayPosition(nextDisplayCursor) {
168
+ const clampedDisplayCursor = Math.max(0, Math.min(nextDisplayCursor, displayValue.length));
169
+ setCursor(mask ? clampedDisplayCursor : valueCursorForDisplayCursor(pasteBlocks, clampedDisplayCursor));
170
+ }
171
+ function moveCursorVertically(direction) {
172
+ const targetLine = wrappedLines[currentCursorPosition.lineIndex + direction];
173
+ if (!targetLine)
174
+ return false;
175
+ const column = preferredColumn.current ?? currentCursorPosition.column;
176
+ preferredColumn.current = column;
177
+ moveCursorToDisplayPosition(Math.min(targetLine.start + column, targetLine.end));
178
+ return true;
179
+ }
108
180
  function submitValue(submitted, historyValue = submitted) {
109
181
  if (recordHistory && historyValue) {
110
182
  if (history.current[history.current.length - 1] !== historyValue)
@@ -146,10 +218,12 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
146
218
  return;
147
219
  }
148
220
  if (key.leftArrow) {
221
+ preferredColumn.current = null;
149
222
  setCursor(current => Math.max(0, current - 1));
150
223
  return;
151
224
  }
152
225
  if (key.rightArrow) {
226
+ preferredColumn.current = null;
153
227
  setCursor(current => Math.min(value.length, current + 1));
154
228
  return;
155
229
  }
@@ -158,6 +232,9 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
158
232
  setSelectedSuggestionIndex(current => Math.max(0, current - 1));
159
233
  return;
160
234
  }
235
+ if (filteredSuggestions.length === 0 && moveCursorVertically(-1))
236
+ return;
237
+ preferredColumn.current = null;
161
238
  if (history.current.length === 0)
162
239
  return;
163
240
  if (historyIndex.current === null) {
@@ -174,6 +251,9 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
174
251
  setSelectedSuggestionIndex(current => Math.min(filteredSuggestions.length - 1, current + 1));
175
252
  return;
176
253
  }
254
+ if (filteredSuggestions.length === 0 && moveCursorVertically(1))
255
+ return;
256
+ preferredColumn.current = null;
177
257
  if (historyIndex.current === null)
178
258
  return;
179
259
  if (historyIndex.current < history.current.length - 1) {
@@ -198,10 +278,12 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
198
278
  return;
199
279
  }
200
280
  if (key.ctrl && input === 'a') {
281
+ preferredColumn.current = null;
201
282
  setCursor(0);
202
283
  return;
203
284
  }
204
285
  if (key.ctrl && input === 'e') {
286
+ preferredColumn.current = null;
205
287
  setCursor(value.length);
206
288
  return;
207
289
  }
@@ -211,10 +293,16 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
211
293
  replaceInput(cursor, cursor, input);
212
294
  }
213
295
  });
214
- const displayValue = mask ? '•'.repeat(value.length) : compactPasteBlocksForDisplay(value, pasteBlocks);
215
- const displayCursor = mask ? cursor : displayCursorForValueCursor(pasteBlocks, cursor);
216
- const beforeCursor = displayValue.slice(0, displayCursor);
217
- const cursorChar = displayValue[displayCursor] ?? ' ';
218
- const afterCursor = displayValue.slice(displayCursor + 1);
219
- return _jsxs(Box, { flexDirection: "column", width: "100%", children: [filteredSuggestions.length > 0 && _jsx(Box, { flexDirection: "column", marginBottom: 1, children: filteredSuggestions.map((suggestion, index) => _jsxs(Text, { color: index === activeSuggestionIndex ? theme.success : theme.muted, wrap: "truncate-end", children: [index === activeSuggestionIndex ? '› ' : ' ', suggestion.value, _jsxs(Text, { color: theme.muted, children: [" ", suggestion.kind ?? 'command', suggestion.description ? ` — ${suggestion.description}` : ''] })] }, suggestion.value)) }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: theme.purple, children: "\u203A " }), value.length === 0 ? _jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsxs(Text, { color: theme.muted, dimColor: true, children: [" ", placeholder ?? 'Type a message...'] })] }) : _jsxs(_Fragment, { children: [beforeCursor, _jsx(Text, { inverse: true, children: cursorChar }), afterCursor] })] })] });
296
+ const maxVisibleLines = 4;
297
+ const firstVisibleLine = Math.max(0, Math.min(currentCursorPosition.lineIndex - maxVisibleLines + 1, wrappedLines.length - maxVisibleLines));
298
+ const visibleLines = wrappedLines.slice(firstVisibleLine, firstVisibleLine + maxVisibleLines);
299
+ return _jsxs(Box, { flexDirection: "column", width: "100%", children: [filteredSuggestions.length > 0 && _jsx(Box, { flexDirection: "column", marginBottom: 1, children: filteredSuggestions.map((suggestion, index) => _jsxs(Text, { color: index === activeSuggestionIndex ? theme.success : theme.muted, wrap: "truncate-end", children: [index === activeSuggestionIndex ? '› ' : ' ', suggestion.value, _jsxs(Text, { color: theme.muted, children: [" ", suggestion.kind ?? 'command', suggestion.description ? ` — ${suggestion.description}` : ''] })] }, suggestion.value)) }), value.length === 0 ? _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: theme.purple, children: "\u203A " }), _jsx(Text, { inverse: true, children: " " }), _jsxs(Text, { color: theme.muted, dimColor: true, children: [" ", placeholder ?? 'Type a message...'] })] }) : visibleLines.map((line, index) => {
300
+ const absoluteLineIndex = firstVisibleLine + index;
301
+ const isCursorLine = absoluteLineIndex === currentCursorPosition.lineIndex;
302
+ const lineCursor = isCursorLine ? Math.max(0, Math.min(displayCursor - line.start, line.text.length)) : -1;
303
+ const beforeCursor = isCursorLine ? line.text.slice(0, lineCursor) : line.text;
304
+ const cursorChar = isCursorLine ? line.text[lineCursor] ?? ' ' : '';
305
+ const afterCursor = isCursorLine ? line.text.slice(lineCursor + 1) : '';
306
+ return _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: absoluteLineIndex === 0 ? theme.purple : theme.muted, children: absoluteLineIndex === 0 ? '› ' : ' ' }), isCursorLine ? _jsxs(_Fragment, { children: [beforeCursor, _jsx(Text, { inverse: true, children: cursorChar }), afterCursor] }) : line.text] }, `${line.start}-${absoluteLineIndex}`);
307
+ })] });
220
308
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denizokcu/haze",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A pragmatic agentic CLI for building apps from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",