1package vt
2
3import (
4 "errors"
5
6 renderer "github.com/bnema/vev-vt/core"
7)
8
9// index moves to the next physical row, scrolling the region when the cursor
10// already sits on its last row. It reports whether the row the cursor was on
11// survived that scroll by moving up to s.Row-1, which only a deferred wrap
12// cares about: a wrap must relink to the row it flowed out of, and
13// buffer.scrollUp severs that link on the way. Callers that merely advance,
14// such as LF and ESC D, ignore the result.
15func (s *Screen) index() (movedUp bool) {
16 if s.Frame.Height == 0 {
17 return false
18 }
19 if s.Row == s.scrollBottom && s.Row >= s.scrollTop {
20 return s.scrollUpRegion(s.scrollTop, s.scrollBottom, 1)
21 }
22 if s.Row+1 < s.Frame.Height {
23 s.Row++
24 return false
25 }
26 s.Row = s.Frame.Height - 1
27 return false
28}
29
30func (s *Screen) nextLine() {
31 s.buffer.hard(s.Row)
32 s.Col = 0
33 s.index()
34}
35
36func (s *Screen) reverseIndex() {
37 if s.Frame.Height == 0 {
38 return
39 }
40 if s.Row == s.scrollTop && s.Row <= s.scrollBottom {
41 s.scrollDownRegion(s.scrollTop, s.scrollBottom, 1)
42 return
43 }
44 if s.Row > 0 {
45 s.Row--
46 }
47}
48
49// scrollUpBy scrolls the current scroll region up by n lines (CSI S / SU).
50// The cursor position is preserved.
51func (s *Screen) scrollUpBy(n int) {
52 if n <= 0 {
53 return
54 }
55 s.scrollUpRegion(s.scrollTop, s.scrollBottom, n)
56}
57
58func (s *Screen) scrollDownBy(n int) {
59 if n <= 0 {
60 return
61 }
62 s.scrollDownRegion(s.scrollTop, s.scrollBottom, n)
63}
64
65// scrollUpRegion scrolls [top,bottom] up by n. It reports whether any row
66// survived by moving up: a region exactly n rows tall is blanked in place, so
67// the receiving range [top,bottom-n] is empty and nothing survives. Reporting
68// what the scroll observed keeps callers from re-deriving a predicate that this
69// function may have declined to act on at all.
70func (s *Screen) scrollUpRegion(top, bottom, n int) (shifted bool) {
71 if s.Frame.Width == 0 || s.Frame.Height == 0 || n <= 0 {
72 return false
73 }
74 top, bottom, ok := s.normalizedRegion(top, bottom)
75 if !ok {
76 return false
77 }
78 w := s.Frame.Width
79 height := bottom - top + 1
80 if n > height {
81 n = height
82 }
83 // VT scroll regions always span the full frame width, so we rotate the
84 // frame's line offsets (recycling and blanking the evicted rows in place)
85 // instead of copying cells. See renderer.Frame.ScrollUp.
86 s.emitLineEvicted(top, n)
87 s.Frame.ScrollUp(top, bottom, n)
88 s.buffer.scrollUp(top, bottom, n)
89 s.fillMissingRowIDs(s.buffer)
90 s.record(renderer.Damage{Kind: renderer.DamageScrollUp, X: 0, Y: top, Width: w, Height: height, Count: n})
91 s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: bottom - n + 1, Width: w, Height: n, Count: 1})
92 return bottom-n >= top
93}
94
95func (s *Screen) emitLineEvicted(top, n int) {
96 // Only rows leaving the top edge of the primary screen belong to global
97 // scrollback. Interior DECSTBM scroll regions are local mutations.
98 if s.alternate != nil || top != 0 {
99 return
100 }
101 // Read boundaries and IDs before the caller rotates the frame: a soft link
102 // belongs to the row it follows, and rotation reassigns row indices.
103 for y := top; y < top+n; y++ {
104 s.recordEvicted(s.Frame.Row(y), s.buffer.bound(y), s.buffer.rowIDs[y])
105 }
106}
107
108func (s *Screen) recordEvicted(row []renderer.Cell, bound LineBound, id RowID) {
109 if s.history != nil {
110 err := s.history.AppendWithID(row, bound, id)
111 if err != nil && !errors.Is(err, ErrHistoryRowTooWide) {
112 panic(err)
113 }
114 // A nil error records the row; ErrHistoryRowTooWide explicitly leaves it
115 // unrecorded. Both cases continue to the eviction observer below.
116 }
117 if s.OnLineEvicted != nil {
118 s.OnLineEvicted(append([]renderer.Cell(nil), row...))
119 }
120}
121
122func (s *Screen) scrollDownRegion(top, bottom, n int) {
123 if s.Frame.Width == 0 || s.Frame.Height == 0 || n <= 0 {
124 return
125 }
126 top, bottom, ok := s.normalizedRegion(top, bottom)
127 if !ok {
128 return
129 }
130 w := s.Frame.Width
131 height := bottom - top + 1
132 if n > height {
133 n = height
134 }
135 // Full-width region: rotate line offsets instead of copying cells.
136 s.Frame.ScrollDown(top, bottom, n)
137 s.buffer.scrollDown(top, bottom, n)
138 s.fillMissingRowIDs(s.buffer)
139 s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: top, Width: w, Height: height, Count: 1})
140}
141
142func (s *Screen) normalizedRegion(top, bottom int) (int, int, bool) {
143 if s.Frame.Height == 0 || top > bottom {
144 return 0, 0, false
145 }
146 top = clamp(top, 0, s.Frame.Height-1)
147 bottom = clamp(bottom, 0, s.Frame.Height-1)
148 return top, bottom, top <= bottom
149}