turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

screen.go · 251 lines · 7.8 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 14h ago1// Package terminal is a terminal emulator: a screen a program can write to
2// with escape sequences, a parser that turns a byte stream into changes to
3// that screen, and a pseudo-terminal to run a shell in.
4//
5// The screen and the parser know nothing about pseudo-terminals, processes or
6// tcell events. They are a pure function from bytes to a grid of cells, which
7// is what makes an emulator — the part that is otherwise hardest to be sure of
8// — testable by writing bytes in and reading cells out.
9package terminal
10
11import "github.com/gdamore/tcell/v2"
12
13// Cell is one character position on the screen.
14type Cell struct {
15 Rune rune
16 Style tcell.Style
17}
18
19// blank is what an erased cell holds. Erasing keeps the current background —
20// that is what makes a program painting a coloured panel and then clearing a
21// line inside it leave the panel's colour behind rather than a hole.
22func blank(style tcell.Style) Cell {
23 return Cell{Rune: ' ', Style: style}
24}
25
26// Line is one row of the grid.
27type Line []Cell
28
29// Cursor is where the next character goes, in zero-based rows and columns.
30type Cursor struct {
31 Row int
32 Col int
33}
34
35// DefaultScrollback is how many lines that have scrolled off the top are kept
36// so they can be scrolled back to.
37const DefaultScrollback = 2000
38
39// Screen is the grid a terminal program writes onto: its cells, the cursor,
40// the region that scrolls, and the lines that have scrolled off the top.
41//
42// Every method is a terminal operation rather than a drawing one, so the
43// parser can express a whole escape sequence as one call.
44//
45// s := terminal.NewScreen(80, 24)
46// s.WriteRune('h')
47// s.WriteRune('i')
48// fmt.Println(s.LineText(0)) // "hi"
49type Screen struct {
50 width int
51 height int
52 lines []Line
53
54 scrollback []Line
55 maxScrollback int
56
57 cursor Cursor
58 style tcell.Style
59
60 savedCursor Cursor
61 savedStyle tcell.Style
62
63 // The scrolling region, as inclusive row numbers. A program narrows it to
64 // keep a status line still while the rest scrolls.
65 top int
66 bottom int
67
68 cursorVisible bool
69 autoWrap bool
70
71 // applicationCursor is DECCKM. When a program sets it, the arrow keys are
72 // expected to arrive as ESC O A rather than ESC [ A — which is what makes
73 // them work inside vim and at a readline prompt.
74 applicationCursor bool
75
76 // wrapPending is set once a character has been written into the last
77 // column. The cursor stays there, and only the *next* character moves it
78 // to the following line — which is why a character written in the last
79 // column does not scroll the screen by itself.
80 wrapPending bool
81
82 // The primary grid, kept aside while the alternate screen is showing. A
83 // full-screen program such as vim asks for the alternate screen so that
84 // what was on the terminal before comes back when it exits.
85 alternate bool
86 primaryLines []Line
87 primaryCursor Cursor
88 primaryStyle tcell.Style
89 primaryHistory []Line
90}
91
92// NewScreen returns a blank screen of the given size, with the cursor at the
93// top left. A size below one in either direction is raised to one, so there is
94// always somewhere for the cursor to be.
95func NewScreen(width, height int) *Screen {
96 s := &Screen{
97 width: max(width, 1),
98 height: max(height, 1),
99 maxScrollback: DefaultScrollback,
100 style: tcell.StyleDefault,
101 cursorVisible: true,
102 autoWrap: true,
103 }
104 s.lines = s.blankLines(s.height)
105 s.resetScrollRegion()
106 return s
107}
108
109// blankLines returns n empty rows in the screen's current style.
110func (s *Screen) blankLines(n int) []Line {
111 lines := make([]Line, n)
112 for i := range lines {
113 lines[i] = s.blankLine()
114 }
115 return lines
116}
117
118// blankLine returns one empty row.
119func (s *Screen) blankLine() Line {
120 line := make(Line, s.width)
121 for i := range line {
122 line[i] = blank(s.style)
123 }
124 return line
125}
126
127// resetScrollRegion opens the scrolling region back up to the whole screen.
128func (s *Screen) resetScrollRegion() {
129 s.top, s.bottom = 0, s.height-1
130}
131
132// Size returns the screen's width and height in cells.
133func (s *Screen) Size() (width, height int) { return s.width, s.height }
134
135// Cursor returns where the next character will go.
136func (s *Screen) Cursor() Cursor { return s.cursor }
137
138// CursorVisible reports whether the program has asked for the cursor to be
139// shown. A full-screen program hides it while it repaints.
140func (s *Screen) CursorVisible() bool { return s.cursorVisible }
141
142// SetCursorVisible shows or hides the cursor.
143func (s *Screen) SetCursorVisible(visible bool) { s.cursorVisible = visible }
144
145// Style returns the style the next character will be written in.
146func (s *Screen) Style() tcell.Style { return s.style }
147
148// SetStyle sets the style the next characters will be written in.
149func (s *Screen) SetStyle(style tcell.Style) { s.style = style }
150
151// AutoWrap reports whether a character written past the right edge moves to
152// the next line.
153func (s *Screen) AutoWrap() bool { return s.autoWrap }
154
155// ApplicationCursor reports whether the program has asked for application
156// cursor keys. Whoever encodes key presses must ask, or the arrow keys will be
157// wrong in every full-screen program.
158func (s *Screen) ApplicationCursor() bool { return s.applicationCursor }
159
160// SetApplicationCursor turns application cursor keys on or off.
161func (s *Screen) SetApplicationCursor(on bool) { s.applicationCursor = on }
162
163// SetAutoWrap turns wrapping at the right edge on or off.
164func (s *Screen) SetAutoWrap(wrap bool) {
165 s.autoWrap = wrap
166 s.wrapPending = false
167}
168
169// Alternate reports whether the alternate screen is showing.
170func (s *Screen) Alternate() bool { return s.alternate }
171
172// CellAt returns the cell at a position, or a blank one when the position is
173// off the screen. Callers may therefore index freely while drawing.
174func (s *Screen) CellAt(row, col int) Cell {
175 if row < 0 || row >= s.height || col < 0 || col >= s.width {
176 return blank(tcell.StyleDefault)
177 }
178 return s.lines[row][col]
179}
180
181// Line returns a copy of one row, or nil when the row is off the screen.
182func (s *Screen) Line(row int) Line {
183 if row < 0 || row >= s.height {
184 return nil
185 }
186 line := make(Line, s.width)
187 copy(line, s.lines[row])
188 return line
189}
190
191// LineText returns a row as a string with its trailing blanks removed, which
192// is what a test wants to assert on.
193func (s *Screen) LineText(row int) string {
194 if row < 0 || row >= s.height {
195 return ""
196 }
197
198 runes := make([]rune, s.width)
199 for i, cell := range s.lines[row] {
200 runes[i] = cell.Rune
201 }
202
203 end := len(runes)
204 for end > 0 && runes[end-1] == ' ' {
205 end--
206 }
207 return string(runes[:end])
208}
209
210// ScrollbackLen returns how many lines have scrolled off the top and are still
211// remembered.
212func (s *Screen) ScrollbackLen() int { return len(s.scrollback) }
213
214// ScrollbackLine returns a line that has scrolled off the top, counting from
215// the oldest, or nil when there is no such line.
216func (s *Screen) ScrollbackLine(i int) Line {
217 if i < 0 || i >= len(s.scrollback) {
218 return nil
219 }
220 line := make(Line, len(s.scrollback[i]))
221 copy(line, s.scrollback[i])
222 return line
223}
224
225// SetMaxScrollback caps how many scrolled-off lines are kept. Lowering it
226// drops the oldest straight away.
227func (s *Screen) SetMaxScrollback(n int) {
228 s.maxScrollback = max(n, 0)
229 s.trimScrollback()
230}
231
232// trimScrollback drops the oldest lines once there are too many.
233func (s *Screen) trimScrollback() {
234 if len(s.scrollback) > s.maxScrollback {
235 s.scrollback = s.scrollback[len(s.scrollback)-s.maxScrollback:]
236 }
237}
238
239// remember puts a line into the scrollback.
240//
241// Only lines leaving the top of a *full-height* region on the primary screen
242// are kept: a program scrolling a narrow region is redrawing part of its own
243// display, and an alternate screen is a scratch surface whose contents are not
244// history.
245func (s *Screen) remember(line Line) {
246 if s.alternate || s.top != 0 || s.maxScrollback == 0 {
247 return
248 }
249 s.scrollback = append(s.scrollback, line)
250 s.trimScrollback()
251}