feat: image in ui
Browse files- src/ink/log-update.ts +19 -1
- src/ink/output.ts +54 -35
- src/ink/screen.ts +12 -1
src/ink/log-update.ts
CHANGED
|
@@ -302,10 +302,22 @@ export class LogUpdate {
|
|
| 302 |
let currentStyleId = stylePool.none
|
| 303 |
let currentHyperlink: Hyperlink = undefined
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
// First pass: render changes to existing rows (rows < prev.screen.height)
|
| 306 |
let needsFullReset = false
|
| 307 |
let resetTriggerY = -1
|
| 308 |
diffEach(prev.screen, next.screen, (x, y, removed, added) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
// Skip new rows - we'll render them directly after
|
| 310 |
if (growing && y >= prev.screen.height) {
|
| 311 |
return
|
|
@@ -400,8 +412,14 @@ export class LogUpdate {
|
|
| 400 |
undefined,
|
| 401 |
)
|
| 402 |
|
| 403 |
-
// Handle growth:
|
| 404 |
if (growing) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
renderFrameSlice(
|
| 406 |
screen,
|
| 407 |
next,
|
|
|
|
| 302 |
let currentStyleId = stylePool.none
|
| 303 |
let currentHyperlink: Hyperlink = undefined
|
| 304 |
|
| 305 |
+
// Emit per-row raw writes (APC, Kitty protocol) before cell diff loop.
|
| 306 |
+
// Track which rows have been emitted to avoid duplicates across damage regions.
|
| 307 |
+
const emittedRawRows = new Set<number>()
|
| 308 |
+
|
| 309 |
// First pass: render changes to existing rows (rows < prev.screen.height)
|
| 310 |
let needsFullReset = false
|
| 311 |
let resetTriggerY = -1
|
| 312 |
diffEach(prev.screen, next.screen, (x, y, removed, added) => {
|
| 313 |
+
// Emit raw write (e.g. Kitty APC) for this row if not yet emitted
|
| 314 |
+
if (!emittedRawRows.has(y)) {
|
| 315 |
+
emittedRawRows.add(y)
|
| 316 |
+
const rawWrite = next.screen.rawWritesAtRow.get(y)
|
| 317 |
+
if (rawWrite) {
|
| 318 |
+
screen.diff.push({ type: 'stdout', content: rawWrite })
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
// Skip new rows - we'll render them directly after
|
| 322 |
if (growing && y >= prev.screen.height) {
|
| 323 |
return
|
|
|
|
| 412 |
undefined,
|
| 413 |
)
|
| 414 |
|
| 415 |
+
// Handle growth: emit raw writes for new rows, then render cells
|
| 416 |
if (growing) {
|
| 417 |
+
for (let y = prev.screen.height; y < next.screen.height; y++) {
|
| 418 |
+
const rawWrite = next.screen.rawWritesAtRow.get(y)
|
| 419 |
+
if (rawWrite) {
|
| 420 |
+
screen.diff.push({ type: 'stdout', content: rawWrite })
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
renderFrameSlice(
|
| 424 |
screen,
|
| 425 |
next,
|
src/ink/output.ts
CHANGED
|
@@ -705,53 +705,43 @@ function writeLineToScreen(
|
|
| 705 |
}
|
| 706 |
}
|
| 707 |
} else if (nextChar === '_') {
|
| 708 |
-
// APC (Application Program Command):
|
| 709 |
-
//
|
| 710 |
-
//
|
| 711 |
-
//
|
| 712 |
-
//
|
| 713 |
-
|
| 714 |
-
char: '\x1b',
|
| 715 |
-
styleId: stylePool.none,
|
| 716 |
-
width: CellWidth.Narrow,
|
| 717 |
-
hyperlink: undefined,
|
| 718 |
-
})
|
| 719 |
-
offsetX++
|
| 720 |
-
setCellAt(screen, offsetX, y, {
|
| 721 |
-
char: '_',
|
| 722 |
-
styleId: stylePool.none,
|
| 723 |
-
width: CellWidth.Narrow,
|
| 724 |
-
hyperlink: undefined,
|
| 725 |
-
})
|
| 726 |
-
offsetX++
|
| 727 |
charIdx++ // skip past ESC
|
| 728 |
while (charIdx < characters.length - 1) {
|
| 729 |
charIdx++
|
| 730 |
const c = characters[charIdx]?.value
|
| 731 |
-
if (c === undefined) break
|
| 732 |
-
setCellAt(screen, offsetX, y, {
|
| 733 |
-
char: c,
|
| 734 |
-
styleId: stylePool.none,
|
| 735 |
-
width: CellWidth.Narrow,
|
| 736 |
-
hyperlink: undefined,
|
| 737 |
-
})
|
| 738 |
-
offsetX++
|
| 739 |
if (c === '\x07') break
|
| 740 |
if (c === '\x1b') {
|
| 741 |
const nextC = characters[charIdx + 1]?.value
|
| 742 |
if (nextC === '\\') {
|
| 743 |
-
charIdx++
|
| 744 |
-
setCellAt(screen, offsetX, y, {
|
| 745 |
-
char: '\\',
|
| 746 |
-
styleId: stylePool.none,
|
| 747 |
-
width: CellWidth.Narrow,
|
| 748 |
-
hyperlink: undefined,
|
| 749 |
-
})
|
| 750 |
-
offsetX++
|
| 751 |
break
|
| 752 |
}
|
| 753 |
}
|
| 754 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
} else if (
|
| 756 |
nextChar === ']' ||
|
| 757 |
nextChar === 'P' ||
|
|
@@ -760,6 +750,7 @@ function writeLineToScreen(
|
|
| 760 |
) {
|
| 761 |
// String-based sequences (OSC, DCS, PM, SOS) terminated by
|
| 762 |
// BEL (0x07) or ST (ESC \): skip as before.
|
|
|
|
| 763 |
charIdx++ // skip the introducer char
|
| 764 |
while (charIdx < characters.length - 1) {
|
| 765 |
charIdx++
|
|
@@ -773,6 +764,34 @@ function writeLineToScreen(
|
|
| 773 |
}
|
| 774 |
}
|
| 775 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
} else if (
|
| 777 |
nextCode !== undefined &&
|
| 778 |
nextCode >= 0x30 &&
|
|
|
|
| 705 |
}
|
| 706 |
}
|
| 707 |
} else if (nextChar === '_') {
|
| 708 |
+
// APC (Application Program Command): store in screen.rawWritesAtRow
|
| 709 |
+
// for emission by log-update.ts before the row's cell content.
|
| 710 |
+
// This avoids the screen buffer width limit — APC payloads (e.g.
|
| 711 |
+
// Kitty base64 images) can be hundreds of KB, but screen rows are
|
| 712 |
+
// only ~80 columns wide.
|
| 713 |
+
const apcStartIdx = charIdx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
charIdx++ // skip past ESC
|
| 715 |
while (charIdx < characters.length - 1) {
|
| 716 |
charIdx++
|
| 717 |
const c = characters[charIdx]?.value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
if (c === '\x07') break
|
| 719 |
if (c === '\x1b') {
|
| 720 |
const nextC = characters[charIdx + 1]?.value
|
| 721 |
if (nextC === '\\') {
|
| 722 |
+
charIdx++ // skip backslash
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
break
|
| 724 |
}
|
| 725 |
}
|
| 726 |
}
|
| 727 |
+
// Reconstruct the full APC sequence from char tokens
|
| 728 |
+
let apc = ''
|
| 729 |
+
for (let i = apcStartIdx; i <= charIdx; i++) {
|
| 730 |
+
const ci = characters[i]
|
| 731 |
+
if (ci) apc += ci.value
|
| 732 |
+
}
|
| 733 |
+
// Append to existing entry (multi-chunk APC) or create new one.
|
| 734 |
+
// First chunk includes CUP positioning; subsequent chunks append
|
| 735 |
+
// the next \x1b_G...\x1b\\ without redundant cursor movement.
|
| 736 |
+
const existing = screen.rawWritesAtRow.get(y)
|
| 737 |
+
if (existing) {
|
| 738 |
+
screen.rawWritesAtRow.set(y, existing + apc)
|
| 739 |
+
} else {
|
| 740 |
+
screen.rawWritesAtRow.set(
|
| 741 |
+
y,
|
| 742 |
+
`\x1b[${y + 1};${offsetX + 1}H${apc}`,
|
| 743 |
+
)
|
| 744 |
+
}
|
| 745 |
} else if (
|
| 746 |
nextChar === ']' ||
|
| 747 |
nextChar === 'P' ||
|
|
|
|
| 750 |
) {
|
| 751 |
// String-based sequences (OSC, DCS, PM, SOS) terminated by
|
| 752 |
// BEL (0x07) or ST (ESC \): skip as before.
|
| 753 |
+
const seqStartIdx = charIdx
|
| 754 |
charIdx++ // skip the introducer char
|
| 755 |
while (charIdx < characters.length - 1) {
|
| 756 |
charIdx++
|
|
|
|
| 764 |
}
|
| 765 |
}
|
| 766 |
}
|
| 767 |
+
|
| 768 |
+
// Tmux DCS passthrough (\x1bPtmux;...\x1b\\): store the entire
|
| 769 |
+
// DCS sequence in rawWritesAtRow so tmux forwards the inner APC
|
| 770 |
+
// to the terminal for native Kitty image rendering.
|
| 771 |
+
if (
|
| 772 |
+
nextChar === 'P' &&
|
| 773 |
+
charIdx + 2 < characters.length &&
|
| 774 |
+
characters[seqStartIdx + 2]?.value === 't' &&
|
| 775 |
+
characters[seqStartIdx + 3]?.value === 'm' &&
|
| 776 |
+
characters[seqStartIdx + 4]?.value === 'u' &&
|
| 777 |
+
characters[seqStartIdx + 5]?.value === 'x' &&
|
| 778 |
+
characters[seqStartIdx + 6]?.value === ';' &&
|
| 779 |
+
characters[charIdx + 1]?.value === '\x1b' &&
|
| 780 |
+
characters[charIdx + 2]?.value === '\\'
|
| 781 |
+
) {
|
| 782 |
+
// Extend to include outer DCS terminator (\x1b\\)
|
| 783 |
+
const dcsEnd = charIdx + 2
|
| 784 |
+
let dcs = ''
|
| 785 |
+
for (let i = seqStartIdx; i <= dcsEnd; i++) {
|
| 786 |
+
const ci = characters[i]
|
| 787 |
+
if (ci) dcs += ci.value
|
| 788 |
+
}
|
| 789 |
+
screen.rawWritesAtRow.set(
|
| 790 |
+
y,
|
| 791 |
+
`\x1b[${y + 1};${offsetX + 1}H${dcs}`,
|
| 792 |
+
)
|
| 793 |
+
charIdx = dcsEnd
|
| 794 |
+
}
|
| 795 |
} else if (
|
| 796 |
nextCode !== undefined &&
|
| 797 |
nextCode >= 0x30 &&
|
src/ink/screen.ts
CHANGED
|
@@ -391,6 +391,15 @@ export type Screen = Size & {
|
|
| 391 |
*/
|
| 392 |
noSelect: Uint8Array
|
| 393 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
/**
|
| 395 |
* Per-ROW soft-wrap continuation marker. softWrap[r]=N>0 means row r
|
| 396 |
* is a word-wrap continuation of row r-1 (the `\n` before it was
|
|
@@ -487,6 +496,7 @@ export function createScreen(
|
|
| 487 |
emptyStyleId: styles.none,
|
| 488 |
damage: undefined,
|
| 489 |
noSelect: new Uint8Array(size),
|
|
|
|
| 490 |
softWrap: new Int32Array(height),
|
| 491 |
}
|
| 492 |
}
|
|
@@ -539,8 +549,9 @@ export function resetScreen(
|
|
| 539 |
|
| 540 |
// Shared pools accumulate — no clearing needed. Unique char/hyperlink sets are bounded.
|
| 541 |
|
| 542 |
-
// Clear damage tracking
|
| 543 |
screen.damage = undefined
|
|
|
|
| 544 |
}
|
| 545 |
|
| 546 |
/**
|
|
|
|
| 391 |
*/
|
| 392 |
noSelect: Uint8Array
|
| 393 |
|
| 394 |
+
/**
|
| 395 |
+
* Per-row raw escape sequences (e.g. Kitty protocol APC) that must be
|
| 396 |
+
* emitted before the row's cell content. Populated by writeLineToScreen
|
| 397 |
+
* in output.ts when a RawAnsi line starts with \x1b_ (APC). Each entry
|
| 398 |
+
* already includes CUP positioning: \x1b[row;colH + raw-sequence.
|
| 399 |
+
* log-update.ts emits these before diffing cells in a row.
|
| 400 |
+
*/
|
| 401 |
+
rawWritesAtRow: Map<number, string>
|
| 402 |
+
|
| 403 |
/**
|
| 404 |
* Per-ROW soft-wrap continuation marker. softWrap[r]=N>0 means row r
|
| 405 |
* is a word-wrap continuation of row r-1 (the `\n` before it was
|
|
|
|
| 496 |
emptyStyleId: styles.none,
|
| 497 |
damage: undefined,
|
| 498 |
noSelect: new Uint8Array(size),
|
| 499 |
+
rawWritesAtRow: new Map(),
|
| 500 |
softWrap: new Int32Array(height),
|
| 501 |
}
|
| 502 |
}
|
|
|
|
| 549 |
|
| 550 |
// Shared pools accumulate — no clearing needed. Unique char/hyperlink sets are bounded.
|
| 551 |
|
| 552 |
+
// Clear damage tracking and per-row raw writes
|
| 553 |
screen.damage = undefined
|
| 554 |
+
screen.rawWritesAtRow.clear()
|
| 555 |
}
|
| 556 |
|
| 557 |
/**
|