diff --git a/frontend/app/element/ansiline.tsx b/frontend/app/element/ansiline.tsx new file mode 100644 index 000000000..7fb238d87 --- /dev/null +++ b/frontend/app/element/ansiline.tsx @@ -0,0 +1,154 @@ +export const ANSI_TAILWIND_MAP = { + // Reset and modifiers + 0: "reset", // special: clear state + 1: "font-bold", + 2: "opacity-75", + 3: "italic", + 4: "underline", + 8: "invisible", + 9: "line-through", + + // Foreground standard colors + 30: "text-ansi-black", + 31: "text-ansi-red", + 32: "text-ansi-green", + 33: "text-ansi-yellow", + 34: "text-ansi-blue", + 35: "text-ansi-magenta", + 36: "text-ansi-cyan", + 37: "text-ansi-white", + + // Foreground bright colors + 90: "text-ansi-brightblack", + 91: "text-ansi-brightred", + 92: "text-ansi-brightgreen", + 93: "text-ansi-brightyellow", + 94: "text-ansi-brightblue", + 95: "text-ansi-brightmagenta", + 96: "text-ansi-brightcyan", + 97: "text-ansi-brightwhite", + + // Background standard colors + 40: "bg-ansi-black", + 41: "bg-ansi-red", + 42: "bg-ansi-green", + 43: "bg-ansi-yellow", + 44: "bg-ansi-blue", + 45: "bg-ansi-magenta", + 46: "bg-ansi-cyan", + 47: "bg-ansi-white", + + // Background bright colors + 100: "bg-ansi-brightblack", + 101: "bg-ansi-brightred", + 102: "bg-ansi-brightgreen", + 103: "bg-ansi-brightyellow", + 104: "bg-ansi-brightblue", + 105: "bg-ansi-brightmagenta", + 106: "bg-ansi-brightcyan", + 107: "bg-ansi-brightwhite", +}; + +type InternalStateType = { + modifiers: Set; + textColor: string | null; + bgColor: string | null; + reverse: boolean; +}; + +type SegmentType = { + text: string; + classes: string; +}; + +const makeInitialState: () => InternalStateType = () => ({ + modifiers: new Set(), + textColor: null, + bgColor: null, + reverse: false, +}); + +const updateStateWithCodes = (state, codes) => { + codes.forEach((code) => { + if (code === 0) { + // Reset state + state.modifiers.clear(); + state.textColor = null; + state.bgColor = null; + state.reverse = false; + return; + } + // Instead of swapping immediately, we set a flag + if (code === 7) { + state.reverse = true; + return; + } + const tailwindClass = ANSI_TAILWIND_MAP[code]; + if (tailwindClass && tailwindClass !== "reset") { + if (tailwindClass.startsWith("text-")) { + state.textColor = tailwindClass; + } else if (tailwindClass.startsWith("bg-")) { + state.bgColor = tailwindClass; + } else { + state.modifiers.add(tailwindClass); + } + } + }); + return state; +}; + +const stateToClasses = (state: InternalStateType) => { + const classes = []; + classes.push(...Array.from(state.modifiers)); + + // Apply reverse: swap text and background colors if flag is set. + let textColor = state.textColor; + let bgColor = state.bgColor; + if (state.reverse) { + [textColor, bgColor] = [bgColor, textColor]; + } + if (textColor) classes.push(textColor); + if (bgColor) classes.push(bgColor); + + return classes.join(" "); +}; + +const ansiRegex = /\x1b\[([0-9;]+)m/g; + +const AnsiLine = ({ line }) => { + const segments: SegmentType[] = []; + let lastIndex = 0; + let currentState = makeInitialState(); + + let match: RegExpExecArray; + while ((match = ansiRegex.exec(line)) !== null) { + if (match.index > lastIndex) { + segments.push({ + text: line.substring(lastIndex, match.index), + classes: stateToClasses(currentState), + }); + } + const codes = match[1].split(";").map(Number); + updateStateWithCodes(currentState, codes); + lastIndex = ansiRegex.lastIndex; + } + + if (lastIndex < line.length) { + segments.push({ + text: line.substring(lastIndex), + classes: stateToClasses(currentState), + }); + } + + return ( +
+ {segments.map((seg, idx) => ( + + {seg.text} + + ))} +
+ ); +}; + +export default AnsiLine; diff --git a/frontend/tailwindsetup.css b/frontend/tailwindsetup.css index 578567a92..acf1c74b0 100644 --- a/frontend/tailwindsetup.css +++ b/frontend/tailwindsetup.css @@ -41,4 +41,22 @@ --text-default: 14px; --radius: 8px; + + /* ANSI Colors (Default Dark Palette) */ + --ansi-black: #757575; + --ansi-red: #cc685c; + --ansi-green: #76c266; + --ansi-yellow: #cbca9b; + --ansi-blue: #85aacb; + --ansi-magenta: #cc72ca; + --ansi-cyan: #74a7cb; + --ansi-white: #c1c1c1; + --ansi-brightblack: #727272; + --ansi-brightred: #cc9d97; + --ansi-brightgreen: #a3dd97; + --ansi-brightyellow: #cbcaaa; + --ansi-brightblue: #9ab6cb; + --ansi-brightmagenta: #cc8ecb; + --ansi-brightcyan: #b7b8cb; + --ansi-brightwhite: #f0f0f0; }