turbo-editors/turbo-corepublic Fork 0
v1.0.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.

🛟 Updated. 28d5985 · on v1.0.0 · k33g · 13h ago
screen.go · 251 lines · 7.8 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
// Package terminal is a terminal emulator: a screen a program can write to
// with escape sequences, a parser that turns a byte stream into changes to
// that screen, and a pseudo-terminal to run a shell in.
//
// The screen and the parser know nothing about pseudo-terminals, processes or
// tcell events. They are a pure function from bytes to a grid of cells, which
// is what makes an emulator — the part that is otherwise hardest to be sure of
// — testable by writing bytes in and reading cells out.
package terminal

import "github.com/gdamore/tcell/v2"

// Cell is one character position on the screen.
type Cell struct {
	Rune  rune
	Style tcell.Style
}

// blank is what an erased cell holds. Erasing keeps the current background —
// that is what makes a program painting a coloured panel and then clearing a
// line inside it leave the panel's colour behind rather than a hole.
func blank(style tcell.Style) Cell {
	return Cell{Rune: ' ', Style: style}
}

// Line is one row of the grid.
type Line []Cell

// Cursor is where the next character goes, in zero-based rows and columns.
type Cursor struct {
	Row int
	Col int
}

// DefaultScrollback is how many lines that have scrolled off the top are kept
// so they can be scrolled back to.
const DefaultScrollback = 2000

// Screen is the grid a terminal program writes onto: its cells, the cursor,
// the region that scrolls, and the lines that have scrolled off the top.
//
// Every method is a terminal operation rather than a drawing one, so the
// parser can express a whole escape sequence as one call.
//
//	s := terminal.NewScreen(80, 24)
//	s.WriteRune('h')
//	s.WriteRune('i')
//	fmt.Println(s.LineText(0)) // "hi"
type Screen struct {
	width  int
	height int
	lines  []Line

	scrollback    []Line
	maxScrollback int

	cursor Cursor
	style  tcell.Style

	savedCursor Cursor
	savedStyle  tcell.Style

	// The scrolling region, as inclusive row numbers. A program narrows it to
	// keep a status line still while the rest scrolls.
	top    int
	bottom int

	cursorVisible bool
	autoWrap      bool

	// applicationCursor is DECCKM. When a program sets it, the arrow keys are
	// expected to arrive as ESC O A rather than ESC [ A — which is what makes
	// them work inside vim and at a readline prompt.
	applicationCursor bool

	// wrapPending is set once a character has been written into the last
	// column. The cursor stays there, and only the *next* character moves it
	// to the following line — which is why a character written in the last
	// column does not scroll the screen by itself.
	wrapPending bool

	// The primary grid, kept aside while the alternate screen is showing. A
	// full-screen program such as vim asks for the alternate screen so that
	// what was on the terminal before comes back when it exits.
	alternate      bool
	primaryLines   []Line
	primaryCursor  Cursor
	primaryStyle   tcell.Style
	primaryHistory []Line
}

// NewScreen returns a blank screen of the given size, with the cursor at the
// top left. A size below one in either direction is raised to one, so there is
// always somewhere for the cursor to be.
func NewScreen(width, height int) *Screen {
	s := &Screen{
		width:         max(width, 1),
		height:        max(height, 1),
		maxScrollback: DefaultScrollback,
		style:         tcell.StyleDefault,
		cursorVisible: true,
		autoWrap:      true,
	}
	s.lines = s.blankLines(s.height)
	s.resetScrollRegion()
	return s
}

// blankLines returns n empty rows in the screen's current style.
func (s *Screen) blankLines(n int) []Line {
	lines := make([]Line, n)
	for i := range lines {
		lines[i] = s.blankLine()
	}
	return lines
}

// blankLine returns one empty row.
func (s *Screen) blankLine() Line {
	line := make(Line, s.width)
	for i := range line {
		line[i] = blank(s.style)
	}
	return line
}

// resetScrollRegion opens the scrolling region back up to the whole screen.
func (s *Screen) resetScrollRegion() {
	s.top, s.bottom = 0, s.height-1
}

