Merge pull request #965 from drudge/fancy-ansi

feat(logs): use fancy-ansi for ansi colors
This commit is contained in:
Mauricio Siu 2024-12-22 23:09:02 -06:00 committed by GitHub
commit 1d023ac9f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 59 additions and 127 deletions

View File

@ -7,9 +7,10 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { FancyAnsi } from "fancy-ansi";
import { escapeRegExp } from "lodash";
import React from "react";
import { type LogLine, getLogType, parseAnsi } from "./utils";
import { type LogLine, getLogType } from "./utils";
interface LogLineProps {
log: LogLine;
@ -17,6 +18,8 @@ interface LogLineProps {
searchTerm?: string;
}
const fancyAnsi = new FancyAnsi();
export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
const { timestamp, message, rawTimestamp } = log;
const { type, variant, color } = getLogType(message);
@ -34,37 +37,42 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
const highlightMessage = (text: string, term: string) => {
if (!term) {
const segments = parseAnsi(text);
return segments.map((segment, index) => (
<span key={index} className={segment.className || undefined}>
{segment.text}
</span>
));
return (
<span
className="transition-colors"
dangerouslySetInnerHTML={{
__html: fancyAnsi.toHtml(text),
}}
/>
);
}
// 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 htmlContent = fancyAnsi.toHtml(text);
const modifiedContent = htmlContent.replace(
/<span([^>]*)>([^<]*)<\/span>/g,
(match, attrs, content) => {
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
if (!content.match(searchRegex)) return match;
const segments = content.split(searchRegex);
const wrappedSegments = segments
.map((segment: string) =>
segment.toLowerCase() === term.toLowerCase()
? `<span${attrs} class="bg-yellow-200/50 dark:bg-yellow-900/50">${segment}</span>`
: segment,
)
.join("");
return `<span${attrs}>${wrappedSegments}</span>`;
},
);
return (
<span
className="transition-colors"
dangerouslySetInnerHTML={{ __html: modifiedContent }}
/>
);
};
const tooltip = (color: string, timestamp: string | null) => {

View File

@ -12,47 +12,6 @@ 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: {
@ -191,56 +150,3 @@ 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;
}

View File

@ -35,8 +35,6 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"react-confetti-explosion":"2.1.2",
"@stepperize/react": "4.0.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
@ -64,6 +62,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@stepperize/react": "4.0.1",
"@stripe/stripe-js": "4.8.0",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.16.0",
@ -87,6 +86,7 @@
"dotenv": "16.4.5",
"drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1",
"fancy-ansi": "^0.1.3",
"i18next": "^23.16.4",
"input-otp": "^1.2.4",
"js-cookie": "^3.0.5",
@ -104,6 +104,7 @@
"postgres": "3.4.4",
"public-ip": "6.0.2",
"react": "18.2.0",
"react-confetti-explosion": "2.1.2",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.3",
"react-i18next": "^15.1.0",

View File

@ -87,7 +87,7 @@ const config = {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [require("tailwindcss-animate"), require("fancy-ansi/plugin")],
} satisfies Config;
export default config;

View File

@ -250,6 +250,9 @@ importers:
drizzle-zod:
specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.30.10(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8)
fancy-ansi:
specifier: ^0.1.3
version: 0.1.3
i18next:
specifier: ^23.16.4
version: 23.16.5
@ -874,9 +877,11 @@ packages:
'@esbuild-kit/core-utils@3.3.2':
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
deprecated: 'Merged into tsx: https://tsx.is'
'@esbuild-kit/esm-loader@2.6.5':
resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
deprecated: 'Merged into tsx: https://tsx.is'
'@esbuild/aix-ppc64@0.19.12':
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
@ -4401,6 +4406,9 @@ packages:
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
engines: {node: '>=6'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
@ -4460,6 +4468,9 @@ packages:
ext@1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
fancy-ansi@0.1.3:
resolution: {integrity: sha512-tRQVTo5jjdSIiydqgzIIEZpKddzSsfGLsSVt6vWdjVm7fbvDTiQkyoPu6Z3dIPlAM4OZk0jP5jmTCX4G8WGgBw==}
fast-copy@3.0.2:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
@ -10735,6 +10746,8 @@ snapshots:
escalade@3.1.2: {}
escape-html@1.0.3: {}
escape-string-regexp@1.0.5: {}
escape-string-regexp@5.0.0: {}
@ -10795,6 +10808,10 @@ snapshots:
dependencies:
type: 2.7.3
fancy-ansi@0.1.3:
dependencies:
escape-html: 1.0.3
fast-copy@3.0.2: {}
fast-deep-equal@2.0.1: {}