1package uv
2
3import (
4 "strings"
5
6 "github.com/charmbracelet/x/ansi"
7)
8
9// scrollOptimize optimizes the screen to transform the old buffer into the new
10// buffer.
11func (s *TerminalRenderer) scrollOptimize(newbuf *Buffer) {
12 height := newbuf.Height()
13 if s.oldnum == nil || len(s.oldnum) < height {
14 s.oldnum = append(s.oldnum, make([]int, height-len(s.oldnum))...)
15 }
16
17 // Calculate the indices
18 s.updateHashmap(newbuf)
19 if len(s.hashtab) < height {
20 return
21 }
22
23 // Pass 1 - from top to bottom scrolling up
24 for i := 0; i < height; {
25 for i < height && (s.oldnum[i] == newIndex || s.oldnum[i] <= i) {
26 i++
27 }
28 if i >= height {
29 break
30 }
31
32 shift := s.oldnum[i] - i // shift > 0
33 start := i
34
35 i++
36 for i < height && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
37 i++
38 }
39 end := i - 1 + shift
40
41 if !s.scrolln(newbuf, shift, start, end, height-1) {
42 continue
43 }
44 }
45
46 // Pass 2 - from bottom to top scrolling down
47 for i := height - 1; i >= 0; {
48 for i >= 0 && (s.oldnum[i] == newIndex || s.oldnum[i] >= i) {
49 i--
50 }
51 if i < 0 {
52 break
53 }
54
55 shift := s.oldnum[i] - i // shift < 0
56 end := i
57
58 i--
59 for i >= 0 && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
60 i--
61 }
62
63 start := i + 1 - (-shift)
64 if !s.scrolln(newbuf, shift, start, end, height-1) {
65 continue
66 }
67 }
68}
69
70// scrolln scrolls the screen up by n lines.
71func (s *TerminalRenderer) scrolln(newbuf *Buffer, n, top, bot, maxY int) (v bool) { //nolint:unparam
72 const (
73 nonDestScrollRegion = false
74 memoryBelow = false
75 )
76
77 blank := s.clearBlank()
78 if n > 0 {
79 // Scroll up (forward)
80 v = s.scrollUp(newbuf, n, top, bot, 0, maxY, blank)
81 if !v {
82 s.buf.WriteString(ansi.SetTopBottomMargins(top+1, bot+1))
83
84 // XXX: How should we handle this in inline mode when not using alternate screen?
85 s.cur.X, s.cur.Y = -1, -1
86 v = s.scrollUp(newbuf, n, top, bot, top, bot, blank)
87 s.buf.WriteString(ansi.SetTopBottomMargins(1, maxY+1))
88 s.cur.X, s.cur.Y = -1, -1
89 }
90
91 if !v {
92 v = s.scrollIdl(newbuf, n, top, bot-n+1, blank)
93 }
94
95 // Clear newly shifted-in lines.
96 if v &&
97 (nonDestScrollRegion || (memoryBelow && bot == maxY)) {
98 if bot == maxY {
99 s.move(newbuf, 0, bot-n+1)
100 s.clearToBottom(nil)
101 } else {
102 for i := 0; i < n; i++ {
103 s.move(newbuf, 0, bot-i)
104 s.clearToEnd(newbuf, nil, false)
105 }
106 }
107 }
108 } else if n < 0 {
109 // Scroll down (backward)
110 v = s.scrollDown(newbuf, -n, top, bot, 0, maxY, blank)
111 if !v {
112 s.buf.WriteString(ansi.SetTopBottomMargins(top+1, bot+1))
113
114 // XXX: How should we handle this in inline mode when not using alternate screen?
115 s.cur.X, s.cur.Y = -1, -1
116 v = s.scrollDown(newbuf, -n, top, bot, top, bot, blank)
117 s.buf.WriteString(ansi.SetTopBottomMargins(1, maxY+1))
118 s.cur.X, s.cur.Y = -1, -1
119
120 if !v {
121 v = s.scrollIdl(newbuf, -n, bot+n+1, top, blank)
122 }
123
124 // Clear newly shifted-in lines.
125 if v &&
126 (nonDestScrollRegion || (memoryBelow && top == 0)) {
127 for i := 0; i < -n; i++ {
128 s.move(newbuf, 0, top+i)
129 s.clearToEnd(newbuf, nil, false)
130 }
131 }
132 }
133 }
134
135 if !v {
136 return
137 }
138
139 s.scrollBuffer(s.curbuf, n, top, bot, blank)
140
141 // shift hash values too, they can be reused
142 s.scrollOldhash(n, top, bot)
143
144 return true
145}
146
147// scrollBuffer scrolls the buffer by n lines.
148func (s *TerminalRenderer) scrollBuffer(b *Buffer, n, top, bot int, blank *Cell) {
149 if top < 0 || bot < top || bot >= b.Height() {
150 // Nothing to scroll
151 return
152 }
153
154 if n < 0 {
155 // shift n lines downwards
156 limit := top - n
157 for line := bot; line >= limit && line >= 0 && line >= top; line-- {
158 copy(b.Lines[line], b.Lines[line+n])
159 }
160 for line := top; line < limit && line <= b.Height()-1 && line <= bot; line++ {
161 b.FillArea(blank, Rect(0, line, b.Width(), 1))
162 }
163 }
164
165 if n > 0 {
166 // shift n lines upwards
167 limit := bot - n
168 for line := top; line <= limit && line <= b.Height()-1 && line <= bot; line++ {
169 copy(b.Lines[line], b.Lines[line+n])
170 }
171 for line := bot; line > limit && line >= 0 && line >= top; line-- {
172 b.FillArea(blank, Rect(0, line, b.Width(), 1))
173 }
174 }
175
176 s.touchLine(b, top, bot-top+1, true)
177}
178
179// touchLine marks the line as touched.
180func (s *TerminalRenderer) touchLine(newbuf *Buffer, y, n int, changed bool) {
181 height := newbuf.Height()
182 if n < 0 || y < 0 || y >= height || newbuf.Touched == nil || len(newbuf.Touched) < height {
183 return // Nothing to touch
184 }
185
186 width := newbuf.Width()
187 for i := y; i < y+n && i < height && i < len(newbuf.Touched); i++ {
188 if changed {
189 newbuf.TouchLine(0, i, width)
190 } else {
191 newbuf.Touched[i] = nil
192 }
193 }
194}
195
196// scrollUp scrolls the screen up by n lines.
197func (s *TerminalRenderer) scrollUp(newbuf *Buffer, n, top, bot, minY, maxY int, blank *Cell) bool {
198 if n == 1 && top == minY && bot == maxY {
199 s.move(newbuf, 0, bot)
200 s.updatePen(blank)
201 s.buf.WriteByte('\n')
202 } else if n == 1 && bot == maxY {
203 s.move(newbuf, 0, top)
204 s.updatePen(blank)
205 s.buf.WriteString(ansi.DeleteLine(1))
206 } else if top == minY && bot == maxY {
207 supportsSU := s.caps.Contains(capSU)
208 if supportsSU {
209 s.move(newbuf, 0, bot)
210 } else {
211 s.move(newbuf, 0, top)
212 }
213 s.updatePen(blank)
214 if supportsSU {
215 s.buf.WriteString(ansi.ScrollUp(n))
216 } else {
217 s.buf.WriteString(strings.Repeat("\n", n))
218 }
219 } else if bot == maxY {
220 s.move(newbuf, 0, top)
221 s.updatePen(blank)
222 s.buf.WriteString(ansi.DeleteLine(n))
223 } else {
224 return false
225 }
226 return true
227}
228
229// scrollDown scrolls the screen down by n lines.
230func (s *TerminalRenderer) scrollDown(newbuf *Buffer, n, top, bot, minY, maxY int, blank *Cell) bool {
231 if n == 1 && top == minY && bot == maxY {
232 s.move(newbuf, 0, top)
233 s.updatePen(blank)
234 s.buf.WriteString(ansi.ReverseIndex)
235 } else if n == 1 && bot == maxY {
236 s.move(newbuf, 0, top)
237 s.updatePen(blank)
238 s.buf.WriteString(ansi.InsertLine(1))
239 } else if top == minY && bot == maxY {
240 s.move(newbuf, 0, top)
241 s.updatePen(blank)
242 if s.caps.Contains(capSD) {
243 s.buf.WriteString(ansi.ScrollDown(n))
244 } else {
245 s.buf.WriteString(strings.Repeat(ansi.ReverseIndex, n))
246 }
247 } else if bot == maxY {
248 s.move(newbuf, 0, top)
249 s.updatePen(blank)
250 s.buf.WriteString(ansi.InsertLine(n))
251 } else {
252 return false
253 }
254 return true
255}
256
257// scrollIdl scrolls the screen n lines by using [ansi.DL] at del and using
258// [ansi.IL] at ins.
259func (s *TerminalRenderer) scrollIdl(newbuf *Buffer, n, del, ins int, blank *Cell) bool {
260 if n < 0 {
261 return false
262 }
263
264 // Delete lines
265 s.move(newbuf, 0, del)
266 s.updatePen(blank)
267 s.buf.WriteString(ansi.DeleteLine(n))
268
269 // Insert lines
270 s.move(newbuf, 0, ins)
271 s.updatePen(blank)
272 s.buf.WriteString(ansi.InsertLine(n))
273
274 return true
275}