@code-yeongyu/senpi-tui 2026.11.11 → 2026.11.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/dist/utils.js CHANGED
@@ -883,8 +883,10 @@ export function flattenLines(lines) {
883
883
  for (const line of lines)
884
884
  Number(line);
885
885
  }
886
- export function wrapTextWithAnsi(text, width) {
886
+ /** onLine receives each output line's consumed UTF-16 interval in the ANSI-stripped input. */
887
+ export function wrapTextWithAnsi(text, width, onLine) {
887
888
  if (!text) {
889
+ onLine?.(0, 0);
888
890
  return [""];
889
891
  }
890
892
  // Handle newlines by processing each line separately
@@ -892,24 +894,31 @@ export function wrapTextWithAnsi(text, width) {
892
894
  const inputLines = text.split(/\r\n|\r|\n/);
893
895
  const result = [];
894
896
  const tracker = new AnsiCodeTracker();
895
- for (const inputLine of inputLines) {
897
+ let sourceOffset = 0;
898
+ const separators = onLine ? (text.match(/\r\n|\r|\n/g) ?? []) : [];
899
+ for (let index = 0; index < inputLines.length; index++) {
900
+ const inputLine = inputLines[index];
896
901
  // Prepend active ANSI codes from previous lines (except for first line)
897
902
  const prefix = result.length > 0 ? tracker.getActiveCodes() : "";
898
- const wrappedLines = wrapSingleLine(prefix + inputLine, width);
903
+ const wrappedLines = wrapSingleLine(prefix + inputLine, width, onLine && ((start, end) => onLine(sourceOffset + start, sourceOffset + end)));
899
904
  for (const wrappedLine of wrappedLines) {
900
905
  result.push(wrappedLine);
901
906
  }
902
907
  // Update tracker with codes from this line for next iteration
903
908
  updateTrackerFromText(inputLine, tracker);
909
+ if (onLine)
910
+ sourceOffset += stripTerminalSequences(inputLine).length + (separators[index]?.length ?? 0);
904
911
  }
905
912
  return result.length > 0 ? result : [""];
906
913
  }
907
- function wrapSingleLine(line, width) {
914
+ function wrapSingleLine(line, width, onLine) {
908
915
  if (!line) {
916
+ onLine?.(0, 0);
909
917
  return [""];
910
918
  }
911
919
  const visibleLength = visibleWidth(line);
912
920
  if (visibleLength <= width) {
921
+ onLine?.(0, stripTerminalSequences(line).length);
913
922
  return [line];
914
923
  }
915
924
  const wrapped = [];
@@ -917,7 +926,15 @@ function wrapSingleLine(line, width) {
917
926
  const tokens = splitIntoTokensWithAnsi(line);
918
927
  let currentLine = "";
919
928
  let currentVisibleLength = 0;
929
+ let sourceOffset = 0;
930
+ let lineStart = 0;
931
+ const emit = (text) => {
932
+ onLine?.(lineStart, lineStart + stripTerminalSequences(text.trimEnd()).length);
933
+ };
920
934
  for (const token of tokens) {
935
+ const tokenStart = sourceOffset;
936
+ if (onLine)
937
+ sourceOffset += stripTerminalSequences(token).length;
921
938
  const tokenVisibleLength = visibleWidth(token);
922
939
  const isWhitespace = token.trim() === "";
923
940
  // Token itself is too long - break it character by character
@@ -929,11 +946,18 @@ function wrapSingleLine(line, width) {
929
946
  currentLine += lineEndReset;
930
947
  }
931
948
  wrapped.push(currentLine);
949
+ emit(currentLine);
932
950
  currentLine = "";
933
951
  currentVisibleLength = 0;
934
952
  }
935
953
  // Break long token - breakLongWord handles its own resets
936
- const broken = breakLongWord(token, width, tracker);
954
+ const broken = breakLongWord(token, width, tracker, onLine &&
955
+ ((start, end) => {
956
+ onLine(tokenStart + start, tokenStart + end);
957
+ }));
958
+ // The final fragment remains in currentLine and is emitted with the next token.
959
+ if (onLine)
960
+ lineStart = sourceOffset - stripTerminalSequences(broken[broken.length - 1]).length;
937
961
  for (let i = 0; i < broken.length - 1; i++) {
938
962
  wrapped.push(broken[i]);
939
963
  }
@@ -951,14 +975,17 @@ function wrapSingleLine(line, width) {
951
975
  lineToWrap += lineEndReset;
952
976
  }
953
977
  wrapped.push(lineToWrap);
978
+ emit(lineToWrap);
954
979
  if (isWhitespace) {
955
980
  // Don't start new line with whitespace
956
981
  currentLine = tracker.getActiveCodes();
957
982
  currentVisibleLength = 0;
983
+ lineStart = sourceOffset;
958
984
  }
959
985
  else {
960
986
  currentLine = tracker.getActiveCodes() + token;
961
987
  currentVisibleLength = tokenVisibleLength;
988
+ lineStart = tokenStart;
962
989
  }
963
990
  }
964
991
  else {
@@ -971,6 +998,7 @@ function wrapSingleLine(line, width) {
971
998
  if (currentLine) {
972
999
  // No reset at end of final line - let caller handle it
973
1000
  wrapped.push(currentLine);
1001
+ emit(currentLine);
974
1002
  }
975
1003
  // Trailing whitespace can cause lines to exceed the requested width
976
1004
  return wrapped.length > 0 ? wrapped.map((line) => line.trimEnd()) : [""];
@@ -982,10 +1010,12 @@ export const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;
982
1010
  export function isWhitespaceChar(char) {
983
1011
  return /\s/.test(char);
984
1012
  }
985
- function breakLongWord(word, width, tracker) {
1013
+ function breakLongWord(word, width, tracker, onLine) {
986
1014
  const lines = [];
987
1015
  let currentLine = tracker.getActiveCodes();
988
1016
  let currentWidth = 0;
1017
+ let sourceOffset = 0;
1018
+ let lineStart = 0;
989
1019
  // First, separate ANSI codes from visible content
990
1020
  // We need to handle ANSI codes specially since they're not graphemes
991
1021
  let i = 0;
@@ -1032,11 +1062,14 @@ function breakLongWord(word, width, tracker) {
1032
1062
  currentLine += lineEndReset;
1033
1063
  }
1034
1064
  lines.push(currentLine);
1065
+ onLine?.(lineStart, sourceOffset);
1066
+ lineStart = sourceOffset;
1035
1067
  currentLine = tracker.getActiveCodes();
1036
1068
  currentWidth = 0;
1037
1069
  }
1038
1070
  currentLine += grapheme;
1039
1071
  currentWidth += graphemeWidth;
1072
+ sourceOffset += grapheme.length;
1040
1073
  }
1041
1074
  if (currentLine) {
1042
1075
  // No reset at end of final segment - caller handles continuation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-tui",
3
- "version": "2026.11.11",
3
+ "version": "2026.11.13",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",