dsh-ssh-tui 0.7.4 → 0.8.1
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.en.md +37 -0
- package/README.md +436 -572
- package/docs/display-mode.md +122 -0
- package/docs/remote-ops.md +104 -0
- package/docs/terminals.md +53 -0
- package/lib/attach.js +4 -4
- package/lib/attach.js.map +1 -1
- package/lib/auth-failure.js +128 -0
- package/lib/auth-failure.js.map +1 -0
- package/lib/commands.js +3 -0
- package/lib/commands.js.map +1 -1
- package/lib/dialogs.js +43 -0
- package/lib/dialogs.js.map +1 -1
- package/lib/display-mode.js +147 -0
- package/lib/display-mode.js.map +1 -0
- package/lib/display-sock.js +361 -21
- package/lib/display-sock.js.map +1 -1
- package/lib/footer.js +6 -9
- package/lib/footer.js.map +1 -1
- package/lib/glyph-measure.js +92 -0
- package/lib/glyph-measure.js.map +1 -0
- package/lib/i18n/en.js +27 -2
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +27 -2
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +74 -5
- package/lib/index.js.map +1 -1
- package/lib/paint.js +22 -9
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +14 -13
- package/lib/picker.js.map +1 -1
- package/lib/plan.js +11 -11
- package/lib/plan.js.map +1 -1
- package/lib/platform.js +96 -0
- package/lib/platform.js.map +1 -1
- package/lib/session-blank.js +81 -0
- package/lib/session-blank.js.map +1 -0
- package/lib/session-list.js +102 -81
- package/lib/session-list.js.map +1 -1
- package/lib/startup.js +7 -0
- package/lib/startup.js.map +1 -1
- package/lib/subagent-model.js +8 -7
- package/lib/subagent-model.js.map +1 -1
- package/lib/term-text.js +296 -28
- package/lib/term-text.js.map +1 -1
- package/lib/terminal-input.js +132 -10
- package/lib/terminal-input.js.map +1 -1
- package/lib/theme.js +318 -0
- package/lib/theme.js.map +1 -0
- package/lib/tool-present.js +11 -9
- package/lib/tool-present.js.map +1 -1
- package/lib/tui.js +535 -37
- package/lib/tui.js.map +1 -1
- package/lib/types/attach.d.ts +6 -2
- package/lib/types/auth-failure.d.ts +78 -0
- package/lib/types/commands.d.ts +9 -0
- package/lib/types/dialogs.d.ts +36 -0
- package/lib/types/display-mode.d.ts +99 -0
- package/lib/types/display-sock.d.ts +75 -0
- package/lib/types/footer.d.ts +1 -1
- package/lib/types/glyph-measure.d.ts +41 -0
- package/lib/types/index.d.ts +20 -0
- package/lib/types/plan.d.ts +5 -2
- package/lib/types/platform.d.ts +83 -0
- package/lib/types/session-blank.d.ts +51 -0
- package/lib/types/session-list.d.ts +34 -0
- package/lib/types/startup.d.ts +6 -0
- package/lib/types/subagent-model.d.ts +7 -6
- package/lib/types/term-text.d.ts +55 -23
- package/lib/types/terminal-input.d.ts +37 -0
- package/lib/types/theme.d.ts +109 -0
- package/lib/types/tool-present.d.ts +2 -2
- package/lib/types/tui.d.ts +121 -1
- package/package.json +92 -93
package/lib/term-text.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { t } from './i18n/index.js';
|
|
8
8
|
import { downgradeSgr } from './color-depth.js';
|
|
9
9
|
import { asciiFallbackEnabled } from './platform.js';
|
|
10
|
+
import { activeTheme, themeExtraToken } from './theme.js';
|
|
10
11
|
/**
|
|
11
12
|
* Codex-style compact elapsed: `0s`, `1m 05s`, `1h 01m 01s`.
|
|
12
13
|
* Used by the workspace wait card while the model has not streamed yet.
|
|
@@ -296,9 +297,246 @@ export function mapAsciiChrome(text) {
|
|
|
296
297
|
* Overflow into the input box is handled by clipping/padding painted rows to
|
|
297
298
|
* the measured column count, not by inflating glyph width.
|
|
298
299
|
*/
|
|
300
|
+
/**
|
|
301
|
+
* East-Asian Ambiguous families that a CJK-configured terminal draws two cells
|
|
302
|
+
* wide, and that this TUI therefore budgets two cells for.
|
|
303
|
+
*
|
|
304
|
+
* The reported bug was `①`-`⑩`: the model writes them, the terminal draws them
|
|
305
|
+
* from a CJK font in the space of two cells, and every row holding one came out
|
|
306
|
+
* a cell short — the same class of failure as the emoji glyphs above, with one
|
|
307
|
+
* important difference. Emoji can be *pinned* ({@link pinEmojiCells} asks for the
|
|
308
|
+
* text presentation and reserves the second cell) because they have variation
|
|
309
|
+
* selectors; these characters have none, so the only lever is to budget what the
|
|
310
|
+
* terminal will actually spend.
|
|
311
|
+
*
|
|
312
|
+
* What is deliberately **not** in this table matters as much. Box drawing (`─`),
|
|
313
|
+
* geometric ornaments (`●`, `▸`) and the chrome glyphs `❯` / `✓` are ambiguous
|
|
314
|
+
* too, but they were measured against a real terminal in the earlier emoji pass
|
|
315
|
+
* and they come from the monospace font at one cell; counting them as two is
|
|
316
|
+
* what once painted half-width rules and parked the cursor past the text. They
|
|
317
|
+
* stay one cell until a measurement says otherwise.
|
|
318
|
+
*/
|
|
319
|
+
const AMBIGUOUS_WIDE_RANGES = [
|
|
320
|
+
[0x2460, 0x24ff], // Enclosed Alphanumerics: ① ⑩ ⑳ ⑴ ⒈ ⓐ ⓿
|
|
321
|
+
[0x2776, 0x2793], // Dingbat circled digits: ❶ ➀ ➊ ➓
|
|
322
|
+
[0x2160, 0x217f], // Roman numerals: Ⅰ Ⅱ ⅰ ⅻ
|
|
323
|
+
[0x1f100, 0x1f1ff], // Enclosed Alphanumeric Supplement, incl. regional indicators
|
|
324
|
+
[0x1f200, 0x1f2ff], // Enclosed Ideographic Supplement
|
|
325
|
+
// Punctuation that a CJK font sets **full width**, and that a Chinese
|
|
326
|
+
// transcript is full of: an em dash, an ellipsis and curly quotes are 全角 in
|
|
327
|
+
// GB2312/GBK. A real session carries `—` tens of thousands of times, so
|
|
328
|
+
// budgeting one cell for it is not a corner case — it is every line.
|
|
329
|
+
[0x00b7, 0x00b7], // · middle dot
|
|
330
|
+
[0x2013, 0x2014], // – — dashes
|
|
331
|
+
[0x2018, 0x201d], // ‘ ’ “ ” quotes
|
|
332
|
+
[0x2022, 0x2022], // • bullet
|
|
333
|
+
[0x2025, 0x2026], // ‥ … ellipsis
|
|
334
|
+
[0x2039, 0x203a], // ‹ › guillemets
|
|
335
|
+
];
|
|
336
|
+
/** Cached decision for {@link ambiguousWidthIsTwo}, keyed on the inputs. */
|
|
337
|
+
let ambiguousCache;
|
|
338
|
+
/**
|
|
339
|
+
* What a measurement of the real terminal said, when there was one.
|
|
340
|
+
*
|
|
341
|
+
* The locale is only a guess about which font the terminal is configured with,
|
|
342
|
+
* and a guess is what produced three rounds of "① is misaligned" reports: the
|
|
343
|
+
* terminal either advances two cells for it or it does not, and that is
|
|
344
|
+
* measurable (`CSI 6n` after the glyph). The launcher measures once at boot, in
|
|
345
|
+
* the process that owns the terminal, and sets this. `undefined` means nobody
|
|
346
|
+
* measured — a pipe, a dumb terminal, or a launch that never got that far — and
|
|
347
|
+
* the locale decides as before.
|
|
348
|
+
*/
|
|
349
|
+
let ambiguousMeasured;
|
|
350
|
+
/**
|
|
351
|
+
* Whether the second cell must be *reserved* with a space.
|
|
352
|
+
*
|
|
353
|
+
* An ambiguous glyph can be drawn wider than the single cell it advances — the
|
|
354
|
+
* same failure the emoji symbols have, and the one behind "`①` collides with the
|
|
355
|
+
* character after it". Emoji can be pinned by asking for the text presentation
|
|
356
|
+
* with VS15; `①` has no variation sequence, so the only half of that trick
|
|
357
|
+
* available is the reserving space: print the glyph, then a space, and the next
|
|
358
|
+
* character starts on a clean cell while the layout still spends two.
|
|
359
|
+
*
|
|
360
|
+
* Set from the measurement, because the two cases need opposite treatment:
|
|
361
|
+
* a terminal that advances two cells spends them itself (`wide`), while one that
|
|
362
|
+
* advances one cell needs the space (`reserve`).
|
|
363
|
+
*/
|
|
364
|
+
let ambiguousReserve;
|
|
365
|
+
/**
|
|
366
|
+
* The resolved policy, cached.
|
|
367
|
+
*
|
|
368
|
+
* This used to be worked out *inside* `displayWidth`'s per-character loop, which
|
|
369
|
+
* read `process.env` and ran regexes for every character of every painted line —
|
|
370
|
+
* a profile of a 2000-row frame put ~75% of the samples there, and it is why
|
|
371
|
+
* rendering in a long session felt slow. The policy cannot change during a frame,
|
|
372
|
+
* so it is resolved once and read as plain booleans afterwards; the setters below
|
|
373
|
+
* and `ambiguousPolicy()`'s own cheap key check are what invalidate it.
|
|
374
|
+
*/
|
|
375
|
+
let ambiguousPolicyCache;
|
|
376
|
+
/** The three inputs the policy depends on, as one cheap-to-compare string. */
|
|
377
|
+
function ambiguousPolicyKey(env, onTerminal) {
|
|
378
|
+
return `${env.DSH_TUI_AMBIGUOUS_WIDTH ?? ''}|${env.DSH_TUI_AMBIGUOUS_RESERVE ?? ''}|`
|
|
379
|
+
+ `${env.LC_ALL ?? ''}|${env.LC_CTYPE ?? ''}|${env.LANG ?? ''}|`
|
|
380
|
+
+ `${onTerminal ? 'tty' : 'pipe'}|${String(ambiguousMeasured)}|${String(ambiguousReserve)}`;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Resolve the ambiguous-glyph policy for this process.
|
|
384
|
+
*
|
|
385
|
+
* Cheap enough to call once per line (it compares one string) and far too
|
|
386
|
+
* expensive to call per character, which is the distinction that matters.
|
|
387
|
+
* @param env - the environment to read.
|
|
388
|
+
* @param onTerminal - whether the output is a terminal at all.
|
|
389
|
+
* @returns whether the glyphs are two cells wide, and whether the second cell is
|
|
390
|
+
* ours to reserve.
|
|
391
|
+
*/
|
|
392
|
+
export function ambiguousPolicy(env = process.env, onTerminal = process.stdout?.isTTY === true) {
|
|
393
|
+
const key = ambiguousPolicyKey(env, onTerminal);
|
|
394
|
+
const cached = ambiguousPolicyCache;
|
|
395
|
+
if (cached !== undefined && cached.key === key)
|
|
396
|
+
return cached;
|
|
397
|
+
const override = String(env.DSH_TUI_AMBIGUOUS_WIDTH ?? '').trim();
|
|
398
|
+
const reserveOverride = String(env.DSH_TUI_AMBIGUOUS_RESERVE ?? '').trim();
|
|
399
|
+
const locale = `${env.LC_ALL ?? ''} ${env.LC_CTYPE ?? ''} ${env.LANG ?? ''}`.toLowerCase();
|
|
400
|
+
const localeIsCjk = /(?:^|[^a-z])(?:zh|ja|ko)[_@.-]/u.test(locale) || /(?:^|\s)(?:zh|ja|ko)(?:\s|$)/u.test(locale);
|
|
401
|
+
let wide;
|
|
402
|
+
if (override === '1')
|
|
403
|
+
wide = false;
|
|
404
|
+
else if (override === '2')
|
|
405
|
+
wide = true;
|
|
406
|
+
else if (ambiguousMeasured !== undefined)
|
|
407
|
+
wide = ambiguousMeasured;
|
|
408
|
+
else
|
|
409
|
+
wide = onTerminal && localeIsCjk;
|
|
410
|
+
// The reservation is only worth its space when the terminal advances one cell
|
|
411
|
+
// for a glyph it draws wider than a cell; a measurement is the evidence for it,
|
|
412
|
+
// and the environment is the manual override.
|
|
413
|
+
let reserve = ambiguousReserve ?? (reserveOverride === '1' || reserveOverride === 'true');
|
|
414
|
+
if (override === '1' || override === '2')
|
|
415
|
+
reserve = ambiguousReserve ?? false;
|
|
416
|
+
ambiguousPolicyCache = { key, wide, reserve };
|
|
417
|
+
return ambiguousPolicyCache;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* The reserve setting from the environment, when a parent passed one.
|
|
421
|
+
*
|
|
422
|
+
* The Host cannot measure, so its launcher hands the verdict over in the
|
|
423
|
+
* environment alongside the advance: `DSH_TUI_AMBIGUOUS_WIDTH` says how many
|
|
424
|
+
* cells the terminal spends, `DSH_TUI_AMBIGUOUS_RESERVE` says whether the second
|
|
425
|
+
* one has to be spent by us. Reading it lazily keeps this module free of a
|
|
426
|
+
* startup order: the value is a property of the deployment, not of the call.
|
|
427
|
+
*/
|
|
428
|
+
function reserveFromEnv() {
|
|
429
|
+
const raw = String(process.env.DSH_TUI_AMBIGUOUS_RESERVE ?? '').trim();
|
|
430
|
+
return raw === '1' || raw === 'true';
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Record what the terminal's own answer said about these glyphs.
|
|
434
|
+
*
|
|
435
|
+
* Called by the launcher after measuring; also the escape hatch for a caller
|
|
436
|
+
* that knows better than the locale (a relay that has already measured for its
|
|
437
|
+
* own accounting, or a test).
|
|
438
|
+
* @param wide - true when the terminal advances two cells, false for one, or
|
|
439
|
+
* undefined to fall back to the locale.
|
|
440
|
+
*/
|
|
441
|
+
export function setAmbiguousWidthMeasured(wide) {
|
|
442
|
+
ambiguousMeasured = wide;
|
|
443
|
+
ambiguousPolicyCache = undefined;
|
|
444
|
+
// A terminal that advances one cell for a glyph drawn wider than a cell is
|
|
445
|
+
// exactly the collision case; reserve the second cell so the next character is
|
|
446
|
+
// not painted on top of it.
|
|
447
|
+
ambiguousReserve = wide === false;
|
|
448
|
+
ambiguousCache = undefined;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Force the reserve behaviour, for a caller that measured the same terminal for
|
|
452
|
+
* its own accounting (a relay) or a test.
|
|
453
|
+
* @param reserve - whether to spend a space after each ambiguous glyph.
|
|
454
|
+
*/
|
|
455
|
+
export function setAmbiguousWidthReserve(reserve) {
|
|
456
|
+
ambiguousReserve = reserve;
|
|
457
|
+
ambiguousPolicyCache = undefined;
|
|
458
|
+
ambiguousCache = undefined;
|
|
459
|
+
}
|
|
460
|
+
/** Whether the second cell is currently reserved with a space. */
|
|
461
|
+
export function ambiguousWidthReserved() {
|
|
462
|
+
return ambiguousPolicy().reserve;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Whether one code point is in a family the ambiguous policy governs.
|
|
466
|
+
*
|
|
467
|
+
* A scan over a handful of pairs, cheap enough for the per-character path — which
|
|
468
|
+
* is why the policy booleans are resolved once per line and passed in, rather
|
|
469
|
+
* than looked up per character.
|
|
470
|
+
* @param cp - the code point.
|
|
471
|
+
* @returns true when the ambiguous table applies to it.
|
|
472
|
+
*/
|
|
473
|
+
function inAmbiguousRange(cp) {
|
|
474
|
+
for (const [start, end] of AMBIGUOUS_WIDE_RANGES) {
|
|
475
|
+
if (cp >= start && cp <= end)
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Cells one character costs in this TUI's layout.
|
|
482
|
+
*
|
|
483
|
+
* The reserve case still costs two: the glyph advances one cell and the reserving
|
|
484
|
+
* space takes the next, so the layout must budget both or every row holding one
|
|
485
|
+
* comes up short. Non-ambiguous characters are unchanged.
|
|
486
|
+
* @param cp - the code point.
|
|
487
|
+
* @returns 0, 1 or 2 cells.
|
|
488
|
+
*/
|
|
489
|
+
export function ambiguousCellCost(cp) {
|
|
490
|
+
if (!inAmbiguousRange(cp))
|
|
491
|
+
return 0;
|
|
492
|
+
const policy = ambiguousPolicy();
|
|
493
|
+
return policy.reserve || policy.wide ? 2 : 1;
|
|
494
|
+
}
|
|
495
|
+
/** What the last measurement decided, for diagnostics and tests. */
|
|
496
|
+
export function ambiguousWidthMeasured() {
|
|
497
|
+
return ambiguousMeasured;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Whether ambiguous glyphs in {@link AMBIGUOUS_WIDE_RANGES} are drawn two cells
|
|
501
|
+
* wide here.
|
|
502
|
+
*
|
|
503
|
+
* `DSH_TUI_AMBIGUOUS_WIDTH=1|2` answers outright. Otherwise the locale decides,
|
|
504
|
+
* and only when there really is a terminal: a zh/ja/ko locale means the terminal
|
|
505
|
+
* is very likely using a CJK font, where these glyphs are full width. The UI
|
|
506
|
+
* language is deliberately *not* consulted — a Chinese reader on a Western
|
|
507
|
+
* terminal has narrow glyphs, and typing in Chinese does not change the font
|
|
508
|
+
* metrics.
|
|
509
|
+
* @param env - the environment to read (tests pass their own).
|
|
510
|
+
* @param onTerminal - whether the output is a terminal at all; a pipe or a test
|
|
511
|
+
* harness has no font metrics, so there the narrow default applies.
|
|
512
|
+
* @returns true when those glyphs should be budgeted two cells.
|
|
513
|
+
*/
|
|
514
|
+
export function ambiguousWidthIsTwo(env = process.env, onTerminal = process.stdout?.isTTY === true) {
|
|
515
|
+
return ambiguousPolicy(env, onTerminal).wide;
|
|
516
|
+
}
|
|
299
517
|
export function displayWidth(text) {
|
|
300
518
|
let width = 0;
|
|
301
|
-
|
|
519
|
+
const measured = mapAsciiChrome(text);
|
|
520
|
+
// Resolved once per call rather than per character: this runs for every
|
|
521
|
+
// character of every painted line, and the old lookup read `process.env` and
|
|
522
|
+
// ran a regex each time (a profile of a 2000-row frame put ~75% of its samples
|
|
523
|
+
// there). Here it costs one string comparison for the whole line.
|
|
524
|
+
const policy = ambiguousPolicy();
|
|
525
|
+
for (let index = 0; index < measured.length;) {
|
|
526
|
+
const char = measured[index] ?? '';
|
|
527
|
+
if (char === '')
|
|
528
|
+
break;
|
|
529
|
+
// A reserved glyph plus our own space is ONE two-cell unit: the pin adds the
|
|
530
|
+
// space so the terminal spends the second cell, and counting it again here
|
|
531
|
+
// would make every padded row one cell short.
|
|
532
|
+
if (char !== ' ' && policy.reserve && inAmbiguousRange(char.codePointAt(0) ?? 0)) {
|
|
533
|
+
width += 2;
|
|
534
|
+
index += char.length;
|
|
535
|
+
if (measured[index] === ' ')
|
|
536
|
+
index += 1;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
index += char.length;
|
|
302
540
|
if (char === '\t') {
|
|
303
541
|
// Tabs are expanded to spaces before rendering; keep the width
|
|
304
542
|
// calculation consistent with `sanitizeTerminalText()`.
|
|
@@ -317,7 +555,10 @@ export function displayWidth(text) {
|
|
|
317
555
|
if (cp <= 0x1f || (cp >= 0x7f && cp <= 0x9f)) {
|
|
318
556
|
continue;
|
|
319
557
|
}
|
|
320
|
-
|
|
558
|
+
// NOTE: `index` has already advanced past this character above; the branches
|
|
559
|
+
// below only decide how many cells it cost.
|
|
560
|
+
const wide = ((policy.wide || policy.reserve) && inAmbiguousRange(cp)) ||
|
|
561
|
+
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
321
562
|
cp === 0x2329 || cp === 0x232a ||
|
|
322
563
|
(cp >= 0x2e80 && cp <= 0xa4cf) ||
|
|
323
564
|
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
@@ -362,6 +603,7 @@ export function pinEmojiCells(text) {
|
|
|
362
603
|
const mapped = mapAsciiChrome(text);
|
|
363
604
|
if (mapped !== text)
|
|
364
605
|
return mapped;
|
|
606
|
+
const reserveWide = ambiguousPolicy().reserve;
|
|
365
607
|
let out = '';
|
|
366
608
|
let index = 0;
|
|
367
609
|
while (index < text.length) {
|
|
@@ -371,6 +613,15 @@ export function pinEmojiCells(text) {
|
|
|
371
613
|
const char = String.fromCodePoint(cp);
|
|
372
614
|
index += char.length;
|
|
373
615
|
out += char;
|
|
616
|
+
// An ambiguous glyph on a terminal that advances it one cell while drawing it
|
|
617
|
+
// wider: reserve the second cell so the next character is not painted on top
|
|
618
|
+
// of it. Skipped when the glyph is already followed by a space (the run may
|
|
619
|
+
// have been pinned once and be repainted), so pinning stays idempotent.
|
|
620
|
+
if (reserveWide && inAmbiguousRange(cp)) {
|
|
621
|
+
if (!/^[ ]/u.test(text.slice(index)))
|
|
622
|
+
out += ' ';
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
374
625
|
if (!EMOJI_SYMBOLS.has(cp) || EMOJI_PRESENTATION_SYMBOLS.has(cp))
|
|
375
626
|
continue;
|
|
376
627
|
const selector = text.codePointAt(index);
|
|
@@ -410,7 +661,10 @@ export function padAnsiToWidth(text, width) {
|
|
|
410
661
|
}
|
|
411
662
|
/** Visible width of an ANSI-styled line, ignoring CSI / OSC sequences. */
|
|
412
663
|
export function visibleWidth(text) {
|
|
413
|
-
|
|
664
|
+
// Strip the escapes, then ask the one width function: summing per character
|
|
665
|
+
// here would count a reserved glyph's space twice (see `displayWidth`), and
|
|
666
|
+
// two width notions in one renderer is how rows drift apart.
|
|
667
|
+
let plain = '';
|
|
414
668
|
let index = 0;
|
|
415
669
|
while (index < text.length) {
|
|
416
670
|
if (text.charCodeAt(index) === 0x1b) {
|
|
@@ -421,10 +675,10 @@ export function visibleWidth(text) {
|
|
|
421
675
|
if (cp === undefined)
|
|
422
676
|
break;
|
|
423
677
|
const char = String.fromCodePoint(cp);
|
|
424
|
-
|
|
678
|
+
plain += char;
|
|
425
679
|
index += char.length;
|
|
426
680
|
}
|
|
427
|
-
return
|
|
681
|
+
return displayWidth(plain);
|
|
428
682
|
}
|
|
429
683
|
/** Advance past one ESC sequence starting at `index`. */
|
|
430
684
|
export function skipAnsiSequence(text, index) {
|
|
@@ -848,27 +1102,30 @@ function wrapMarkdownSegments(segments, width, prefixSegments = []) {
|
|
|
848
1102
|
return lines.map(line => line.length === 0 ? [{ kind: 'text', text: '' }] : line);
|
|
849
1103
|
}
|
|
850
1104
|
function markdownSegmentCode(kind) {
|
|
1105
|
+
const theme = activeTheme();
|
|
851
1106
|
switch (kind) {
|
|
852
1107
|
// Bright + bold so **span** still pops when the font has no heavy CJK weight.
|
|
853
|
-
case 'bold': return '
|
|
854
|
-
case 'italic': return '
|
|
855
|
-
case 'code': return '
|
|
856
|
-
case 'link': return '
|
|
857
|
-
case 'muted': return '
|
|
1108
|
+
case 'bold': return themeExtraToken(theme, 'md-bold');
|
|
1109
|
+
case 'italic': return themeExtraToken(theme, 'md-italic');
|
|
1110
|
+
case 'code': return themeExtraToken(theme, 'md-code');
|
|
1111
|
+
case 'link': return themeExtraToken(theme, 'md-link');
|
|
1112
|
+
case 'muted': return themeExtraToken(theme, 'md-muted');
|
|
858
1113
|
default: return '';
|
|
859
1114
|
}
|
|
860
1115
|
}
|
|
861
1116
|
function markdownBaseCode(kind) {
|
|
1117
|
+
const theme = activeTheme();
|
|
862
1118
|
switch (kind) {
|
|
863
|
-
case 'heading1': return '
|
|
864
|
-
case 'heading2': return '
|
|
865
|
-
case 'heading3': return '
|
|
866
|
-
case 'code': return '
|
|
867
|
-
case 'quote': return '
|
|
868
|
-
case 'rule': return '
|
|
869
|
-
// Body
|
|
870
|
-
//
|
|
871
|
-
|
|
1119
|
+
case 'heading1': return themeExtraToken(theme, 'md-h1');
|
|
1120
|
+
case 'heading2': return themeExtraToken(theme, 'md-h2');
|
|
1121
|
+
case 'heading3': return themeExtraToken(theme, 'md-h3');
|
|
1122
|
+
case 'code': return themeExtraToken(theme, 'md-code');
|
|
1123
|
+
case 'quote': return themeExtraToken(theme, 'md-quote');
|
|
1124
|
+
case 'rule': return themeExtraToken(theme, 'md-rule');
|
|
1125
|
+
// Body keeps the terminal's own foreground: freezing it to white was
|
|
1126
|
+
// unreadable on a light background, and bold/italic runs carry their own
|
|
1127
|
+
// emphasis (CJK fonts often have only one weight).
|
|
1128
|
+
default: return '';
|
|
872
1129
|
}
|
|
873
1130
|
}
|
|
874
1131
|
function wrapHyperlink(label, href, hyperlinks) {
|
|
@@ -887,14 +1144,19 @@ function renderMarkdownBlockLine(block, color, hyperlinks) {
|
|
|
887
1144
|
return segments.map(segment => wrapHyperlink(segment.text, segment.href, hyperlinks)).join('');
|
|
888
1145
|
}
|
|
889
1146
|
const base = markdownBaseCode(block.base);
|
|
890
|
-
|
|
1147
|
+
// An empty base means "the terminal's own foreground", and opening with
|
|
1148
|
+
// `\x1b[m` would emit a reset the row does not need — on a slow link that is
|
|
1149
|
+
// one wasted escape per line, and it makes the transcript harder to read in a
|
|
1150
|
+
// log. So the base is only emitted when the palette actually asks for it.
|
|
1151
|
+
const open = base === '' ? '' : `\x1b[${base}m`;
|
|
1152
|
+
let out = open;
|
|
891
1153
|
for (const segment of segments) {
|
|
892
1154
|
const code = markdownSegmentCode(segment.kind);
|
|
893
1155
|
const body = code === ''
|
|
894
1156
|
? segment.text
|
|
895
1157
|
// SGR 0 first: restoring only the base codes does not clear italic,
|
|
896
1158
|
// underline, or bold, so those attributes would leak into later spans.
|
|
897
|
-
: `\x1b[${code}m${segment.text}\x1b[0m
|
|
1159
|
+
: `\x1b[${code}m${segment.text}\x1b[0m${open}`;
|
|
898
1160
|
out += wrapHyperlink(body, segment.href, hyperlinks);
|
|
899
1161
|
}
|
|
900
1162
|
return `${out}\x1b[0m`;
|
|
@@ -1150,6 +1412,8 @@ function backwardSliceByWidth(text, end, maxWidth) {
|
|
|
1150
1412
|
* caret in the middle of later text. Fold the *current line* (between the
|
|
1151
1413
|
* surrounding newlines) and keep `\n` out of the visible slice.
|
|
1152
1414
|
*/
|
|
1415
|
+
/** The character that marks a folded edge of the input row. */
|
|
1416
|
+
const FOLD_MARKER = '…';
|
|
1153
1417
|
export function foldInputView(input, cursor, maxWidth) {
|
|
1154
1418
|
const width = Math.max(1, maxWidth);
|
|
1155
1419
|
const safeCursor = Math.max(0, Math.min(cursor, input.length));
|
|
@@ -1168,16 +1432,16 @@ export function foldInputView(input, cursor, maxWidth) {
|
|
|
1168
1432
|
// One-row fold: keep a blank cell for the caret when the line fills
|
|
1169
1433
|
// the row, otherwise CSI lands on the last glyph.
|
|
1170
1434
|
if (cursorOffset >= width && width > 1) {
|
|
1171
|
-
let budget = width - 1;
|
|
1435
|
+
let budget = Math.max(1, width - 1);
|
|
1172
1436
|
const probe = backwardSliceByWidth(line, lineCursor, budget);
|
|
1173
1437
|
const left = probe.start > 0;
|
|
1174
1438
|
if (left)
|
|
1175
|
-
budget = Math.max(1, width -
|
|
1439
|
+
budget = Math.max(1, width - displayWidth(FOLD_MARKER) - 1);
|
|
1176
1440
|
const clipped = backwardSliceByWidth(line, lineCursor, budget);
|
|
1177
1441
|
const beforeText = line.slice(clipped.start, lineCursor);
|
|
1178
1442
|
return {
|
|
1179
|
-
text: `${left ?
|
|
1180
|
-
cursorOffset: (left ?
|
|
1443
|
+
text: `${left ? FOLD_MARKER : ''}${beforeText}`,
|
|
1444
|
+
cursorOffset: (left ? displayWidth(FOLD_MARKER) : 0) + displayWidth(beforeText),
|
|
1181
1445
|
folded: true,
|
|
1182
1446
|
};
|
|
1183
1447
|
}
|
|
@@ -1187,7 +1451,11 @@ export function foldInputView(input, cursor, maxWidth) {
|
|
|
1187
1451
|
const after = totalWidth - cursorOffset;
|
|
1188
1452
|
const leftFolded = before > 0;
|
|
1189
1453
|
const rightFolded = after > 0;
|
|
1190
|
-
|
|
1454
|
+
// The marker's own width, not a hardcoded cell: `…` is full width on a CJK
|
|
1455
|
+
// terminal, and counting it as one made the folded row overrun its budget and
|
|
1456
|
+
// put the caret a column off.
|
|
1457
|
+
const markerWidth = displayWidth(FOLD_MARKER);
|
|
1458
|
+
const markers = (leftFolded ? markerWidth : 0) + (rightFolded ? markerWidth : 0);
|
|
1191
1459
|
// Leave one cell for the caret so it never sits on the last glyph
|
|
1192
1460
|
// (DEC auto-margin would otherwise punch the caret through that cell).
|
|
1193
1461
|
const available = Math.max(1, width - markers - 1);
|
|
@@ -1200,8 +1468,8 @@ export function foldInputView(input, cursor, maxWidth) {
|
|
|
1200
1468
|
const afterSlice = forwardSliceByWidth(line.slice(lineCursor), afterBudget);
|
|
1201
1469
|
const beforeText = line.slice(beforeSlice.start, lineCursor);
|
|
1202
1470
|
return {
|
|
1203
|
-
text: `${leftFolded ?
|
|
1204
|
-
cursorOffset: (leftFolded ?
|
|
1471
|
+
text: `${leftFolded ? FOLD_MARKER : ''}${beforeText}${afterSlice.text}${rightFolded ? FOLD_MARKER : ''}`,
|
|
1472
|
+
cursorOffset: (leftFolded ? markerWidth : 0) + displayWidth(beforeText),
|
|
1205
1473
|
folded: true,
|
|
1206
1474
|
};
|
|
1207
1475
|
}
|