spawn-term 3.5.4 → 3.5.6

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.
@@ -12,6 +12,7 @@ var _jsxruntime = require("react/jsx-runtime");
12
12
  var _ink = require("ink");
13
13
  var _react = require("react");
14
14
  var _constantsts = require("../constants.js");
15
+ var _ansiRegexts = /*#__PURE__*/ _interop_require_default(require("../lib/ansiRegex.js"));
15
16
  var _clipTextts = require("../lib/clipText.js");
16
17
  var _figurests = /*#__PURE__*/ _interop_require_default(require("../lib/figures.js"));
17
18
  var _formatts = require("../lib/format.js");
@@ -51,6 +52,21 @@ function _object_spread(target) {
51
52
  }
52
53
  return target;
53
54
  }
55
+ var ansi = (0, _ansiRegexts.default)();
56
+ /**
57
+ * Strip ANSI escape codes from a string.
58
+ */ function stripAnsi(str) {
59
+ return str.replace(ansi, '');
60
+ }
61
+ /**
62
+ * Simple truncation for plain text (no ANSI codes).
63
+ * Adds ellipsis if truncated.
64
+ */ function truncate(str, maxWidth) {
65
+ if (maxWidth <= 0) return '';
66
+ if (maxWidth === 1) return '…';
67
+ if (str.length <= maxWidth) return str;
68
+ return "".concat(str.slice(0, maxWidth - 1), "…");
69
+ }
54
70
  function getLastOutputLine(lines) {
55
71
  for(var i = lines.length - 1; i >= 0; i--){
56
72
  if (lines[i].text.length > 0) {
@@ -86,12 +102,13 @@ var _default = /*#__PURE__*/ (0, _react.memo)(function CompactProcessLine(param)
86
102
  var statusText = (0, _react.useMemo)(function() {
87
103
  if (state === 'running') {
88
104
  var lastLine = getLastOutputLine(lines);
89
- return lastLine ? (0, _clipTextts.clipText)(lastLine, statusWidth) : '';
105
+ var stripped = lastLine ? stripAnsi(lastLine) : '';
106
+ return stripped ? truncate(stripped, statusWidth) : '';
90
107
  }
91
108
  if (state === 'error') {
92
109
  var errorCount = getErrorCount(lines);
93
110
  var text = errorCount > 0 ? "".concat(errorCount, " error").concat(errorCount > 1 ? 's' : '') : 'failed';
94
- return (0, _clipTextts.clipText)(text, statusWidth);
111
+ return truncate(text, statusWidth);
95
112
  }
96
113
  return ''; // success - no status text
97
114
  }, [
@@ -118,8 +135,6 @@ var _default = /*#__PURE__*/ (0, _react.memo)(function CompactProcessLine(param)
118
135
  }, [
119
136
  state
120
137
  ]);
121
- // Status text color
122
- var statusColor = state === 'error' ? 'red' : 'gray';
123
138
  return /*#__PURE__*/ (0, _jsxruntime.jsxs)(_ink.Box, {
124
139
  width: terminalWidth,
125
140
  children: [
@@ -141,7 +156,6 @@ var _default = /*#__PURE__*/ (0, _react.memo)(function CompactProcessLine(param)
141
156
  statusWidth > 0 && statusText && /*#__PURE__*/ (0, _jsxruntime.jsx)(_ink.Box, {
142
157
  width: statusWidth + gap,
143
158
  children: /*#__PURE__*/ (0, _jsxruntime.jsxs)(_ink.Text, {
144
- color: statusColor,
145
159
  children: [
146
160
  " ",
147
161
  statusText
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/components/CompactProcessLine.tsx"],"sourcesContent":["import { Box, Text, useStdout } from 'ink';\nimport { memo, useMemo } from 'react';\nimport { SPINNER } from '../constants.ts';\nimport { clipText } from '../lib/clipText.ts';\nimport figures from '../lib/figures.ts';\nimport { calculateColumnWidth } from '../lib/format.ts';\nimport { useStore } from '../state/StoreContext.ts';\nimport type { ChildProcess, Line } from '../types.ts';\nimport { LineType } from '../types.ts';\nimport Spinner from './Spinner.ts';\n\ntype Props = {\n item: ChildProcess;\n isSelected?: boolean;\n};\n\nfunction getLastOutputLine(lines: Line[]): string {\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i].text.length > 0) {\n return lines[i].text;\n }\n }\n return '';\n}\n\nfunction getErrorCount(lines: Line[]): number {\n return lines.filter((line) => line.type === LineType.stderr).length;\n}\n\nexport default memo(function CompactProcessLine({ item, isSelected = false }: Props) {\n const store = useStore();\n const { stdout } = useStdout();\n const terminalWidth = stdout?.columns || 120;\n\n const { group, title, state, lines } = item;\n const selectionIndicator = isSelected ? figures.pointer : ' ';\n\n // Display name: prefer group, fall back to title\n const displayName = group || title;\n\n // Calculate widths - use dynamic column width based on longest name\n const selectionWidth = 1; // selection indicator\n const iconWidth = 2; // icon + space\n const maxGroupLength = store.getMaxGroupLength();\n const nameColumnWidth = calculateColumnWidth('max', terminalWidth, maxGroupLength);\n const gap = 1; // space between name and status\n const statusWidth = Math.max(0, terminalWidth - selectionWidth - iconWidth - nameColumnWidth - gap);\n\n // Clip name to column width and pad\n const clippedName = clipText(displayName, nameColumnWidth).padEnd(nameColumnWidth);\n\n // Status text based on state - clip to available width\n const statusText = useMemo(() => {\n if (state === 'running') {\n const lastLine = getLastOutputLine(lines);\n return lastLine ? clipText(lastLine, statusWidth) : '';\n }\n if (state === 'error') {\n const errorCount = getErrorCount(lines);\n const text = errorCount > 0 ? `${errorCount} error${errorCount > 1 ? 's' : ''}` : 'failed';\n return clipText(text, statusWidth);\n }\n return ''; // success - no status text\n }, [state, lines, statusWidth]);\n\n // Icon based on state\n const icon = useMemo(() => {\n switch (state) {\n case 'running':\n return <Spinner {...SPINNER} />;\n case 'success':\n return <Text color=\"green\">{figures.tick}</Text>;\n case 'error':\n return <Text color=\"red\">{figures.cross}</Text>;\n }\n }, [state]);\n\n // Status text color\n const statusColor = state === 'error' ? 'red' : 'gray';\n\n return (\n <Box width={terminalWidth}>\n <Text color={isSelected ? 'cyan' : undefined}>{selectionIndicator}</Text>\n <Box width={iconWidth}>{icon}</Box>\n <Box width={nameColumnWidth}>\n <Text inverse={isSelected}>{clippedName}</Text>\n </Box>\n {statusWidth > 0 && statusText && (\n <Box width={statusWidth + gap}>\n <Text color={statusColor}> {statusText}</Text>\n </Box>\n )}\n </Box>\n );\n});\n"],"names":["getLastOutputLine","lines","i","length","text","getErrorCount","filter","line","type","LineType","stderr","memo","CompactProcessLine","item","isSelected","store","useStore","stdout","useStdout","terminalWidth","columns","group","title","state","selectionIndicator","figures","pointer","displayName","selectionWidth","iconWidth","maxGroupLength","getMaxGroupLength","nameColumnWidth","calculateColumnWidth","gap","statusWidth","Math","max","clippedName","clipText","padEnd","statusText","useMemo","lastLine","errorCount","icon","Spinner","SPINNER","Text","color","tick","cross","statusColor","Box","width","undefined","inverse"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/components/CompactProcessLine.tsx"],"sourcesContent":["import { Box, Text, useStdout } from 'ink';\nimport { memo, useMemo } from 'react';\nimport { SPINNER } from '../constants.ts';\nimport ansiRegex from '../lib/ansiRegex.ts';\nimport { clipText } from '../lib/clipText.ts';\nimport figures from '../lib/figures.ts';\nimport { calculateColumnWidth } from '../lib/format.ts';\nimport { useStore } from '../state/StoreContext.ts';\nimport type { ChildProcess, Line } from '../types.ts';\nimport { LineType } from '../types.ts';\nimport Spinner from './Spinner.ts';\n\nconst ansi = ansiRegex();\n\n/**\n * Strip ANSI escape codes from a string.\n */\nfunction stripAnsi(str: string): string {\n return str.replace(ansi, '');\n}\n\n/**\n * Simple truncation for plain text (no ANSI codes).\n * Adds ellipsis if truncated.\n */\nfunction truncate(str: string, maxWidth: number): string {\n if (maxWidth <= 0) return '';\n if (maxWidth === 1) return '…';\n if (str.length <= maxWidth) return str;\n return `${str.slice(0, maxWidth - 1)}…`;\n}\n\ntype Props = {\n item: ChildProcess;\n isSelected?: boolean;\n};\n\nfunction getLastOutputLine(lines: Line[]): string {\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i].text.length > 0) {\n return lines[i].text;\n }\n }\n return '';\n}\n\nfunction getErrorCount(lines: Line[]): number {\n return lines.filter((line) => line.type === LineType.stderr).length;\n}\n\nexport default memo(function CompactProcessLine({ item, isSelected = false }: Props) {\n const store = useStore();\n const { stdout } = useStdout();\n const terminalWidth = stdout?.columns || 120;\n\n const { group, title, state, lines } = item;\n const selectionIndicator = isSelected ? figures.pointer : ' ';\n\n // Display name: prefer group, fall back to title\n const displayName = group || title;\n\n // Calculate widths - use dynamic column width based on longest name\n const selectionWidth = 1; // selection indicator\n const iconWidth = 2; // icon + space\n const maxGroupLength = store.getMaxGroupLength();\n const nameColumnWidth = calculateColumnWidth('max', terminalWidth, maxGroupLength);\n const gap = 1; // space between name and status\n const statusWidth = Math.max(0, terminalWidth - selectionWidth - iconWidth - nameColumnWidth - gap);\n\n // Clip name to column width and pad\n const clippedName = clipText(displayName, nameColumnWidth).padEnd(nameColumnWidth);\n\n // Status text based on state - clip to available width\n const statusText = useMemo(() => {\n if (state === 'running') {\n const lastLine = getLastOutputLine(lines);\n const stripped = lastLine ? stripAnsi(lastLine) : '';\n return stripped ? truncate(stripped, statusWidth) : '';\n }\n if (state === 'error') {\n const errorCount = getErrorCount(lines);\n const text = errorCount > 0 ? `${errorCount} error${errorCount > 1 ? 's' : ''}` : 'failed';\n return truncate(text, statusWidth);\n }\n return ''; // success - no status text\n }, [state, lines, statusWidth]);\n\n // Icon based on state\n const icon = useMemo(() => {\n switch (state) {\n case 'running':\n return <Spinner {...SPINNER} />;\n case 'success':\n return <Text color=\"green\">{figures.tick}</Text>;\n case 'error':\n return <Text color=\"red\">{figures.cross}</Text>;\n }\n }, [state]);\n\n return (\n <Box width={terminalWidth}>\n <Text color={isSelected ? 'cyan' : undefined}>{selectionIndicator}</Text>\n <Box width={iconWidth}>{icon}</Box>\n <Box width={nameColumnWidth}>\n <Text inverse={isSelected}>{clippedName}</Text>\n </Box>\n {statusWidth > 0 && statusText && (\n <Box width={statusWidth + gap}>\n <Text> {statusText}</Text>\n </Box>\n )}\n </Box>\n );\n});\n"],"names":["ansi","ansiRegex","stripAnsi","str","replace","truncate","maxWidth","length","slice","getLastOutputLine","lines","i","text","getErrorCount","filter","line","type","LineType","stderr","memo","CompactProcessLine","item","isSelected","store","useStore","stdout","useStdout","terminalWidth","columns","group","title","state","selectionIndicator","figures","pointer","displayName","selectionWidth","iconWidth","maxGroupLength","getMaxGroupLength","nameColumnWidth","calculateColumnWidth","gap","statusWidth","Math","max","clippedName","clipText","padEnd","statusText","useMemo","lastLine","stripped","errorCount","icon","Spinner","SPINNER","Text","color","tick","cross","Box","width","undefined","inverse"],"mappings":";;;;+BAkDA;;;eAAA;;;;mBAlDqC;qBACP;2BACN;kEACF;0BACG;gEACL;wBACiB;8BACZ;uBAEA;gEACL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEpB,IAAMA,OAAOC,IAAAA,oBAAS;AAEtB;;CAEC,GACD,SAASC,UAAUC,GAAW;IAC5B,OAAOA,IAAIC,OAAO,CAACJ,MAAM;AAC3B;AAEA;;;CAGC,GACD,SAASK,SAASF,GAAW,EAAEG,QAAgB;IAC7C,IAAIA,YAAY,GAAG,OAAO;IAC1B,IAAIA,aAAa,GAAG,OAAO;IAC3B,IAAIH,IAAII,MAAM,IAAID,UAAU,OAAOH;IACnC,OAAO,AAAC,GAA6B,OAA3BA,IAAIK,KAAK,CAAC,GAAGF,WAAW,IAAG;AACvC;AAOA,SAASG,kBAAkBC,KAAa;IACtC,IAAK,IAAIC,IAAID,MAAMH,MAAM,GAAG,GAAGI,KAAK,GAAGA,IAAK;QAC1C,IAAID,KAAK,CAACC,EAAE,CAACC,IAAI,CAACL,MAAM,GAAG,GAAG;YAC5B,OAAOG,KAAK,CAACC,EAAE,CAACC,IAAI;QACtB;IACF;IACA,OAAO;AACT;AAEA,SAASC,cAAcH,KAAa;IAClC,OAAOA,MAAMI,MAAM,CAAC,SAACC;eAASA,KAAKC,IAAI,KAAKC,iBAAQ,CAACC,MAAM;OAAEX,MAAM;AACrE;IAEA,yBAAeY,IAAAA,WAAI,EAAC,SAASC,mBAAmB,KAAmC;QAAjCC,OAAF,MAAEA,0BAAF,MAAQC,YAAAA,4CAAa;IACnE,IAAMC,QAAQC,IAAAA,wBAAQ;IACtB,IAAM,AAAEC,SAAWC,IAAAA,cAAS,IAApBD;IACR,IAAME,gBAAgBF,CAAAA,mBAAAA,6BAAAA,OAAQG,OAAO,KAAI;IAEzC,IAAQC,QAA+BR,KAA/BQ,OAAOC,QAAwBT,KAAxBS,OAAOC,QAAiBV,KAAjBU,OAAOrB,QAAUW,KAAVX;IAC7B,IAAMsB,qBAAqBV,aAAaW,kBAAO,CAACC,OAAO,GAAG;IAE1D,iDAAiD;IACjD,IAAMC,cAAcN,SAASC;IAE7B,oEAAoE;IACpE,IAAMM,iBAAiB,GAAG,sBAAsB;IAChD,IAAMC,YAAY,GAAG,eAAe;IACpC,IAAMC,iBAAiBf,MAAMgB,iBAAiB;IAC9C,IAAMC,kBAAkBC,IAAAA,8BAAoB,EAAC,OAAOd,eAAeW;IACnE,IAAMI,MAAM,GAAG,gCAAgC;IAC/C,IAAMC,cAAcC,KAAKC,GAAG,CAAC,GAAGlB,gBAAgBS,iBAAiBC,YAAYG,kBAAkBE;IAE/F,oCAAoC;IACpC,IAAMI,cAAcC,IAAAA,oBAAQ,EAACZ,aAAaK,iBAAiBQ,MAAM,CAACR;IAElE,uDAAuD;IACvD,IAAMS,aAAaC,IAAAA,cAAO,EAAC;QACzB,IAAInB,UAAU,WAAW;YACvB,IAAMoB,WAAW1C,kBAAkBC;YACnC,IAAM0C,WAAWD,WAAWjD,UAAUiD,YAAY;YAClD,OAAOC,WAAW/C,SAAS+C,UAAUT,eAAe;QACtD;QACA,IAAIZ,UAAU,SAAS;YACrB,IAAMsB,aAAaxC,cAAcH;YACjC,IAAME,OAAOyC,aAAa,IAAI,AAAC,GAAqBA,OAAnBA,YAAW,UAAkC,OAA1BA,aAAa,IAAI,MAAM,MAAO;YAClF,OAAOhD,SAASO,MAAM+B;QACxB;QACA,OAAO,IAAI,2BAA2B;IACxC,GAAG;QAACZ;QAAOrB;QAAOiC;KAAY;IAE9B,sBAAsB;IACtB,IAAMW,OAAOJ,IAAAA,cAAO,EAAC;QACnB,OAAQnB;YACN,KAAK;gBACH,qBAAO,qBAACwB,kBAAO,qBAAKC,oBAAO;YAC7B,KAAK;gBACH,qBAAO,qBAACC,SAAI;oBAACC,OAAM;8BAASzB,kBAAO,CAAC0B,IAAI;;YAC1C,KAAK;gBACH,qBAAO,qBAACF,SAAI;oBAACC,OAAM;8BAAOzB,kBAAO,CAAC2B,KAAK;;QAC3C;IACF,GAAG;QAAC7B;KAAM;IAEV,qBACE,sBAAC8B,QAAG;QAACC,OAAOnC;;0BACV,qBAAC8B,SAAI;gBAACC,OAAOpC,aAAa,SAASyC;0BAAY/B;;0BAC/C,qBAAC6B,QAAG;gBAACC,OAAOzB;0BAAYiB;;0BACxB,qBAACO,QAAG;gBAACC,OAAOtB;0BACV,cAAA,qBAACiB,SAAI;oBAACO,SAAS1C;8BAAawB;;;YAE7BH,cAAc,KAAKM,4BAClB,qBAACY,QAAG;gBAACC,OAAOnB,cAAcD;0BACxB,cAAA,sBAACe,SAAI;;wBAAC;wBAAER;;;;;;AAKlB"}
@@ -12,6 +12,12 @@ var _jsxruntime = require("react/jsx-runtime");
12
12
  var _ink = require("ink");
13
13
  var _react = require("react");
14
14
  var _constantsts = require("../constants.js");
15
+ var _AnsiTextts = /*#__PURE__*/ _interop_require_default(require("../lib/AnsiText.js"));
16
+ function _interop_require_default(obj) {
17
+ return obj && obj.__esModule ? obj : {
18
+ default: obj
19
+ };
20
+ }
15
21
  var isMac = process.platform === 'darwin';
16
22
  var _default = /*#__PURE__*/ (0, _react.memo)(function ExpandedOutput(param) {
17
23
  var lines = param.lines, scrollOffset = param.scrollOffset, _param_maxVisible = param.maxVisible, maxVisible = _param_maxVisible === void 0 ? _constantsts.EXPANDED_MAX_VISIBLE_LINES : _param_maxVisible;
@@ -36,7 +42,9 @@ var _default = /*#__PURE__*/ (0, _react.memo)(function ExpandedOutput(param) {
36
42
  /*#__PURE__*/ (0, _jsxruntime.jsxs)(_ink.Text, {
37
43
  children: [
38
44
  "│ ",
39
- line.text || ' '
45
+ /*#__PURE__*/ (0, _jsxruntime.jsx)(_AnsiTextts.default, {
46
+ children: line.text || ' '
47
+ })
40
48
  ]
41
49
  }, scrollOffset + i));
42
50
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/components/ExpandedOutput.tsx"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { memo } from 'react';\nimport { EXPANDED_MAX_VISIBLE_LINES } from '../constants.ts';\nimport type { Line } from '../types.ts';\n\nconst isMac = process.platform === 'darwin';\n\ntype Props = {\n lines: Line[];\n scrollOffset: number;\n maxVisible?: number;\n};\n\nexport default memo(function ExpandedOutput({ lines, scrollOffset, maxVisible = EXPANDED_MAX_VISIBLE_LINES }: Props) {\n const visibleLines = lines.slice(scrollOffset, scrollOffset + maxVisible);\n const hasMore = lines.length > scrollOffset + maxVisible;\n const remaining = lines.length - scrollOffset - maxVisible;\n\n if (lines.length === 0) {\n return (\n <Box paddingLeft={2}>\n <Text dimColor>│ (no output)</Text>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\" paddingLeft={2}>\n {visibleLines.map((line, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: Lines have no unique ID, index is stable for this scrolling view\n <Text key={scrollOffset + i}>│ {line.text || ' '}</Text>\n ))}\n {hasMore ? (\n <Text dimColor>\n │ [+{remaining} more, Tab/⇧Tab page, {isMac ? '⌥↑/↓' : 'g/G'} top/bottom, ↵ close]\n </Text>\n ) : (\n <Text dimColor>│ [↵ close]</Text>\n )}\n </Box>\n );\n});\n"],"names":["isMac","process","platform","memo","ExpandedOutput","lines","scrollOffset","maxVisible","EXPANDED_MAX_VISIBLE_LINES","visibleLines","slice","hasMore","length","remaining","Box","paddingLeft","Text","dimColor","flexDirection","map","line","i","text"],"mappings":";;;;+BAaA;;;eAAA;;;;mBAb0B;qBACL;2BACsB;AAG3C,IAAMA,QAAQC,QAAQC,QAAQ,KAAK;IAQnC,yBAAeC,IAAAA,WAAI,EAAC,SAASC,eAAe,KAAuE;QAArEC,QAAF,MAAEA,OAAOC,eAAT,MAASA,kCAAT,MAAuBC,YAAAA,4CAAaC,uCAA0B;IACxG,IAAMC,eAAeJ,MAAMK,KAAK,CAACJ,cAAcA,eAAeC;IAC9D,IAAMI,UAAUN,MAAMO,MAAM,GAAGN,eAAeC;IAC9C,IAAMM,YAAYR,MAAMO,MAAM,GAAGN,eAAeC;IAEhD,IAAIF,MAAMO,MAAM,KAAK,GAAG;QACtB,qBACE,qBAACE,QAAG;YAACC,aAAa;sBAChB,cAAA,qBAACC,SAAI;gBAACC,QAAQ;0BAAC;;;IAGrB;IAEA,qBACE,sBAACH,QAAG;QAACI,eAAc;QAASH,aAAa;;YACtCN,aAAaU,GAAG,CAAC,SAACC,MAAMC;uBACvB,iHAAiH;8BACjH,sBAACL,SAAI;;wBAAwB;wBAAGI,KAAKE,IAAI,IAAI;;mBAAlChB,eAAee;;YAE3BV,wBACC,sBAACK,SAAI;gBAACC,QAAQ;;oBAAC;oBACRJ;oBAAU;oBAAuBb,QAAQ,SAAS;oBAAM;;+BAG/D,qBAACgB,SAAI;gBAACC,QAAQ;0BAAC;;;;AAIvB"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/components/ExpandedOutput.tsx"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { memo } from 'react';\nimport { EXPANDED_MAX_VISIBLE_LINES } from '../constants.ts';\nimport AnsiText from '../lib/AnsiText.ts';\nimport type { Line } from '../types.ts';\n\nconst isMac = process.platform === 'darwin';\n\ntype Props = {\n lines: Line[];\n scrollOffset: number;\n maxVisible?: number;\n};\n\nexport default memo(function ExpandedOutput({ lines, scrollOffset, maxVisible = EXPANDED_MAX_VISIBLE_LINES }: Props) {\n const visibleLines = lines.slice(scrollOffset, scrollOffset + maxVisible);\n const hasMore = lines.length > scrollOffset + maxVisible;\n const remaining = lines.length - scrollOffset - maxVisible;\n\n if (lines.length === 0) {\n return (\n <Box paddingLeft={2}>\n <Text dimColor>│ (no output)</Text>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\" paddingLeft={2}>\n {visibleLines.map((line, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: Lines have no unique ID, index is stable for this scrolling view\n <Text key={scrollOffset + i}>\n │ <AnsiText>{line.text || ' '}</AnsiText>\n </Text>\n ))}\n {hasMore ? (\n <Text dimColor>\n │ [+{remaining} more, Tab/⇧Tab page, {isMac ? '⌥↑/↓' : 'g/G'} top/bottom, ↵ close]\n </Text>\n ) : (\n <Text dimColor>│ [↵ close]</Text>\n )}\n </Box>\n );\n});\n"],"names":["isMac","process","platform","memo","ExpandedOutput","lines","scrollOffset","maxVisible","EXPANDED_MAX_VISIBLE_LINES","visibleLines","slice","hasMore","length","remaining","Box","paddingLeft","Text","dimColor","flexDirection","map","line","i","AnsiText","text"],"mappings":";;;;+BAcA;;;eAAA;;;;mBAd0B;qBACL;2BACsB;iEACtB;;;;;;AAGrB,IAAMA,QAAQC,QAAQC,QAAQ,KAAK;IAQnC,yBAAeC,IAAAA,WAAI,EAAC,SAASC,eAAe,KAAuE;QAArEC,QAAF,MAAEA,OAAOC,eAAT,MAASA,kCAAT,MAAuBC,YAAAA,4CAAaC,uCAA0B;IACxG,IAAMC,eAAeJ,MAAMK,KAAK,CAACJ,cAAcA,eAAeC;IAC9D,IAAMI,UAAUN,MAAMO,MAAM,GAAGN,eAAeC;IAC9C,IAAMM,YAAYR,MAAMO,MAAM,GAAGN,eAAeC;IAEhD,IAAIF,MAAMO,MAAM,KAAK,GAAG;QACtB,qBACE,qBAACE,QAAG;YAACC,aAAa;sBAChB,cAAA,qBAACC,SAAI;gBAACC,QAAQ;0BAAC;;;IAGrB;IAEA,qBACE,sBAACH,QAAG;QAACI,eAAc;QAASH,aAAa;;YACtCN,aAAaU,GAAG,CAAC,SAACC,MAAMC;uBACvB,iHAAiH;8BACjH,sBAACL,SAAI;;wBAAwB;sCACzB,qBAACM,mBAAQ;sCAAEF,KAAKG,IAAI,IAAI;;;mBADjBjB,eAAee;;YAI3BV,wBACC,sBAACK,SAAI;gBAACC,QAAQ;;oBAAC;oBACRJ;oBAAU;oBAAuBb,QAAQ,SAAS;oBAAM;;+BAG/D,qBAACgB,SAAI;gBAACC,QAAQ;0BAAC;;;;AAIvB"}
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, /**
6
+ * Component that renders text with ANSI escape codes as styled Ink Text components.
7
+ * Parses ANSI color and style codes and applies them as Ink props.
8
+ */ "default", {
9
+ enumerable: true,
10
+ get: function() {
11
+ return _default;
12
+ }
13
+ });
14
+ var _jsxruntime = require("react/jsx-runtime");
15
+ var _ink = require("ink");
16
+ var _react = require("react");
17
+ function _array_like_to_array(arr, len) {
18
+ if (len == null || len > arr.length) len = arr.length;
19
+ for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
20
+ return arr2;
21
+ }
22
+ function _array_with_holes(arr) {
23
+ if (Array.isArray(arr)) return arr;
24
+ }
25
+ function _define_property(obj, key, value) {
26
+ if (key in obj) {
27
+ Object.defineProperty(obj, key, {
28
+ value: value,
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true
32
+ });
33
+ } else {
34
+ obj[key] = value;
35
+ }
36
+ return obj;
37
+ }
38
+ function _iterable_to_array_limit(arr, i) {
39
+ var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
40
+ if (_i == null) return;
41
+ var _arr = [];
42
+ var _n = true;
43
+ var _d = false;
44
+ var _s, _e;
45
+ try {
46
+ for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){
47
+ _arr.push(_s.value);
48
+ if (i && _arr.length === i) break;
49
+ }
50
+ } catch (err) {
51
+ _d = true;
52
+ _e = err;
53
+ } finally{
54
+ try {
55
+ if (!_n && _i["return"] != null) _i["return"]();
56
+ } finally{
57
+ if (_d) throw _e;
58
+ }
59
+ }
60
+ return _arr;
61
+ }
62
+ function _non_iterable_rest() {
63
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
64
+ }
65
+ function _object_spread(target) {
66
+ for(var i = 1; i < arguments.length; i++){
67
+ var source = arguments[i] != null ? arguments[i] : {};
68
+ var ownKeys = Object.keys(source);
69
+ if (typeof Object.getOwnPropertySymbols === "function") {
70
+ ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
71
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
72
+ }));
73
+ }
74
+ ownKeys.forEach(function(key) {
75
+ _define_property(target, key, source[key]);
76
+ });
77
+ }
78
+ return target;
79
+ }
80
+ function _sliced_to_array(arr, i) {
81
+ return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest();
82
+ }
83
+ function _unsupported_iterable_to_array(o, minLen) {
84
+ if (!o) return;
85
+ if (typeof o === "string") return _array_like_to_array(o, minLen);
86
+ var n = Object.prototype.toString.call(o).slice(8, -1);
87
+ if (n === "Object" && o.constructor) n = o.constructor.name;
88
+ if (n === "Map" || n === "Set") return Array.from(n);
89
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
90
+ }
91
+ // ANSI color codes to Ink color mapping
92
+ // Based on standard ANSI SGR (Select Graphic Rendition) parameters
93
+ var ANSI_COLORS = {
94
+ '\x1b[30m': 'black',
95
+ '\x1b[31m': 'red',
96
+ '\x1b[32m': 'green',
97
+ '\x1b[33m': 'yellow',
98
+ '\x1b[34m': 'blue',
99
+ '\x1b[35m': 'magenta',
100
+ '\x1b[36m': 'cyan',
101
+ '\x1b[37m': 'white',
102
+ // Bright colors
103
+ '\x1b[90m': 'gray',
104
+ '\x1b[91m': 'redBright',
105
+ '\x1b[92m': 'greenBright',
106
+ '\x1b[93m': 'yellowBright',
107
+ '\x1b[94m': 'blueBright',
108
+ '\x1b[95m': 'magentaBright',
109
+ '\x1b[96m': 'cyanBright',
110
+ '\x1b[97m': 'whiteBright'
111
+ };
112
+ // ANSI style codes
113
+ var ANSI_STYLES = {
114
+ '\x1b[1m': 'bold',
115
+ '\x1b[2m': 'dim',
116
+ '\x1b[3m': 'italic',
117
+ '\x1b[4m': 'underline',
118
+ '\x1b[9m': 'strikethrough',
119
+ '\x1b[7m': 'inverse'
120
+ };
121
+ // Reset codes
122
+ var RESET_CODES = [
123
+ '\x1b[0m',
124
+ '\x1b[39m',
125
+ '\x1b[49m'
126
+ ];
127
+ /**
128
+ * Parse a string with ANSI codes into segments with styling information.
129
+ */ function parseAnsiString(input) {
130
+ var segments = [];
131
+ var currentSegment = {
132
+ text: ''
133
+ };
134
+ var i = 0;
135
+ while(i < input.length){
136
+ // Check for ANSI escape sequence
137
+ if (input[i] === '\x1b' && input[i + 1] === '[') {
138
+ // Find the end of the escape sequence (ends with a letter)
139
+ var j = i + 2;
140
+ while(j < input.length && !/[a-zA-Z]/.test(input[j])){
141
+ j++;
142
+ }
143
+ j++; // Include the letter
144
+ var code = input.slice(i, j);
145
+ // If we have accumulated text, save it
146
+ if (currentSegment.text.length > 0) {
147
+ segments.push(currentSegment);
148
+ currentSegment = _object_spread({
149
+ text: ''
150
+ }, Object.fromEntries(Object.entries(currentSegment).filter(function(param) {
151
+ var _param = _sliced_to_array(param, 1), key = _param[0];
152
+ return key !== 'text';
153
+ }).filter(function(param) {
154
+ var _param = _sliced_to_array(param, 2), _ = _param[0], value = _param[1];
155
+ return value !== undefined;
156
+ })));
157
+ }
158
+ // Apply the style
159
+ if (RESET_CODES.includes(code)) {
160
+ // Reset all styles
161
+ currentSegment = {
162
+ text: ''
163
+ };
164
+ } else if (ANSI_COLORS[code]) {
165
+ currentSegment.color = ANSI_COLORS[code];
166
+ } else if (ANSI_STYLES[code]) {
167
+ var style = ANSI_STYLES[code];
168
+ currentSegment[style] = true;
169
+ }
170
+ i = j;
171
+ } else {
172
+ // Regular character
173
+ currentSegment.text += input[i];
174
+ i++;
175
+ }
176
+ }
177
+ // Add the last segment if it has text
178
+ if (currentSegment.text.length > 0) {
179
+ segments.push(currentSegment);
180
+ }
181
+ return segments;
182
+ }
183
+ var _default = /*#__PURE__*/ (0, _react.memo)(function AnsiText(param) {
184
+ var children = param.children;
185
+ // If the input is empty or has no ANSI codes, just render it directly
186
+ if (!children || !children.includes('\x1b')) {
187
+ return /*#__PURE__*/ (0, _jsxruntime.jsx)(_react.Fragment, {
188
+ children: children
189
+ });
190
+ }
191
+ var segments = parseAnsiString(children);
192
+ // If parsing resulted in a single unstyled segment, render it directly
193
+ if (segments.length === 1 && !segments[0].color && !segments[0].bold && !segments[0].dim && !segments[0].italic && !segments[0].underline && !segments[0].strikethrough && !segments[0].inverse) {
194
+ return /*#__PURE__*/ (0, _jsxruntime.jsx)(_react.Fragment, {
195
+ children: segments[0].text
196
+ });
197
+ }
198
+ return /*#__PURE__*/ (0, _jsxruntime.jsx)(_react.Fragment, {
199
+ children: segments.map(function(segment, index) {
200
+ return(// biome-ignore lint/suspicious/noArrayIndexKey: Segments have no unique ID, index is stable for parsed content
201
+ /*#__PURE__*/ (0, _jsxruntime.jsx)(_ink.Text, {
202
+ color: segment.color,
203
+ bold: segment.bold,
204
+ dimColor: segment.dim,
205
+ italic: segment.italic,
206
+ underline: segment.underline,
207
+ strikethrough: segment.strikethrough,
208
+ inverse: segment.inverse,
209
+ children: segment.text
210
+ }, index));
211
+ })
212
+ });
213
+ });
214
+ /* CJS INTEROP */ if (exports.__esModule && exports.default) { try { Object.defineProperty(exports.default, '__esModule', { value: true }); for (var key in exports) { exports.default[key] = exports[key]; } } catch (_) {}; module.exports = exports.default; }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/lib/AnsiText.tsx"],"sourcesContent":["import { Text } from 'ink';\nimport { Fragment, memo } from 'react';\n\n// ANSI color codes to Ink color mapping\n// Based on standard ANSI SGR (Select Graphic Rendition) parameters\nconst ANSI_COLORS: Record<string, string> = {\n '\\x1b[30m': 'black',\n '\\x1b[31m': 'red',\n '\\x1b[32m': 'green',\n '\\x1b[33m': 'yellow',\n '\\x1b[34m': 'blue',\n '\\x1b[35m': 'magenta',\n '\\x1b[36m': 'cyan',\n '\\x1b[37m': 'white',\n // Bright colors\n '\\x1b[90m': 'gray',\n '\\x1b[91m': 'redBright',\n '\\x1b[92m': 'greenBright',\n '\\x1b[93m': 'yellowBright',\n '\\x1b[94m': 'blueBright',\n '\\x1b[95m': 'magentaBright',\n '\\x1b[96m': 'cyanBright',\n '\\x1b[97m': 'whiteBright',\n};\n\n// ANSI style codes\nconst ANSI_STYLES: Record<string, string> = {\n '\\x1b[1m': 'bold',\n '\\x1b[2m': 'dim',\n '\\x1b[3m': 'italic',\n '\\x1b[4m': 'underline',\n '\\x1b[9m': 'strikethrough',\n '\\x1b[7m': 'inverse',\n};\n\n// Reset codes\nconst RESET_CODES = ['\\x1b[0m', '\\x1b[39m', '\\x1b[49m'];\n\ntype TextSegment = {\n text: string;\n color?: string;\n bold?: boolean;\n dim?: boolean;\n italic?: boolean;\n underline?: boolean;\n strikethrough?: boolean;\n inverse?: boolean;\n};\n\n/**\n * Parse a string with ANSI codes into segments with styling information.\n */\nfunction parseAnsiString(input: string): TextSegment[] {\n const segments: TextSegment[] = [];\n let currentSegment: TextSegment = { text: '' };\n let i = 0;\n\n while (i < input.length) {\n // Check for ANSI escape sequence\n if (input[i] === '\\x1b' && input[i + 1] === '[') {\n // Find the end of the escape sequence (ends with a letter)\n let j = i + 2;\n while (j < input.length && !/[a-zA-Z]/.test(input[j])) {\n j++;\n }\n j++; // Include the letter\n\n const code = input.slice(i, j);\n\n // If we have accumulated text, save it\n if (currentSegment.text.length > 0) {\n segments.push(currentSegment);\n currentSegment = {\n text: '',\n ...Object.fromEntries(\n Object.entries(currentSegment)\n .filter(([key]) => key !== 'text')\n .filter(([_, value]) => value !== undefined)\n ),\n } as TextSegment;\n }\n\n // Apply the style\n if (RESET_CODES.includes(code)) {\n // Reset all styles\n currentSegment = { text: '' };\n } else if (ANSI_COLORS[code]) {\n currentSegment.color = ANSI_COLORS[code];\n } else if (ANSI_STYLES[code]) {\n const style = ANSI_STYLES[code] as keyof Omit<TextSegment, 'text' | 'color'>;\n currentSegment[style] = true;\n }\n\n i = j;\n } else {\n // Regular character\n currentSegment.text += input[i];\n i++;\n }\n }\n\n // Add the last segment if it has text\n if (currentSegment.text.length > 0) {\n segments.push(currentSegment);\n }\n\n return segments;\n}\n\ntype Props = {\n children: string;\n};\n\n/**\n * Component that renders text with ANSI escape codes as styled Ink Text components.\n * Parses ANSI color and style codes and applies them as Ink props.\n */\nexport default memo(function AnsiText({ children }: Props) {\n // If the input is empty or has no ANSI codes, just render it directly\n if (!children || !children.includes('\\x1b')) {\n return <Fragment>{children}</Fragment>;\n }\n\n const segments = parseAnsiString(children);\n\n // If parsing resulted in a single unstyled segment, render it directly\n if (segments.length === 1 && !segments[0].color && !segments[0].bold && !segments[0].dim && !segments[0].italic && !segments[0].underline && !segments[0].strikethrough && !segments[0].inverse) {\n return <Fragment>{segments[0].text}</Fragment>;\n }\n\n return (\n <Fragment>\n {segments.map((segment, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: Segments have no unique ID, index is stable for parsed content\n <Text key={index} color={segment.color} bold={segment.bold} dimColor={segment.dim} italic={segment.italic} underline={segment.underline} strikethrough={segment.strikethrough} inverse={segment.inverse}>\n {segment.text}\n </Text>\n ))}\n </Fragment>\n );\n});\n"],"names":["ANSI_COLORS","ANSI_STYLES","RESET_CODES","parseAnsiString","input","segments","currentSegment","text","i","length","j","test","code","slice","push","Object","fromEntries","entries","filter","key","_","value","undefined","includes","color","style","memo","AnsiText","children","Fragment","bold","dim","italic","underline","strikethrough","inverse","map","segment","index","Text","dimColor"],"mappings":"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"}
@@ -51,6 +51,6 @@ function clipText(str, maxWidth) {
51
51
  i++;
52
52
  }
53
53
  }
54
- return "".concat(result, "…");
54
+ return "".concat(result, "\x1b[0m…");
55
55
  }
56
56
  /* CJS INTEROP */ if (exports.__esModule && exports.default) { try { Object.defineProperty(exports.default, '__esModule', { value: true }); for (var key in exports) { exports.default[key] = exports[key]; } } catch (_) {}; module.exports = exports.default; }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/lib/clipText.ts"],"sourcesContent":["import ansiRegex from './ansiRegex.ts';\n\nconst regex = ansiRegex();\n\n/**\n * Get the visible length of a string, ignoring ANSI escape codes.\n */\nexport function visibleLength(str: string): number {\n return str.replace(regex, '').length;\n}\n\n/**\n * Clip text to a maximum visible width, accounting for ANSI escape codes.\n * Adds ellipsis (…) if truncated.\n */\nexport function clipText(str: string, maxWidth: number): string {\n if (maxWidth <= 0) return '';\n if (maxWidth === 1) return '…';\n\n const stripped = str.replace(regex, '');\n if (stripped.length <= maxWidth) return str;\n\n // Need to truncate - walk through and preserve ANSI codes\n let visibleCount = 0;\n let result = '';\n let i = 0;\n\n while (i < str.length && visibleCount < maxWidth - 1) {\n // Check for ANSI escape sequence (ESC [ params m)\n const remaining = str.slice(i);\n // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape requires control char\n const match = remaining.match(/^(\\u001B\\[[0-9;]*m)/);\n\n if (match) {\n // Include the ANSI code in output but don't count toward visible length\n result += match[1];\n i += match[1].length;\n } else {\n // Regular character\n result += str[i];\n visibleCount++;\n i++;\n }\n }\n\n return `${result}…`;\n}\n"],"names":["clipText","visibleLength","regex","ansiRegex","str","replace","length","maxWidth","stripped","visibleCount","result","i","remaining","slice","match"],"mappings":";;;;;;;;;;;QAegBA;eAAAA;;QARAC;eAAAA;;;kEAPM;;;;;;AAEtB,IAAMC,QAAQC,IAAAA,oBAAS;AAKhB,SAASF,cAAcG,GAAW;IACvC,OAAOA,IAAIC,OAAO,CAACH,OAAO,IAAII,MAAM;AACtC;AAMO,SAASN,SAASI,GAAW,EAAEG,QAAgB;IACpD,IAAIA,YAAY,GAAG,OAAO;IAC1B,IAAIA,aAAa,GAAG,OAAO;IAE3B,IAAMC,WAAWJ,IAAIC,OAAO,CAACH,OAAO;IACpC,IAAIM,SAASF,MAAM,IAAIC,UAAU,OAAOH;IAExC,0DAA0D;IAC1D,IAAIK,eAAe;IACnB,IAAIC,SAAS;IACb,IAAIC,IAAI;IAER,MAAOA,IAAIP,IAAIE,MAAM,IAAIG,eAAeF,WAAW,EAAG;QACpD,kDAAkD;QAClD,IAAMK,YAAYR,IAAIS,KAAK,CAACF;QAC5B,6FAA6F;QAC7F,IAAMG,QAAQF,UAAUE,KAAK,CAAC;QAE9B,IAAIA,OAAO;YACT,wEAAwE;YACxEJ,UAAUI,KAAK,CAAC,EAAE;YAClBH,KAAKG,KAAK,CAAC,EAAE,CAACR,MAAM;QACtB,OAAO;YACL,oBAAoB;YACpBI,UAAUN,GAAG,CAACO,EAAE;YAChBF;YACAE;QACF;IACF;IAEA,OAAO,AAAC,GAAS,OAAPD,QAAO;AACnB"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/lib/clipText.ts"],"sourcesContent":["import ansiRegex from './ansiRegex.ts';\n\nconst regex = ansiRegex();\n\n/**\n * Get the visible length of a string, ignoring ANSI escape codes.\n */\nexport function visibleLength(str: string): number {\n return str.replace(regex, '').length;\n}\n\n/**\n * Clip text to a maximum visible width, accounting for ANSI escape codes.\n * Adds ellipsis (…) if truncated.\n */\nexport function clipText(str: string, maxWidth: number): string {\n if (maxWidth <= 0) return '';\n if (maxWidth === 1) return '…';\n\n const stripped = str.replace(regex, '');\n if (stripped.length <= maxWidth) return str;\n\n // Need to truncate - walk through and preserve ANSI codes\n let visibleCount = 0;\n let result = '';\n let i = 0;\n\n while (i < str.length && visibleCount < maxWidth - 1) {\n // Check for ANSI escape sequence (ESC [ params m)\n const remaining = str.slice(i);\n // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape requires control char\n const match = remaining.match(/^(\\u001B\\[[0-9;]*m)/);\n\n if (match) {\n // Include the ANSI code in output but don't count toward visible length\n result += match[1];\n i += match[1].length;\n } else {\n // Regular character\n result += str[i];\n visibleCount++;\n i++;\n }\n }\n\n return `${result}\\x1b[0m…`;\n}\n"],"names":["clipText","visibleLength","regex","ansiRegex","str","replace","length","maxWidth","stripped","visibleCount","result","i","remaining","slice","match"],"mappings":";;;;;;;;;;;QAegBA;eAAAA;;QARAC;eAAAA;;;kEAPM;;;;;;AAEtB,IAAMC,QAAQC,IAAAA,oBAAS;AAKhB,SAASF,cAAcG,GAAW;IACvC,OAAOA,IAAIC,OAAO,CAACH,OAAO,IAAII,MAAM;AACtC;AAMO,SAASN,SAASI,GAAW,EAAEG,QAAgB;IACpD,IAAIA,YAAY,GAAG,OAAO;IAC1B,IAAIA,aAAa,GAAG,OAAO;IAE3B,IAAMC,WAAWJ,IAAIC,OAAO,CAACH,OAAO;IACpC,IAAIM,SAASF,MAAM,IAAIC,UAAU,OAAOH;IAExC,0DAA0D;IAC1D,IAAIK,eAAe;IACnB,IAAIC,SAAS;IACb,IAAIC,IAAI;IAER,MAAOA,IAAIP,IAAIE,MAAM,IAAIG,eAAeF,WAAW,EAAG;QACpD,kDAAkD;QAClD,IAAMK,YAAYR,IAAIS,KAAK,CAACF;QAC5B,6FAA6F;QAC7F,IAAMG,QAAQF,UAAUE,KAAK,CAAC;QAE9B,IAAIA,OAAO;YACT,wEAAwE;YACxEJ,UAAUI,KAAK,CAAC,EAAE;YAClBH,KAAKG,KAAK,CAAC,EAAE,CAACR,MAAM;QACtB,OAAO;YACL,oBAAoB;YACpBI,UAAUN,GAAG,CAACO,EAAE;YAChBF;YACAE;QACF;IACF;IAEA,OAAO,AAAC,GAAS,OAAPD,QAAO;AACnB"}
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ children: string;
3
+ };
4
+ /**
5
+ * Component that renders text with ANSI escape codes as styled Ink Text components.
6
+ * Parses ANSI color and style codes and applies them as Ink props.
7
+ */
8
+ declare const _default: import("react").NamedExoticComponent<Props>;
9
+ export default _default;
@@ -2,12 +2,28 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useStdout } from 'ink';
3
3
  import { memo, useMemo } from 'react';
4
4
  import { SPINNER } from '../constants.js';
5
+ import ansiRegex from '../lib/ansiRegex.js';
5
6
  import { clipText } from '../lib/clipText.js';
6
7
  import figures from '../lib/figures.js';
7
8
  import { calculateColumnWidth } from '../lib/format.js';
8
9
  import { useStore } from '../state/StoreContext.js';
9
10
  import { LineType } from '../types.js';
10
11
  import Spinner from './Spinner.js';
12
+ const ansi = ansiRegex();
13
+ /**
14
+ * Strip ANSI escape codes from a string.
15
+ */ function stripAnsi(str) {
16
+ return str.replace(ansi, '');
17
+ }
18
+ /**
19
+ * Simple truncation for plain text (no ANSI codes).
20
+ * Adds ellipsis if truncated.
21
+ */ function truncate(str, maxWidth) {
22
+ if (maxWidth <= 0) return '';
23
+ if (maxWidth === 1) return '…';
24
+ if (str.length <= maxWidth) return str;
25
+ return `${str.slice(0, maxWidth - 1)}…`;
26
+ }
11
27
  function getLastOutputLine(lines) {
12
28
  for(let i = lines.length - 1; i >= 0; i--){
13
29
  if (lines[i].text.length > 0) {
@@ -40,12 +56,13 @@ export default /*#__PURE__*/ memo(function CompactProcessLine({ item, isSelected
40
56
  const statusText = useMemo(()=>{
41
57
  if (state === 'running') {
42
58
  const lastLine = getLastOutputLine(lines);
43
- return lastLine ? clipText(lastLine, statusWidth) : '';
59
+ const stripped = lastLine ? stripAnsi(lastLine) : '';
60
+ return stripped ? truncate(stripped, statusWidth) : '';
44
61
  }
45
62
  if (state === 'error') {
46
63
  const errorCount = getErrorCount(lines);
47
64
  const text = errorCount > 0 ? `${errorCount} error${errorCount > 1 ? 's' : ''}` : 'failed';
48
- return clipText(text, statusWidth);
65
+ return truncate(text, statusWidth);
49
66
  }
50
67
  return ''; // success - no status text
51
68
  }, [
@@ -74,8 +91,6 @@ export default /*#__PURE__*/ memo(function CompactProcessLine({ item, isSelected
74
91
  }, [
75
92
  state
76
93
  ]);
77
- // Status text color
78
- const statusColor = state === 'error' ? 'red' : 'gray';
79
94
  return /*#__PURE__*/ _jsxs(Box, {
80
95
  width: terminalWidth,
81
96
  children: [
@@ -97,7 +112,6 @@ export default /*#__PURE__*/ memo(function CompactProcessLine({ item, isSelected
97
112
  statusWidth > 0 && statusText && /*#__PURE__*/ _jsx(Box, {
98
113
  width: statusWidth + gap,
99
114
  children: /*#__PURE__*/ _jsxs(Text, {
100
- color: statusColor,
101
115
  children: [
102
116
  " ",
103
117
  statusText
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/components/CompactProcessLine.tsx"],"sourcesContent":["import { Box, Text, useStdout } from 'ink';\nimport { memo, useMemo } from 'react';\nimport { SPINNER } from '../constants.ts';\nimport { clipText } from '../lib/clipText.ts';\nimport figures from '../lib/figures.ts';\nimport { calculateColumnWidth } from '../lib/format.ts';\nimport { useStore } from '../state/StoreContext.ts';\nimport type { ChildProcess, Line } from '../types.ts';\nimport { LineType } from '../types.ts';\nimport Spinner from './Spinner.ts';\n\ntype Props = {\n item: ChildProcess;\n isSelected?: boolean;\n};\n\nfunction getLastOutputLine(lines: Line[]): string {\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i].text.length > 0) {\n return lines[i].text;\n }\n }\n return '';\n}\n\nfunction getErrorCount(lines: Line[]): number {\n return lines.filter((line) => line.type === LineType.stderr).length;\n}\n\nexport default memo(function CompactProcessLine({ item, isSelected = false }: Props) {\n const store = useStore();\n const { stdout } = useStdout();\n const terminalWidth = stdout?.columns || 120;\n\n const { group, title, state, lines } = item;\n const selectionIndicator = isSelected ? figures.pointer : ' ';\n\n // Display name: prefer group, fall back to title\n const displayName = group || title;\n\n // Calculate widths - use dynamic column width based on longest name\n const selectionWidth = 1; // selection indicator\n const iconWidth = 2; // icon + space\n const maxGroupLength = store.getMaxGroupLength();\n const nameColumnWidth = calculateColumnWidth('max', terminalWidth, maxGroupLength);\n const gap = 1; // space between name and status\n const statusWidth = Math.max(0, terminalWidth - selectionWidth - iconWidth - nameColumnWidth - gap);\n\n // Clip name to column width and pad\n const clippedName = clipText(displayName, nameColumnWidth).padEnd(nameColumnWidth);\n\n // Status text based on state - clip to available width\n const statusText = useMemo(() => {\n if (state === 'running') {\n const lastLine = getLastOutputLine(lines);\n return lastLine ? clipText(lastLine, statusWidth) : '';\n }\n if (state === 'error') {\n const errorCount = getErrorCount(lines);\n const text = errorCount > 0 ? `${errorCount} error${errorCount > 1 ? 's' : ''}` : 'failed';\n return clipText(text, statusWidth);\n }\n return ''; // success - no status text\n }, [state, lines, statusWidth]);\n\n // Icon based on state\n const icon = useMemo(() => {\n switch (state) {\n case 'running':\n return <Spinner {...SPINNER} />;\n case 'success':\n return <Text color=\"green\">{figures.tick}</Text>;\n case 'error':\n return <Text color=\"red\">{figures.cross}</Text>;\n }\n }, [state]);\n\n // Status text color\n const statusColor = state === 'error' ? 'red' : 'gray';\n\n return (\n <Box width={terminalWidth}>\n <Text color={isSelected ? 'cyan' : undefined}>{selectionIndicator}</Text>\n <Box width={iconWidth}>{icon}</Box>\n <Box width={nameColumnWidth}>\n <Text inverse={isSelected}>{clippedName}</Text>\n </Box>\n {statusWidth > 0 && statusText && (\n <Box width={statusWidth + gap}>\n <Text color={statusColor}> {statusText}</Text>\n </Box>\n )}\n </Box>\n );\n});\n"],"names":["Box","Text","useStdout","memo","useMemo","SPINNER","clipText","figures","calculateColumnWidth","useStore","LineType","Spinner","getLastOutputLine","lines","i","length","text","getErrorCount","filter","line","type","stderr","CompactProcessLine","item","isSelected","store","stdout","terminalWidth","columns","group","title","state","selectionIndicator","pointer","displayName","selectionWidth","iconWidth","maxGroupLength","getMaxGroupLength","nameColumnWidth","gap","statusWidth","Math","max","clippedName","padEnd","statusText","lastLine","errorCount","icon","color","tick","cross","statusColor","width","undefined","inverse"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/components/CompactProcessLine.tsx"],"sourcesContent":["import { Box, Text, useStdout } from 'ink';\nimport { memo, useMemo } from 'react';\nimport { SPINNER } from '../constants.ts';\nimport ansiRegex from '../lib/ansiRegex.ts';\nimport { clipText } from '../lib/clipText.ts';\nimport figures from '../lib/figures.ts';\nimport { calculateColumnWidth } from '../lib/format.ts';\nimport { useStore } from '../state/StoreContext.ts';\nimport type { ChildProcess, Line } from '../types.ts';\nimport { LineType } from '../types.ts';\nimport Spinner from './Spinner.ts';\n\nconst ansi = ansiRegex();\n\n/**\n * Strip ANSI escape codes from a string.\n */\nfunction stripAnsi(str: string): string {\n return str.replace(ansi, '');\n}\n\n/**\n * Simple truncation for plain text (no ANSI codes).\n * Adds ellipsis if truncated.\n */\nfunction truncate(str: string, maxWidth: number): string {\n if (maxWidth <= 0) return '';\n if (maxWidth === 1) return '…';\n if (str.length <= maxWidth) return str;\n return `${str.slice(0, maxWidth - 1)}…`;\n}\n\ntype Props = {\n item: ChildProcess;\n isSelected?: boolean;\n};\n\nfunction getLastOutputLine(lines: Line[]): string {\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i].text.length > 0) {\n return lines[i].text;\n }\n }\n return '';\n}\n\nfunction getErrorCount(lines: Line[]): number {\n return lines.filter((line) => line.type === LineType.stderr).length;\n}\n\nexport default memo(function CompactProcessLine({ item, isSelected = false }: Props) {\n const store = useStore();\n const { stdout } = useStdout();\n const terminalWidth = stdout?.columns || 120;\n\n const { group, title, state, lines } = item;\n const selectionIndicator = isSelected ? figures.pointer : ' ';\n\n // Display name: prefer group, fall back to title\n const displayName = group || title;\n\n // Calculate widths - use dynamic column width based on longest name\n const selectionWidth = 1; // selection indicator\n const iconWidth = 2; // icon + space\n const maxGroupLength = store.getMaxGroupLength();\n const nameColumnWidth = calculateColumnWidth('max', terminalWidth, maxGroupLength);\n const gap = 1; // space between name and status\n const statusWidth = Math.max(0, terminalWidth - selectionWidth - iconWidth - nameColumnWidth - gap);\n\n // Clip name to column width and pad\n const clippedName = clipText(displayName, nameColumnWidth).padEnd(nameColumnWidth);\n\n // Status text based on state - clip to available width\n const statusText = useMemo(() => {\n if (state === 'running') {\n const lastLine = getLastOutputLine(lines);\n const stripped = lastLine ? stripAnsi(lastLine) : '';\n return stripped ? truncate(stripped, statusWidth) : '';\n }\n if (state === 'error') {\n const errorCount = getErrorCount(lines);\n const text = errorCount > 0 ? `${errorCount} error${errorCount > 1 ? 's' : ''}` : 'failed';\n return truncate(text, statusWidth);\n }\n return ''; // success - no status text\n }, [state, lines, statusWidth]);\n\n // Icon based on state\n const icon = useMemo(() => {\n switch (state) {\n case 'running':\n return <Spinner {...SPINNER} />;\n case 'success':\n return <Text color=\"green\">{figures.tick}</Text>;\n case 'error':\n return <Text color=\"red\">{figures.cross}</Text>;\n }\n }, [state]);\n\n return (\n <Box width={terminalWidth}>\n <Text color={isSelected ? 'cyan' : undefined}>{selectionIndicator}</Text>\n <Box width={iconWidth}>{icon}</Box>\n <Box width={nameColumnWidth}>\n <Text inverse={isSelected}>{clippedName}</Text>\n </Box>\n {statusWidth > 0 && statusText && (\n <Box width={statusWidth + gap}>\n <Text> {statusText}</Text>\n </Box>\n )}\n </Box>\n );\n});\n"],"names":["Box","Text","useStdout","memo","useMemo","SPINNER","ansiRegex","clipText","figures","calculateColumnWidth","useStore","LineType","Spinner","ansi","stripAnsi","str","replace","truncate","maxWidth","length","slice","getLastOutputLine","lines","i","text","getErrorCount","filter","line","type","stderr","CompactProcessLine","item","isSelected","store","stdout","terminalWidth","columns","group","title","state","selectionIndicator","pointer","displayName","selectionWidth","iconWidth","maxGroupLength","getMaxGroupLength","nameColumnWidth","gap","statusWidth","Math","max","clippedName","padEnd","statusText","lastLine","stripped","errorCount","icon","color","tick","cross","width","undefined","inverse"],"mappings":"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"}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { memo } from 'react';
4
4
  import { EXPANDED_MAX_VISIBLE_LINES } from '../constants.js';
5
+ import AnsiText from '../lib/AnsiText.js';
5
6
  const isMac = process.platform === 'darwin';
6
7
  export default /*#__PURE__*/ memo(function ExpandedOutput({ lines, scrollOffset, maxVisible = EXPANDED_MAX_VISIBLE_LINES }) {
7
8
  const visibleLines = lines.slice(scrollOffset, scrollOffset + maxVisible);
@@ -24,7 +25,9 @@ export default /*#__PURE__*/ memo(function ExpandedOutput({ lines, scrollOffset,
24
25
  /*#__PURE__*/ _jsxs(Text, {
25
26
  children: [
26
27
  "│ ",
27
- line.text || ' '
28
+ /*#__PURE__*/ _jsx(AnsiText, {
29
+ children: line.text || ' '
30
+ })
28
31
  ]
29
32
  }, scrollOffset + i)),
30
33
  hasMore ? /*#__PURE__*/ _jsxs(Text, {
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/components/ExpandedOutput.tsx"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { memo } from 'react';\nimport { EXPANDED_MAX_VISIBLE_LINES } from '../constants.ts';\nimport type { Line } from '../types.ts';\n\nconst isMac = process.platform === 'darwin';\n\ntype Props = {\n lines: Line[];\n scrollOffset: number;\n maxVisible?: number;\n};\n\nexport default memo(function ExpandedOutput({ lines, scrollOffset, maxVisible = EXPANDED_MAX_VISIBLE_LINES }: Props) {\n const visibleLines = lines.slice(scrollOffset, scrollOffset + maxVisible);\n const hasMore = lines.length > scrollOffset + maxVisible;\n const remaining = lines.length - scrollOffset - maxVisible;\n\n if (lines.length === 0) {\n return (\n <Box paddingLeft={2}>\n <Text dimColor>│ (no output)</Text>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\" paddingLeft={2}>\n {visibleLines.map((line, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: Lines have no unique ID, index is stable for this scrolling view\n <Text key={scrollOffset + i}>│ {line.text || ' '}</Text>\n ))}\n {hasMore ? (\n <Text dimColor>\n │ [+{remaining} more, Tab/⇧Tab page, {isMac ? '⌥↑/↓' : 'g/G'} top/bottom, ↵ close]\n </Text>\n ) : (\n <Text dimColor>│ [↵ close]</Text>\n )}\n </Box>\n );\n});\n"],"names":["Box","Text","memo","EXPANDED_MAX_VISIBLE_LINES","isMac","process","platform","ExpandedOutput","lines","scrollOffset","maxVisible","visibleLines","slice","hasMore","length","remaining","paddingLeft","dimColor","flexDirection","map","line","i","text"],"mappings":";AAAA,SAASA,GAAG,EAAEC,IAAI,QAAQ,MAAM;AAChC,SAASC,IAAI,QAAQ,QAAQ;AAC7B,SAASC,0BAA0B,QAAQ,kBAAkB;AAG7D,MAAMC,QAAQC,QAAQC,QAAQ,KAAK;AAQnC,6BAAeJ,KAAK,SAASK,eAAe,EAAEC,KAAK,EAAEC,YAAY,EAAEC,aAAaP,0BAA0B,EAAS;IACjH,MAAMQ,eAAeH,MAAMI,KAAK,CAACH,cAAcA,eAAeC;IAC9D,MAAMG,UAAUL,MAAMM,MAAM,GAAGL,eAAeC;IAC9C,MAAMK,YAAYP,MAAMM,MAAM,GAAGL,eAAeC;IAEhD,IAAIF,MAAMM,MAAM,KAAK,GAAG;QACtB,qBACE,KAACd;YAAIgB,aAAa;sBAChB,cAAA,KAACf;gBAAKgB,QAAQ;0BAAC;;;IAGrB;IAEA,qBACE,MAACjB;QAAIkB,eAAc;QAASF,aAAa;;YACtCL,aAAaQ,GAAG,CAAC,CAACC,MAAMC,IACvB,iHAAiH;8BACjH,MAACpB;;wBAA4B;wBAAGmB,KAAKE,IAAI,IAAI;;mBAAlCb,eAAeY;YAE3BR,wBACC,MAACZ;gBAAKgB,QAAQ;;oBAAC;oBACRF;oBAAU;oBAAuBX,QAAQ,SAAS;oBAAM;;+BAG/D,KAACH;gBAAKgB,QAAQ;0BAAC;;;;AAIvB,GAAG"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/components/ExpandedOutput.tsx"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { memo } from 'react';\nimport { EXPANDED_MAX_VISIBLE_LINES } from '../constants.ts';\nimport AnsiText from '../lib/AnsiText.ts';\nimport type { Line } from '../types.ts';\n\nconst isMac = process.platform === 'darwin';\n\ntype Props = {\n lines: Line[];\n scrollOffset: number;\n maxVisible?: number;\n};\n\nexport default memo(function ExpandedOutput({ lines, scrollOffset, maxVisible = EXPANDED_MAX_VISIBLE_LINES }: Props) {\n const visibleLines = lines.slice(scrollOffset, scrollOffset + maxVisible);\n const hasMore = lines.length > scrollOffset + maxVisible;\n const remaining = lines.length - scrollOffset - maxVisible;\n\n if (lines.length === 0) {\n return (\n <Box paddingLeft={2}>\n <Text dimColor>│ (no output)</Text>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\" paddingLeft={2}>\n {visibleLines.map((line, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: Lines have no unique ID, index is stable for this scrolling view\n <Text key={scrollOffset + i}>\n │ <AnsiText>{line.text || ' '}</AnsiText>\n </Text>\n ))}\n {hasMore ? (\n <Text dimColor>\n │ [+{remaining} more, Tab/⇧Tab page, {isMac ? '⌥↑/↓' : 'g/G'} top/bottom, ↵ close]\n </Text>\n ) : (\n <Text dimColor>│ [↵ close]</Text>\n )}\n </Box>\n );\n});\n"],"names":["Box","Text","memo","EXPANDED_MAX_VISIBLE_LINES","AnsiText","isMac","process","platform","ExpandedOutput","lines","scrollOffset","maxVisible","visibleLines","slice","hasMore","length","remaining","paddingLeft","dimColor","flexDirection","map","line","i","text"],"mappings":";AAAA,SAASA,GAAG,EAAEC,IAAI,QAAQ,MAAM;AAChC,SAASC,IAAI,QAAQ,QAAQ;AAC7B,SAASC,0BAA0B,QAAQ,kBAAkB;AAC7D,OAAOC,cAAc,qBAAqB;AAG1C,MAAMC,QAAQC,QAAQC,QAAQ,KAAK;AAQnC,6BAAeL,KAAK,SAASM,eAAe,EAAEC,KAAK,EAAEC,YAAY,EAAEC,aAAaR,0BAA0B,EAAS;IACjH,MAAMS,eAAeH,MAAMI,KAAK,CAACH,cAAcA,eAAeC;IAC9D,MAAMG,UAAUL,MAAMM,MAAM,GAAGL,eAAeC;IAC9C,MAAMK,YAAYP,MAAMM,MAAM,GAAGL,eAAeC;IAEhD,IAAIF,MAAMM,MAAM,KAAK,GAAG;QACtB,qBACE,KAACf;YAAIiB,aAAa;sBAChB,cAAA,KAAChB;gBAAKiB,QAAQ;0BAAC;;;IAGrB;IAEA,qBACE,MAAClB;QAAImB,eAAc;QAASF,aAAa;;YACtCL,aAAaQ,GAAG,CAAC,CAACC,MAAMC,IACvB,iHAAiH;8BACjH,MAACrB;;wBAA4B;sCACzB,KAACG;sCAAUiB,KAAKE,IAAI,IAAI;;;mBADjBb,eAAeY;YAI3BR,wBACC,MAACb;gBAAKiB,QAAQ;;oBAAC;oBACRF;oBAAU;oBAAuBX,QAAQ,SAAS;oBAAM;;+BAG/D,KAACJ;gBAAKiB,QAAQ;0BAAC;;;;AAIvB,GAAG"}
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { Fragment, memo } from 'react';
4
+ // ANSI color codes to Ink color mapping
5
+ // Based on standard ANSI SGR (Select Graphic Rendition) parameters
6
+ const ANSI_COLORS = {
7
+ '\x1b[30m': 'black',
8
+ '\x1b[31m': 'red',
9
+ '\x1b[32m': 'green',
10
+ '\x1b[33m': 'yellow',
11
+ '\x1b[34m': 'blue',
12
+ '\x1b[35m': 'magenta',
13
+ '\x1b[36m': 'cyan',
14
+ '\x1b[37m': 'white',
15
+ // Bright colors
16
+ '\x1b[90m': 'gray',
17
+ '\x1b[91m': 'redBright',
18
+ '\x1b[92m': 'greenBright',
19
+ '\x1b[93m': 'yellowBright',
20
+ '\x1b[94m': 'blueBright',
21
+ '\x1b[95m': 'magentaBright',
22
+ '\x1b[96m': 'cyanBright',
23
+ '\x1b[97m': 'whiteBright'
24
+ };
25
+ // ANSI style codes
26
+ const ANSI_STYLES = {
27
+ '\x1b[1m': 'bold',
28
+ '\x1b[2m': 'dim',
29
+ '\x1b[3m': 'italic',
30
+ '\x1b[4m': 'underline',
31
+ '\x1b[9m': 'strikethrough',
32
+ '\x1b[7m': 'inverse'
33
+ };
34
+ // Reset codes
35
+ const RESET_CODES = [
36
+ '\x1b[0m',
37
+ '\x1b[39m',
38
+ '\x1b[49m'
39
+ ];
40
+ /**
41
+ * Parse a string with ANSI codes into segments with styling information.
42
+ */ function parseAnsiString(input) {
43
+ const segments = [];
44
+ let currentSegment = {
45
+ text: ''
46
+ };
47
+ let i = 0;
48
+ while(i < input.length){
49
+ // Check for ANSI escape sequence
50
+ if (input[i] === '\x1b' && input[i + 1] === '[') {
51
+ // Find the end of the escape sequence (ends with a letter)
52
+ let j = i + 2;
53
+ while(j < input.length && !/[a-zA-Z]/.test(input[j])){
54
+ j++;
55
+ }
56
+ j++; // Include the letter
57
+ const code = input.slice(i, j);
58
+ // If we have accumulated text, save it
59
+ if (currentSegment.text.length > 0) {
60
+ segments.push(currentSegment);
61
+ currentSegment = {
62
+ text: '',
63
+ ...Object.fromEntries(Object.entries(currentSegment).filter(([key])=>key !== 'text').filter(([_, value])=>value !== undefined))
64
+ };
65
+ }
66
+ // Apply the style
67
+ if (RESET_CODES.includes(code)) {
68
+ // Reset all styles
69
+ currentSegment = {
70
+ text: ''
71
+ };
72
+ } else if (ANSI_COLORS[code]) {
73
+ currentSegment.color = ANSI_COLORS[code];
74
+ } else if (ANSI_STYLES[code]) {
75
+ const style = ANSI_STYLES[code];
76
+ currentSegment[style] = true;
77
+ }
78
+ i = j;
79
+ } else {
80
+ // Regular character
81
+ currentSegment.text += input[i];
82
+ i++;
83
+ }
84
+ }
85
+ // Add the last segment if it has text
86
+ if (currentSegment.text.length > 0) {
87
+ segments.push(currentSegment);
88
+ }
89
+ return segments;
90
+ }
91
+ /**
92
+ * Component that renders text with ANSI escape codes as styled Ink Text components.
93
+ * Parses ANSI color and style codes and applies them as Ink props.
94
+ */ export default /*#__PURE__*/ memo(function AnsiText({ children }) {
95
+ // If the input is empty or has no ANSI codes, just render it directly
96
+ if (!children || !children.includes('\x1b')) {
97
+ return /*#__PURE__*/ _jsx(Fragment, {
98
+ children: children
99
+ });
100
+ }
101
+ const segments = parseAnsiString(children);
102
+ // If parsing resulted in a single unstyled segment, render it directly
103
+ if (segments.length === 1 && !segments[0].color && !segments[0].bold && !segments[0].dim && !segments[0].italic && !segments[0].underline && !segments[0].strikethrough && !segments[0].inverse) {
104
+ return /*#__PURE__*/ _jsx(Fragment, {
105
+ children: segments[0].text
106
+ });
107
+ }
108
+ return /*#__PURE__*/ _jsx(Fragment, {
109
+ children: segments.map((segment, index)=>// biome-ignore lint/suspicious/noArrayIndexKey: Segments have no unique ID, index is stable for parsed content
110
+ /*#__PURE__*/ _jsx(Text, {
111
+ color: segment.color,
112
+ bold: segment.bold,
113
+ dimColor: segment.dim,
114
+ italic: segment.italic,
115
+ underline: segment.underline,
116
+ strikethrough: segment.strikethrough,
117
+ inverse: segment.inverse,
118
+ children: segment.text
119
+ }, index))
120
+ });
121
+ });
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/lib/AnsiText.tsx"],"sourcesContent":["import { Text } from 'ink';\nimport { Fragment, memo } from 'react';\n\n// ANSI color codes to Ink color mapping\n// Based on standard ANSI SGR (Select Graphic Rendition) parameters\nconst ANSI_COLORS: Record<string, string> = {\n '\\x1b[30m': 'black',\n '\\x1b[31m': 'red',\n '\\x1b[32m': 'green',\n '\\x1b[33m': 'yellow',\n '\\x1b[34m': 'blue',\n '\\x1b[35m': 'magenta',\n '\\x1b[36m': 'cyan',\n '\\x1b[37m': 'white',\n // Bright colors\n '\\x1b[90m': 'gray',\n '\\x1b[91m': 'redBright',\n '\\x1b[92m': 'greenBright',\n '\\x1b[93m': 'yellowBright',\n '\\x1b[94m': 'blueBright',\n '\\x1b[95m': 'magentaBright',\n '\\x1b[96m': 'cyanBright',\n '\\x1b[97m': 'whiteBright',\n};\n\n// ANSI style codes\nconst ANSI_STYLES: Record<string, string> = {\n '\\x1b[1m': 'bold',\n '\\x1b[2m': 'dim',\n '\\x1b[3m': 'italic',\n '\\x1b[4m': 'underline',\n '\\x1b[9m': 'strikethrough',\n '\\x1b[7m': 'inverse',\n};\n\n// Reset codes\nconst RESET_CODES = ['\\x1b[0m', '\\x1b[39m', '\\x1b[49m'];\n\ntype TextSegment = {\n text: string;\n color?: string;\n bold?: boolean;\n dim?: boolean;\n italic?: boolean;\n underline?: boolean;\n strikethrough?: boolean;\n inverse?: boolean;\n};\n\n/**\n * Parse a string with ANSI codes into segments with styling information.\n */\nfunction parseAnsiString(input: string): TextSegment[] {\n const segments: TextSegment[] = [];\n let currentSegment: TextSegment = { text: '' };\n let i = 0;\n\n while (i < input.length) {\n // Check for ANSI escape sequence\n if (input[i] === '\\x1b' && input[i + 1] === '[') {\n // Find the end of the escape sequence (ends with a letter)\n let j = i + 2;\n while (j < input.length && !/[a-zA-Z]/.test(input[j])) {\n j++;\n }\n j++; // Include the letter\n\n const code = input.slice(i, j);\n\n // If we have accumulated text, save it\n if (currentSegment.text.length > 0) {\n segments.push(currentSegment);\n currentSegment = {\n text: '',\n ...Object.fromEntries(\n Object.entries(currentSegment)\n .filter(([key]) => key !== 'text')\n .filter(([_, value]) => value !== undefined)\n ),\n } as TextSegment;\n }\n\n // Apply the style\n if (RESET_CODES.includes(code)) {\n // Reset all styles\n currentSegment = { text: '' };\n } else if (ANSI_COLORS[code]) {\n currentSegment.color = ANSI_COLORS[code];\n } else if (ANSI_STYLES[code]) {\n const style = ANSI_STYLES[code] as keyof Omit<TextSegment, 'text' | 'color'>;\n currentSegment[style] = true;\n }\n\n i = j;\n } else {\n // Regular character\n currentSegment.text += input[i];\n i++;\n }\n }\n\n // Add the last segment if it has text\n if (currentSegment.text.length > 0) {\n segments.push(currentSegment);\n }\n\n return segments;\n}\n\ntype Props = {\n children: string;\n};\n\n/**\n * Component that renders text with ANSI escape codes as styled Ink Text components.\n * Parses ANSI color and style codes and applies them as Ink props.\n */\nexport default memo(function AnsiText({ children }: Props) {\n // If the input is empty or has no ANSI codes, just render it directly\n if (!children || !children.includes('\\x1b')) {\n return <Fragment>{children}</Fragment>;\n }\n\n const segments = parseAnsiString(children);\n\n // If parsing resulted in a single unstyled segment, render it directly\n if (segments.length === 1 && !segments[0].color && !segments[0].bold && !segments[0].dim && !segments[0].italic && !segments[0].underline && !segments[0].strikethrough && !segments[0].inverse) {\n return <Fragment>{segments[0].text}</Fragment>;\n }\n\n return (\n <Fragment>\n {segments.map((segment, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: Segments have no unique ID, index is stable for parsed content\n <Text key={index} color={segment.color} bold={segment.bold} dimColor={segment.dim} italic={segment.italic} underline={segment.underline} strikethrough={segment.strikethrough} inverse={segment.inverse}>\n {segment.text}\n </Text>\n ))}\n </Fragment>\n );\n});\n"],"names":["Text","Fragment","memo","ANSI_COLORS","ANSI_STYLES","RESET_CODES","parseAnsiString","input","segments","currentSegment","text","i","length","j","test","code","slice","push","Object","fromEntries","entries","filter","key","_","value","undefined","includes","color","style","AnsiText","children","bold","dim","italic","underline","strikethrough","inverse","map","segment","index","dimColor"],"mappings":";AAAA,SAASA,IAAI,QAAQ,MAAM;AAC3B,SAASC,QAAQ,EAAEC,IAAI,QAAQ,QAAQ;AAEvC,wCAAwC;AACxC,mEAAmE;AACnE,MAAMC,cAAsC;IAC1C,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,gBAAgB;IAChB,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;AACd;AAEA,mBAAmB;AACnB,MAAMC,cAAsC;IAC1C,WAAW;IACX,WAAW;IACX,WAAW;IACX,WAAW;IACX,WAAW;IACX,WAAW;AACb;AAEA,cAAc;AACd,MAAMC,cAAc;IAAC;IAAW;IAAY;CAAW;AAavD;;CAEC,GACD,SAASC,gBAAgBC,KAAa;IACpC,MAAMC,WAA0B,EAAE;IAClC,IAAIC,iBAA8B;QAAEC,MAAM;IAAG;IAC7C,IAAIC,IAAI;IAER,MAAOA,IAAIJ,MAAMK,MAAM,CAAE;QACvB,iCAAiC;QACjC,IAAIL,KAAK,CAACI,EAAE,KAAK,UAAUJ,KAAK,CAACI,IAAI,EAAE,KAAK,KAAK;YAC/C,2DAA2D;YAC3D,IAAIE,IAAIF,IAAI;YACZ,MAAOE,IAAIN,MAAMK,MAAM,IAAI,CAAC,WAAWE,IAAI,CAACP,KAAK,CAACM,EAAE,EAAG;gBACrDA;YACF;YACAA,KAAK,qBAAqB;YAE1B,MAAME,OAAOR,MAAMS,KAAK,CAACL,GAAGE;YAE5B,uCAAuC;YACvC,IAAIJ,eAAeC,IAAI,CAACE,MAAM,GAAG,GAAG;gBAClCJ,SAASS,IAAI,CAACR;gBACdA,iBAAiB;oBACfC,MAAM;oBACN,GAAGQ,OAAOC,WAAW,CACnBD,OAAOE,OAAO,CAACX,gBACZY,MAAM,CAAC,CAAC,CAACC,IAAI,GAAKA,QAAQ,QAC1BD,MAAM,CAAC,CAAC,CAACE,GAAGC,MAAM,GAAKA,UAAUC,WACrC;gBACH;YACF;YAEA,kBAAkB;YAClB,IAAIpB,YAAYqB,QAAQ,CAACX,OAAO;gBAC9B,mBAAmB;gBACnBN,iBAAiB;oBAAEC,MAAM;gBAAG;YAC9B,OAAO,IAAIP,WAAW,CAACY,KAAK,EAAE;gBAC5BN,eAAekB,KAAK,GAAGxB,WAAW,CAACY,KAAK;YAC1C,OAAO,IAAIX,WAAW,CAACW,KAAK,EAAE;gBAC5B,MAAMa,QAAQxB,WAAW,CAACW,KAAK;gBAC/BN,cAAc,CAACmB,MAAM,GAAG;YAC1B;YAEAjB,IAAIE;QACN,OAAO;YACL,oBAAoB;YACpBJ,eAAeC,IAAI,IAAIH,KAAK,CAACI,EAAE;YAC/BA;QACF;IACF;IAEA,sCAAsC;IACtC,IAAIF,eAAeC,IAAI,CAACE,MAAM,GAAG,GAAG;QAClCJ,SAASS,IAAI,CAACR;IAChB;IAEA,OAAOD;AACT;AAMA;;;CAGC,GACD,6BAAeN,KAAK,SAAS2B,SAAS,EAAEC,QAAQ,EAAS;IACvD,sEAAsE;IACtE,IAAI,CAACA,YAAY,CAACA,SAASJ,QAAQ,CAAC,SAAS;QAC3C,qBAAO,KAACzB;sBAAU6B;;IACpB;IAEA,MAAMtB,WAAWF,gBAAgBwB;IAEjC,uEAAuE;IACvE,IAAItB,SAASI,MAAM,KAAK,KAAK,CAACJ,QAAQ,CAAC,EAAE,CAACmB,KAAK,IAAI,CAACnB,QAAQ,CAAC,EAAE,CAACuB,IAAI,IAAI,CAACvB,QAAQ,CAAC,EAAE,CAACwB,GAAG,IAAI,CAACxB,QAAQ,CAAC,EAAE,CAACyB,MAAM,IAAI,CAACzB,QAAQ,CAAC,EAAE,CAAC0B,SAAS,IAAI,CAAC1B,QAAQ,CAAC,EAAE,CAAC2B,aAAa,IAAI,CAAC3B,QAAQ,CAAC,EAAE,CAAC4B,OAAO,EAAE;QAC/L,qBAAO,KAACnC;sBAAUO,QAAQ,CAAC,EAAE,CAACE,IAAI;;IACpC;IAEA,qBACE,KAACT;kBACEO,SAAS6B,GAAG,CAAC,CAACC,SAASC,QACtB,+GAA+G;0BAC/G,KAACvC;gBAAiB2B,OAAOW,QAAQX,KAAK;gBAAEI,MAAMO,QAAQP,IAAI;gBAAES,UAAUF,QAAQN,GAAG;gBAAEC,QAAQK,QAAQL,MAAM;gBAAEC,WAAWI,QAAQJ,SAAS;gBAAEC,eAAeG,QAAQH,aAAa;gBAAEC,SAASE,QAAQF,OAAO;0BACpME,QAAQ5B,IAAI;eADJ6B;;AAMnB,GAAG"}
@@ -33,5 +33,5 @@ const regex = ansiRegex();
33
33
  i++;
34
34
  }
35
35
  }
36
- return `${result}…`;
36
+ return `${result}\x1b[0m…`;
37
37
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/lib/clipText.ts"],"sourcesContent":["import ansiRegex from './ansiRegex.ts';\n\nconst regex = ansiRegex();\n\n/**\n * Get the visible length of a string, ignoring ANSI escape codes.\n */\nexport function visibleLength(str: string): number {\n return str.replace(regex, '').length;\n}\n\n/**\n * Clip text to a maximum visible width, accounting for ANSI escape codes.\n * Adds ellipsis (…) if truncated.\n */\nexport function clipText(str: string, maxWidth: number): string {\n if (maxWidth <= 0) return '';\n if (maxWidth === 1) return '…';\n\n const stripped = str.replace(regex, '');\n if (stripped.length <= maxWidth) return str;\n\n // Need to truncate - walk through and preserve ANSI codes\n let visibleCount = 0;\n let result = '';\n let i = 0;\n\n while (i < str.length && visibleCount < maxWidth - 1) {\n // Check for ANSI escape sequence (ESC [ params m)\n const remaining = str.slice(i);\n // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape requires control char\n const match = remaining.match(/^(\\u001B\\[[0-9;]*m)/);\n\n if (match) {\n // Include the ANSI code in output but don't count toward visible length\n result += match[1];\n i += match[1].length;\n } else {\n // Regular character\n result += str[i];\n visibleCount++;\n i++;\n }\n }\n\n return `${result}…`;\n}\n"],"names":["ansiRegex","regex","visibleLength","str","replace","length","clipText","maxWidth","stripped","visibleCount","result","i","remaining","slice","match"],"mappings":"AAAA,OAAOA,eAAe,iBAAiB;AAEvC,MAAMC,QAAQD;AAEd;;CAEC,GACD,OAAO,SAASE,cAAcC,GAAW;IACvC,OAAOA,IAAIC,OAAO,CAACH,OAAO,IAAII,MAAM;AACtC;AAEA;;;CAGC,GACD,OAAO,SAASC,SAASH,GAAW,EAAEI,QAAgB;IACpD,IAAIA,YAAY,GAAG,OAAO;IAC1B,IAAIA,aAAa,GAAG,OAAO;IAE3B,MAAMC,WAAWL,IAAIC,OAAO,CAACH,OAAO;IACpC,IAAIO,SAASH,MAAM,IAAIE,UAAU,OAAOJ;IAExC,0DAA0D;IAC1D,IAAIM,eAAe;IACnB,IAAIC,SAAS;IACb,IAAIC,IAAI;IAER,MAAOA,IAAIR,IAAIE,MAAM,IAAII,eAAeF,WAAW,EAAG;QACpD,kDAAkD;QAClD,MAAMK,YAAYT,IAAIU,KAAK,CAACF;QAC5B,6FAA6F;QAC7F,MAAMG,QAAQF,UAAUE,KAAK,CAAC;QAE9B,IAAIA,OAAO;YACT,wEAAwE;YACxEJ,UAAUI,KAAK,CAAC,EAAE;YAClBH,KAAKG,KAAK,CAAC,EAAE,CAACT,MAAM;QACtB,OAAO;YACL,oBAAoB;YACpBK,UAAUP,GAAG,CAACQ,EAAE;YAChBF;YACAE;QACF;IACF;IAEA,OAAO,GAAGD,OAAO,CAAC,CAAC;AACrB"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/node/spawn-term/src/lib/clipText.ts"],"sourcesContent":["import ansiRegex from './ansiRegex.ts';\n\nconst regex = ansiRegex();\n\n/**\n * Get the visible length of a string, ignoring ANSI escape codes.\n */\nexport function visibleLength(str: string): number {\n return str.replace(regex, '').length;\n}\n\n/**\n * Clip text to a maximum visible width, accounting for ANSI escape codes.\n * Adds ellipsis (…) if truncated.\n */\nexport function clipText(str: string, maxWidth: number): string {\n if (maxWidth <= 0) return '';\n if (maxWidth === 1) return '…';\n\n const stripped = str.replace(regex, '');\n if (stripped.length <= maxWidth) return str;\n\n // Need to truncate - walk through and preserve ANSI codes\n let visibleCount = 0;\n let result = '';\n let i = 0;\n\n while (i < str.length && visibleCount < maxWidth - 1) {\n // Check for ANSI escape sequence (ESC [ params m)\n const remaining = str.slice(i);\n // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape requires control char\n const match = remaining.match(/^(\\u001B\\[[0-9;]*m)/);\n\n if (match) {\n // Include the ANSI code in output but don't count toward visible length\n result += match[1];\n i += match[1].length;\n } else {\n // Regular character\n result += str[i];\n visibleCount++;\n i++;\n }\n }\n\n return `${result}\\x1b[0m…`;\n}\n"],"names":["ansiRegex","regex","visibleLength","str","replace","length","clipText","maxWidth","stripped","visibleCount","result","i","remaining","slice","match"],"mappings":"AAAA,OAAOA,eAAe,iBAAiB;AAEvC,MAAMC,QAAQD;AAEd;;CAEC,GACD,OAAO,SAASE,cAAcC,GAAW;IACvC,OAAOA,IAAIC,OAAO,CAACH,OAAO,IAAII,MAAM;AACtC;AAEA;;;CAGC,GACD,OAAO,SAASC,SAASH,GAAW,EAAEI,QAAgB;IACpD,IAAIA,YAAY,GAAG,OAAO;IAC1B,IAAIA,aAAa,GAAG,OAAO;IAE3B,MAAMC,WAAWL,IAAIC,OAAO,CAACH,OAAO;IACpC,IAAIO,SAASH,MAAM,IAAIE,UAAU,OAAOJ;IAExC,0DAA0D;IAC1D,IAAIM,eAAe;IACnB,IAAIC,SAAS;IACb,IAAIC,IAAI;IAER,MAAOA,IAAIR,IAAIE,MAAM,IAAII,eAAeF,WAAW,EAAG;QACpD,kDAAkD;QAClD,MAAMK,YAAYT,IAAIU,KAAK,CAACF;QAC5B,6FAA6F;QAC7F,MAAMG,QAAQF,UAAUE,KAAK,CAAC;QAE9B,IAAIA,OAAO;YACT,wEAAwE;YACxEJ,UAAUI,KAAK,CAAC,EAAE;YAClBH,KAAKG,KAAK,CAAC,EAAE,CAACT,MAAM;QACtB,OAAO;YACL,oBAAoB;YACpBK,UAAUP,GAAG,CAACQ,EAAE;YAChBF;YACAE;QACF;IACF;IAEA,OAAO,GAAGD,OAAO,QAAQ,CAAC;AAC5B"}
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ children: string;
3
+ };
4
+ /**
5
+ * Component that renders text with ANSI escape codes as styled Ink Text components.
6
+ * Parses ANSI color and style codes and applies them as Ink props.
7
+ */
8
+ declare const _default: import("react").NamedExoticComponent<Props>;
9
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spawn-term",
3
- "version": "3.5.4",
3
+ "version": "3.5.6",
4
4
  "description": "Formats spawn with for terminal grouping",
5
5
  "keywords": [
6
6
  "spawn",