restty 0.1.12 → 0.1.13

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.md CHANGED
@@ -9,6 +9,15 @@ Powered by:
9
9
  - `WebGPU` (with WebGL2 fallback)
10
10
  - `text-shaper` (shaping + raster)
11
11
 
12
+ ## Release Status
13
+
14
+ `restty` is in an early release stage.
15
+
16
+ - Known issue: kitty image protocol handling can still fail in some edge cases.
17
+ - API note: high-level APIs are usable now, but some APIs may still change to improve DX.
18
+
19
+ If you hit an issue, please open one on GitHub with repro steps.
20
+
12
21
  ## Install
13
22
 
14
23
  ```bash
package/dist/app/index.js CHANGED
@@ -8672,7 +8672,6 @@ var COLOR_EMOJI_FONT_HINTS = [
8672
8672
  /apple color emoji/i,
8673
8673
  /noto color emoji/i,
8674
8674
  /segoe ui emoji/i,
8675
- /openmoji/i,
8676
8675
  /twemoji/i
8677
8676
  ];
8678
8677
  var WIDE_FONT_HINTS = [
@@ -25641,9 +25640,17 @@ var DEFAULT_FONT_SOURCES = [
25641
25640
  type: "url",
25642
25641
  url: "https://cdn.jsdelivr.net/gh/notofonts/noto-fonts@main/unhinted/ttf/NotoSansSymbols2/NotoSansSymbols2-Regular.ttf"
25643
25642
  },
25643
+ {
25644
+ type: "url",
25645
+ url: "https://cdn.jsdelivr.net/gh/googlefonts/noto-emoji@main/fonts/NotoColorEmoji.ttf"
25646
+ },
25644
25647
  {
25645
25648
  type: "url",
25646
25649
  url: "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/font/OpenMoji-black-glyf/OpenMoji-black-glyf.ttf"
25650
+ },
25651
+ {
25652
+ type: "url",
25653
+ url: "https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf"
25647
25654
  }
25648
25655
  ];
25649
25656
  function validateFontSource(source, index) {
@@ -53192,6 +53199,46 @@ function createResttyApp(options) {
53192
53199
  const firstCp = text.codePointAt(0) ?? 0;
53193
53200
  const nerdSymbol = isNerdSymbolCodepoint(firstCp);
53194
53201
  const preferSymbol = nerdSymbol || isSymbolCp(firstCp);
53202
+ const preferEmoji = text.includes("‍") || chars.some((ch) => {
53203
+ const cp = ch.codePointAt(0) ?? 0;
53204
+ if (cp >= 127462 && cp <= 127487)
53205
+ return true;
53206
+ if (cp >= 127744 && cp <= 129791)
53207
+ return true;
53208
+ return false;
53209
+ });
53210
+ if (preferEmoji) {
53211
+ let bestEmojiIndex = -1;
53212
+ let bestEmojiScore = Number.POSITIVE_INFINITY;
53213
+ for (let i3 = 0;i3 < fontState.fonts.length; i3 += 1) {
53214
+ const entry = fontState.fonts[i3];
53215
+ if (!entry?.font || !isColorEmojiFont(entry))
53216
+ continue;
53217
+ let ok = true;
53218
+ for (const ch of chars) {
53219
+ if (!fontHasGlyph2(entry.font, ch)) {
53220
+ ok = false;
53221
+ break;
53222
+ }
53223
+ }
53224
+ if (!ok)
53225
+ continue;
53226
+ const shaped = shapeClusterWithFont(entry, text);
53227
+ let score = shaped.glyphs.length;
53228
+ if (/noto color emoji/i.test(entry.label))
53229
+ score -= 0.25;
53230
+ if (score < bestEmojiScore) {
53231
+ bestEmojiScore = score;
53232
+ bestEmojiIndex = i3;
53233
+ if (score <= 0.75)
53234
+ break;
53235
+ }
53236
+ }
53237
+ if (bestEmojiIndex >= 0) {
53238
+ setBoundedMap(fontState.fontPickCache, cacheKey, bestEmojiIndex, FONT_PICK_CACHE_LIMIT);
53239
+ return bestEmojiIndex;
53240
+ }
53241
+ }
53195
53242
  if (nerdSymbol) {
53196
53243
  const symbolIndex = fontState.fonts.findIndex((entry) => isSymbolFont(entry));
53197
53244
  if (symbolIndex >= 0) {
@@ -53639,6 +53686,29 @@ function createResttyApp(options) {
53639
53686
  } = render;
53640
53687
  if (!codepoints || !fgBytes)
53641
53688
  return;
53689
+ const mergedEmojiSkip = new Uint8Array(codepoints.length);
53690
+ const isRegionalIndicator = (value) => value >= 127462 && value <= 127487;
53691
+ const readCellCluster = (cellIndex) => {
53692
+ const flag = wide ? wide[cellIndex] ?? 0 : 0;
53693
+ if (flag === 2 || flag === 3)
53694
+ return null;
53695
+ const cp = codepoints[cellIndex] ?? 0;
53696
+ if (!cp)
53697
+ return null;
53698
+ let text = String.fromCodePoint(cp);
53699
+ const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[cellIndex] ?? 0 : 0;
53700
+ if (extra > 0 && graphemeOffset && graphemeBuffer) {
53701
+ const start = graphemeOffset[cellIndex] ?? 0;
53702
+ const cps = [cp];
53703
+ for (let j = 0;j < extra; j += 1) {
53704
+ const extraCp = graphemeBuffer[start + j];
53705
+ if (extraCp)
53706
+ cps.push(extraCp);
53707
+ }
53708
+ text = String.fromCodePoint(...cps);
53709
+ }
53710
+ return { cp, text, span: flag === 1 ? 2 : 1 };
53711
+ };
53642
53712
  const { useLinearBlending, useLinearCorrection } = resolveBlendFlags("webgpu", state);
53643
53713
  const clearColor = useLinearBlending ? srgbToLinearColor(defaultBg) : defaultBg;
53644
53714
  reportTermSize(cols, rows);
@@ -53874,28 +53944,43 @@ function createResttyApp(options) {
53874
53944
  }
53875
53945
  if (bgOnly || textHidden)
53876
53946
  continue;
53877
- const wideFlag = wide ? wide[idx] : 0;
53878
- if (wideFlag === 2 || wideFlag === 3)
53947
+ if (mergedEmojiSkip[idx])
53879
53948
  continue;
53880
- const cp = codepoints[idx];
53881
- if (!cp)
53949
+ const cluster = readCellCluster(idx);
53950
+ if (!cluster)
53882
53951
  continue;
53952
+ const cp = cluster.cp;
53883
53953
  if (cp === KITTY_PLACEHOLDER_CP)
53884
53954
  continue;
53885
- const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[idx] ?? 0 : 0;
53886
- if (extra === 0 && isSpaceCp(cp))
53887
- continue;
53888
- let text = String.fromCodePoint(cp);
53889
- if (extra > 0 && graphemeOffset && graphemeBuffer) {
53890
- const start = graphemeOffset[idx] ?? 0;
53891
- const cps = [cp];
53892
- for (let j = 0;j < extra; j += 1) {
53893
- const extraCp = graphemeBuffer[start + j];
53894
- if (extraCp)
53895
- cps.push(extraCp);
53955
+ let text = cluster.text;
53956
+ let baseSpan = cluster.span;
53957
+ const rowEnd = row * cols + cols;
53958
+ if (isRegionalIndicator(cp)) {
53959
+ const nextIdx = idx + baseSpan;
53960
+ if (nextIdx < rowEnd && !mergedEmojiSkip[nextIdx]) {
53961
+ const next = readCellCluster(nextIdx);
53962
+ if (next && isRegionalIndicator(next.cp)) {
53963
+ text += next.text;
53964
+ baseSpan += next.span;
53965
+ mergedEmojiSkip[nextIdx] = 1;
53966
+ }
53896
53967
  }
53897
- text = String.fromCodePoint(...cps);
53898
53968
  }
53969
+ let nextSeqIdx = idx + baseSpan;
53970
+ let guard = 0;
53971
+ while ((text.codePointAt(text.length - 1) ?? 0) === 8205 && nextSeqIdx < rowEnd && guard < 8) {
53972
+ const next = readCellCluster(nextSeqIdx);
53973
+ if (!next || !next.cp || isSpaceCp(next.cp))
53974
+ break;
53975
+ text += next.text;
53976
+ baseSpan += next.span;
53977
+ mergedEmojiSkip[nextSeqIdx] = 1;
53978
+ nextSeqIdx += next.span;
53979
+ guard += 1;
53980
+ }
53981
+ const extra = text.length > String.fromCodePoint(cp).length ? 1 : 0;
53982
+ if (extra === 0 && isSpaceCp(cp))
53983
+ continue;
53899
53984
  if (cursorBlock && cursorCell && row === cursorCell.row && col >= cursorCell.col && col < cursorCell.col + (cursorCell.wide ? 2 : 1)) {
53900
53985
  fg = [bgForText[0], bgForText[1], bgForText[2], 1];
53901
53986
  }
@@ -53917,7 +54002,6 @@ function createResttyApp(options) {
53917
54002
  }
53918
54003
  if (extra > 0 && text.trim() === "")
53919
54004
  continue;
53920
- const baseSpan = wideFlag === 1 ? 2 : 1;
53921
54005
  const fontIndex = pickFontIndexForText2(text, baseSpan);
53922
54006
  const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
53923
54007
  const shaped = shapeClusterWithFont(fontEntry, text);
@@ -54515,6 +54599,29 @@ function createResttyApp(options) {
54515
54599
  clearKittyOverlay();
54516
54600
  return;
54517
54601
  }
54602
+ const mergedEmojiSkip = new Uint8Array(codepoints.length);
54603
+ const isRegionalIndicator = (value) => value >= 127462 && value <= 127487;
54604
+ const readCellCluster = (cellIndex) => {
54605
+ const flag = wide ? wide[cellIndex] ?? 0 : 0;
54606
+ if (flag === 2 || flag === 3)
54607
+ return null;
54608
+ const cp = codepoints[cellIndex] ?? 0;
54609
+ if (!cp)
54610
+ return null;
54611
+ let text = String.fromCodePoint(cp);
54612
+ const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[cellIndex] ?? 0 : 0;
54613
+ if (extra > 0 && graphemeOffset && graphemeBuffer) {
54614
+ const start = graphemeOffset[cellIndex] ?? 0;
54615
+ const cps = [cp];
54616
+ for (let j = 0;j < extra; j += 1) {
54617
+ const extraCp = graphemeBuffer[start + j];
54618
+ if (extraCp)
54619
+ cps.push(extraCp);
54620
+ }
54621
+ text = String.fromCodePoint(...cps);
54622
+ }
54623
+ return { cp, text, span: flag === 1 ? 2 : 1 };
54624
+ };
54518
54625
  const { useLinearBlending, useLinearCorrection } = resolveBlendFlags("webgl2");
54519
54626
  reportTermSize(cols, rows);
54520
54627
  const cursorPos = cursor ? resolveCursorPosition(cursor) : null;
@@ -54735,28 +54842,43 @@ function createResttyApp(options) {
54735
54842
  }
54736
54843
  if (bgOnly || textHidden)
54737
54844
  continue;
54738
- const wideFlag = wide ? wide[idx] : 0;
54739
- if (wideFlag === 2 || wideFlag === 3)
54845
+ if (mergedEmojiSkip[idx])
54740
54846
  continue;
54741
- const cp = codepoints[idx];
54742
- if (!cp)
54847
+ const cluster = readCellCluster(idx);
54848
+ if (!cluster)
54743
54849
  continue;
54850
+ const cp = cluster.cp;
54744
54851
  if (cp === KITTY_PLACEHOLDER_CP)
54745
54852
  continue;
54746
- const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[idx] ?? 0 : 0;
54747
- if (extra === 0 && isSpaceCp(cp))
54748
- continue;
54749
- let text = String.fromCodePoint(cp);
54750
- if (extra > 0 && graphemeOffset && graphemeBuffer) {
54751
- const start = graphemeOffset[idx] ?? 0;
54752
- const cps = [cp];
54753
- for (let j = 0;j < extra; j += 1) {
54754
- const extraCp = graphemeBuffer[start + j];
54755
- if (extraCp)
54756
- cps.push(extraCp);
54853
+ let text = cluster.text;
54854
+ let baseSpan = cluster.span;
54855
+ const rowEnd = row * cols + cols;
54856
+ if (isRegionalIndicator(cp)) {
54857
+ const nextIdx = idx + baseSpan;
54858
+ if (nextIdx < rowEnd && !mergedEmojiSkip[nextIdx]) {
54859
+ const next = readCellCluster(nextIdx);
54860
+ if (next && isRegionalIndicator(next.cp)) {
54861
+ text += next.text;
54862
+ baseSpan += next.span;
54863
+ mergedEmojiSkip[nextIdx] = 1;
54864
+ }
54757
54865
  }
54758
- text = String.fromCodePoint(...cps);
54759
54866
  }
54867
+ let nextSeqIdx = idx + baseSpan;
54868
+ let guard = 0;
54869
+ while ((text.codePointAt(text.length - 1) ?? 0) === 8205 && nextSeqIdx < rowEnd && guard < 8) {
54870
+ const next = readCellCluster(nextSeqIdx);
54871
+ if (!next || !next.cp || isSpaceCp(next.cp))
54872
+ break;
54873
+ text += next.text;
54874
+ baseSpan += next.span;
54875
+ mergedEmojiSkip[nextSeqIdx] = 1;
54876
+ nextSeqIdx += next.span;
54877
+ guard += 1;
54878
+ }
54879
+ const extra = text.length > String.fromCodePoint(cp).length ? 1 : 0;
54880
+ if (extra === 0 && isSpaceCp(cp))
54881
+ continue;
54760
54882
  if (cursorBlock && cursorCell && row === cursorCell.row && col >= cursorCell.col && col < cursorCell.col + (cursorCell.wide ? 2 : 1)) {
54761
54883
  fg = [bgForText[0], bgForText[1], bgForText[2], 1];
54762
54884
  }
@@ -54778,7 +54900,6 @@ function createResttyApp(options) {
54778
54900
  }
54779
54901
  if (extra > 0 && text.trim() === "")
54780
54902
  continue;
54781
- const baseSpan = wideFlag === 1 ? 2 : 1;
54782
54903
  const fontIndex = pickFontIndexForText2(text, baseSpan);
54783
54904
  const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
54784
54905
  const shaped = shapeClusterWithFont(fontEntry, text);
@@ -96,7 +96,6 @@ var COLOR_EMOJI_FONT_HINTS = [
96
96
  /apple color emoji/i,
97
97
  /noto color emoji/i,
98
98
  /segoe ui emoji/i,
99
- /openmoji/i,
100
99
  /twemoji/i
101
100
  ];
102
101
  var WIDE_FONT_HINTS = [
package/dist/index.js CHANGED
@@ -8672,7 +8672,6 @@ var COLOR_EMOJI_FONT_HINTS = [
8672
8672
  /apple color emoji/i,
8673
8673
  /noto color emoji/i,
8674
8674
  /segoe ui emoji/i,
8675
- /openmoji/i,
8676
8675
  /twemoji/i
8677
8676
  ];
8678
8677
  var WIDE_FONT_HINTS = [
@@ -25641,9 +25640,17 @@ var DEFAULT_FONT_SOURCES = [
25641
25640
  type: "url",
25642
25641
  url: "https://cdn.jsdelivr.net/gh/notofonts/noto-fonts@main/unhinted/ttf/NotoSansSymbols2/NotoSansSymbols2-Regular.ttf"
25643
25642
  },
25643
+ {
25644
+ type: "url",
25645
+ url: "https://cdn.jsdelivr.net/gh/googlefonts/noto-emoji@main/fonts/NotoColorEmoji.ttf"
25646
+ },
25644
25647
  {
25645
25648
  type: "url",
25646
25649
  url: "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/font/OpenMoji-black-glyf/OpenMoji-black-glyf.ttf"
25650
+ },
25651
+ {
25652
+ type: "url",
25653
+ url: "https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf"
25647
25654
  }
25648
25655
  ];
25649
25656
  function validateFontSource(source, index) {
@@ -53192,6 +53199,46 @@ function createResttyApp(options) {
53192
53199
  const firstCp = text.codePointAt(0) ?? 0;
53193
53200
  const nerdSymbol = isNerdSymbolCodepoint(firstCp);
53194
53201
  const preferSymbol = nerdSymbol || isSymbolCp(firstCp);
53202
+ const preferEmoji = text.includes("‍") || chars.some((ch) => {
53203
+ const cp = ch.codePointAt(0) ?? 0;
53204
+ if (cp >= 127462 && cp <= 127487)
53205
+ return true;
53206
+ if (cp >= 127744 && cp <= 129791)
53207
+ return true;
53208
+ return false;
53209
+ });
53210
+ if (preferEmoji) {
53211
+ let bestEmojiIndex = -1;
53212
+ let bestEmojiScore = Number.POSITIVE_INFINITY;
53213
+ for (let i3 = 0;i3 < fontState.fonts.length; i3 += 1) {
53214
+ const entry = fontState.fonts[i3];
53215
+ if (!entry?.font || !isColorEmojiFont(entry))
53216
+ continue;
53217
+ let ok = true;
53218
+ for (const ch of chars) {
53219
+ if (!fontHasGlyph2(entry.font, ch)) {
53220
+ ok = false;
53221
+ break;
53222
+ }
53223
+ }
53224
+ if (!ok)
53225
+ continue;
53226
+ const shaped = shapeClusterWithFont(entry, text);
53227
+ let score = shaped.glyphs.length;
53228
+ if (/noto color emoji/i.test(entry.label))
53229
+ score -= 0.25;
53230
+ if (score < bestEmojiScore) {
53231
+ bestEmojiScore = score;
53232
+ bestEmojiIndex = i3;
53233
+ if (score <= 0.75)
53234
+ break;
53235
+ }
53236
+ }
53237
+ if (bestEmojiIndex >= 0) {
53238
+ setBoundedMap(fontState.fontPickCache, cacheKey, bestEmojiIndex, FONT_PICK_CACHE_LIMIT);
53239
+ return bestEmojiIndex;
53240
+ }
53241
+ }
53195
53242
  if (nerdSymbol) {
53196
53243
  const symbolIndex = fontState.fonts.findIndex((entry) => isSymbolFont(entry));
53197
53244
  if (symbolIndex >= 0) {
@@ -53639,6 +53686,29 @@ function createResttyApp(options) {
53639
53686
  } = render;
53640
53687
  if (!codepoints || !fgBytes)
53641
53688
  return;
53689
+ const mergedEmojiSkip = new Uint8Array(codepoints.length);
53690
+ const isRegionalIndicator = (value) => value >= 127462 && value <= 127487;
53691
+ const readCellCluster = (cellIndex) => {
53692
+ const flag = wide ? wide[cellIndex] ?? 0 : 0;
53693
+ if (flag === 2 || flag === 3)
53694
+ return null;
53695
+ const cp = codepoints[cellIndex] ?? 0;
53696
+ if (!cp)
53697
+ return null;
53698
+ let text = String.fromCodePoint(cp);
53699
+ const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[cellIndex] ?? 0 : 0;
53700
+ if (extra > 0 && graphemeOffset && graphemeBuffer) {
53701
+ const start = graphemeOffset[cellIndex] ?? 0;
53702
+ const cps = [cp];
53703
+ for (let j = 0;j < extra; j += 1) {
53704
+ const extraCp = graphemeBuffer[start + j];
53705
+ if (extraCp)
53706
+ cps.push(extraCp);
53707
+ }
53708
+ text = String.fromCodePoint(...cps);
53709
+ }
53710
+ return { cp, text, span: flag === 1 ? 2 : 1 };
53711
+ };
53642
53712
  const { useLinearBlending, useLinearCorrection } = resolveBlendFlags("webgpu", state);
53643
53713
  const clearColor = useLinearBlending ? srgbToLinearColor(defaultBg) : defaultBg;
53644
53714
  reportTermSize(cols, rows);
@@ -53874,28 +53944,43 @@ function createResttyApp(options) {
53874
53944
  }
53875
53945
  if (bgOnly || textHidden)
53876
53946
  continue;
53877
- const wideFlag = wide ? wide[idx] : 0;
53878
- if (wideFlag === 2 || wideFlag === 3)
53947
+ if (mergedEmojiSkip[idx])
53879
53948
  continue;
53880
- const cp = codepoints[idx];
53881
- if (!cp)
53949
+ const cluster = readCellCluster(idx);
53950
+ if (!cluster)
53882
53951
  continue;
53952
+ const cp = cluster.cp;
53883
53953
  if (cp === KITTY_PLACEHOLDER_CP)
53884
53954
  continue;
53885
- const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[idx] ?? 0 : 0;
53886
- if (extra === 0 && isSpaceCp(cp))
53887
- continue;
53888
- let text = String.fromCodePoint(cp);
53889
- if (extra > 0 && graphemeOffset && graphemeBuffer) {
53890
- const start = graphemeOffset[idx] ?? 0;
53891
- const cps = [cp];
53892
- for (let j = 0;j < extra; j += 1) {
53893
- const extraCp = graphemeBuffer[start + j];
53894
- if (extraCp)
53895
- cps.push(extraCp);
53955
+ let text = cluster.text;
53956
+ let baseSpan = cluster.span;
53957
+ const rowEnd = row * cols + cols;
53958
+ if (isRegionalIndicator(cp)) {
53959
+ const nextIdx = idx + baseSpan;
53960
+ if (nextIdx < rowEnd && !mergedEmojiSkip[nextIdx]) {
53961
+ const next = readCellCluster(nextIdx);
53962
+ if (next && isRegionalIndicator(next.cp)) {
53963
+ text += next.text;
53964
+ baseSpan += next.span;
53965
+ mergedEmojiSkip[nextIdx] = 1;
53966
+ }
53896
53967
  }
53897
- text = String.fromCodePoint(...cps);
53898
53968
  }
53969
+ let nextSeqIdx = idx + baseSpan;
53970
+ let guard = 0;
53971
+ while ((text.codePointAt(text.length - 1) ?? 0) === 8205 && nextSeqIdx < rowEnd && guard < 8) {
53972
+ const next = readCellCluster(nextSeqIdx);
53973
+ if (!next || !next.cp || isSpaceCp(next.cp))
53974
+ break;
53975
+ text += next.text;
53976
+ baseSpan += next.span;
53977
+ mergedEmojiSkip[nextSeqIdx] = 1;
53978
+ nextSeqIdx += next.span;
53979
+ guard += 1;
53980
+ }
53981
+ const extra = text.length > String.fromCodePoint(cp).length ? 1 : 0;
53982
+ if (extra === 0 && isSpaceCp(cp))
53983
+ continue;
53899
53984
  if (cursorBlock && cursorCell && row === cursorCell.row && col >= cursorCell.col && col < cursorCell.col + (cursorCell.wide ? 2 : 1)) {
53900
53985
  fg = [bgForText[0], bgForText[1], bgForText[2], 1];
53901
53986
  }
@@ -53917,7 +54002,6 @@ function createResttyApp(options) {
53917
54002
  }
53918
54003
  if (extra > 0 && text.trim() === "")
53919
54004
  continue;
53920
- const baseSpan = wideFlag === 1 ? 2 : 1;
53921
54005
  const fontIndex = pickFontIndexForText2(text, baseSpan);
53922
54006
  const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
53923
54007
  const shaped = shapeClusterWithFont(fontEntry, text);
@@ -54515,6 +54599,29 @@ function createResttyApp(options) {
54515
54599
  clearKittyOverlay();
54516
54600
  return;
54517
54601
  }
54602
+ const mergedEmojiSkip = new Uint8Array(codepoints.length);
54603
+ const isRegionalIndicator = (value) => value >= 127462 && value <= 127487;
54604
+ const readCellCluster = (cellIndex) => {
54605
+ const flag = wide ? wide[cellIndex] ?? 0 : 0;
54606
+ if (flag === 2 || flag === 3)
54607
+ return null;
54608
+ const cp = codepoints[cellIndex] ?? 0;
54609
+ if (!cp)
54610
+ return null;
54611
+ let text = String.fromCodePoint(cp);
54612
+ const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[cellIndex] ?? 0 : 0;
54613
+ if (extra > 0 && graphemeOffset && graphemeBuffer) {
54614
+ const start = graphemeOffset[cellIndex] ?? 0;
54615
+ const cps = [cp];
54616
+ for (let j = 0;j < extra; j += 1) {
54617
+ const extraCp = graphemeBuffer[start + j];
54618
+ if (extraCp)
54619
+ cps.push(extraCp);
54620
+ }
54621
+ text = String.fromCodePoint(...cps);
54622
+ }
54623
+ return { cp, text, span: flag === 1 ? 2 : 1 };
54624
+ };
54518
54625
  const { useLinearBlending, useLinearCorrection } = resolveBlendFlags("webgl2");
54519
54626
  reportTermSize(cols, rows);
54520
54627
  const cursorPos = cursor ? resolveCursorPosition(cursor) : null;
@@ -54735,28 +54842,43 @@ function createResttyApp(options) {
54735
54842
  }
54736
54843
  if (bgOnly || textHidden)
54737
54844
  continue;
54738
- const wideFlag = wide ? wide[idx] : 0;
54739
- if (wideFlag === 2 || wideFlag === 3)
54845
+ if (mergedEmojiSkip[idx])
54740
54846
  continue;
54741
- const cp = codepoints[idx];
54742
- if (!cp)
54847
+ const cluster = readCellCluster(idx);
54848
+ if (!cluster)
54743
54849
  continue;
54850
+ const cp = cluster.cp;
54744
54851
  if (cp === KITTY_PLACEHOLDER_CP)
54745
54852
  continue;
54746
- const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[idx] ?? 0 : 0;
54747
- if (extra === 0 && isSpaceCp(cp))
54748
- continue;
54749
- let text = String.fromCodePoint(cp);
54750
- if (extra > 0 && graphemeOffset && graphemeBuffer) {
54751
- const start = graphemeOffset[idx] ?? 0;
54752
- const cps = [cp];
54753
- for (let j = 0;j < extra; j += 1) {
54754
- const extraCp = graphemeBuffer[start + j];
54755
- if (extraCp)
54756
- cps.push(extraCp);
54853
+ let text = cluster.text;
54854
+ let baseSpan = cluster.span;
54855
+ const rowEnd = row * cols + cols;
54856
+ if (isRegionalIndicator(cp)) {
54857
+ const nextIdx = idx + baseSpan;
54858
+ if (nextIdx < rowEnd && !mergedEmojiSkip[nextIdx]) {
54859
+ const next = readCellCluster(nextIdx);
54860
+ if (next && isRegionalIndicator(next.cp)) {
54861
+ text += next.text;
54862
+ baseSpan += next.span;
54863
+ mergedEmojiSkip[nextIdx] = 1;
54864
+ }
54757
54865
  }
54758
- text = String.fromCodePoint(...cps);
54759
54866
  }
54867
+ let nextSeqIdx = idx + baseSpan;
54868
+ let guard = 0;
54869
+ while ((text.codePointAt(text.length - 1) ?? 0) === 8205 && nextSeqIdx < rowEnd && guard < 8) {
54870
+ const next = readCellCluster(nextSeqIdx);
54871
+ if (!next || !next.cp || isSpaceCp(next.cp))
54872
+ break;
54873
+ text += next.text;
54874
+ baseSpan += next.span;
54875
+ mergedEmojiSkip[nextSeqIdx] = 1;
54876
+ nextSeqIdx += next.span;
54877
+ guard += 1;
54878
+ }
54879
+ const extra = text.length > String.fromCodePoint(cp).length ? 1 : 0;
54880
+ if (extra === 0 && isSpaceCp(cp))
54881
+ continue;
54760
54882
  if (cursorBlock && cursorCell && row === cursorCell.row && col >= cursorCell.col && col < cursorCell.col + (cursorCell.wide ? 2 : 1)) {
54761
54883
  fg = [bgForText[0], bgForText[1], bgForText[2], 1];
54762
54884
  }
@@ -54778,7 +54900,6 @@ function createResttyApp(options) {
54778
54900
  }
54779
54901
  if (extra > 0 && text.trim() === "")
54780
54902
  continue;
54781
- const baseSpan = wideFlag === 1 ? 2 : 1;
54782
54903
  const fontIndex = pickFontIndexForText2(text, baseSpan);
54783
54904
  const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
54784
54905
  const shaped = shapeClusterWithFont(fontEntry, text);
package/dist/internal.js CHANGED
@@ -8672,7 +8672,6 @@ var COLOR_EMOJI_FONT_HINTS = [
8672
8672
  /apple color emoji/i,
8673
8673
  /noto color emoji/i,
8674
8674
  /segoe ui emoji/i,
8675
- /openmoji/i,
8676
8675
  /twemoji/i
8677
8676
  ];
8678
8677
  var WIDE_FONT_HINTS = [
@@ -25641,9 +25640,17 @@ var DEFAULT_FONT_SOURCES = [
25641
25640
  type: "url",
25642
25641
  url: "https://cdn.jsdelivr.net/gh/notofonts/noto-fonts@main/unhinted/ttf/NotoSansSymbols2/NotoSansSymbols2-Regular.ttf"
25643
25642
  },
25643
+ {
25644
+ type: "url",
25645
+ url: "https://cdn.jsdelivr.net/gh/googlefonts/noto-emoji@main/fonts/NotoColorEmoji.ttf"
25646
+ },
25644
25647
  {
25645
25648
  type: "url",
25646
25649
  url: "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/font/OpenMoji-black-glyf/OpenMoji-black-glyf.ttf"
25650
+ },
25651
+ {
25652
+ type: "url",
25653
+ url: "https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf"
25647
25654
  }
25648
25655
  ];
25649
25656
  function validateFontSource(source, index) {
@@ -53192,6 +53199,46 @@ function createResttyApp(options) {
53192
53199
  const firstCp = text.codePointAt(0) ?? 0;
53193
53200
  const nerdSymbol = isNerdSymbolCodepoint(firstCp);
53194
53201
  const preferSymbol = nerdSymbol || isSymbolCp(firstCp);
53202
+ const preferEmoji = text.includes("‍") || chars.some((ch) => {
53203
+ const cp = ch.codePointAt(0) ?? 0;
53204
+ if (cp >= 127462 && cp <= 127487)
53205
+ return true;
53206
+ if (cp >= 127744 && cp <= 129791)
53207
+ return true;
53208
+ return false;
53209
+ });
53210
+ if (preferEmoji) {
53211
+ let bestEmojiIndex = -1;
53212
+ let bestEmojiScore = Number.POSITIVE_INFINITY;
53213
+ for (let i3 = 0;i3 < fontState.fonts.length; i3 += 1) {
53214
+ const entry = fontState.fonts[i3];
53215
+ if (!entry?.font || !isColorEmojiFont(entry))
53216
+ continue;
53217
+ let ok = true;
53218
+ for (const ch of chars) {
53219
+ if (!fontHasGlyph2(entry.font, ch)) {
53220
+ ok = false;
53221
+ break;
53222
+ }
53223
+ }
53224
+ if (!ok)
53225
+ continue;
53226
+ const shaped = shapeClusterWithFont(entry, text);
53227
+ let score = shaped.glyphs.length;
53228
+ if (/noto color emoji/i.test(entry.label))
53229
+ score -= 0.25;
53230
+ if (score < bestEmojiScore) {
53231
+ bestEmojiScore = score;
53232
+ bestEmojiIndex = i3;
53233
+ if (score <= 0.75)
53234
+ break;
53235
+ }
53236
+ }
53237
+ if (bestEmojiIndex >= 0) {
53238
+ setBoundedMap(fontState.fontPickCache, cacheKey, bestEmojiIndex, FONT_PICK_CACHE_LIMIT);
53239
+ return bestEmojiIndex;
53240
+ }
53241
+ }
53195
53242
  if (nerdSymbol) {
53196
53243
  const symbolIndex = fontState.fonts.findIndex((entry) => isSymbolFont(entry));
53197
53244
  if (symbolIndex >= 0) {
@@ -53639,6 +53686,29 @@ function createResttyApp(options) {
53639
53686
  } = render;
53640
53687
  if (!codepoints || !fgBytes)
53641
53688
  return;
53689
+ const mergedEmojiSkip = new Uint8Array(codepoints.length);
53690
+ const isRegionalIndicator = (value) => value >= 127462 && value <= 127487;
53691
+ const readCellCluster = (cellIndex) => {
53692
+ const flag = wide ? wide[cellIndex] ?? 0 : 0;
53693
+ if (flag === 2 || flag === 3)
53694
+ return null;
53695
+ const cp = codepoints[cellIndex] ?? 0;
53696
+ if (!cp)
53697
+ return null;
53698
+ let text = String.fromCodePoint(cp);
53699
+ const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[cellIndex] ?? 0 : 0;
53700
+ if (extra > 0 && graphemeOffset && graphemeBuffer) {
53701
+ const start = graphemeOffset[cellIndex] ?? 0;
53702
+ const cps = [cp];
53703
+ for (let j = 0;j < extra; j += 1) {
53704
+ const extraCp = graphemeBuffer[start + j];
53705
+ if (extraCp)
53706
+ cps.push(extraCp);
53707
+ }
53708
+ text = String.fromCodePoint(...cps);
53709
+ }
53710
+ return { cp, text, span: flag === 1 ? 2 : 1 };
53711
+ };
53642
53712
  const { useLinearBlending, useLinearCorrection } = resolveBlendFlags("webgpu", state);
53643
53713
  const clearColor = useLinearBlending ? srgbToLinearColor(defaultBg) : defaultBg;
53644
53714
  reportTermSize(cols, rows);
@@ -53874,28 +53944,43 @@ function createResttyApp(options) {
53874
53944
  }
53875
53945
  if (bgOnly || textHidden)
53876
53946
  continue;
53877
- const wideFlag = wide ? wide[idx] : 0;
53878
- if (wideFlag === 2 || wideFlag === 3)
53947
+ if (mergedEmojiSkip[idx])
53879
53948
  continue;
53880
- const cp = codepoints[idx];
53881
- if (!cp)
53949
+ const cluster = readCellCluster(idx);
53950
+ if (!cluster)
53882
53951
  continue;
53952
+ const cp = cluster.cp;
53883
53953
  if (cp === KITTY_PLACEHOLDER_CP)
53884
53954
  continue;
53885
- const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[idx] ?? 0 : 0;
53886
- if (extra === 0 && isSpaceCp(cp))
53887
- continue;
53888
- let text = String.fromCodePoint(cp);
53889
- if (extra > 0 && graphemeOffset && graphemeBuffer) {
53890
- const start = graphemeOffset[idx] ?? 0;
53891
- const cps = [cp];
53892
- for (let j = 0;j < extra; j += 1) {
53893
- const extraCp = graphemeBuffer[start + j];
53894
- if (extraCp)
53895
- cps.push(extraCp);
53955
+ let text = cluster.text;
53956
+ let baseSpan = cluster.span;
53957
+ const rowEnd = row * cols + cols;
53958
+ if (isRegionalIndicator(cp)) {
53959
+ const nextIdx = idx + baseSpan;
53960
+ if (nextIdx < rowEnd && !mergedEmojiSkip[nextIdx]) {
53961
+ const next = readCellCluster(nextIdx);
53962
+ if (next && isRegionalIndicator(next.cp)) {
53963
+ text += next.text;
53964
+ baseSpan += next.span;
53965
+ mergedEmojiSkip[nextIdx] = 1;
53966
+ }
53896
53967
  }
53897
- text = String.fromCodePoint(...cps);
53898
53968
  }
53969
+ let nextSeqIdx = idx + baseSpan;
53970
+ let guard = 0;
53971
+ while ((text.codePointAt(text.length - 1) ?? 0) === 8205 && nextSeqIdx < rowEnd && guard < 8) {
53972
+ const next = readCellCluster(nextSeqIdx);
53973
+ if (!next || !next.cp || isSpaceCp(next.cp))
53974
+ break;
53975
+ text += next.text;
53976
+ baseSpan += next.span;
53977
+ mergedEmojiSkip[nextSeqIdx] = 1;
53978
+ nextSeqIdx += next.span;
53979
+ guard += 1;
53980
+ }
53981
+ const extra = text.length > String.fromCodePoint(cp).length ? 1 : 0;
53982
+ if (extra === 0 && isSpaceCp(cp))
53983
+ continue;
53899
53984
  if (cursorBlock && cursorCell && row === cursorCell.row && col >= cursorCell.col && col < cursorCell.col + (cursorCell.wide ? 2 : 1)) {
53900
53985
  fg = [bgForText[0], bgForText[1], bgForText[2], 1];
53901
53986
  }
@@ -53917,7 +54002,6 @@ function createResttyApp(options) {
53917
54002
  }
53918
54003
  if (extra > 0 && text.trim() === "")
53919
54004
  continue;
53920
- const baseSpan = wideFlag === 1 ? 2 : 1;
53921
54005
  const fontIndex = pickFontIndexForText2(text, baseSpan);
53922
54006
  const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
53923
54007
  const shaped = shapeClusterWithFont(fontEntry, text);
@@ -54515,6 +54599,29 @@ function createResttyApp(options) {
54515
54599
  clearKittyOverlay();
54516
54600
  return;
54517
54601
  }
54602
+ const mergedEmojiSkip = new Uint8Array(codepoints.length);
54603
+ const isRegionalIndicator = (value) => value >= 127462 && value <= 127487;
54604
+ const readCellCluster = (cellIndex) => {
54605
+ const flag = wide ? wide[cellIndex] ?? 0 : 0;
54606
+ if (flag === 2 || flag === 3)
54607
+ return null;
54608
+ const cp = codepoints[cellIndex] ?? 0;
54609
+ if (!cp)
54610
+ return null;
54611
+ let text = String.fromCodePoint(cp);
54612
+ const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[cellIndex] ?? 0 : 0;
54613
+ if (extra > 0 && graphemeOffset && graphemeBuffer) {
54614
+ const start = graphemeOffset[cellIndex] ?? 0;
54615
+ const cps = [cp];
54616
+ for (let j = 0;j < extra; j += 1) {
54617
+ const extraCp = graphemeBuffer[start + j];
54618
+ if (extraCp)
54619
+ cps.push(extraCp);
54620
+ }
54621
+ text = String.fromCodePoint(...cps);
54622
+ }
54623
+ return { cp, text, span: flag === 1 ? 2 : 1 };
54624
+ };
54518
54625
  const { useLinearBlending, useLinearCorrection } = resolveBlendFlags("webgl2");
54519
54626
  reportTermSize(cols, rows);
54520
54627
  const cursorPos = cursor ? resolveCursorPosition(cursor) : null;
@@ -54735,28 +54842,43 @@ function createResttyApp(options) {
54735
54842
  }
54736
54843
  if (bgOnly || textHidden)
54737
54844
  continue;
54738
- const wideFlag = wide ? wide[idx] : 0;
54739
- if (wideFlag === 2 || wideFlag === 3)
54845
+ if (mergedEmojiSkip[idx])
54740
54846
  continue;
54741
- const cp = codepoints[idx];
54742
- if (!cp)
54847
+ const cluster = readCellCluster(idx);
54848
+ if (!cluster)
54743
54849
  continue;
54850
+ const cp = cluster.cp;
54744
54851
  if (cp === KITTY_PLACEHOLDER_CP)
54745
54852
  continue;
54746
- const extra = graphemeLen && graphemeOffset && graphemeBuffer ? graphemeLen[idx] ?? 0 : 0;
54747
- if (extra === 0 && isSpaceCp(cp))
54748
- continue;
54749
- let text = String.fromCodePoint(cp);
54750
- if (extra > 0 && graphemeOffset && graphemeBuffer) {
54751
- const start = graphemeOffset[idx] ?? 0;
54752
- const cps = [cp];
54753
- for (let j = 0;j < extra; j += 1) {
54754
- const extraCp = graphemeBuffer[start + j];
54755
- if (extraCp)
54756
- cps.push(extraCp);
54853
+ let text = cluster.text;
54854
+ let baseSpan = cluster.span;
54855
+ const rowEnd = row * cols + cols;
54856
+ if (isRegionalIndicator(cp)) {
54857
+ const nextIdx = idx + baseSpan;
54858
+ if (nextIdx < rowEnd && !mergedEmojiSkip[nextIdx]) {
54859
+ const next = readCellCluster(nextIdx);
54860
+ if (next && isRegionalIndicator(next.cp)) {
54861
+ text += next.text;
54862
+ baseSpan += next.span;
54863
+ mergedEmojiSkip[nextIdx] = 1;
54864
+ }
54757
54865
  }
54758
- text = String.fromCodePoint(...cps);
54759
54866
  }
54867
+ let nextSeqIdx = idx + baseSpan;
54868
+ let guard = 0;
54869
+ while ((text.codePointAt(text.length - 1) ?? 0) === 8205 && nextSeqIdx < rowEnd && guard < 8) {
54870
+ const next = readCellCluster(nextSeqIdx);
54871
+ if (!next || !next.cp || isSpaceCp(next.cp))
54872
+ break;
54873
+ text += next.text;
54874
+ baseSpan += next.span;
54875
+ mergedEmojiSkip[nextSeqIdx] = 1;
54876
+ nextSeqIdx += next.span;
54877
+ guard += 1;
54878
+ }
54879
+ const extra = text.length > String.fromCodePoint(cp).length ? 1 : 0;
54880
+ if (extra === 0 && isSpaceCp(cp))
54881
+ continue;
54760
54882
  if (cursorBlock && cursorCell && row === cursorCell.row && col >= cursorCell.col && col < cursorCell.col + (cursorCell.wide ? 2 : 1)) {
54761
54883
  fg = [bgForText[0], bgForText[1], bgForText[2], 1];
54762
54884
  }
@@ -54778,7 +54900,6 @@ function createResttyApp(options) {
54778
54900
  }
54779
54901
  if (extra > 0 && text.trim() === "")
54780
54902
  continue;
54781
- const baseSpan = wideFlag === 1 ? 2 : 1;
54782
54903
  const fontIndex = pickFontIndexForText2(text, baseSpan);
54783
54904
  const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
54784
54905
  const shaped = shapeClusterWithFont(fontEntry, text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "restty",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Browser terminal rendering library powered by WASM, WebGPU/WebGL2, and TypeScript text shaping.",
5
5
  "keywords": [
6
6
  "terminal",