raw two image
Browse files- src/ink/log-update.ts +22 -19
- src/tools/ImageShowTool/ImageShowTool.tsx +15 -7
src/ink/log-update.ts
CHANGED
|
@@ -302,22 +302,10 @@ export class LogUpdate {
|
|
| 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,14 +400,8 @@ export class LogUpdate {
|
|
| 412 |
undefined,
|
| 413 |
)
|
| 414 |
|
| 415 |
-
// Handle growth:
|
| 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,
|
|
@@ -429,6 +411,20 @@ export class LogUpdate {
|
|
| 429 |
)
|
| 430 |
}
|
| 431 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
// Restore cursor. Skipped in alt-screen: the cursor is hidden, its
|
| 433 |
// position only matters as the starting point for the NEXT frame's
|
| 434 |
// relative moves, and in alt-screen the next frame always begins with
|
|
@@ -536,6 +532,12 @@ function renderFrame(
|
|
| 536 |
stylePool: StylePool,
|
| 537 |
): void {
|
| 538 |
renderFrameSlice(screen, frame, 0, frame.screen.height, stylePool)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
}
|
| 540 |
|
| 541 |
/**
|
|
@@ -576,6 +578,7 @@ function renderFrameSlice(
|
|
| 576 |
return [patches, { dx: -prev.x, dy: rowsToAdvance }]
|
| 577 |
})
|
| 578 |
}
|
|
|
|
| 579 |
// Reset at start of each line — no cell rendered yet
|
| 580 |
lastRenderedStyleId = -1
|
| 581 |
|
|
|
|
| 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 |
undefined,
|
| 401 |
)
|
| 402 |
|
| 403 |
+
// Handle growth: render new rows directly (they naturally scroll the terminal)
|
| 404 |
if (growing) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
renderFrameSlice(
|
| 406 |
screen,
|
| 407 |
next,
|
|
|
|
| 411 |
)
|
| 412 |
}
|
| 413 |
|
| 414 |
+
// Emit raw writes (APC/DCS) at viewport-adjusted positions so native
|
| 415 |
+
// images appear at the correct scroll position every frame, even when
|
| 416 |
+
// the diff loop skips unchanged rows. The CUP is rewritten to target
|
| 417 |
+
// a viewport-relative row instead of an absolute screen-buffer row.
|
| 418 |
+
for (const [row, rawWrite] of next.screen.rawWritesAtRow) {
|
| 419 |
+
if (row < viewportY) continue
|
| 420 |
+
const vpRow = row - viewportY
|
| 421 |
+
const adjusted = rawWrite.replace(
|
| 422 |
+
/^\x1b\[(\d+);(\d+)H/,
|
| 423 |
+
(_, __, col) => `\x1b[${vpRow + 1};${col}H`,
|
| 424 |
+
)
|
| 425 |
+
screen.diff.push({ type: 'stdout', content: adjusted })
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
// Restore cursor. Skipped in alt-screen: the cursor is hidden, its
|
| 429 |
// position only matters as the starting point for the NEXT frame's
|
| 430 |
// relative moves, and in alt-screen the next frame always begins with
|
|
|
|
| 532 |
stylePool: StylePool,
|
| 533 |
): void {
|
| 534 |
renderFrameSlice(screen, frame, 0, frame.screen.height, stylePool)
|
| 535 |
+
// Emit raw writes (APC/DCS) after cells so native images are drawn at
|
| 536 |
+
// the correct viewport position. Full reset always starts from (0,0),
|
| 537 |
+
// so the original CUP row (screen-buffer row + 1) is already correct.
|
| 538 |
+
for (const [, rawWrite] of frame.screen.rawWritesAtRow) {
|
| 539 |
+
screen.diff.push({ type: 'stdout', content: rawWrite })
|
| 540 |
+
}
|
| 541 |
}
|
| 542 |
|
| 543 |
/**
|
|
|
|
| 578 |
return [patches, { dx: -prev.x, dy: rowsToAdvance }]
|
| 579 |
})
|
| 580 |
}
|
| 581 |
+
|
| 582 |
// Reset at start of each line — no cell rendered yet
|
| 583 |
lastRenderedStyleId = -1
|
| 584 |
|
src/tools/ImageShowTool/ImageShowTool.tsx
CHANGED
|
@@ -139,19 +139,27 @@ export const ImageShowTool = buildTool({
|
|
| 139 |
}
|
| 140 |
|
| 141 |
// Block-mode placeholder rendered in-band via RawAnsi so Ink knows the
|
| 142 |
-
// image dimensions and its virtual cursor stays in sync.
|
| 143 |
-
//
|
| 144 |
-
//
|
| 145 |
-
//
|
|
|
|
| 146 |
if (content.timgOutput) {
|
| 147 |
const cleaned = content.timgOutput.replace(/\x1b\[\?25[hl]/g, '')
|
| 148 |
-
|
| 149 |
if (lines.length > 0) {
|
| 150 |
-
const ansiStrip = (s: string) => s.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
|
| 151 |
-
const width = Math.max(...lines.map(l => ansiStrip(l).length))
|
| 152 |
if (content.kittyOutput) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
lines[0] = content.kittyOutput + lines[0]
|
|
|
|
| 154 |
}
|
|
|
|
|
|
|
| 155 |
return <RawAnsi lines={lines} width={width} />
|
| 156 |
}
|
| 157 |
}
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
// Block-mode placeholder rendered in-band via RawAnsi so Ink knows the
|
| 142 |
+
// image dimensions and its virtual cursor stays in sync. When Kitty
|
| 143 |
+
// protocol is available, visible block characters are replaced with
|
| 144 |
+
// spaces so the native image shows through transparently, while the
|
| 145 |
+
// line structure (row count, per-row visual width) is preserved for
|
| 146 |
+
// Ink DOM cursor tracking.
|
| 147 |
if (content.timgOutput) {
|
| 148 |
const cleaned = content.timgOutput.replace(/\x1b\[\?25[hl]/g, '')
|
| 149 |
+
let lines = cleaned.split('\n').filter(l => l.length > 0)
|
| 150 |
if (lines.length > 0) {
|
|
|
|
|
|
|
| 151 |
if (content.kittyOutput) {
|
| 152 |
+
// Strip ANSI SGR codes and replace non-space visible chars with
|
| 153 |
+
// spaces. The native Kitty image will show through these spaces.
|
| 154 |
+
lines = lines.map(l =>
|
| 155 |
+
l.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').replace(/[^\s]/g, ' '),
|
| 156 |
+
)
|
| 157 |
+
const width = Math.max(...lines.map(l => l.length))
|
| 158 |
lines[0] = content.kittyOutput + lines[0]
|
| 159 |
+
return <RawAnsi lines={lines} width={width} />
|
| 160 |
}
|
| 161 |
+
const ansiStrip = (s: string) => s.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
|
| 162 |
+
const width = Math.max(...lines.map(l => ansiStrip(l).length))
|
| 163 |
return <RawAnsi lines={lines} width={width} />
|
| 164 |
}
|
| 165 |
}
|