// Size returns the screen's width and height in cells.
func (s *Screen) Size() (width, height int) { return s.width, s.height }

// Cursor returns where the next character will go.
func (s *Screen) Cursor() Cursor { return s.cursor }

// CursorVisible reports whether the program has asked for the cursor to be
// shown. A full-screen program hides it while it repaints.
func (s *Screen) CursorVisible() bool { return s.cursorVisible }

// SetCursorVisible shows or hides the cursor.
func (s *Screen) SetCursorVisible(visible bool) { s.cursorVisible = visible }

// Style returns the style the next character will be written in.
func (s *Screen) Style() tcell.Style { return s.style }

// SetStyle sets the style the next characters will be written in.
func (s *Screen) SetStyle(style tcell.Style) { s.style = style }

// AutoWrap reports whether a character written past the right edge moves to
// the next line.
func (s *Screen) AutoWrap() bool { return s.autoWrap }

// ApplicationCursor reports whether the program has asked for application
// cursor keys. Whoever encodes key presses must ask, or the arrow keys will be
// wrong in every full-screen program.
func (s *Screen) ApplicationCursor() bool { return s.applicationCursor }

// SetApplicationCursor turns application cursor keys on or off.
func (s *Screen) SetApplicationCursor(on bool) { s.applicationCursor = on }

// SetAutoWrap turns wrapping at the right edge on or off.
func (s *Screen) SetAutoWrap(wrap bool) {
	s.autoWrap = wrap
	s.wrapPending = false
}

// Alternate reports whether the alternate screen is showing.
func (s *Screen) Alternate() bool { return s.alternate }

// CellAt returns the cell at a position, or a blank one when the position is
// off the screen. Callers may therefore index freely while drawing.
func (s *Screen) CellAt(row, col int) Cell {
	if row < 0 || row >= s.height || col < 0 || col >= s.width {
		return blank(tcell.StyleDefault)
	}
	return s.lines[row][col]
}

// Line returns a copy of one row, or nil when the row is off the screen.
func (s *Screen) Line(row int) Line {
	if row < 0 || row >= s.height {
		return nil
	}
	line := make(Line, s.width)
	copy(line, s.lines[row])
	return line
}

// LineText returns a row as a string with its trailing blanks removed, which
// is what a test wants to assert on.
func (s *Screen) LineText(row int) string {
	if row < 0 || row >= s.height {
		return ""
	}

	runes := make([]rune, s.width)
	for i, cell := range s.lines[row] {
		runes[i] = cell.Rune
	}

	end := len(runes)
	for end > 0 && runes[end-1] == ' ' {
		end--
	}
	return string(runes[:end])
}

// ScrollbackLen returns how many lines have scrolled off the top and are still
// remembered.
func (s *Screen) ScrollbackLen() int { return len(s.scrollback) }

// ScrollbackLine returns a line that has scrolled off the top, counting from
// the oldest, or nil when there is no such line.
func (s *Screen) ScrollbackLine(i int) Line {
	if i < 0 || i >= len(s.scrollback) {
		return nil
	}
	line := make(Line, len(s.scrollback[i]))
	copy(line, s.scrollback[i])
	return line
}

// SetMaxScrollback caps how many scrolled-off lines are kept. Lowering it
// drops the oldest straight away.
func (s *Screen) SetMaxScrollback(n int) {
	s.maxScrollback = max(n, 0)
	s.trimScrollback()
}

// trimScrollback drops the oldest lines once there are too many.
func (s *Screen) trimScrollback() {
	if len(s.scrollback) > s.maxScrollback {
		s.scrollback = s.scrollback[len(s.scrollback)-s.maxScrollback:]
	}
}

// remember puts a line into the scrollback.
//
// Only lines leaving the top of a *full-height* region on the primary screen
// are kept: a program scrolling a narrow region is redrawing part of its own
// display, and an alternate screen is a scratch surface whose contents are not
// history.
func (s *Screen) remember(line Line) {
	if s.alternate || s.top != 0 || s.maxScrollback == 0 {
		return
	}
	s.scrollback = append(s.scrollback, line)
	s.trimScrollback()
}