Merge pull request #912 from drudge/new-ansi-logs

feat(logs): support ansi codes
This commit is contained in:
Mauricio Siu
2024-12-17 23:55:12 -06:00
committed by GitHub
2 changed files with 127 additions and 13 deletions

View File

@@ -9,7 +9,7 @@ import {
import { cn } from "@/lib/utils";
import { escapeRegExp } from "lodash";
import React from "react";
import { type LogLine, getLogType } from "./utils";
import { type LogLine, getLogType, parseAnsi } from "./utils";
interface LogLineProps {
log: LogLine;
@@ -33,18 +33,38 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
: "--- No time found ---";
const highlightMessage = (text: string, term: string) => {
if (!term) return text;
const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi"));
return parts.map((part, index) =>
part.toLowerCase() === term.toLowerCase() ? (
<span key={index} className="bg-yellow-200 dark:bg-yellow-900">
{part}
if (!term) {
const segments = parseAnsi(text);
return segments.map((segment, index) => (
<span key={index} className={segment.className || undefined}>
{segment.text}
</span>
) : (
part
),
);
));
}
// For search, we need to handle both ANSI and search highlighting
const segments = parseAnsi(text);
return segments.map((segment, index) => {
const parts = segment.text.split(
new RegExp(`(${escapeRegExp(term)})`, "gi"),
);
return (
<span key={index} className={segment.className || undefined}>
{parts.map((part, partIndex) =>
part.toLowerCase() === term.toLowerCase() ? (
<span
key={partIndex}
className="bg-yellow-200 dark:bg-yellow-900"
>
{part}
</span>
) : (
part
),
)}
</span>
);
});
};
const tooltip = (color: string, timestamp: string | null) => {
@@ -104,7 +124,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
</Badge>
</div>
<span className="dark:text-gray-200 font-mono text-foreground whitespace-pre-wrap break-all">
{searchTerm ? highlightMessage(message, searchTerm) : message}
{highlightMessage(message, searchTerm || "")}
</span>
</div>
);

View File

@@ -12,6 +12,47 @@ interface LogStyle {
variant: LogVariant;
color: string;
}
interface AnsiSegment {
text: string;
className: string;
}
const ansiToTailwind: Record<number, string> = {
// Reset
0: "",
// Regular colors
30: "text-black dark:text-gray-900",
31: "text-red-600 dark:text-red-500",
32: "text-green-600 dark:text-green-500",
33: "text-yellow-600 dark:text-yellow-500",
34: "text-blue-600 dark:text-blue-500",
35: "text-purple-600 dark:text-purple-500",
36: "text-cyan-600 dark:text-cyan-500",
37: "text-gray-600 dark:text-gray-400",
// Bright colors
90: "text-gray-500 dark:text-gray-600",
91: "text-red-500 dark:text-red-600",
92: "text-green-500 dark:text-green-600",
93: "text-yellow-500 dark:text-yellow-600",
94: "text-blue-500 dark:text-blue-600",
95: "text-purple-500 dark:text-purple-600",
96: "text-cyan-500 dark:text-cyan-600",
97: "text-white dark:text-gray-300",
// Background colors
40: "bg-black",
41: "bg-red-600",
42: "bg-green-600",
43: "bg-yellow-600",
44: "bg-blue-600",
45: "bg-purple-600",
46: "bg-cyan-600",
47: "bg-white",
// Formatting
1: "font-bold",
2: "opacity-75",
3: "italic",
4: "underline",
};
const LOG_STYLES: Record<LogType, LogStyle> = {
error: {
@@ -150,3 +191,56 @@ export const getLogType = (message: string): LogStyle => {
return LOG_STYLES.info;
};
export function parseAnsi(text: string) {
const segments: { text: string; className: string }[] = [];
let currentIndex = 0;
let currentClasses: string[] = [];
while (currentIndex < text.length) {
const escStart = text.indexOf("\x1b[", currentIndex);
// No more escape sequences found
if (escStart === -1) {
if (currentIndex < text.length) {
segments.push({
text: text.slice(currentIndex),
className: currentClasses.join(" "),
});
}
break;
}
// Add text before escape sequence
if (escStart > currentIndex) {
segments.push({
text: text.slice(currentIndex, escStart),
className: currentClasses.join(" "),
});
}
const escEnd = text.indexOf("m", escStart);
if (escEnd === -1) break;
// Handle multiple codes in one sequence (e.g., \x1b[1;31m)
const codesStr = text.slice(escStart + 2, escEnd);
const codes = codesStr.split(";").map((c) => Number.parseInt(c, 10));
if (codes.includes(0)) {
// Reset all formatting
currentClasses = [];
} else {
// Add new classes for each code
for (const code of codes) {
const className = ansiToTailwind[code];
if (className && !currentClasses.includes(className)) {
currentClasses.push(className);
}
}
}
currentIndex = escEnd + 1;
}
return segments;
}