v0.4.2 baf7aea

renderer.go RawHistoryFolder

1package ansi
2
3import (
4	"bytes"
5	"strconv"
6	"sync"
7)
8
9const maxPooledBufferCap = 1 << 20
10
11var bufferPool = sync.Pool{
12	New: func() any { return new(bytes.Buffer) },
13}
14
15// Capabilities describes the fixed output features of a Renderer target.
16type Capabilities struct {
17	SynchronizedOutput bool
18}
19
20type Renderer struct {
21	caps         Capabilities
22	colorProfile ColorProfile
23	width        int
24	height       int
25	shadow       []Cell
26	committed    Frame
27}
28
29// PreparedDraw owns encoded output and its transactional delta until Commit.
30// It is returned by value so ordinary prepared draws do not allocate.
31type PreparedDraw struct {
32	renderer   *Renderer
33	candidate  DeltaCandidate
34	data       []byte
35	commitOnce *sync.Once
36}
37
38func New(caps Capabilities) *Renderer {
39	return NewWithColorProfile(caps, ColorProfileTrueColor)
40}
41
42// NewWithColorProfile constructs a renderer for a target color profile.
43func NewWithColorProfile(caps Capabilities, profile ColorProfile) *Renderer {
44	return &Renderer{caps: caps, colorProfile: profile}
45}
46
47func (r *Renderer) Reset() {
48	r.width = 0
49	r.height = 0
50	r.shadow = nil
51	r.committed = Frame{}
52}
53
54// Bytes returns the prepared ANSI output. The returned bytes remain valid after
55// another draw.
56func (p PreparedDraw) Bytes() []byte { return p.data }
57
58// Commit applies the prepared delta exactly once. Discarding it leaves the
59// renderer's committed state unchanged.
60func (p *PreparedDraw) Commit() {
61	if p == nil || p.renderer == nil || p.commitOnce == nil {
62		return
63	}
64	p.commitOnce.Do(func() {
65		committed := p.renderer.committedFrame()
66		p.candidate.Commit(&committed)
67		p.renderer.setCommittedFrame(committed)
68	})
69}
70
71// Prepare plans and encodes a transactional draw. The renderer advances only
72// when the returned draw is committed. Keep at most one prepared draw
73// outstanding; commit or discard it before calling Prepare again.
74func (r *Renderer) Prepare(frame Frame, damage []Damage, reset bool) (PreparedDraw, error) {
75	var candidate DeltaCandidate
76	var err error
77	if !reset && len(r.shadow) != 0 && r.width == frame.Width && r.height == frame.Height && len(damage) == 1 && (damage[0].Kind == DamageText || damage[0].Kind == DamageClear) {
78		if err = frame.Validate(); err == nil {
79			plan := planSingleDamage(frame, damage[0])
80			candidate = DeltaCandidate{Plan: plan}
81			if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
82				candidate.frame = frame.Clone()
83			}
84		}
85	} else {
86		candidate, err = PlanDelta(frame, damage, r.committedFrame(), reset || len(r.shadow) == 0)
87	}
88	if err != nil {
89		return PreparedDraw{}, err
90	}
91	prepared := PreparedDraw{renderer: r, candidate: candidate, commitOnce: new(sync.Once)}
92	plan := candidate.Plan
93	if !plan.Snapshot && plan.Scroll.Height == 0 && len(plan.Spans) == 0 {
94		return prepared, nil
95	}
96
97	buf, ok := bufferPool.Get().(*bytes.Buffer)
98	if !ok {
99		buf = new(bytes.Buffer)
100	}
101	buf.Reset()
102	defer putBuffer(buf)
103	if r.caps.SynchronizedOutput {
104		buf.WriteString(SyncStartCSI)
105	}
106
107	st := newDrawStateForProfile(r.colorProfile)
108	if plan.Snapshot {
109		if len(plan.Spans) > 0 {
110			r.emitDamageSpans(buf, frame, plan.Spans, &st)
111		} else {
112			r.writeFull(buf, frame, &st)
113		}
114	} else {
115		if plan.Scroll.Height != 0 {
116			scroll := plan.Scroll
117			emitScrollUp(buf, Damage{Kind: DamageScrollUp, X: 0, Y: scroll.Y, Width: frame.Width, Height: scroll.Height, Count: scroll.Count})
118		}
119		for _, span := range plan.Spans {
120			r.emitSpan(buf, frame, span.Y, span.X, span.Width, &st)
121		}
122		buf.WriteString("\x1b[0m")
123	}
124	if r.caps.SynchronizedOutput {
125		buf.WriteString(SyncEndCSI)
126	}
127	prepared.data = copyBytes(buf)
128	return prepared, nil
129}
130
131func (r *Renderer) Draw(frame Frame, damage []Damage) ([]byte, error) {
132	prepared, err := r.Prepare(frame, damage, false)
133	if err != nil {
134		return nil, err
135	}
136	prepared.Commit()
137	return prepared.Bytes(), nil
138}
139
140func (r *Renderer) committedFrame() Frame {
141	return r.committed
142}
143
144func (r *Renderer) setCommittedFrame(frame Frame) {
145	r.width = frame.Width
146	r.height = frame.Height
147	r.shadow = frame.Cells
148	r.committed = frame
149}
150
151// copyBytes copies the buffer contents into a fresh byte slice and is used
152// to return output that is independent of the pooled scratch buffer.
153func copyBytes(buf *bytes.Buffer) []byte {
154	b := buf.Bytes()
155	out := make([]byte, len(b))
156	copy(out, b)
157	return out
158}
159
160func putBuffer(buf *bytes.Buffer) {
161	if buf.Cap() > maxPooledBufferCap {
162		return
163	}
164	bufferPool.Put(buf)
165}
166
167func needsFull(damage []Damage) bool {
168	if len(damage) == 0 {
169		return false
170	}
171	for _, d := range damage {
172		if d.Kind == DamageFullRedraw {
173			return true
174		}
175	}
176	return false
177}
178
179func hasScrollDamage(damage []Damage) bool {
180	for _, d := range damage {
181		if d.Kind == DamageScrollUp {
182			return true
183		}
184	}
185	return false
186}
187
188func (r *Renderer) writeFull(out *bytes.Buffer, frame Frame, st *drawState) {
189	for y := range frame.Height {
190		r.emitSpan(out, frame, y, 0, frame.Width, st)
191	}
192	out.WriteString("\x1b[0m")
193}
194
195func (r *Renderer) emitDamageSpans(out *bytes.Buffer, frame Frame, spans []Span, st *drawState) {
196	for _, span := range spans {
197		r.emitSpan(out, frame, span.Y, span.X, span.Width, st)
198	}
199	out.WriteString("\x1b[0m")
200}
201func clampRect(frame Frame, x, y, width, height int) (int, int, int, int, bool) {
202	x, width, okX := clampRange(x, width, frame.Width)
203	y, height, okY := clampRange(y, height, frame.Height)
204	if !okX || !okY {
205		return 0, 0, 0, 0, false
206	}
207	return x, y, width, height, true
208}
209
210// clampRange intersects [pos, pos+size) with [0, limit) without evaluating an
211// overflowing endpoint from untrusted damage coordinates.
212func clampRange(pos, size, limit int) (int, int, bool) {
213	if size <= 0 || limit <= 0 || pos >= limit {
214		return 0, 0, false
215	}
216	if pos < 0 {
217		// -size is safe because size is positive. If pos is at or before that
218		// point, the rectangle ends at or before zero.
219		if pos <= -size {
220			return 0, 0, false
221		}
222		end := pos + size // pos > -size proves this addition cannot overflow.
223		if end > limit {
224			end = limit
225		}
226		return 0, end, true
227	}
228
229	available := limit - pos
230	return pos, min(size, available), true
231}
232
233// writeCursor emits a cursor-positioning CSI sequence without fmt.Fprintf
234// allocations. It uses a stack-allocated buffer for integer formatting.
235func writeCursor(out *bytes.Buffer, y, x int) {
236	out.WriteString("\x1b[")
237	var b [16]byte
238	n := strconv.AppendInt(b[:0], int64(y+1), 10)
239	out.Write(n)
240	out.WriteByte(';')
241	n = strconv.AppendInt(b[:0], int64(x+1), 10)
242	out.Write(n)
243	out.WriteByte('H')
244}
245
246func sameDamage(a, b Damage) bool {
247	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
248}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close