chenbhao commited on
Commit
b752dc9
·
1 Parent(s): f79a720

feat: image in ui

Browse files
Files changed (3) hide show
  1. src/ink/log-update.ts +19 -1
  2. src/ink/output.ts +54 -35
  3. 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: render new rows directly (they naturally scroll the terminal)
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): pass through to screen
709
- // buffer. Terminal receives the raw \x1b_G...\x1b\\ sequence
710
- // and interprets it as a Kitty graphics protocol command,
711
- // rendering the native image at this position within Ink's
712
- // layout no \x1b[H or writeRaw positioning needed.
713
- setCellAt(screen, offsetX, y, {
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
  /**