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