package ansi import ( "bytes" "strconv" "sync" ) const maxPooledBufferCap = 1 << 20 var bufferPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } // Capabilities describes the fixed output features of a Renderer target. type Capabilities struct { SynchronizedOutput bool } type Renderer struct { caps Capabilities colorProfile ColorProfile width int height int hasCommitted bool committed Frame } // PreparedDraw owns encoded output and its transactional delta until Commit. // It is returned by value so ordinary prepared draws do not allocate. type PreparedDraw struct { renderer *Renderer candidate DeltaCandidate data []byte commitOnce *sync.Once } func New(caps Capabilities) *Renderer { return NewWithColorProfile(caps, ColorProfileTrueColor) } // NewWithColorProfile constructs a renderer for a target color profile. func NewWithColorProfile(caps Capabilities, profile ColorProfile) *Renderer { return &Renderer{caps: caps, colorProfile: profile} } func (r *Renderer) Reset() { r.width = 0 r.height = 0 r.hasCommitted = false r.committed = Frame{} } // Bytes returns the prepared ANSI output. The returned bytes remain valid after // another draw. func (p PreparedDraw) Bytes() []byte { return p.data } // Commit applies the prepared delta exactly once. Discarding it leaves the // renderer's committed state unchanged. func (p *PreparedDraw) Commit() { if p == nil || p.renderer == nil || p.commitOnce == nil { return } p.commitOnce.Do(func() { committed := p.renderer.committedFrame() if p.candidate.Plan.Snapshot { // PreparedDraw privately owns this snapshot. Its shared Once makes // transfer safe even when the caller retains copies of the draw. committed = p.candidate.frame } else { p.candidate.Commit(&committed) } p.renderer.setCommittedFrame(committed) }) } // Prepare plans and encodes a transactional draw. The renderer advances only // when the returned draw is committed. Keep at most one prepared draw // outstanding; commit or discard it before calling Prepare again. func (r *Renderer) Prepare(frame CellSource, damage []Damage, reset bool) (PreparedDraw, error) { var candidate DeltaCandidate var err error if err = validateCellSource(frame); err != nil { return PreparedDraw{}, err } columns, rows := frame.Columns(), frame.Rows() if !reset && r.hasCommitted && r.width == columns && r.height == rows && len(damage) == 1 && (damage[0].Kind == DamageText || damage[0].Kind == DamageClear) { plan := planSingleDamage(frame, damage[0]) candidate = DeltaCandidate{Plan: plan} if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 { candidate.frame = cloneCellSource(frame) } } else { candidate, err = PlanDelta(frame, damage, r.committedFrame(), reset || !r.hasCommitted) } if err != nil { return PreparedDraw{}, err } prepared := PreparedDraw{renderer: r, candidate: candidate, commitOnce: new(sync.Once)} plan := candidate.Plan if !plan.Snapshot && plan.Scroll.Height == 0 && len(plan.Spans) == 0 { return prepared, nil } buf, ok := bufferPool.Get().(*bytes.Buffer) if !ok { buf = new(bytes.Buffer) } buf.Reset() defer putBuffer(buf) if r.caps.SynchronizedOutput { buf.WriteString(SyncStartCSI) } st := newDrawStateForProfile(r.colorProfile) if plan.Snapshot { if len(plan.Spans) > 0 { r.emitDamageSpans(buf, frame, plan.Spans, &st) } else { r.writeFull(buf, frame, &st) } } else { if plan.Scroll.Height != 0 { scroll := plan.Scroll emitScrollUp(buf, Damage{Kind: DamageScrollUp, X: 0, Y: scroll.Y, Width: columns, Height: scroll.Height, Count: scroll.Count}) } for _, span := range plan.Spans { r.emitSpan(buf, frame, span.Y, span.X, span.Width, &st) } buf.WriteString("\x1b[0m") } if r.caps.SynchronizedOutput { buf.WriteString(SyncEndCSI) } prepared.data = copyBytes(buf) return prepared, nil } func (r *Renderer) Draw(frame CellSource, damage []Damage) ([]byte, error) { prepared, err := r.Prepare(frame, damage, false) if err != nil { return nil, err } prepared.Commit() return prepared.Bytes(), nil } func (r *Renderer) committedFrame() Frame { return r.committed } func (r *Renderer) setCommittedFrame(frame Frame) { r.width = frame.Width r.height = frame.Height r.hasCommitted = true r.committed = frame } // copyBytes copies the buffer contents into a fresh byte slice and is used // to return output that is independent of the pooled scratch buffer. func copyBytes(buf *bytes.Buffer) []byte { b := buf.Bytes() out := make([]byte, len(b)) copy(out, b) return out } func putBuffer(buf *bytes.Buffer) { if buf.Cap() > maxPooledBufferCap { return } bufferPool.Put(buf) } func needsFull(damage []Damage) bool { if len(damage) == 0 { return false } for _, d := range damage { if d.Kind == DamageFullRedraw { return true } } return false } func hasScrollDamage(damage []Damage) bool { for _, d := range damage { if d.Kind == DamageScrollUp { return true } } return false } func (r *Renderer) writeFull(out *bytes.Buffer, frame CellSource, st *drawState) { columns := frame.Columns() for y := range frame.Rows() { r.emitSpan(out, frame, y, 0, columns, st) } out.WriteString("\x1b[0m") } func (r *Renderer) emitDamageSpans(out *bytes.Buffer, frame CellSource, spans []Span, st *drawState) { for _, span := range spans { r.emitSpan(out, frame, span.Y, span.X, span.Width, st) } out.WriteString("\x1b[0m") } func clampRect(frame CellSource, x, y, width, height int) (int, int, int, int, bool) { x, width, okX := clampRange(x, width, frame.Columns()) y, height, okY := clampRange(y, height, frame.Rows()) if !okX || !okY { return 0, 0, 0, 0, false } return x, y, width, height, true } // clampRange intersects [pos, pos+size) with [0, limit) without evaluating an // overflowing endpoint from untrusted damage coordinates. func clampRange(pos, size, limit int) (int, int, bool) { if size <= 0 || limit <= 0 || pos >= limit { return 0, 0, false } if pos < 0 { // -size is safe because size is positive. If pos is at or before that // point, the rectangle ends at or before zero. if pos <= -size { return 0, 0, false } end := pos + size // pos > -size proves this addition cannot overflow. if end > limit { end = limit } return 0, end, true } available := limit - pos return pos, min(size, available), true } // writeCursor emits a cursor-positioning CSI sequence without fmt.Fprintf // allocations. It uses a stack-allocated buffer for integer formatting. func writeCursor(out *bytes.Buffer, y, x int) { out.WriteString("\x1b[") var b [16]byte n := strconv.AppendInt(b[:0], int64(y+1), 10) out.Write(n) out.WriteByte(';') n = strconv.AppendInt(b[:0], int64(x+1), 10) out.Write(n) out.WriteByte('H') } func sameDamage(a, b Damage) bool { return a.Kind == b.Kind && a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height && a.Count == b.Count }