| ... | @@ -238,14 +238,14 @@ export function writeOutput(text: string) { | ... | @@ -238,14 +238,14 @@ export function writeOutput(text: string) { |
| 238 | globalWidgetHost().writeOutput(text); | 238 | globalWidgetHost().writeOutput(text); |
| 239 | } | 239 | } |
| 240 | | 240 | |
| 241 | // TODO: | 241 | /** |
| 242 | // /** | 242 | * like {@linkcode writeOutput}, but the text is written to the error stream |
| 243 | // * no built-in prefix, formatting, or newlline. ensures the text does not interweave. | 243 | * (stderr in node.js). relative ordering between output and error text is |
| 244 | // * data will be flushed in the next frame or when drawing is {@link getDrawLock|unlocked}. | 244 | * preserved through the shared flush buffer. |
| 245 | // */ | 245 | */ |
| 246 | // export function writeError(text: string) { | 246 | export function writeError(text: string) { |
| 247 | // globalWidgetHost().writeError(text); | 247 | globalWidgetHost().writeError(text); |
| 248 | // } | 248 | } |
| 249 | | 249 | |
| 250 | /** write a {@linkcode Message} object directly. */ | 250 | /** write a {@linkcode Message} object directly. */ |
| 251 | export function writeMessage(m: Message) { | 251 | export function writeMessage(m: Message) { |
| ... | @@ -390,7 +390,11 @@ export interface TerminalWidgetHostOptions { | ... | @@ -390,7 +390,11 @@ export interface TerminalWidgetHostOptions { |
| 390 | * interface to communicate everything about the terminal state correctly. | 390 | * interface to communicate everything about the terminal state correctly. |
| 391 | */ | 391 | */ |
| 392 | export interface TerminalLock { | 392 | export interface TerminalLock { |
| 393 | /** recieves ANSI escape sequences for interactive data (should flush immediately) */ | 393 | /** |
| | 394 | * recieves ANSI escape sequences for interactive data, as well as error log |
| | 395 | * content from `writeError` (should flush immediately). the interactive |
| | 396 | * stream and the error stream are the same: stderr. |
| | 397 | */ |
| 394 | writeInteractive(text: string): void; | 398 | writeInteractive(text: string): void; |
| 395 | /** recieves log content from `write` (pre-buffered; should flush immediately) */ | 399 | /** recieves log content from `write` (pre-buffered; should flush immediately) */ |
| 396 | writeOutput(text: string): void; | 400 | writeOutput(text: string): void; |
| ... | @@ -460,7 +464,12 @@ export function createTerminalWidgetHost( | ... | @@ -460,7 +464,12 @@ export function createTerminalWidgetHost( |
| 460 | let locks = 0; | 464 | let locks = 0; |
| 461 | let redrawTime = 0; | 465 | let redrawTime = 0; |
| 462 | let lastFlush = 0; | 466 | let lastFlush = 0; |
| 463 | let buffer = ""; | 467 | // log text awaiting a flush. error text shares the queue so that relative |
| | 468 | // write order is preserved, but flushes to the interactive stream (stderr) |
| | 469 | // and always lands on the widget screen, while plain output rows only |
| | 470 | // count when `outputSharesScreen`. consecutive same-stream writes merge |
| | 471 | // into one chunk. |
| | 472 | let buffer: { text: string; err: boolean }[] = []; |
| 464 | // visible width of the trailing partial log line (text since the last "\n" | 473 | // visible width of the trailing partial log line (text since the last "\n" |
| 465 | // written to output). the cursor column is derived as `partialWidth % | 474 | // written to output). the cursor column is derived as `partialWidth % |
| 466 | // columns` at draw time, so the value survives resizes and wrapped lines. | 475 | // columns` at draw time, so the value survives resizes and wrapped lines. |
| ... | @@ -536,7 +545,7 @@ export function createTerminalWidgetHost( | ... | @@ -536,7 +545,7 @@ export function createTerminalWidgetHost( |
| 536 | } | 545 | } |
| 537 | // cancel() during this render pass (an exit handler unwinding through | 546 | // cancel() during this render pass (an exit handler unwinding through |
| 538 | // a crashed format callback) skips its teardown; finish it here. | 547 | // a crashed format callback) skips its teardown; finish it here. |
| 539 | if (widgets.length === 0 && terminal && !timer && !buffer) { | 548 | if (widgets.length === 0 && terminal && !timer && !buffer.length) { |
| 540 | lines = []; | 549 | lines = []; |
| 541 | closeTerminal(); | 550 | closeTerminal(); |
| 542 | } | 551 | } |
| ... | @@ -550,21 +559,24 @@ export function createTerminalWidgetHost( | ... | @@ -550,21 +559,24 @@ export function createTerminalWidgetHost( |
| 550 | if (!lines.length && !widgets.length) { | 559 | if (!lines.length && !widgets.length) { |
| 551 | // a redraw can get scheduled with nothing to write (e.g. widgets torn | 560 | // a redraw can get scheduled with nothing to write (e.g. widgets torn |
| 552 | // down before the timer fired); it is a no-op, not an error | 561 | // down before the timer fired); it is a no-op, not an error |
| 553 | if (!buffer) return; | 562 | if (!buffer.length) return; |
| 554 | needsToRestoreCursor = false; | 563 | needsToRestoreCursor = false; |
| 555 | needsToSaveCursor = false; | 564 | needsToSaveCursor = false; |
| 556 | if (!terminal && writeOutputTemporaryLock) { | 565 | if ( |
| 557 | writeOutputTemporaryLock(buffer); | 566 | !terminal && writeOutputTemporaryLock && !buffer.some((c) => c.err) |
| | 567 | ) { |
| | 568 | const text = buffer.map((c) => c.text).join(""); |
| | 569 | buffer = []; |
| | 570 | writeOutputTemporaryLock(text); |
| | 571 | if (outputOnScreen) trackPartialWidth(text); |
| 558 | } else { | 572 | } else { |
| 559 | acquireTerminal().writeOutput(buffer); | 573 | trackPartialWidth(flushChunks(acquireTerminal())); |
| 560 | } | 574 | } |
| 561 | if (hasSyncStart) { | 575 | if (hasSyncStart) { |
| 562 | acquireTerminal().writeInteractive(ansi.syncEnd); | 576 | acquireTerminal().writeInteractive(ansi.syncEnd); |
| 563 | hasSyncStart = false; | 577 | hasSyncStart = false; |
| 564 | } | 578 | } |
| 565 | closeTerminal(); | 579 | closeTerminal(); |
| 566 | trackPartialWidth(buffer); | | |
| 567 | buffer = ""; | | |
| 568 | return; | 580 | return; |
| 569 | } | 581 | } |
| 570 | | 582 | |
| ... | @@ -624,10 +636,8 @@ export function createTerminalWidgetHost( | ... | @@ -624,10 +636,8 @@ export function createTerminalWidgetHost( |
| 624 | needsToRestoreCursor = false; | 636 | needsToRestoreCursor = false; |
| 625 | lines = []; | 637 | lines = []; |
| 626 | } | 638 | } |
| 627 | if (buffer) { | 639 | if (buffer.length) { |
| 628 | term.writeOutput(buffer); | 640 | trackPartialWidth(flushChunks(term)); |
| 629 | trackPartialWidth(buffer); | | |
| 630 | buffer = ""; | | |
| 631 | } | 641 | } |
| 632 | if (hasSyncStart) { | 642 | if (hasSyncStart) { |
| 633 | term.writeInteractive(ansi.syncEnd); | 643 | term.writeInteractive(ansi.syncEnd); |
| ... | @@ -639,15 +649,20 @@ export function createTerminalWidgetHost( | ... | @@ -639,15 +649,20 @@ export function createTerminalWidgetHost( |
| 639 | return; | 649 | return; |
| 640 | } | 650 | } |
| 641 | | 651 | |
| 642 | if (buffer && !outputOnScreen) { | 652 | // concatenation of the buffered text that lands on the widget screen, |
| | 653 | // which is what the cursor math must account for |
| | 654 | const screenText = buffer |
| | 655 | .filter((c) => c.err || outputOnScreen) |
| | 656 | .map((c) => c.text) |
| | 657 | .join(""); |
| | 658 | if (buffer.length && screenText === "") { |
| 643 | // off-screen output (e.g. stdout redirected to a file) does not | 659 | // off-screen output (e.g. stdout redirected to a file) does not |
| 644 | // interact with the widget block; flush it plainly and fall through | 660 | // interact with the widget block; flush it plainly and fall through |
| 645 | // to a pure widget redraw | 661 | // to a pure widget redraw |
| 646 | term.writeOutput(buffer); | 662 | flushChunks(term); |
| 647 | buffer = ""; | | |
| 648 | } | 663 | } |
| 649 | | 664 | |
| 650 | if (buffer) { | 665 | if (buffer.length) { |
| 651 | // when writing a buffer alongside widgets, the screen may look like this | 666 | // when writing a buffer alongside widgets, the screen may look like this |
| 652 | // > [existing log] | 667 | // > [existing log] |
| 653 | // > [optional partial line] | 668 | // > [optional partial line] |
| ... | @@ -659,7 +674,7 @@ export function createTerminalWidgetHost( | ... | @@ -659,7 +674,7 @@ export function createTerminalWidgetHost( |
| 659 | // first, clear out the space where new lines are going to intersect. | 674 | // first, clear out the space where new lines are going to intersect. |
| 660 | // `span.rows` measures the cursor descent in physical rows, so log | 675 | // `span.rows` measures the cursor descent in physical rows, so log |
| 661 | // lines wider than the terminal are accounted for correctly. | 676 | // lines wider than the terminal are accounted for correctly. |
| 662 | const span = measureTerminalSpan(buffer, pCol, columns); | 677 | const span = measureTerminalSpan(screenText, pCol, columns); |
| 663 | // if more rows are buffered than there are widgets, only some are | 678 | // if more rows are buffered than there are widgets, only some are |
| 664 | // needed. when a partial line exists, the buffer starts on its row | 679 | // needed. when a partial line exists, the buffer starts on its row |
| 665 | // (one above the widget block), hence the -1. | 680 | // (one above the widget block), hence the -1. |
| ... | @@ -689,8 +704,8 @@ export function createTerminalWidgetHost( | ... | @@ -689,8 +704,8 @@ export function createTerminalWidgetHost( |
| 689 | : "") | 704 | : "") |
| 690 | : ""), | 705 | : ""), |
| 691 | ); | 706 | ); |
| 692 | // then write output lines on standard out | 707 | // then write the buffered log content to its streams |
| 693 | term.writeOutput(buffer); | 708 | flushChunks(term); |
| 694 | term.writeInteractive( | 709 | term.writeInteractive( |
| 695 | // if the buffer leaves a partial line, the widgets have to go on the | 710 | // if the buffer leaves a partial line, the widgets have to go on the |
| 696 | // next line. to avoid breaking stdout, the newline gets emitted on | 711 | // next line. to avoid breaking stdout, the newline gets emitted on |
| ... | @@ -753,17 +768,37 @@ export function createTerminalWidgetHost( | ... | @@ -753,17 +768,37 @@ export function createTerminalWidgetHost( |
| 753 | needsToRestoreCursor ||= needsToSaveCursor; | 768 | needsToRestoreCursor ||= needsToSaveCursor; |
| 754 | needsToSaveCursor = false; | 769 | needsToSaveCursor = false; |
| 755 | lines = newWidgetLines; | 770 | lines = newWidgetLines; |
| 756 | buffer = ""; | | |
| 757 | } | 771 | } |
| 758 | | 772 | |
| 759 | /** | 773 | /** |
| 760 | * update `partialWidth` after writing `text` to the output. rows written | 774 | * write every buffered chunk to its stream in order, returning the |
| 761 | * off-screen never displace the widget block, so they are not tracked. | 775 | * concatenation of the chunks that landed on the widget screen. error |
| | 776 | * chunks flush through `writeInteractive` since the interactive stream is |
| | 777 | * the error stream; plain output rows land on screen only when |
| | 778 | * `outputSharesScreen`. |
| 762 | */ | 779 | */ |
| 763 | function trackPartialWidth(text: string) { | 780 | function flushChunks(term: TerminalLock): string { |
| 764 | if (!outputOnScreen) return; | 781 | let screen = ""; |
| 765 | const i = text.lastIndexOf("\n"); | 782 | for (const chunk of buffer) { |
| 766 | partialWidth = ansi.widthInTerminal(text.slice(i + 1)) | 783 | if (chunk.err) { |
| | 784 | term.writeInteractive(chunk.text); |
| | 785 | screen += chunk.text; |
| | 786 | } else { |
| | 787 | term.writeOutput(chunk.text); |
| | 788 | if (outputOnScreen) screen += chunk.text; |
| | 789 | } |
| | 790 | } |
| | 791 | buffer = []; |
| | 792 | return screen; |
| | 793 | } |
| | 794 | |
| | 795 | /** |
| | 796 | * update `partialWidth` after `screenText` (already filtered to the chunks |
| | 797 | * that landed on the widget screen) was written. |
| | 798 | */ |
| | 799 | function trackPartialWidth(screenText: string) { |
| | 800 | const i = screenText.lastIndexOf("\n"); |
| | 801 | partialWidth = ansi.widthInTerminal(screenText.slice(i + 1)) |
| 767 | + (i === -1 ? partialWidth : 0); | 802 | + (i === -1 ? partialWidth : 0); |
| 768 | } | 803 | } |
| 769 | | 804 | |
| ... | @@ -807,24 +842,35 @@ export function createTerminalWidgetHost( | ... | @@ -807,24 +842,35 @@ export function createTerminalWidgetHost( |
| 807 | hasSyncStart = shortTermDrawLock; | 842 | hasSyncStart = shortTermDrawLock; |
| 808 | } | 843 | } |
| 809 | if (buffer.length > 0) { | 844 | if (buffer.length > 0) { |
| 810 | if (widgets.length === 0 && !terminal && writeOutputTemporaryLock) { | 845 | if ( |
| 811 | writeOutputTemporaryLock(buffer); | 846 | widgets.length === 0 && !terminal && writeOutputTemporaryLock |
| | 847 | && !buffer.some((c) => c.err) |
| | 848 | ) { |
| | 849 | const text = buffer.map((c) => c.text).join(""); |
| | 850 | buffer = []; |
| | 851 | writeOutputTemporaryLock(text); |
| | 852 | if (outputOnScreen) trackPartialWidth(text); |
| 812 | } else { | 853 | } else { |
| 813 | acquireTerminal().writeOutput(buffer); | 854 | trackPartialWidth(flushChunks(acquireTerminal())); |
| 814 | if (widgets.length === 0) closeTerminal(); | 855 | if (widgets.length === 0) closeTerminal(); |
| 815 | } | 856 | } |
| 816 | trackPartialWidth(buffer); | | |
| 817 | buffer = ""; | | |
| 818 | } | 857 | } |
| 819 | } | 858 | } |
| 820 | | 859 | |
| | 860 | /** append a write to the flush buffer, scheduling the flush */ |
| | 861 | function bufferChunk(chunk: string, err: boolean) { |
| | 862 | const last = buffer[buffer.length - 1]; |
| | 863 | if (last && last.err === err) last.text += chunk; |
| | 864 | else buffer.push({ text: chunk, err }); |
| | 865 | redrawSoon(0); |
| | 866 | } |
| | 867 | |
| 821 | return { | 868 | return { |
| 822 | writeOutput(chunk) { | 869 | writeOutput(chunk) { |
| 823 | if (chunk) buffer += chunk, redrawSoon(0); | 870 | if (chunk) bufferChunk(chunk, false); |
| 824 | }, | 871 | }, |
| 825 | writeError(chunk) { | 872 | writeError(chunk) { |
| 826 | // TODO: write to stderr. when this was introduced it was not a regression from v3 | 873 | if (chunk) bufferChunk(chunk, true); |
| 827 | if (chunk) buffer += chunk, redrawSoon(0); | | |
| 828 | }, | 874 | }, |
| 829 | getDrawLock(mode) { | 875 | getDrawLock(mode) { |
| 830 | if (rendering) ASSERT(locks === 0); | 876 | if (rendering) ASSERT(locks === 0); |
| ... | @@ -1375,11 +1421,15 @@ const globalLog = /* @__PURE__ */ (() => | ... | @@ -1375,11 +1421,15 @@ const globalLog = /* @__PURE__ */ (() => |
| 1375 | if (global.writeMessage) { | 1421 | if (global.writeMessage) { |
| 1376 | global.writeMessage(m); | 1422 | global.writeMessage(m); |
| 1377 | } else if (node.process) { | 1423 | } else if (node.process) { |
| 1378 | globalWidgetHost()[ | 1424 | // info/debug levels land on stdout, warnings and errors on stderr; |
| 1379 | (m.level ?? "info") === "info" ? "writeOutput" : "writeError" | 1425 | // colors are keyed off the destination stream |
| 1380 | // colors keyed off stdout, the destination of host log output; | 1426 | const err = (m.level ?? "info") !== "info" && m.level !== "debug"; |
| 1381 | // `logColors` (used for inspect formatting) matches. | 1427 | globalWidgetHost()[err ? "writeError" : "writeOutput"]( |
| 1382 | ](formatAnsiMessage(m, node.process.stdout.isTTY)); | 1428 | formatAnsiMessage( |
| | 1429 | m, |
| | 1430 | (err ? node.process.stderr : node.process.stdout).isTTY, |
| | 1431 | ), |
| | 1432 | ); |
| 1383 | } else { | 1433 | } else { |
| 1384 | let { level = "info", [originalLogArgs]: args = [m.text], scope } = m; | 1434 | let { level = "info", [originalLogArgs]: args = [m.text], scope } = m; |
| 1385 | if (scope) { | 1435 | if (scope) { |