@kolisachint/hoocode-tui 0.4.157 → 0.4.159
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/dist/components/editor.d.ts +25 -0
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +93 -34
- package/dist/components/editor.js.map +1 -1
- package/dist/editor-component.d.ts +3 -0
- package/dist/editor-component.d.ts.map +1 -1
- package/dist/editor-component.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -125,7 +125,23 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
|
|
|
125
125
|
// in a narrow terminal). Re-wrap it at grapheme granularity.
|
|
126
126
|
// The segment remains logically atomic for cursor
|
|
127
127
|
// movement / editing — the split is purely visual for word-wrap layout.
|
|
128
|
-
const
|
|
128
|
+
const subSegments = [...baseSegmenter.segment(grapheme)];
|
|
129
|
+
if (subSegments.length <= 1) {
|
|
130
|
+
// Nothing left to split: a single grapheme wider than the editor
|
|
131
|
+
// (a double-width character or emoji in a very narrow terminal).
|
|
132
|
+
// Emit it on its own chunk and let it overflow — recursing here
|
|
133
|
+
// would re-enter with identical arguments and never terminate.
|
|
134
|
+
const end = charIndex + grapheme.length;
|
|
135
|
+
if (chunkStart < charIndex) {
|
|
136
|
+
chunks.push({ text: line.slice(chunkStart, charIndex), startIndex: chunkStart, endIndex: charIndex });
|
|
137
|
+
}
|
|
138
|
+
chunks.push({ text: grapheme, startIndex: charIndex, endIndex: end });
|
|
139
|
+
chunkStart = end;
|
|
140
|
+
currentWidth = 0;
|
|
141
|
+
wrapOppIndex = -1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const subChunks = wordWrapLine(grapheme, maxWidth, subSegments);
|
|
129
145
|
for (let j = 0; j < subChunks.length - 1; j++) {
|
|
130
146
|
const sc = subChunks[j];
|
|
131
147
|
chunks.push({ text: sc.text, startIndex: charIndex + sc.startIndex, endIndex: charIndex + sc.endIndex });
|
|
@@ -147,10 +163,21 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
|
|
|
147
163
|
wrapOppWidth = currentWidth;
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
|
-
// Push final chunk
|
|
151
|
-
|
|
166
|
+
// Push final chunk, unless the line was already fully consumed (an atomic
|
|
167
|
+
// oversized grapheme ends exactly on the line end and needs no empty tail).
|
|
168
|
+
if (chunkStart < line.length || chunks.length === 0) {
|
|
169
|
+
chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
|
|
170
|
+
}
|
|
152
171
|
return chunks;
|
|
153
172
|
}
|
|
173
|
+
export const DEFAULT_EDITOR_BORDER_CHARS = {
|
|
174
|
+
horizontal: "─",
|
|
175
|
+
vertical: "│",
|
|
176
|
+
topLeft: "┌",
|
|
177
|
+
topRight: "┐",
|
|
178
|
+
bottomLeft: "└",
|
|
179
|
+
bottomRight: "┘",
|
|
180
|
+
};
|
|
154
181
|
const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
|
|
155
182
|
minPrimaryColumnWidth: 12,
|
|
156
183
|
maxPrimaryColumnWidth: 32,
|
|
@@ -167,6 +194,8 @@ export class Editor {
|
|
|
167
194
|
tui;
|
|
168
195
|
theme;
|
|
169
196
|
paddingX = 0;
|
|
197
|
+
border = "rule";
|
|
198
|
+
borderChars;
|
|
170
199
|
// Store last render width for cursor navigation
|
|
171
200
|
lastWidth = 80;
|
|
172
201
|
// Vertical scrolling support
|
|
@@ -219,6 +248,8 @@ export class Editor {
|
|
|
219
248
|
this.tui = tui;
|
|
220
249
|
this.theme = theme;
|
|
221
250
|
this.borderColor = theme.borderColor;
|
|
251
|
+
this.borderChars = { ...DEFAULT_EDITOR_BORDER_CHARS, ...theme.borderChars };
|
|
252
|
+
this.border = options.border ?? "rule";
|
|
222
253
|
const paddingX = options.paddingX ?? 0;
|
|
223
254
|
this.paddingX = Number.isFinite(paddingX) ? Math.max(0, Math.floor(paddingX)) : 0;
|
|
224
255
|
const maxVisible = options.autocompleteMaxVisible ?? 5;
|
|
@@ -242,6 +273,15 @@ export class Editor {
|
|
|
242
273
|
this.tui.requestRender();
|
|
243
274
|
}
|
|
244
275
|
}
|
|
276
|
+
getBorder() {
|
|
277
|
+
return this.border;
|
|
278
|
+
}
|
|
279
|
+
setBorder(border) {
|
|
280
|
+
if (this.border !== border) {
|
|
281
|
+
this.border = border;
|
|
282
|
+
this.tui.requestRender();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
245
285
|
getAutocompleteMaxVisible() {
|
|
246
286
|
return this.autocompleteMaxVisible;
|
|
247
287
|
}
|
|
@@ -321,19 +361,55 @@ export class Editor {
|
|
|
321
361
|
invalidate() {
|
|
322
362
|
// No cached state to invalidate currently
|
|
323
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Render a horizontal border, optionally carrying a scroll indicator.
|
|
366
|
+
* In box mode the bar is flanked by corners and padded so they stay aligned.
|
|
367
|
+
* @param edge - Which border to draw (selects arrow direction and corners)
|
|
368
|
+
* @param hidden - Number of layout lines hidden past this edge (0 = no indicator)
|
|
369
|
+
* @param barWidth - Width of the horizontal bar, excluding corners
|
|
370
|
+
* @param box - Whether to draw corners
|
|
371
|
+
*/
|
|
372
|
+
renderBorder(edge, hidden, barWidth, box) {
|
|
373
|
+
const chars = this.borderChars;
|
|
374
|
+
let bar;
|
|
375
|
+
if (hidden > 0) {
|
|
376
|
+
const arrow = edge === "top" ? "↑" : "↓";
|
|
377
|
+
const indicator = `${chars.horizontal.repeat(3)} ${arrow} ${hidden} more `;
|
|
378
|
+
const remaining = barWidth - visibleWidth(indicator);
|
|
379
|
+
bar = remaining >= 0 ? indicator + chars.horizontal.repeat(remaining) : truncateToWidth(indicator, barWidth);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
bar = chars.horizontal.repeat(barWidth);
|
|
383
|
+
}
|
|
384
|
+
if (!box)
|
|
385
|
+
return this.borderColor(bar);
|
|
386
|
+
// Corners must stay aligned, so pad back any width lost to truncation.
|
|
387
|
+
bar += chars.horizontal.repeat(Math.max(0, barWidth - visibleWidth(bar)));
|
|
388
|
+
const left = edge === "top" ? chars.topLeft : chars.bottomLeft;
|
|
389
|
+
const right = edge === "top" ? chars.topRight : chars.bottomRight;
|
|
390
|
+
return this.borderColor(`${left}${bar}${right}`);
|
|
391
|
+
}
|
|
324
392
|
render(width) {
|
|
325
|
-
|
|
393
|
+
// Box mode needs 2 columns for the side borders plus at least 1 content
|
|
394
|
+
// column; below that it degrades to rule mode rather than overflowing.
|
|
395
|
+
const box = this.border === "box" && width >= 4;
|
|
396
|
+
const borderWidth = box ? 1 : 0;
|
|
397
|
+
const innerWidth = Math.max(1, width - borderWidth * 2);
|
|
398
|
+
const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
|
|
326
399
|
const paddingX = Math.min(this.paddingX, maxPadding);
|
|
327
|
-
const contentWidth = Math.max(1,
|
|
400
|
+
const contentWidth = Math.max(1, innerWidth - paddingX * 2);
|
|
328
401
|
// Prompt prefix reserves space on the first line
|
|
329
402
|
const promptPrefixWidth = this.promptPrefix ? visibleWidth(`${this.promptPrefix} `) : 0;
|
|
330
|
-
// Layout width: with padding the cursor can overflow into
|
|
331
|
-
//
|
|
403
|
+
// Layout width: in rule mode with padding the cursor can overflow into the
|
|
404
|
+
// padding, so no column is reserved. Without padding, or in box mode where
|
|
405
|
+
// the cursor must never overwrite the side border, we reserve one.
|
|
332
406
|
// Also reserve space for prompt prefix on the first line.
|
|
333
|
-
const
|
|
407
|
+
const cursorColumn = box || !paddingX ? 1 : 0;
|
|
408
|
+
const layoutWidth = Math.max(1, contentWidth - cursorColumn - promptPrefixWidth);
|
|
334
409
|
// Store for cursor navigation (must match wrapping width)
|
|
335
410
|
this.lastWidth = layoutWidth;
|
|
336
|
-
const
|
|
411
|
+
const barWidth = box ? width - 2 : width;
|
|
412
|
+
const vertical = box ? this.borderColor(this.borderChars.vertical) : "";
|
|
337
413
|
// Layout the text
|
|
338
414
|
const layoutLines = this.layoutText(layoutWidth);
|
|
339
415
|
// Calculate max visible lines: 30% of terminal height, minimum 5 lines
|
|
@@ -359,19 +435,7 @@ export class Editor {
|
|
|
359
435
|
const leftPadding = " ".repeat(paddingX);
|
|
360
436
|
const rightPadding = leftPadding;
|
|
361
437
|
// Render top border (with scroll indicator if scrolled down)
|
|
362
|
-
|
|
363
|
-
const indicator = `─── ↑ ${this.scrollOffset} more `;
|
|
364
|
-
const remaining = width - visibleWidth(indicator);
|
|
365
|
-
if (remaining >= 0) {
|
|
366
|
-
result.push(this.borderColor(indicator + "─".repeat(remaining)));
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
result.push(this.borderColor(truncateToWidth(indicator, width)));
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
result.push(horizontal.repeat(width));
|
|
374
|
-
}
|
|
438
|
+
result.push(this.renderBorder("top", this.scrollOffset, barWidth, box));
|
|
375
439
|
// Render each visible layout line
|
|
376
440
|
// Emit hardware cursor marker only when focused and not showing autocomplete
|
|
377
441
|
const emitCursorMarker = this.focused && !this.autocompleteState;
|
|
@@ -402,7 +466,7 @@ export class Editor {
|
|
|
402
466
|
displayText = before + marker + cursor;
|
|
403
467
|
lineVisibleWidth = lineVisibleWidth + 1;
|
|
404
468
|
// If cursor overflows content width into the padding, flag it
|
|
405
|
-
if (lineVisibleWidth > contentWidth && paddingX > 0) {
|
|
469
|
+
if (!box && lineVisibleWidth > contentWidth && paddingX > 0) {
|
|
406
470
|
cursorInPadding = true;
|
|
407
471
|
}
|
|
408
472
|
}
|
|
@@ -416,26 +480,21 @@ export class Editor {
|
|
|
416
480
|
// Calculate padding based on actual visible width
|
|
417
481
|
const padding = " ".repeat(Math.max(0, contentWidth - lineVisibleWidth));
|
|
418
482
|
const lineRightPadding = cursorInPadding ? rightPadding.slice(1) : rightPadding;
|
|
419
|
-
// Render the line (
|
|
420
|
-
result.push(`${leftPadding}${displayText}${padding}${lineRightPadding}`);
|
|
483
|
+
// Render the line (side borders only in box mode)
|
|
484
|
+
result.push(`${vertical}${leftPadding}${displayText}${padding}${lineRightPadding}${vertical}`);
|
|
421
485
|
}
|
|
422
486
|
// Render bottom border (with scroll indicator if more content below)
|
|
423
487
|
const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
|
|
424
|
-
|
|
425
|
-
const indicator = `─── ↓ ${linesBelow} more `;
|
|
426
|
-
const remaining = width - visibleWidth(indicator);
|
|
427
|
-
result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
result.push(horizontal.repeat(width));
|
|
431
|
-
}
|
|
488
|
+
result.push(this.renderBorder("bottom", linesBelow, barWidth, box));
|
|
432
489
|
// Add autocomplete list if active
|
|
433
490
|
if (this.autocompleteState && this.autocompleteList) {
|
|
434
491
|
const autocompleteResult = this.autocompleteList.render(contentWidth);
|
|
492
|
+
// Align the dropdown to the inner text edge, not the outer box edge.
|
|
493
|
+
const acPadding = " ".repeat(borderWidth + paddingX);
|
|
435
494
|
for (const line of autocompleteResult) {
|
|
436
495
|
const lineWidth = visibleWidth(line);
|
|
437
496
|
const linePadding = " ".repeat(Math.max(0, contentWidth - lineWidth));
|
|
438
|
-
result.push(`${
|
|
497
|
+
result.push(`${acPadding}${line}${linePadding}${acPadding}`);
|
|
439
498
|
}
|
|
440
499
|
}
|
|
441
500
|
return result;
|