| ... | @@ -1,8 +1,11 @@ | ... | @@ -1,8 +1,11 @@ |
| 1 | /** | 1 | /** |
| 2 | * by using `lib/log.ts`, an application gets easy scoped logging as well as | 2 | * by using `@clo/lib/log.ts`, an application gets easy scoped logging as well |
| 3 | * integration with terminal widgets such as `lib/progress.ts`. even when these | 3 | * as integration with terminal widgets such as `progress.ts`. even when these |
| 4 | * widgets are active, using the logging interface is optional; global I/O with | 4 | * widgets are active, using the logging interface is optional; global I/O with |
| 5 | * `console.*` and `process.std{out/err}` are automatically patched to play nice. | 5 | * `console.*` and `process.std{out/err}` are automatically patched to play nice |
| | 6 | * so nearly any attempts at writing to the terminal should display fine. |
| | 7 | * additionally, the clover library writes to a global symbol to communicate with |
| | 8 | * other copies of this library (even across versions) to coordinate drawing. |
| 6 | * | 9 | * |
| 7 | * the pattern for using this module is to shadow the global `console` with a | 10 | * the pattern for using this module is to shadow the global `console` with a |
| 8 | * per-file logging scope, which makes it impossible to use the wrong logger. | 11 | * per-file logging scope, which makes it impossible to use the wrong logger. |
| ... | @@ -20,7 +23,7 @@ | ... | @@ -20,7 +23,7 @@ |
| 20 | * import * as console from "@clo/lib/log"; | 23 | * import * as console from "@clo/lib/log"; |
| 21 | * ``` | 24 | * ``` |
| 22 | * | 25 | * |
| 23 | * now, the code reads familiarly (`console.log` is universally understood), | 26 | * now code reads familiarly (`console.log` is universally understood), |
| 24 | * but the output is organized into relevant scopes. | 27 | * but the output is organized into relevant scopes. |
| 25 | * | 28 | * |
| 26 | * in addition to static log messages, a system for interactive I/O via the | 29 | * in addition to static log messages, a system for interactive I/O via the |
| ... | @@ -88,19 +91,31 @@ export interface RootScope extends Scope { | ... | @@ -88,19 +91,31 @@ export interface RootScope extends Scope { |
| 88 | } | 91 | } |
| 89 | | 92 | |
| 90 | export const originalLogArgs = Symbol("originalLogArgs"); | 93 | export const originalLogArgs = Symbol("originalLogArgs"); |
| | 94 | |
| | 95 | /** |
| | 96 | * a message written in a log scope. |
| | 97 | * this API will never be altered in a breaking way. |
| | 98 | */ |
| 91 | export interface Message { | 99 | export interface Message { |
| 92 | level: "error" | "warn" | "info" | "debug"; | 100 | /** ANSI-styled text */ |
| 93 | /** ANSI-styled unicode text */ | | |
| 94 | text: string; | 101 | text: string; |
| 95 | /** datetime in milliseconds since UNIX epoch */ | 102 | /** datetime in milliseconds since UNIX epoch */ |
| 96 | time: number; | 103 | time: number; |
| | 104 | /** |
| | 105 | * type of message, if known. |
| | 106 | * @default info |
| | 107 | */ |
| | 108 | level?: MessageLevel; |
| 97 | /** scope name */ | 109 | /** scope name */ |
| 98 | scope?: string | null; | 110 | scope?: string | null; |
| 99 | /** captured stack. */ | 111 | /** captured stack, if available */ |
| 100 | stack?: stack.Frame[]; | 112 | stack?: stack.Frame[]; |
| 101 | /** arbitrary data from the logging source. */ | 113 | /** arbitrary data from the logging source. */ |
| 102 | custom?: Partial<Record<string, ts.Json>>; | 114 | custom?: Partial<Record<string, ts.Json>>; |
| 103 | /** print a newline at the end of this log line? */ | 115 | /** |
| | 116 | * if there is a newline at the end of this log line. |
| | 117 | * @default true |
| | 118 | */ |
| 104 | newline?: boolean; | 119 | newline?: boolean; |
| 105 | /** | 120 | /** |
| 106 | * original logging arguments, if present. this field is indexed by a symbol | 121 | * original logging arguments, if present. this field is indexed by a symbol |
| ... | @@ -109,6 +124,8 @@ export interface Message { | ... | @@ -109,6 +124,8 @@ export interface Message { |
| 109 | */ | 124 | */ |
| 110 | [originalLogArgs]?: unknown[]; | 125 | [originalLogArgs]?: unknown[]; |
| 111 | } | 126 | } |
| | 127 | /** this API will never be altered in a breaking way. */ |
| | 128 | type MessageLevel = "error" | "warn" | "info" | "debug"; |
| 112 | | 129 | |
| 113 | // these functions implement `Scope` for the module's namespace. that means if | 130 | // these functions implement `Scope` for the module's namespace. that means if |
| 114 | // a function takes in `Scope`, this file's namespace satisfies that. | 131 | // a function takes in `Scope`, this file's namespace satisfies that. |
| ... | @@ -143,29 +160,48 @@ export function scoped(name: string): Scope { | ... | @@ -143,29 +160,48 @@ export function scoped(name: string): Scope { |
| 143 | } | 160 | } |
| 144 | /** redirect all log messages to another writer */ | 161 | /** redirect all log messages to another writer */ |
| 145 | export function tee(destination: DispatchFunction): ts.Dispose { | 162 | export function tee(destination: DispatchFunction): ts.Dispose { |
| 146 | return globalLog.tee(destination); | 163 | global.tees.add(destination); |
| | 164 | return ts.defer(() => global.tees.delete(destination)) |
| 147 | } | 165 | } |
| 148 | | 166 | |
| 149 | /** replace the default message writer */ | 167 | /** replace the default message writer */ |
| 150 | export function replaceGlobalMessageDestination(destination: DispatchFunction) { | 168 | export function replaceGlobalMessageDestination(destination: DispatchFunction) { |
| 151 | globalOutputFunction = destination; | 169 | global.writeMessage = destination; |
| 152 | } | 170 | } |
| 153 | | 171 | |
| 154 | /** replace the default interactive widget host */ | 172 | /** |
| 155 | export function replaceGlobalWidgetHost(widgetHost: WidgetHost) { | 173 | * replace the default interactive widget host. this applies for all separately |
| 156 | globalWidgetHost = widgetHost; | 174 | * installed copies of the library, across versions. once the widget host has |
| | 175 | * been activated, it must be preserved forever. |
| | 176 | */ |
| | 177 | export function replaceGlobalWidgetHost(host: WidgetHost) { |
| | 178 | if (global.widget?.frozen) { |
| | 179 | throw new Error( |
| | 180 | `Cannot change widget host implementation, it is locked by another implementation: ${global.widget.source}`, |
| | 181 | ); |
| | 182 | } |
| | 183 | global.widget = { |
| | 184 | frozen: false, |
| | 185 | source: stack.capture()[0]?.file ?? |
| | 186 | ("untracable call to replaceGlobalWidgetHost in " + import.meta.url), |
| | 187 | host, |
| | 188 | version: 0, |
| | 189 | }; |
| 157 | } | 190 | } |
| 158 | | 191 | |
| 159 | /** replace the default message formatter */ | 192 | /** |
| 160 | export function replaceGlobalFormatFunction( | 193 | * replace the default message formatter. does not affect browsers because that |
| | 194 | * code path does not use `formatAnsiMessage` |
| | 195 | */ |
| | 196 | export function replaceGlobalFormatAnsiMessage( |
| 161 | format: (msg: Message, colors: boolean) => string, | 197 | format: (msg: Message, colors: boolean) => string, |
| 162 | ) { | 198 | ) { |
| 163 | globalMessageFormatFunction = format; | 199 | global.formatAnsiMessage = format; |
| 164 | } | 200 | } |
| 165 | | 201 | |
| 166 | /** includes the trailing newline for standard log messages */ | 202 | /** includes the trailing newline for standard log messages */ |
| 167 | export function formatMessage(msg: Message, colors: boolean): string { | 203 | export function formatAnsiMessage(msg: Message, colors: boolean): string { |
| 168 | return globalMessageFormatFunction(msg, colors); | 204 | return (global.formatAnsiMessage ?? defaultFormatAnsiMessage)(msg, colors); |
| 169 | } | 205 | } |
| 170 | | 206 | |
| 171 | /** | 207 | /** |
| ... | @@ -173,6 +209,9 @@ export function formatMessage(msg: Message, colors: boolean): string { | ... | @@ -173,6 +209,9 @@ export function formatMessage(msg: Message, colors: boolean): string { |
| 173 | * of the log. this can be used to implement status bars, progress | 209 | * of the log. this can be used to implement status bars, progress |
| 174 | * indicators, and other human I/O. only 'format' is required. | 210 | * indicators, and other human I/O. only 'format' is required. |
| 175 | * | 211 | * |
| | 212 | * returns `null` if the terminal is not interactive or the global renderer is |
| | 213 | * incapable of displaying this log (such as in a browser) |
| | 214 | * |
| 176 | * ```ts | 215 | * ```ts |
| 177 | * using _ = log.startWidget({ | 216 | * using _ = log.startWidget({ |
| 178 | * format: (now) => `It is ${new Date().toString()} right now\n` | 217 | * format: (now) => `It is ${new Date().toString()} right now\n` |
| ... | @@ -185,18 +224,29 @@ export function formatMessage(msg: Message, colors: boolean): string { | ... | @@ -185,18 +224,29 @@ export function formatMessage(msg: Message, colors: boolean): string { |
| 185 | * import * as async from "@clo/lib/async.ts"; | 224 | * import * as async from "@clo/lib/async.ts"; |
| 186 | * ``` | 225 | * ``` |
| 187 | */ | 226 | */ |
| 188 | export function startWidget(widget: Widget): ts.Dispose { | 227 | export function startWidget<T extends WidgetOptions>( |
| 189 | return globalWidgetHost.startWidget(widget); | 228 | widget: T, |
| | 229 | ): WidgetInstance<T> | null { |
| | 230 | return globalWidgetHost().startWidget(widget); |
| 190 | } | 231 | } |
| 191 | | 232 | |
| 192 | /** | 233 | /** |
| 193 | * no built-in prefix or formatting. ensures the text does not interweave. data | 234 | * no built-in prefix, formatting, or newlline. ensures the text does not interweave. |
| 194 | * will be flushed in the next frame or when drawing is {@link getDrawLock|unlocked}. | 235 | * data will be flushed in the next frame or when drawing is {@link getDrawLock|unlocked}. |
| 195 | */ | 236 | */ |
| 196 | export function write(text: string) { | 237 | export function writeOutput(text: string) { |
| 197 | globalWidgetHost.write(text); | 238 | globalWidgetHost().writeOutput(text); |
| 198 | } | 239 | } |
| 199 | | 240 | |
| | 241 | // TODO: |
| | 242 | // /** |
| | 243 | // * no built-in prefix, formatting, or newlline. ensures the text does not interweave. |
| | 244 | // * data will be flushed in the next frame or when drawing is {@link getDrawLock|unlocked}. |
| | 245 | // */ |
| | 246 | // export function writeError(text: string) { |
| | 247 | // globalWidgetHost().writeError(text); |
| | 248 | // } |
| | 249 | |
| 200 | /** write a {@linkcode Message} object directly. */ | 250 | /** write a {@linkcode Message} object directly. */ |
| 201 | export function writeMessage(m: Message) { | 251 | export function writeMessage(m: Message) { |
| 202 | globalLog.writeMessage(m); | 252 | globalLog.writeMessage(m); |
| ... | @@ -211,7 +261,7 @@ export function writeMessage(m: Message) { | ... | @@ -211,7 +261,7 @@ export function writeMessage(m: Message) { |
| 211 | * `"short"` which will allow more optimized use of ansi synchronization codes. | 261 | * `"short"` which will allow more optimized use of ansi synchronization codes. |
| 212 | */ | 262 | */ |
| 213 | export function getDrawLock(mode: "long" | "short"): DrawLock { | 263 | export function getDrawLock(mode: "long" | "short"): DrawLock { |
| 214 | return globalWidgetHost.getDrawLock(mode); | 264 | return globalWidgetHost().getDrawLock(mode); |
| 215 | } | 265 | } |
| 216 | | 266 | |
| 217 | /** | 267 | /** |
| ... | @@ -227,17 +277,25 @@ export function getDrawLock(mode: "long" | "short"): DrawLock { | ... | @@ -227,17 +277,25 @@ export function getDrawLock(mode: "long" | "short"): DrawLock { |
| 227 | * you most likely do not need to call this API. | 277 | * you most likely do not need to call this API. |
| 228 | */ | 278 | */ |
| 229 | export function ensureGlobalsArePatched(): ts.Dispose { | 279 | export function ensureGlobalsArePatched(): ts.Dispose { |
| 230 | return globalWidgetHost.startWidget({ format: () => "" }); | 280 | if (global.widget?.frozen && global.widget?.version !== version) { |
| | 281 | throw new Error( |
| | 282 | `Cannot change widget host implementation, it is locked by another implementation: ${global.widget.source}`, |
| | 283 | ); |
| | 284 | } |
| | 285 | const w = globalWidgetHost().startWidget?.({ format: () => "" }); |
| | 286 | return ts.defer(() => w?.stop()); |
| 231 | } | 287 | } |
| 232 | | 288 | |
| | 289 | /** |
| | 290 | * a non-exclusive lock to drawing |
| | 291 | * this API will never be altered in a breaking way. |
| | 292 | */ |
| 233 | export interface DrawLock { | 293 | export interface DrawLock { |
| 234 | /** | 294 | /** |
| 235 | * decides if widget drawing requires an extra newline, which is needed if | 295 | * decides if widget drawing requires an extra newline, which is needed if |
| 236 | * there is extra text on the line that the lock is being released on. | 296 | * there is extra text on the line that the lock is being released on. |
| 237 | */ | 297 | */ |
| 238 | release( | 298 | release(endState: "cursor-start-of-line" | "cursor-middle-of-line"): void; |
| 239 | cursorPosition: "cursor-start-of-line" | "cursor-middle-of-line", | | |
| 240 | ): void; | | |
| 241 | /** Assumes worst case `cursor-middle-of-line` */ | 299 | /** Assumes worst case `cursor-middle-of-line` */ |
| 242 | [Symbol.dispose](): void; | 300 | [Symbol.dispose](): void; |
| 243 | } | 301 | } |
| ... | @@ -246,29 +304,53 @@ export function headlessScope(dispatch: DispatchFunction): RootScope { | ... | @@ -246,29 +304,53 @@ export function headlessScope(dispatch: DispatchFunction): RootScope { |
| 246 | return new ScopeImpl(dispatch); | 304 | return new ScopeImpl(dispatch); |
| 247 | } | 305 | } |
| 248 | | 306 | |
| 249 | /** see {@linkcode startWidget} */ | 307 | /** |
| 250 | export interface Widget { | 308 | * see {@linkcode startWidget} |
| | 309 | * this API will never be altered in a breaking way. |
| | 310 | */ |
| | 311 | export interface WidgetOptions { |
| 251 | /** | 312 | /** |
| 252 | * return the widget's text. return null to detach the widget. | 313 | * return the widget's text. return null to detach the widget. |
| 253 | * may get called more often than the specified `fps`. | 314 | * may get called more often than the specified `fps`. |
| 254 | * supports color codes but not ansi cursor movements. | 315 | * supports color codes but not ansi cursor movements. |
| 255 | */ | 316 | */ |
| 256 | format( | 317 | format(ctx: WidgetFormatContext): string | { text: string } | null; |
| 257 | now: ReturnType<typeof performance.now>, | 318 | /** 'null' to never update automatically */ |
| 258 | ): | 319 | fps?: number | null; |
| 259 | | string | 320 | } |
| 260 | | null; | 321 | |
| 261 | /** 'null' to never update (use 'onChange') */ | 322 | /** |
| 262 | fps?: | 323 | * control for a widget (see {@linkcode startWidget}) |
| 263 | | number | 324 | * this API will never be altered in a breaking way. |
| 264 | | null; /** Subscribe to manual widget updates. Call `rerender` when needed. */ | 325 | */ |
| 265 | onChange?(rerender: () => void): () => void; | 326 | export interface WidgetInstance<T extends WidgetOptions = WidgetOptions> { |
| 266 | /** listen for keyboard events. */ | 327 | /** the provided widget options */ |
| 267 | onKey?(key: string): void; | 328 | options: T; |
| | 329 | get fps(): number | null; |
| | 330 | set fps(fps: number | null); |
| | 331 | /** schedules a new frame to be drawn as soon as possible */ |
| | 332 | redraw(): void; |
| | 333 | /** remove the widget from the screen */ |
| | 334 | stop(): void; |
| | 335 | /** alias of `stop` */ |
| | 336 | [Symbol.dispose](): void; |
| | 337 | } |
| | 338 | |
| | 339 | /** |
| | 340 | * this API will never be altered in a breaking way. |
| | 341 | */ |
| | 342 | export interface WidgetFormatContext { |
| | 343 | /** the current time according to the widget host */ |
| | 344 | now: ReturnType<typeof performance.now>; |
| | 345 | /** advisory */ |
| | 346 | width: number; |
| | 347 | /** advisory */ |
| | 348 | height: number; |
| | 349 | host?: WidgetHost; |
| 268 | } | 350 | } |
| 269 | | 351 | |
| 270 | /** {@linkcode widgetHost}'s input takes terminal I/O as well as timing APIs */ | 352 | /** {@linkcode widgetHost}'s input takes terminal I/O as well as timing APIs */ |
| 271 | export interface WidgetHostOptions { | 353 | export interface TerminalWidgetHostOptions { |
| 272 | /** | 354 | /** |
| 273 | * an exclusive lock on the terminal is held whenever widgets are active. a | 355 | * an exclusive lock on the terminal is held whenever widgets are active. a |
| 274 | * secondary purpose of this is to instrument/deinstrument other code to | 356 | * secondary purpose of this is to instrument/deinstrument other code to |
| ... | @@ -290,10 +372,12 @@ export interface WidgetHostOptions { | ... | @@ -290,10 +372,12 @@ export interface WidgetHostOptions { |
| 290 | now: () => ReturnType<typeof performance.now>; | 372 | now: () => ReturnType<typeof performance.now>; |
| 291 | /** after resolving, `now()` should have increased by the delay time */ | 373 | /** after resolving, `now()` should have increased by the delay time */ |
| 292 | delay: typeof async.delay; | 374 | delay: typeof async.delay; |
| | 375 | /** is there color support? */ |
| | 376 | color: boolean; |
| 293 | } | 377 | } |
| 294 | | 378 | |
| 295 | /** | 379 | /** |
| 296 | * When `@clo/lib` requests a lock on the terminal, the adapter provides this | 380 | * when `@clo/lib` requests a lock on the terminal, the adapter provides this |
| 297 | * interface to communicate everything about the terminal state correctly. | 381 | * interface to communicate everything about the terminal state correctly. |
| 298 | */ | 382 | */ |
| 299 | export interface TerminalLock { | 383 | export interface TerminalLock { |
| ... | @@ -310,34 +394,46 @@ export interface TerminalLock { | ... | @@ -310,34 +394,46 @@ export interface TerminalLock { |
| 310 | close(): void; | 394 | close(): void; |
| 311 | } | 395 | } |
| 312 | | 396 | |
| 313 | /** an implementation of an ANSI-based widget host */ | 397 | /** |
| | 398 | * an implementation of an ANSI-based widget host. |
| | 399 | * this structure must not be altered in a breaking way. |
| | 400 | */ |
| 314 | export interface WidgetHost { | 401 | export interface WidgetHost { |
| 315 | /** see the top-level {@linkcode writeLine} function */ | 402 | /** see the top-level {@linkcode writeOutput} function */ |
| 316 | write(text: string): void; | 403 | writeOutput(text: string): void; |
| | 404 | /** see the top-level {@linkcode writeError} function */ |
| | 405 | writeError(text: string): void; |
| 317 | /** see the top-level {@linkcode getDrawLock} function */ | 406 | /** see the top-level {@linkcode getDrawLock} function */ |
| 318 | getDrawLock(mode: "long" | "short"): DrawLock; | 407 | getDrawLock(mode: "long" | "short"): DrawLock; |
| 319 | /** see the top-level {@linkcode startWidget} function */ | 408 | /** see the top-level {@linkcode startWidget} function */ |
| 320 | startWidget(widget: Widget): ts.Dispose; | 409 | startWidget<T extends WidgetOptions>(widget: T): WidgetInstance<T> | null; |
| 321 | /** stop all widgets and remove all timers. */ | 410 | /** stop all widgets and remove all timers. */ |
| 322 | cancel(): void; | 411 | cancel(): void; |
| 323 | /** generic delay function */ | 412 | /** generic delay function */ |
| 324 | delay?: typeof async.delay; | 413 | delay?: typeof async.delay; |
| 325 | /** generic now function */ | 414 | /** generic now function */ |
| 326 | now?: typeof performance.now; | 415 | now?: typeof performance.now; |
| | 416 | /** |
| | 417 | * what does this widget host support? |
| | 418 | * - color: ANSI escape sequences are displayed and not stripped |
| | 419 | * - widget: `startWidget` can meaninglyful display the widget |
| | 420 | */ |
| | 421 | capabilities: ReadonlyArray<"widget" | "color">; |
| 327 | } | 422 | } |
| 328 | | 423 | |
| 329 | /** @internal state */ | 424 | /** @internal state */ |
| 330 | interface WidgetState { | 425 | interface WidgetState { |
| 331 | frameTime: number; | 426 | frameTime: number; |
| 332 | next: number; | 427 | next: number; |
| 333 | unsub: (() => void) | null; | | |
| 334 | } | 428 | } |
| 335 | | 429 | |
| 336 | /** | 430 | /** |
| 337 | * terminal widget rendering is done by specifying all system APIs up front in | 431 | * terminal widget rendering is done by specifying all system APIs up front in |
| 338 | * an interface, creating an instance of the "widget host". | 432 | * an interface, creating an instance of the "widget host". |
| 339 | */ | 433 | */ |
| 340 | export function createWidgetHost(env: WidgetHostOptions): WidgetHost { | 434 | export function createTerminalWidgetHost( |
| | 435 | env: TerminalWidgetHostOptions, |
| | 436 | ): WidgetHost { |
| 341 | const { lockTerminal, now, delay, writeOutputTemporaryLock } = env; | 437 | const { lockTerminal, now, delay, writeOutputTemporaryLock } = env; |
| 342 | | 438 | |
| 343 | let timer: async.Cancelable<void> | null = null; | 439 | let timer: async.Cancelable<void> | null = null; |
| ... | @@ -349,7 +445,7 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { | ... | @@ -349,7 +445,7 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { |
| 349 | let partialLineIndex = 0; | 445 | let partialLineIndex = 0; |
| 350 | let needsToSaveCursor = false; | 446 | let needsToSaveCursor = false; |
| 351 | let needsToRestoreCursor = false; | 447 | let needsToRestoreCursor = false; |
| 352 | const widgets: Widget[] = []; | 448 | const widgets: WidgetOptions[] = []; |
| 353 | const internals: WidgetState[] = []; | 449 | const internals: WidgetState[] = []; |
| 354 | let lines: string[] = []; | 450 | let lines: string[] = []; |
| 355 | let hasSyncStart = false; | 451 | let hasSyncStart = false; |
| ... | @@ -392,14 +488,19 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { | ... | @@ -392,14 +488,19 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { |
| 392 | let newWidgetLines: string[] = []; | 488 | let newWidgetLines: string[] = []; |
| 393 | let next = Infinity; | 489 | let next = Infinity; |
| 394 | for (let w = 0, { length } = widgets; w < length; w += 1) { | 490 | for (let w = 0, { length } = widgets; w < length; w += 1) { |
| 395 | const outText = UNWRAP(widgets[w]).format(lastFlush); | 491 | const out = UNWRAP(widgets[w]).format({ |
| 396 | if (!outText) { | 492 | now: lastFlush, |
| | 493 | width: columns, |
| | 494 | height: rows, |
| | 495 | }); |
| | 496 | if (!out) { |
| 397 | widgets.splice(w, 1); | 497 | widgets.splice(w, 1); |
| 398 | UNWRAP(internals.splice(w, 1)[0]).unsub?.(); | 498 | UNWRAP(internals.splice(w, 1)[0]); |
| 399 | w -= 1; | 499 | w -= 1; |
| 400 | length -= 1; | 500 | length -= 1; |
| 401 | continue; | 501 | continue; |
| 402 | } | 502 | } |
| | 503 | const outText = typeof out === "string" ? out : out.text; |
| 403 | const rowsLeft = Math.max(1, rows - newWidgetLines.length - 1); | 504 | const rowsLeft = Math.max(1, rows - newWidgetLines.length - 1); |
| 404 | if (rowsLeft === 1) break; | 505 | if (rowsLeft === 1) break; |
| 405 | const lines = outText.split("\n").slice(0, rowsLeft); | 506 | const lines = outText.split("\n").slice(0, rowsLeft); |
| ... | @@ -581,7 +682,11 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { | ... | @@ -581,7 +682,11 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { |
| 581 | } | 682 | } |
| 582 | | 683 | |
| 583 | return { | 684 | return { |
| 584 | write(chunk) { | 685 | writeOutput(chunk) { |
| | 686 | if (chunk) buffer += chunk, redrawSoon(0); |
| | 687 | }, |
| | 688 | writeError(chunk) { |
| | 689 | // TODO: write to stderr. when this was introduced it was not a regression from v3 |
| 585 | if (chunk) buffer += chunk, redrawSoon(0); | 690 | if (chunk) buffer += chunk, redrawSoon(0); |
| 586 | }, | 691 | }, |
| 587 | getDrawLock(mode) { | 692 | getDrawLock(mode) { |
| ... | @@ -615,27 +720,40 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { | ... | @@ -615,27 +720,40 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { |
| 615 | }, | 720 | }, |
| 616 | }; | 721 | }; |
| 617 | }, | 722 | }, |
| 618 | startWidget(w) { | 723 | startWidget(options) { |
| 619 | ASSERT(!widgets.includes(w), "Cannot start the same widget twice."); | 724 | ASSERT(!widgets.includes(options), "Cannot start the same widget twice."); |
| | 725 | let fps = options.fps ?? null; |
| 620 | const state: WidgetState = { | 726 | const state: WidgetState = { |
| 621 | next: 0, | 727 | next: 0, |
| 622 | unsub: null, | 728 | frameTime: 1000 / (fps ?? 0), |
| 623 | frameTime: 1000 / (w.fps ?? 0), | | |
| 624 | }; | 729 | }; |
| 625 | widgets.push(w); | 730 | widgets.push(options); |
| 626 | internals.push(state); | 731 | internals.push(state); |
| 627 | state.unsub = w.onChange?.(() => { | | |
| 628 | state.next = 0; | | |
| 629 | redrawSoon(0); | | |
| 630 | }) ?? null; | | |
| 631 | redrawSoon(0); | 732 | redrawSoon(0); |
| 632 | return ts.defer(() => { | 733 | return { |
| 633 | const i = widgets.indexOf(w); | 734 | options, |
| 634 | if (i === -1) return; | 735 | get fps() { |
| 635 | widgets.splice(i, 1); | 736 | return fps; |
| 636 | UNWRAP(internals.splice(i, 1)[0]).unsub?.(); | 737 | }, |
| 637 | redrawSoon(0); | 738 | set fps(value) { |
| 638 | }); | 739 | fps = value; |
| | 740 | state.frameTime = 1000 / (fps ?? 0); |
| | 741 | }, |
| | 742 | redraw() { |
| | 743 | state.next = 0; |
| | 744 | redrawSoon(0); |
| | 745 | }, |
| | 746 | stop() { |
| | 747 | const i = widgets.indexOf(options); |
| | 748 | if (i === -1) return; |
| | 749 | widgets.splice(i, 1); |
| | 750 | UNWRAP(internals.splice(i, 1)[0]); |
| | 751 | redrawSoon(0); |
| | 752 | }, |
| | 753 | [Symbol.dispose]() { |
| | 754 | this.stop(); |
| | 755 | }, |
| | 756 | }; |
| 639 | }, | 757 | }, |
| 640 | cancel() { | 758 | cancel() { |
| 641 | flushAndClear(false); | 759 | flushAndClear(false); |
| ... | @@ -643,6 +761,7 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { | ... | @@ -643,6 +761,7 @@ export function createWidgetHost(env: WidgetHostOptions): WidgetHost { |
| 643 | }, | 761 | }, |
| 644 | delay, | 762 | delay, |
| 645 | now, | 763 | now, |
| | 764 | capabilities: env.color ? ["widget", "color"] : ["widget"], |
| 646 | }; | 765 | }; |
| 647 | } | 766 | } |
| 648 | | 767 | |
| ... | @@ -732,7 +851,7 @@ const ScopeImpl = class Scope implements RootScope { | ... | @@ -732,7 +851,7 @@ const ScopeImpl = class Scope implements RootScope { |
| 732 | }; | 851 | }; |
| 733 | | 852 | |
| 734 | writeMessage: (m: Message) => void = (m) => { | 853 | writeMessage: (m: Message) => void = (m) => { |
| 735 | if (withinDispatch) return void globalOutputFunction(m); | 854 | if (withinDispatch) return void globalLog.#dispatch(m); |
| 736 | withinDispatch = true; | 855 | withinDispatch = true; |
| 737 | this.#dispatch(m); | 856 | this.#dispatch(m); |
| 738 | withinDispatch = false; | 857 | withinDispatch = false; |
| ... | @@ -771,26 +890,127 @@ const ScopeImpl = class Scope implements RootScope { | ... | @@ -771,26 +890,127 @@ const ScopeImpl = class Scope implements RootScope { |
| 771 | } | 890 | } |
| 772 | }; | 891 | }; |
| 773 | | 892 | |
| 774 | let globalWidgetHost = node.process | 893 | /** |
| 775 | ? /* @__PURE__*/ ((process: NonNullable<typeof node.process>) => { | 894 | * global integration is done in a special manner with IIFE expressions to |
| 776 | const widget = createWidgetHost({ | 895 | * ensure that the logic is removed when a bundler tree-shakes this file. |
| 777 | lockTerminal() { | 896 | * additionally, this ensures that replacing a global function properly affects |
| 778 | const { stdout, stderr } = process; | 897 | * other installations. this is safe to do because these shared interfaces have |
| 779 | let disposed = false; | 898 | * a commitment to never change in a breaking way. |
| 780 | | 899 | */ |
| 781 | // patch calls to `process.std{out,err}` | 900 | interface GlobalCommunication { |
| 782 | // note: `pipe` uses managed calls to `write`, so this is plenty | 901 | readme: string; |
| 783 | const stdoutWrite = stdout.write; | 902 | /** urls / file paths */ |
| 784 | const stderrWrite = stderr.write; | 903 | instances: string[]; |
| 785 | const stdoutEnd = stdout.end; | 904 | widget?: { |
| 786 | const stderrEnd = stderr.end; | 905 | version: number; |
| 787 | const newStdoutWrite = stdout.write = widget.write; | 906 | frozen: boolean; |
| 788 | const newStderrWrite = stderr.write = function ( | 907 | host: WidgetHost; |
| 789 | this: typeof stderr, | 908 | /** url / file path / line number / identifying information */ |
| 790 | ...args | 909 | source: string; |
| 791 | ) { | 910 | }; |
| 792 | using lock = disposed ? null : widget.getDrawLock("short"); | 911 | /** overwrite the message writer */ |
| 793 | stderrWrite.apply(stderr, args); | 912 | writeMessage?: DispatchFunction; |
| | 913 | /** overwrite the message formatter */ |
| | 914 | formatAnsiMessage?: (m: Message, colors: boolean) => string; |
| | 915 | /** calls to the global `tee()` */ |
| | 916 | tees: Set<DispatchFunction>; |
| | 917 | } |
| | 918 | |
| | 919 | const globalSymbol = /* @__PURE__ */ Symbol.for("@clo/lib/log"); |
| | 920 | const version = 4; |
| | 921 | let global: GlobalCommunication = /* @__PURE__ */ (() => { |
| | 922 | const global = |
| | 923 | (globalThis as { [globalSymbol]?: GlobalCommunication })[globalSymbol] ??= { |
| | 924 | readme: "this global holds shared state for \"@clo/lib/log\", to allow different instances of itself to coordinate with each other", |
| | 925 | instances: [], |
| | 926 | tees: new Set, |
| | 927 | }; |
| | 928 | global.instances.push(import.meta.url); |
| | 929 | return global; |
| | 930 | })(); |
| | 931 | |
| | 932 | function defaultFormatAnsiMessage( |
| | 933 | { level, scope, text, newline }: Message, |
| | 934 | colors: boolean, |
| | 935 | ) { |
| | 936 | if (!text) return ""; |
| | 937 | if (newline === false) return text; |
| | 938 | const prefix = colors |
| | 939 | // colorful |
| | 940 | ? `${levelToAnsi[level ?? "info"]}${ |
| | 941 | scope ? `(${scope})` : "" |
| | 942 | }${ansi.fgReset}${ansi.dim}:${ansi.reset} ` |
| | 943 | // colorless |
| | 944 | : scope |
| | 945 | ? `${level}(${scope}): ` |
| | 946 | : `${level}: `; |
| | 947 | return prefix + text + "\n"; |
| | 948 | } |
| | 949 | |
| | 950 | let initWidgetHost = false; |
| | 951 | function globalWidgetHost(): WidgetHost { |
| | 952 | if ( |
| | 953 | global.widget && ( |
| | 954 | initWidgetHost || |
| | 955 | global.widget.frozen || |
| | 956 | // By default, pick the latest version of the widget host implementation. |
| | 957 | // This is most likely to resolve the most issues as possible. To opt out of |
| | 958 | // this, import the desired implementation and call its |
| | 959 | // `ensureGlobalsArePatched` or `replaceGlobalWidgetHost` method. |
| | 960 | global.widget.version >= version |
| | 961 | ) |
| | 962 | ) { |
| | 963 | initWidgetHost = true; |
| | 964 | return global.widget.host; |
| | 965 | } |
| | 966 | initWidgetHost = true; |
| | 967 | global.widget = { |
| | 968 | frozen: false, |
| | 969 | host: node.process |
| | 970 | ? defaultNodeProcessWidgetHost(node.process) |
| | 971 | : defaultFallbackWidgetHost(), |
| | 972 | source: import.meta.url, |
| | 973 | version, |
| | 974 | }; |
| | 975 | return global.widget.host; |
| | 976 | } |
| | 977 | |
| | 978 | export function defaultNodeProcessWidgetHost( |
| | 979 | process: NonNullable<typeof node.process>, |
| | 980 | forceWidgetSupport = false, |
| | 981 | ): WidgetHost { |
| | 982 | // fallback |
| | 983 | if (!forceWidgetSupport && !process.stderr.isTTY) { |
| | 984 | return { |
| | 985 | writeOutput: (string) => process.stdout.write(string), |
| | 986 | writeError: (string) => process.stderr.write(string), |
| | 987 | getDrawLock: () => ({ |
| | 988 | release() {}, |
| | 989 | [Symbol.dispose]() {}, |
| | 990 | }), |
| | 991 | startWidget: () => null, |
| | 992 | cancel: () => {}, |
| | 993 | capabilities: [], |
| | 994 | }; |
| | 995 | } |
| | 996 | |
| | 997 | const host = createTerminalWidgetHost({ |
| | 998 | lockTerminal() { |
| | 999 | const { stdout, stderr } = process; |
| | 1000 | let disposed = false; |
| | 1001 | |
| | 1002 | // patch calls to `process.std{out,err}` |
| | 1003 | // note: `pipe` uses managed calls to `write`, so this is plenty |
| | 1004 | const stdoutWrite = stdout.write; |
| | 1005 | const stderrWrite = stderr.write; |
| | 1006 | const stdoutEnd = stdout.end; |
| | 1007 | const stderrEnd = stderr.end; |
| | 1008 | function patchWriteMethod<T, R, A extends [string | Uint8Array]>( |
| | 1009 | fn: (this: T, ...args: A) => R, |
| | 1010 | ) { |
| | 1011 | return function (this: T, ...args: A) { |
| | 1012 | using lock = disposed ? null : host.getDrawLock("short"); |
| | 1013 | const ret = fn.apply(this, args); |
| 794 | if (lock) { | 1014 | if (lock) { |
| 795 | lock.release( | 1015 | lock.release( |
| 796 | (typeof args[0] === "string" | 1016 | (typeof args[0] === "string" |
| ... | @@ -800,112 +1020,137 @@ let globalWidgetHost = node.process | ... | @@ -800,112 +1020,137 @@ let globalWidgetHost = node.process |
| 800 | : "cursor-middle-of-line", | 1020 | : "cursor-middle-of-line", |
| 801 | ); | 1021 | ); |
| 802 | } | 1022 | } |
| | 1023 | return ret; |
| 803 | }; | 1024 | }; |
| 804 | function patchEndMethod<T, A extends unknown[]>( | 1025 | } |
| 805 | fn: (this: T, ...args: A) => void, | 1026 | const newStdoutWrite = stdout.write = patchWriteMethod(stdoutWrite); |
| 806 | ) { | 1027 | const newStderrWrite = stderr.write = patchWriteMethod(stderrWrite); |
| 807 | return function (this: T, ...args: A) { | 1028 | function patchEndMethod<T, R, A extends unknown[]>( |
| 808 | using lock = disposed ? null : widget.getDrawLock("short"); | 1029 | fn: (this: T, ...args: A) => R, |
| 809 | fn.apply(this, args); | 1030 | ) { |
| 810 | // TODO: this is not handled correctly, but nobody closes | 1031 | return function (this: T, ...args: A) { |
| 811 | // their fucking standard error! this likely should just | 1032 | using lock = disposed ? null : host.getDrawLock("short"); |
| 812 | // disable the library if you call end. | 1033 | const ret = fn.apply(this, args); |
| 813 | if (lock) lock.release("cursor-start-of-line"); | 1034 | // TODO: this is not handled correctly, but people rarely close their |
| 814 | }; | 1035 | // standard I/O! this likely should just disable the library if you |
| 815 | } | 1036 | // call end. i'm not particularly worried. |
| 816 | const newStdoutEnd = stdout.end = patchEndMethod(stdoutEnd); | 1037 | if (lock) lock.release("cursor-start-of-line"); |
| 817 | const newStderrEnd = stderr.end = patchEndMethod(stderrEnd); | 1038 | return ret; |
| 818 | | | |
| 819 | // non-node runtimes will typically implement console in a way that | | |
| 820 | // doesn't use `node:process`, so it must also get patched. this is | | |
| 821 | // okay because the lock is re-enterant. | | |
| 822 | function patchSyncMethod<T, A extends unknown[]>( | | |
| 823 | fn: (this: T, ...args: A) => void, | | |
| 824 | ) { | | |
| 825 | return function (this: T, ...args: A) { | | |
| 826 | using lock = disposed ? null : widget.getDrawLock("short"); | | |
| 827 | fn.apply(this, args); | | |
| 828 | if (lock) lock.release("cursor-start-of-line"); | | |
| 829 | }; | | |
| 830 | } | | |
| 831 | const console = globalThis | | |
| 832 | .console as unknown as Record<string, () => void>; | | |
| 833 | const restoreConsole: [string, old: () => void, patch: () => void][] = | | |
| 834 | []; | | |
| 835 | for (const [key, old] of Object.entries(console)) { | | |
| 836 | if (typeof old !== "function") continue; | | |
| 837 | try { | | |
| 838 | const patched = console[key] = patchSyncMethod(old); | | |
| 839 | restoreConsole.push([key, old, patched]); | | |
| 840 | } catch { /* skip */ } | | |
| 841 | } | | |
| 842 | | | |
| 843 | return { | | |
| 844 | writeOutput: (string) => stdoutWrite.call(stderr, string), | | |
| 845 | writeInteractive: (string) => stderrWrite.call(stderr, string), | | |
| 846 | getSize: () => process.stderr, | | |
| 847 | temporarilyUnlock() { | | |
| 848 | // no action needed | | |
| 849 | }, | | |
| 850 | close() { | | |
| 851 | disposed = true; | | |
| 852 | // leave patches in place if something else tampered with it. | | |
| 853 | if (stdout.write === newStdoutWrite) stdout.write = stdoutWrite; | | |
| 854 | if (stderr.write === newStderrWrite) stdout.write = stderrWrite; | | |
| 855 | if (stdout.end === newStdoutEnd) stdout.end = stdoutEnd; | | |
| 856 | if (stderr.end === newStderrEnd) stdout.end = stderrEnd; | | |
| 857 | for (const [key, old, patched] of restoreConsole) { | | |
| 858 | if (console[key] === patched) console[key] = old; | | |
| 859 | } | | |
| 860 | }, | | |
| 861 | }; | 1039 | }; |
| 862 | }, | 1040 | } |
| 863 | now: () => performance.now(), | 1041 | const newStdoutEnd = stdout.end = patchEndMethod(stdoutEnd); |
| 864 | delay: async.delay, | 1042 | const newStderrEnd = stderr.end = patchEndMethod(stderrEnd); |
| 865 | }); | | |
| 866 | process.addListener("beforeExit", () => widget.cancel()); | | |
| 867 | process.addListener("exit", () => widget.cancel()); | | |
| 868 | | 1043 | |
| 869 | return widget; | 1044 | // non-node runtimes will typically implement console in a way that |
| 870 | })(node.process) | 1045 | // doesn't use `node:process`, so it must also get patched. this is |
| 871 | : /* @__PURE__ */ ((warned = false) => { | 1046 | // okay because the lock is re-enterant. |
| 872 | return { | 1047 | function patchSyncMethod<T, A extends unknown[]>( |
| 873 | write: (line: string) => console.log(line), | 1048 | fn: (this: T, ...args: A) => void, |
| 874 | getDrawLock: () => ({ [Symbol.dispose]() {}, release() {} }), | 1049 | ) { |
| 875 | startWidget: (w: Widget) => { | 1050 | return function (this: T, ...args: A) { |
| 876 | if (!warned) { | 1051 | using lock = disposed ? null : host.getDrawLock("short"); |
| 877 | console.warn( | 1052 | fn.apply(this, args); |
| 878 | '"@clo/lib/log.ts"\'s startWidget was called in an environment ' + | 1053 | if (lock) lock.release("cursor-start-of-line"); |
| 879 | "that does not support the Node.js 'process' API. Widgets " + | 1054 | }; |
| 880 | "will not be visible.", | 1055 | } |
| 881 | ); | 1056 | const console = globalThis |
| 882 | warned = true; | 1057 | .console as Console & Record<string, () => void>; |
| 883 | } | 1058 | const restoreConsole: [string, old: () => void, patch: () => void][] = []; |
| 884 | const close = w.onChange?.(() => {}); | 1059 | for (const [key, old] of Object.entries(console)) { |
| 885 | return ts.defer(close ?? (() => {})); | 1060 | if (typeof old !== "function") continue; |
| 886 | }, | 1061 | try { |
| 887 | cancel: () => {}, | 1062 | const patched = console[key] = patchSyncMethod(old); |
| 888 | }; | 1063 | restoreConsole.push([key, old, patched]); |
| 889 | })(); | 1064 | } catch { /* skip */ } |
| | 1065 | } |
| 890 | | 1066 | |
| 891 | export function simpleNodeProcessWidgetHost(process: node.Process) { | | |
| 892 | return createWidgetHost({ | | |
| 893 | lockTerminal() { | | |
| 894 | return { | 1067 | return { |
| 895 | writeOutput: (string) => process.stdout.write(string), | 1068 | writeOutput: (string) => stdoutWrite.call(stderr, string), |
| 896 | writeInteractive: (string) => process.stderr.write(string), | 1069 | writeInteractive: (string) => stderrWrite.call(stderr, string), |
| 897 | getSize: () => process.stderr, | 1070 | getSize: () => process.stderr, |
| 898 | temporarilyUnlock() { | 1071 | temporarilyUnlock() { |
| 899 | // no action needed | 1072 | // no action needed |
| 900 | }, | 1073 | }, |
| 901 | close() { | 1074 | close() { |
| 902 | // no action needed | 1075 | disposed = true; |
| | 1076 | // leave patches in place if something else tampered with it. |
| | 1077 | if (stdout.write === newStdoutWrite) stdout.write = stdoutWrite; |
| | 1078 | if (stderr.write === newStderrWrite) stdout.write = stderrWrite; |
| | 1079 | if (stdout.end === newStdoutEnd) stdout.end = stdoutEnd; |
| | 1080 | if (stderr.end === newStderrEnd) stdout.end = stderrEnd; |
| | 1081 | for (const [key, old, patched] of restoreConsole) { |
| | 1082 | if (console[key] === patched) console[key] = old; |
| | 1083 | } |
| 903 | }, | 1084 | }, |
| 904 | }; | 1085 | }; |
| 905 | }, | 1086 | }, |
| 906 | now: () => performance.now(), | 1087 | now: () => performance.now(), |
| 907 | delay: async.delay, | 1088 | delay: async.delay, |
| | 1089 | color: process.stderr.isTTY, |
| 908 | }); | 1090 | }); |
| | 1091 | process.addListener("beforeExit", () => host.cancel()); |
| | 1092 | process.addListener("exit", () => host.cancel()); |
| | 1093 | return host; |
| | 1094 | } |
| | 1095 | |
| | 1096 | function defaultFallbackWidgetHost(): WidgetHost { |
| | 1097 | let warned = false; |
| | 1098 | return { |
| | 1099 | writeOutput: (line: string) => console.log(ansi.strip(line)), |
| | 1100 | writeError: (line: string) => console.error(ansi.strip(line)), |
| | 1101 | getDrawLock: () => ({ [Symbol.dispose]() {}, release() {} }), |
| | 1102 | startWidget: (options) => { |
| | 1103 | if (!warned) { |
| | 1104 | console.warn( |
| | 1105 | '"@clo/lib/log.ts"\'s startWidget was called in an environment ' + |
| | 1106 | "that does not support the Node.js 'process' API. Widgets " + |
| | 1107 | "will not be visible.", |
| | 1108 | ); |
| | 1109 | warned = true; |
| | 1110 | } |
| | 1111 | return { |
| | 1112 | options, |
| | 1113 | fps: options.fps ?? null, |
| | 1114 | redraw() {}, |
| | 1115 | stop() {}, |
| | 1116 | [Symbol.dispose]() {}, |
| | 1117 | }; |
| | 1118 | }, |
| | 1119 | cancel: () => {}, |
| | 1120 | capabilities: [], |
| | 1121 | }; |
| | 1122 | } |
| | 1123 | |
| | 1124 | /** returns a WidgetHost from `node:process`, but without patching its methods */ |
| | 1125 | export function simpleNodeProcessWidgetHost( |
| | 1126 | process: NonNullable<typeof node.process>, |
| | 1127 | forceWidgetSupport = false, |
| | 1128 | ): WidgetHost { |
| | 1129 | return forceWidgetSupport || process.stderr.isTTY |
| | 1130 | ? createTerminalWidgetHost({ |
| | 1131 | lockTerminal: () => ({ |
| | 1132 | writeOutput: (string) => process.stdout.write(string), |
| | 1133 | writeInteractive: (string) => process.stderr.write(string), |
| | 1134 | getSize: () => process.stderr, |
| | 1135 | close() { |
| | 1136 | // no action needed |
| | 1137 | }, |
| | 1138 | }), |
| | 1139 | now: () => performance.now(), |
| | 1140 | delay: async.delay, |
| | 1141 | color: process.stderr.isTTY, |
| | 1142 | }) |
| | 1143 | : { |
| | 1144 | writeOutput: (string) => process.stdout.write(string), |
| | 1145 | writeError: (string) => process.stderr.write(string), |
| | 1146 | getDrawLock: () => ({ |
| | 1147 | release() {}, |
| | 1148 | [Symbol.dispose]() {}, |
| | 1149 | }), |
| | 1150 | startWidget: () => null, |
| | 1151 | cancel: () => {}, |
| | 1152 | capabilities: [], |
| | 1153 | }; |
| 909 | } | 1154 | } |
| 910 | | 1155 | |
| 911 | function bufferEndsInNewline(buffer: ArrayBufferView | undefined) { | 1156 | function bufferEndsInNewline(buffer: ArrayBufferView | undefined) { |
| ... | @@ -917,41 +1162,23 @@ function bufferEndsInNewline(buffer: ArrayBufferView | undefined) { | ... | @@ -917,41 +1162,23 @@ function bufferEndsInNewline(buffer: ArrayBufferView | undefined) { |
| 917 | : false; | 1162 | : false; |
| 918 | } | 1163 | } |
| 919 | | 1164 | |
| 920 | const levelToAnsi: Record<Message["level"], string> = { | 1165 | const levelToAnsi: Record<MessageLevel, string> = { |
| 921 | info: `${ansi.fgBlue}info`, | 1166 | info: `${ansi.fgBlue}info`, |
| 922 | warn: `${ansi.fgYellow}warn`, | 1167 | warn: `${ansi.fgYellow}warn`, |
| 923 | error: `${ansi.fgRed}error`, | 1168 | error: `${ansi.fgRed}error`, |
| 924 | debug: `${ansi.dim}dbg`, | 1169 | debug: `${ansi.dim}dbg`, |
| 925 | }; | 1170 | }; |
| 926 | | 1171 | |
| 927 | let globalMessageFormatFunction: MessageFormatFunction = ( | 1172 | const globalLog = /* @__PURE__ */ (() => |
| 928 | { level, scope, text, newline }, | 1173 | new ScopeImpl((m) => { |
| 929 | colors, | 1174 | if (global.writeMessage) { |
| 930 | ) => { | 1175 | global.writeMessage(m); |
| 931 | if (!text) return ""; | 1176 | } else if (node.process) { |
| 932 | if (newline === false) return text; | 1177 | globalWidgetHost()[ |
| 933 | const prefix = colors | 1178 | (m.level ?? "info") === "info" ? "writeOutput" : "writeError" |
| 934 | // colorful | 1179 | ](formatAnsiMessage(m, node.process.stdout.isTTY)); |
| 935 | ? `${levelToAnsi[level]}${ | 1180 | } else { |
| 936 | scope ? `(${scope})` : "" | 1181 | let { level = "info", [originalLogArgs]: args = [m.text], scope } = m; |
| 937 | }${ansi.fgReset}${ansi.dim}:${ansi.reset} ` | | |
| 938 | // colorless | | |
| 939 | : scope | | |
| 940 | ? `${level}(${scope}): ` | | |
| 941 | : `${level}: `; | | |
| 942 | return prefix + text + "\n"; | | |
| 943 | }; | | |
| 944 | let globalOutputFunction!: DispatchFunction; | | |
| 945 | const globalLog = /* @__PURE__ */ (() => { | | |
| 946 | const colors = node.process?.stderr.isTTY ?? false; | | |
| 947 | globalOutputFunction = node.process | | |
| 948 | // In Node.js, coordinate with the widget host | | |
| 949 | ? (message) => { | | |
| 950 | globalWidgetHost.write(globalMessageFormatFunction(message, colors)); | | |
| 951 | } | | |
| 952 | // Otherwise, forward to `console` | | |
| 953 | : (m) => { | | |
| 954 | let { level, [originalLogArgs]: args = [m.text], scope } = m; | | |
| 955 | if (scope) { | 1182 | if (scope) { |
| 956 | const arg0 = args[0]; | 1183 | const arg0 = args[0]; |
| 957 | const prefix = `[${scope}]`; | 1184 | const prefix = `[${scope}]`; |
| ... | @@ -959,9 +1186,9 @@ const globalLog = /* @__PURE__ */ (() => { | ... | @@ -959,9 +1186,9 @@ const globalLog = /* @__PURE__ */ (() => { |
| 959 | else args.unshift(prefix); | 1186 | else args.unshift(prefix); |
| 960 | } | 1187 | } |
| 961 | console[level](...args); | 1188 | console[level](...args); |
| 962 | }; | 1189 | } |
| 963 | return new ScopeImpl(globalOutputFunction); | 1190 | global.tees.forEach((cb) => cb(m)); |
| 964 | })(); | 1191 | }))(); |
| 965 | | 1192 | |
| 966 | export type DispatchFunction = (message: Message) => void; | 1193 | export type DispatchFunction = (message: Message) => void; |
| 967 | /** | 1194 | /** |