1package vt
2
3import (
4 renderer "github.com/bnema/vev-vt/core"
5 "github.com/bnema/vev-vt/protocol/kittygraphics"
6)
7
8type screenState struct {
9 frame renderer.Frame
10 buffer *buffer
11 row int
12 col int
13 style renderer.Style
14 scrollTop int
15 scrollBottom int
16 savedCursor cursorState
17 graphics *screenGraphicsState
18 kittyPendingDisplay *kittygraphics.Controls
19 originMode bool
20 insertMode bool
21}
22
23// SyncUpdateActive reports whether DEC private mode 2026 (synchronized update)
24// is currently enabled by the child process.
25func (s *Screen) SyncUpdateActive() bool { return s.syncUpdateActive }
26
27// BracketedPasteMode reports whether DEC private mode 2004 is currently enabled
28// by the child process.
29func (s *Screen) BracketedPasteMode() bool { return s.bracketedPaste }
30
31// ColorSchemeMode reports whether DEC private mode 2031 is currently enabled.
32func (s *Screen) ColorSchemeMode() bool { return s.colorSchemeMode }
33
34// SetColorScheme updates the host color scheme and notifies subscribed child apps.
35func (s *Screen) SetColorScheme(light bool) {
36 if s.colorSchemeSet && s.colorSchemeLight == light {
37 return
38 }
39 s.colorSchemeSet = true
40 s.colorSchemeLight = light
41 if s.colorSchemeMode {
42 s.respond(s.colorSchemeReport())
43 }
44}
45
46// ClearColorScheme marks the host color scheme as unknown. Future child color
47// scheme queries are silent until SetColorScheme supplies a known value again.
48func (s *Screen) ClearColorScheme() {
49 s.colorSchemeSet = false
50}
51
52// ForceSyncEnd forcibly leaves DEC private mode 2026 (synchronized update).
53// Hosts use this as a safety valve if a child enters synchronized update mode
54// and never sends the matching end sequence.
55func (s *Screen) ForceSyncEnd() { s.syncUpdateActive = false }
56
57// SetDefaultColors sets the terminal default foreground/background colors used
58// to answer child OSC 10/11 color queries. Passing ok=false makes color queries
59// silent until known colors are supplied again.
60func (s *Screen) SetDefaultColors(fg, bg renderer.RGB, ok bool) {
61 s.defaultFG = fg
62 s.defaultBG = bg
63 s.defaultColorsKnown = ok
64}
65
66func (s *Screen) CursorRow() int { return s.Row }
67func (s *Screen) CursorCol() int { return s.Col }
68func (s *Screen) CursorVisible() bool {
69 return s.cursorVisible
70}
71func (s *Screen) CursorStyle() (int, bool) { return s.cursorStyle, s.cursorStyleSet }
72func (s *Screen) MouseMode() (int, bool) { return s.mouseMode, s.mouseSGR }
73func (s *Screen) AltScreenActive() bool { return s.alternate != nil }
74
75// TerminalTitle returns the latest title set by OSC 0 or OSC 2.
76func (s *Screen) TerminalTitle() string { return s.terminalTitle }
77
78func (s *Screen) respond(b []byte) {
79 if s.OnResponse != nil {
80 s.OnResponse(b)
81 }
82}
83
84func (s *Screen) colorSchemeReport() []byte {
85 if !s.colorSchemeSet {
86 return nil
87 }
88 if s.colorSchemeLight {
89 return []byte(ColorSchemeReportLight)
90 }
91 return []byte(ColorSchemeReportDark)
92}
93
94func (s *Screen) reset() {
95 s.buffer = s.newBuffer(s.Frame.Width, s.Frame.Height)
96 s.Frame = s.buffer.frame
97 s.Row, s.Col = 0, 0
98 s.Style = renderer.DefaultStyle()
99 s.escapeBuf = s.escapeBuf[:0]
100 s.kittyDiscard = false
101 s.kittyDiscardEscaped = false
102 s.kittyPendingDisplay = nil
103 s.savedCursor = cursorState{}
104 s.alternate = nil
105 s.graphics = nil
106 s.originMode = false
107 s.insertMode = false
108 s.cursorVisible = true
109 s.cursorStyle = 0
110 s.cursorStyleSet = false
111 s.mouseMode = 0
112 s.mouseSGR = false
113 s.bracketedPaste = false
114 s.colorSchemeMode = false
115 s.resetScrollRegion()
116 s.fullRedraw()
117}
118
119func (s *Screen) setScrollRegion(parts []int) {
120 if s.Frame.Height == 0 {
121 return
122 }
123 top, bottom := 1, s.Frame.Height
124 if len(parts) > 0 && parts[0] > 0 {
125 top = parts[0]
126 }
127 if len(parts) > 1 && parts[1] > 0 {
128 bottom = parts[1]
129 }
130 if top >= bottom {
131 s.resetScrollRegion()
132 } else {
133 s.scrollTop = clamp(top-1, 0, s.Frame.Height-1)
134 s.scrollBottom = clamp(bottom-1, 0, s.Frame.Height-1)
135 if s.scrollTop >= s.scrollBottom {
136 s.resetScrollRegion()
137 }
138 }
139 s.homeCursor()
140}
141
142func (s *Screen) resetScrollRegion() {
143 s.scrollTop = 0
144 if s.Frame.Height > 0 {
145 s.scrollBottom = s.Frame.Height - 1
146 } else {
147 s.scrollBottom = 0
148 }
149}
150
151func (s *Screen) setMode(private bool, parts []int, enabled bool) {
152 if !private {
153 for _, mode := range parts {
154 switch mode {
155 case 4:
156 s.insertMode = enabled
157 default:
158 // Other ANSI modes (for example LNM) are intentionally ignored
159 // until the screen model implements their observable behavior.
160 continue
161 }
162 }
163 return
164 }
165 for _, mode := range parts {
166 switch mode {
167 case 6:
168 s.originMode = enabled
169 s.homeCursor()
170 case 47, 1047, 1049:
171 if enabled {
172 s.enterAlternateScreen()
173 } else {
174 s.exitAlternateScreen()
175 }
176 case 2026:
177 s.syncUpdateActive = enabled
178 case 2031:
179 s.colorSchemeMode = enabled
180 case 25:
181 s.cursorVisible = enabled
182 case 1000, 1002, 1003:
183 if enabled {
184 s.mouseMode = mode
185 } else if s.mouseMode == mode {
186 s.mouseMode = 0
187 }
188 case 1006:
189 s.mouseSGR = enabled
190 case 2004:
191 s.bracketedPaste = enabled
192 case 1, 1004, 1005:
193 // Trackable terminal modes that do not directly affect the current
194 // cell model yet. Consuming them prevents mode bytes from leaking.
195 continue
196 }
197 }
198}
199
200func (s *Screen) enterAlternateScreen() {
201 if s.alternate == nil {
202 s.alternate = &screenState{
203 frame: cloneFrame(s.Frame),
204 buffer: s.buffer.clone(),
205 row: s.Row,
206 col: s.Col,
207 style: s.Style,
208 scrollTop: s.scrollTop,
209 scrollBottom: s.scrollBottom,
210 savedCursor: s.savedCursor,
211 graphics: s.graphics,
212 kittyPendingDisplay: s.kittyPendingDisplay,
213 originMode: s.originMode,
214 insertMode: s.insertMode,
215 }
216 }
217 s.graphics = nil
218 s.kittyPendingDisplay = nil
219 s.buffer = s.newBuffer(s.Frame.Width, s.Frame.Height)
220 s.Frame = s.buffer.frame
221 s.Row, s.Col = 0, 0
222 s.Style = renderer.DefaultStyle()
223 s.savedCursor = cursorState{}
224 s.resetScrollRegion()
225 s.fullRedraw()
226}
227
228func (s *Screen) exitAlternateScreen() {
229 if s.alternate == nil {
230 return
231 }
232 state := s.alternate
233 s.buffer = state.buffer
234 if s.buffer == nil {
235 s.buffer = bufferFromFrame(cloneFrame(state.frame))
236 s.fillMissingRowIDs(s.buffer)
237 }
238 s.Frame = s.buffer.frame
239 s.Row = clamp(state.row, 0, s.Frame.Height-1)
240 s.Col = clamp(state.col, 0, s.Frame.Width-1)
241 s.Style = state.style
242 s.scrollTop = state.scrollTop
243 s.scrollBottom = state.scrollBottom
244 s.savedCursor = state.savedCursor
245 s.graphics = state.graphics
246 s.kittyPendingDisplay = state.kittyPendingDisplay
247 s.originMode = state.originMode
248 s.insertMode = state.insertMode
249 s.alternate = nil
250 s.fullRedraw()
251}