Skip to content

Commit 41975df

Browse files
Add ANSI.replay to collapse captured repaints
Apply viewport-independent terminal controls to a line grid so captured spinner and progress-bar repaints collapse to their final output. Parse streams with a VT-style state machine, discard presentation and alternate-screen content, and use the internal Unicode terminal-width helper for correct wide-glyph cursor placement without changing existing layout APIs. Co-authored-by: River <river@shopify.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Assisted-By: devx/b840c1e6-d979-473d-9216-a53477380bf1
1 parent 621f749 commit 41975df

3 files changed

Lines changed: 1028 additions & 0 deletions

File tree

lib/cli/ui/ansi.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
module CLI
55
module UI
66
module ANSI
7+
autoload :Replay, 'cli/ui/ansi/replay'
8+
private_constant :Replay
9+
710
ESC = "\x1b"
811
# https://ghostty.org/docs/vt/concepts/sequences#csi-sequences
912
CSI_SEQUENCE = /\x1b\[[\d;:]+[\x20-\x2f]*?[\x40-\x7e]/
@@ -48,6 +51,33 @@ def strip_codes(str)
4851
str.gsub(Regexp.union(CSI_SEQUENCE, OSC_SEQUENCE, /\r/), '')
4952
end
5053

54+
# Replays the viewport-independent cursor controls in a captured
55+
# terminal stream, so repaints (spinners, progress bars) collapse
56+
# onto their final state instead of accumulating one frame per tick.
57+
#
58+
# Where +strip_codes+ deletes control sequences, this applies them.
59+
# Operations that assume a viewport -- screen-relative positioning,
60+
# display erasure, wrapping -- are ignored: a capture does not
61+
# record scrolling, so screen coordinates have no buffer row to map
62+
# onto. Alternate-screen content (a full-screen prompt, a pager) is
63+
# discarded on exit, as a terminal discards it. Commands a repaint
64+
# has no use for, from character editing to charset translation,
65+
# are dropped without effect. The stream is decoded as UTF-8
66+
# whatever its tagged encoding, replacing bytes that don't decode.
67+
# Columns hold one grapheme cluster each, using Unicode terminal
68+
# widths so wide glyphs keep their two columns when overwritten.
69+
# Trailing whitespace on every line is trimmed: a terminal renders
70+
# nothing there.
71+
#
72+
# ==== Attributes
73+
#
74+
# - +str+ - The captured terminal stream to replay
75+
#
76+
#: (String str) -> String
77+
def replay(str)
78+
Replay.render(str)
79+
end
80+
5181
# Returns an ANSI control sequence
5282
#
5383
# ==== Attributes

0 commit comments

Comments
 (0)