PaceText prints text with an adaptive typewriter cadence. Short bodies use rune-by-rune animation at the requested delay (the effect reads as animation); long bodies switch to a chunked mode where multiple runes paint per ~10ms tick so the total animation completes within the configured budget; very
(text string, requested time.Duration)
| 87 | // chunk they land in — they don't trigger sleeps or count toward the |
| 88 | // printable budget so color transitions never pause the eye. |
| 89 | func PaceText(text string, requested time.Duration) { |
| 90 | if typewriterDisabled() { |
| 91 | fmt.Print(text) |
| 92 | return |
| 93 | } |
| 94 | |
| 95 | requested = resolveDelay(requested) |
| 96 | budget := resolveBudget() |
| 97 | |
| 98 | printable := countPrintableRunes(text) |
| 99 | if printable == 0 || requested <= 0 { |
| 100 | fmt.Print(text) |
| 101 | return |
| 102 | } |
| 103 | if printable >= hardSkipChars { |
| 104 | fmt.Print(text) |
| 105 | return |
| 106 | } |
| 107 | |
| 108 | // Path A — short body: requested cadence fits the budget, so we |
| 109 | // keep the per-rune animation. Most chat replies live here. |
| 110 | requestedTotal := time.Duration(printable) * requested |
| 111 | if budget <= 0 || requestedTotal <= budget { |
| 112 | typewriterPrint(text, requested) |
| 113 | return |
| 114 | } |
| 115 | |
| 116 | // Path B — long body: switch to chunked mode. Spread the printable |
| 117 | // runes evenly across the budget at tickInterval cadence. |
| 118 | totalTicks := int(budget / tickInterval) |
| 119 | if totalTicks < 1 { |
| 120 | totalTicks = 1 |
| 121 | } |
| 122 | runesPerTick := (printable + totalTicks - 1) / totalTicks |
| 123 | if runesPerTick < 1 { |
| 124 | runesPerTick = 1 |
| 125 | } |
| 126 | chunkedTypewriterPrint(text, runesPerTick, tickInterval) |
| 127 | } |
| 128 | |
| 129 | // chunkedTypewriterPrint walks text writing runesPerTick visible runes |
| 130 | // per pass and sleeping interval between passes. ANSI escape sequences |