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