Compare commits
4 Commits
62f936cdef
...
1e4d66cbe4
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e4d66cbe4 | |||
| a824d62058 | |||
| 33d91747af | |||
| 33d21777d3 |
71
src/client/ansi.ts
Normal file
71
src/client/ansi.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
const ESC = /\x1b\[([0-9;]*)m/g
|
||||
|
||||
const STYLES: Record<number, string> = {
|
||||
1: 'font-weight:bold',
|
||||
2: 'opacity:0.7',
|
||||
30: 'color:#666',
|
||||
31: 'color:#f87171',
|
||||
32: 'color:#4ade80',
|
||||
33: 'color:#facc15',
|
||||
34: 'color:#60a5fa',
|
||||
35: 'color:#c084fc',
|
||||
36: 'color:#22d3ee',
|
||||
37: 'color:#e5e5e5',
|
||||
90: 'color:#999',
|
||||
91: 'color:#fca5a5',
|
||||
92: 'color:#86efac',
|
||||
93: 'color:#fde047',
|
||||
94: 'color:#93c5fd',
|
||||
95: 'color:#d8b4fe',
|
||||
96: 'color:#67e8f9',
|
||||
97: 'color:#fff',
|
||||
}
|
||||
|
||||
const escape = (s: string) =>
|
||||
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
||||
export const stripAnsi = (s: string) =>
|
||||
s.replace(/\x1b\[[0-9;]*m/g, '')
|
||||
|
||||
export function ansiToHtml(text: string): string {
|
||||
if (!text.includes('\x1b')) return escape(text)
|
||||
|
||||
ESC.lastIndex = 0
|
||||
let result = ''
|
||||
let last = 0
|
||||
let open = false
|
||||
const styles: string[] = []
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = ESC.exec(text)) !== null) {
|
||||
result += escape(text.slice(last, match.index))
|
||||
last = match.index + match[0].length
|
||||
|
||||
const codes = match[1] ? match[1].split(';').map(Number) : [0]
|
||||
|
||||
for (const code of codes) {
|
||||
if (code === 0) {
|
||||
styles.length = 0
|
||||
if (open) { result += '</span>'; open = false }
|
||||
} else if (code === 39) {
|
||||
const filtered = styles.filter(s => !s.startsWith('color:'))
|
||||
styles.length = 0
|
||||
styles.push(...filtered)
|
||||
if (open) { result += '</span>'; open = false }
|
||||
} else if (STYLES[code]) {
|
||||
styles.push(STYLES[code])
|
||||
}
|
||||
}
|
||||
|
||||
if (styles.length) {
|
||||
if (open) result += '</span>'
|
||||
result += `<span style="${styles.join(';')}">`
|
||||
open = true
|
||||
}
|
||||
}
|
||||
|
||||
result += escape(text.slice(last))
|
||||
if (open) result += '</span>'
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { define } from '@because/forge'
|
||||
import type { App, LogLine as LogLineType } from '../../shared/types'
|
||||
import { ansiToHtml, stripAnsi } from '../ansi'
|
||||
import { getLogDates, getLogsForDate } from '../api'
|
||||
import { isNarrow } from '../state'
|
||||
import { LogLine, LogsContainer, LogsHeader, LogTime, Section, SectionTitle } from '../styles'
|
||||
|
|
@ -91,7 +92,7 @@ function LogsContent() {
|
|||
|
||||
const state = getState(currentApp.name)
|
||||
const isLive = state.selectedDate === 'live'
|
||||
const filteredLiveLogs = filterLogs(currentApp.logs ?? [], state.searchFilter, l => l.text)
|
||||
const filteredLiveLogs = filterLogs(currentApp.logs ?? [], state.searchFilter, l => stripAnsi(l.text))
|
||||
const filteredHistoricalLogs = filterLogs(state.historicalLogs, state.searchFilter, l => l)
|
||||
|
||||
return (
|
||||
|
|
@ -107,7 +108,7 @@ function LogsContent() {
|
|||
filteredLiveLogs.map((line, i) => (
|
||||
<LogLine key={i}>
|
||||
<LogTime>{new Date(line.time).toLocaleTimeString()}</LogTime>
|
||||
<span>{line.text}</span>
|
||||
<span dangerouslySetInnerHTML={{ __html: ansiToHtml(line.text) }} />
|
||||
</LogLine>
|
||||
))
|
||||
) : (
|
||||
|
|
@ -126,7 +127,7 @@ function LogsContent() {
|
|||
<span style={{ color: theme('colors-textFaintest'), marginRight: 12 }}>
|
||||
{line.match(/^\[([^\]]+)\]/)?.[1]?.split('T')[1]?.slice(0, 8) ?? '--:--:--'}
|
||||
</span>
|
||||
<span>{line.replace(/^\[[^\]]+\] \[[^\]]+\] /, '')}</span>
|
||||
<span dangerouslySetInnerHTML={{ __html: ansiToHtml(line.replace(/^\[[^\]]+\] \[[^\]]+\] /, '')) }} />
|
||||
</LogLine>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ansiToHtml, stripAnsi } from '../ansi'
|
||||
import { isNarrow } from '../state'
|
||||
import {
|
||||
LogApp,
|
||||
|
|
@ -58,16 +59,17 @@ function parseLogText(text: string): { method?: string, path?: string, status?:
|
|||
}
|
||||
|
||||
function LogLineEntry({ log }: { log: UnifiedLogLine }) {
|
||||
const parsed = parseLogText(log.text)
|
||||
const parsed = parseLogText(stripAnsi(log.text))
|
||||
const statusColor = getStatusColor(parsed.status)
|
||||
const narrow = isNarrow || undefined
|
||||
return (
|
||||
<LogEntry narrow={narrow}>
|
||||
<LogTimestamp narrow={narrow}>{formatTime(log.time)}</LogTimestamp>
|
||||
<LogApp narrow={narrow}>{log.app}</LogApp>
|
||||
<LogText style={statusColor ? { color: statusColor } : undefined}>
|
||||
{log.text}
|
||||
</LogText>
|
||||
<LogText
|
||||
style={statusColor ? { color: statusColor } : undefined}
|
||||
dangerouslySetInnerHTML={{ __html: ansiToHtml(log.text) }}
|
||||
/>
|
||||
</LogEntry>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user