diff --git a/src/app/logs/page.tsx b/src/app/logs/page.tsx
index d240717..dd6c53d 100644
--- a/src/app/logs/page.tsx
+++ b/src/app/logs/page.tsx
@@ -10,6 +10,7 @@ import { cn } from "@/lib/utils";
import { Log } from "@/lib/types";
import { getAllContainerLogs } from "@/lib/dockship/docker";
import { useAsyncInterval } from "@/hooks/use-async";
+import { AnsiText } from "@/components/ansi/Ansi";
export default function LogsPage() {
const [isPaused, setIsPaused] = useState(false);
@@ -182,9 +183,10 @@ export default function LogsPage() {
{/* Message */}
-
+ {/*
{log.message}
-
+ */}
+
))}
diff --git a/src/components/ansi/Ansi.tsx b/src/components/ansi/Ansi.tsx
new file mode 100644
index 0000000..d62789f
--- /dev/null
+++ b/src/components/ansi/Ansi.tsx
@@ -0,0 +1,50 @@
+import { tokenizeAnsi } from "./tokenize";
+
+type AnsiTextProps = {
+ text: string;
+ className?: string;
+};
+
+const ANSI_TW_CLASSES: Record = {
+ black: "text-black",
+ red: "text-red-500",
+ green: "text-emerald-500",
+ yellow: "text-yellow-400",
+ blue: "text-blue-400",
+ magenta: "text-purple-400",
+ cyan: "text-cyan-400",
+ white: "text-gray-100",
+
+ brightBlack: "text-gray-500",
+ brightRed: "text-red-400",
+ brightGreen: "text-emerald-400",
+ brightYellow: "text-yellow-300",
+ brightBlue: "text-blue-300",
+ brightMagenta: "text-purple-300",
+ brightCyan: "text-cyan-300",
+ brightWhite: "text-white",
+};
+
+export function AnsiText({ text, className }: AnsiTextProps) {
+ const tokens = tokenizeAnsi(text);
+
+ return (
+
+ {tokens.map((token, i) => {
+ const classes = [
+ token.style.color && ANSI_TW_CLASSES[token.style.color],
+ token.style.bold && "font-semibold",
+ token.style.underline && "underline",
+ ]
+ .filter(Boolean)
+ .join(" ");
+
+ return (
+
+ {token.text}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/ansi/parser.ts b/src/components/ansi/parser.ts
new file mode 100644
index 0000000..3daa603
--- /dev/null
+++ b/src/components/ansi/parser.ts
@@ -0,0 +1,45 @@
+export type StyleState = {
+ color?: string;
+ bold?: boolean;
+ underline?: boolean;
+};
+
+export const ANSI_REGEX = /\u001b\[([0-9;]*)m/g;
+
+const COLOR_CODES: Record = {
+ 30: "black",
+ 31: "red",
+ 32: "green",
+ 33: "yellow",
+ 34: "blue",
+ 35: "magenta",
+ 36: "cyan",
+ 37: "white",
+
+ 90: "brightBlack",
+ 91: "brightRed",
+ 92: "brightGreen",
+ 93: "brightYellow",
+ 94: "brightBlue",
+ 95: "brightMagenta",
+ 96: "brightCyan",
+ 97: "brightWhite",
+};
+
+export function applyAnsiCodes(codes: number[], state: StyleState): StyleState {
+ let next = { ...state };
+
+ for (const code of codes) {
+ if (code === 0) {
+ next = {};
+ } else if (code === 1) {
+ next.bold = true;
+ } else if (code === 4) {
+ next.underline = true;
+ } else if (COLOR_CODES[code]) {
+ next.color = COLOR_CODES[code];
+ }
+ }
+
+ return next;
+}
diff --git a/src/components/ansi/tokenize.ts b/src/components/ansi/tokenize.ts
new file mode 100644
index 0000000..654b4a6
--- /dev/null
+++ b/src/components/ansi/tokenize.ts
@@ -0,0 +1,39 @@
+import { ANSI_REGEX, applyAnsiCodes, StyleState } from "./parser";
+
+export type Token = {
+ text: string;
+ style: StyleState;
+};
+
+export function tokenizeAnsi(input: string): Token[] {
+ const tokens: Token[] = [];
+
+ let lastIndex = 0;
+ let match: RegExpExecArray | null;
+ let style: StyleState = {};
+
+ while ((match = ANSI_REGEX.exec(input))) {
+ const index = match.index;
+
+ if (index > lastIndex) {
+ tokens.push({
+ text: input.slice(lastIndex, index),
+ style,
+ });
+ }
+
+ const codes = match[1].split(";").map(Number);
+ style = applyAnsiCodes(codes, style);
+
+ lastIndex = ANSI_REGEX.lastIndex;
+ }
+
+ if (lastIndex < input.length) {
+ tokens.push({
+ text: input.slice(lastIndex),
+ style,
+ });
+ }
+
+ return tokens;
+}