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