1
1
import type { ClarityConfig , Formatter , LogEntry } from '../types'
2
+ import process from 'node:process'
2
3
import { format } from '../format'
3
4
4
5
const COLORS = {
@@ -28,24 +29,44 @@ const COLORS = {
28
29
bgWhite : '\x1B[47m' ,
29
30
bold : '\x1B[1m' ,
30
31
underline : '\x1B[4m' ,
32
+ gray : '\x1B[90m' ,
33
+ } as const
34
+
35
+ // Check if Unicode is supported
36
+ function isUnicodeSupported ( ) : boolean {
37
+ try {
38
+ return process . platform !== 'win32'
39
+ || Boolean ( process . env . CI )
40
+ || Boolean ( process . env . WT_SESSION ) // Windows Terminal
41
+ || process . env . TERM_PROGRAM === 'vscode'
42
+ || process . env . TERM === 'xterm-256color'
43
+ || process . env . TERM === 'alacritty'
44
+ }
45
+ catch {
46
+ return false
47
+ }
31
48
}
32
49
50
+ const unicode = isUnicodeSupported ( )
51
+ const s = ( c : string , fallback : string ) => ( unicode ? c : fallback )
52
+
33
53
const ICONS = {
34
- debug : '🔍' ,
35
- info : 'ℹ️' ,
36
- success : '✅' ,
37
- warning : '⚠️' ,
38
- error : '❌' ,
39
- }
54
+ debug : s ( '🔍' , 'D' ) ,
55
+ info : s ( 'ℹ️' , 'i' ) ,
56
+ success : s ( '✅' , '√' ) ,
57
+ warning : s ( '⚠️' , '‼' ) ,
58
+ error : s ( '❌' , '×' ) ,
59
+ } as const
40
60
41
61
interface LevelStyle {
42
62
color : string
43
63
label : string
44
64
box ?: boolean
65
+ bgColor ?: string
45
66
}
46
67
47
68
const LEVEL_STYLES : Record < string , LevelStyle > = {
48
- debug : { color : COLORS . dim + COLORS . white , label : 'DEBUG' } ,
69
+ debug : { color : COLORS . gray , label : 'DEBUG' } ,
49
70
info : { color : COLORS . brightBlue , label : 'INFO' } ,
50
71
success : { color : COLORS . brightGreen , label : 'SUCCESS' } ,
51
72
warning : { color : COLORS . brightYellow , label : 'WARN' , box : true } ,
@@ -79,6 +100,11 @@ function stripAnsi(str: string): string {
79
100
return result
80
101
}
81
102
103
+ // Calculate string width accounting for ANSI codes
104
+ function stringWidth ( str : string ) : number {
105
+ return stripAnsi ( str ) . length
106
+ }
107
+
82
108
// Helper to create a box around text
83
109
function createBox ( text : string , color : string , forFile : boolean = false ) : string {
84
110
const lines = text . split ( '\n' )
@@ -122,6 +148,48 @@ function createBox(text: string, color: string, forFile: boolean = false): strin
122
148
}
123
149
}
124
150
151
+ // Format text with character highlighting (backticks and underscores)
152
+ function characterFormat ( str : string ) : string {
153
+ return str
154
+ // highlight backticks
155
+ . replace ( / ` ( [ ^ ` ] + ) ` / g, ( _ , m ) => `${ COLORS . cyan } ${ m } ${ COLORS . reset } ` )
156
+ // underline underscores
157
+ . replace ( / \s + _ ( [ ^ _ ] + ) _ \s + / g, ( _ , m ) => ` ${ COLORS . underline } ${ m } ${ COLORS . reset } ` )
158
+ }
159
+
160
+ // Format stack traces
161
+ function formatStack ( stack : string , forFile : boolean = false ) : string {
162
+ if ( ! stack )
163
+ return ''
164
+
165
+ const lines = stack . split ( '\n' )
166
+ const formattedLines = lines . map ( ( line , i ) => {
167
+ if ( i === 0 )
168
+ return line // Keep the first line as is
169
+
170
+ if ( line . trim ( ) . startsWith ( 'at ' ) ) {
171
+ // Use a safer pattern to avoid backtracking issues
172
+ const atParts = line . trim ( ) . split ( / ^ a t \s + / )
173
+ if ( atParts . length > 1 ) {
174
+ const funcLocationParts = atParts [ 1 ] . split ( ' (' )
175
+ if ( funcLocationParts . length > 1 ) {
176
+ const fnName = funcLocationParts [ 0 ]
177
+ const location = funcLocationParts [ 1 ] . replace ( ')' , '' )
178
+ return forFile
179
+ ? ` at ${ fnName } (${ location } )`
180
+ : ` ${ COLORS . dim } at ${ COLORS . cyan } ${ fnName } ${ COLORS . dim } (${ location } )${ COLORS . reset } `
181
+ }
182
+ }
183
+ return forFile
184
+ ? ` ${ line . trim ( ) } `
185
+ : ` ${ COLORS . dim } ${ line . trim ( ) } ${ COLORS . reset } `
186
+ }
187
+ return ` ${ line } `
188
+ } )
189
+
190
+ return formattedLines . join ( '\n' )
191
+ }
192
+
125
193
export class PrettyFormatter implements Formatter {
126
194
private config : ClarityConfig
127
195
private terminalWidth : number
@@ -139,7 +207,7 @@ export class PrettyFormatter implements Formatter {
139
207
const style = LEVEL_STYLES [ level ]
140
208
const icon = ICONS [ level ]
141
209
142
- // Format timestamp first for files
210
+ // Format timestamp
143
211
const timestampStr = timestamp . toISOString ( )
144
212
const formattedTimestamp = forFile
145
213
? timestampStr
@@ -162,32 +230,13 @@ export class PrettyFormatter implements Formatter {
162
230
if ( level === 'error' && formattedMessage . includes ( '\n' )
163
231
&& ( formattedMessage . includes ( 'at ' ) || formattedMessage . includes ( 'stack:' ) ) ) {
164
232
// Format the stack trace with proper indentation and coloring
165
- const lines = formattedMessage . split ( '\n' )
166
- const formattedLines = lines . map ( ( line , i ) => {
167
- if ( i === 0 )
168
- return line // Keep the first line as is
169
-
170
- if ( line . trim ( ) . startsWith ( 'at ' ) ) {
171
- // Use a safer pattern to avoid backtracking issues
172
- const atParts = line . trim ( ) . split ( / ^ a t \s + / )
173
- if ( atParts . length > 1 ) {
174
- const funcLocationParts = atParts [ 1 ] . split ( ' (' )
175
- if ( funcLocationParts . length > 1 ) {
176
- const fnName = funcLocationParts [ 0 ]
177
- const location = funcLocationParts [ 1 ] . replace ( ')' , '' )
178
- return forFile
179
- ? ` at ${ fnName } (${ location } )`
180
- : ` ${ COLORS . dim } at ${ COLORS . cyan } ${ fnName } ${ COLORS . dim } (${ location } )${ COLORS . reset } `
181
- }
182
- }
183
- return forFile
184
- ? ` ${ line . trim ( ) } `
185
- : ` ${ COLORS . dim } ${ line . trim ( ) } ${ COLORS . reset } `
186
- }
187
- return ` ${ line } `
188
- } )
189
-
190
- formattedContent = formattedLines . join ( '\n' )
233
+ formattedContent = formatStack ( formattedMessage , forFile )
234
+ }
235
+ else {
236
+ // Apply character formatting for normal messages
237
+ formattedContent = forFile
238
+ ? formattedContent
239
+ : characterFormat ( formattedContent )
191
240
}
192
241
193
242
// For file output, construct message with timestamp at start
@@ -230,8 +279,8 @@ export class PrettyFormatter implements Formatter {
230
279
const lines = logContent . split ( '\n' )
231
280
232
281
// Calculate right padding for timestamp on first line
233
- const firstLineVisibleLength = stripAnsi ( lines [ 0 ] ) . length
234
- const timestampVisibleLength = stripAnsi ( formattedTimestamp ) . length
282
+ const firstLineVisibleLength = stringWidth ( lines [ 0 ] )
283
+ const timestampVisibleLength = stringWidth ( formattedTimestamp )
235
284
236
285
// Calculate padding to push timestamp to the right
237
286
const padding = Math . max ( 0 , this . terminalWidth - firstLineVisibleLength - timestampVisibleLength - 1 )
@@ -243,8 +292,8 @@ export class PrettyFormatter implements Formatter {
243
292
}
244
293
else {
245
294
// Calculate right padding for timestamp
246
- const logContentVisibleLength = stripAnsi ( logContent ) . length
247
- const timestampVisibleLength = stripAnsi ( formattedTimestamp ) . length
295
+ const logContentVisibleLength = stringWidth ( logContent )
296
+ const timestampVisibleLength = stringWidth ( formattedTimestamp )
248
297
249
298
// Calculate padding to push timestamp to the right
250
299
const padding = Math . max ( 0 , this . terminalWidth - logContentVisibleLength - timestampVisibleLength - 1 )
@@ -254,7 +303,7 @@ export class PrettyFormatter implements Formatter {
254
303
}
255
304
}
256
305
257
- // New method to format for file output (without ANSI colors)
306
+ // Format for file output (without ANSI colors)
258
307
async formatForFile ( entry : LogEntry ) : Promise < string > {
259
308
return this . format ( entry , true )
260
309
}
0 commit comments