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