Colors in logs
This commit is contained in:
@@ -10,6 +10,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { Log } from "@/lib/types";
|
import { Log } from "@/lib/types";
|
||||||
import { getAllContainerLogs } from "@/lib/dockship/docker";
|
import { getAllContainerLogs } from "@/lib/dockship/docker";
|
||||||
import { useAsyncInterval } from "@/hooks/use-async";
|
import { useAsyncInterval } from "@/hooks/use-async";
|
||||||
|
import { AnsiText } from "@/components/ansi/Ansi";
|
||||||
|
|
||||||
export default function LogsPage() {
|
export default function LogsPage() {
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
@@ -182,9 +183,10 @@ export default function LogsPage() {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Message */}
|
{/* Message */}
|
||||||
<span className="text-foreground wrap-break-words">
|
{/* <span className="text-foreground wrap-break-words">
|
||||||
{log.message}
|
{log.message}
|
||||||
</span>
|
</span> */}
|
||||||
|
<AnsiText text={log.message} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { tokenizeAnsi } from "./tokenize";
|
||||||
|
|
||||||
|
type AnsiTextProps = {
|
||||||
|
text: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ANSI_TW_CLASSES: Record<string, string> = {
|
||||||
|
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 (
|
||||||
|
<span className={`whitespace-pre-wrap font-mono ${className ?? ""}`}>
|
||||||
|
{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 (
|
||||||
|
<span key={i} className={classes}>
|
||||||
|
{token.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<number, string> = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user