| 🛟 Updated. 28d5985 k33g 18h ago | 1 | // 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. |
| 9 | package terminal |
| 10 | |
| 11 | import "github.com/gdamore/tcell/v2" |
| 12 | |
| 13 | // Cell is one character position on the screen. |
| 14 | type 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. |
| 22 | func blank(style tcell.Style) Cell { |
| 23 | return Cell{Rune: ' ', Style: style} |
| 24 | } |
| 25 | |
| 26 | // Line is one row of the grid. |
| 27 | type Line []Cell |
| 28 | |
| 29 | // Cursor is where the next character goes, in zero-based rows and columns. |
| 30 | type 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. |
| 37 | const 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" |
| 49 | type 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. |
| 95 | func 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. |
| 110 | func (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. |
| 119 | func (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. |
| 128 | func (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. |
| 133 | func (s *Screen) Size() (width, height int) { return s.width, s.height } |
| 134 | |
| 135 | // Cursor returns where the next character will go. |
| 136 | func (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. |
| 140 | func (s *Screen) CursorVisible() bool { return s.cursorVisible } |
| 141 | |
| 142 | // SetCursorVisible shows or hides the cursor. |
| 143 | func (s *Screen) SetCursorVisible(visible bool) { s.cursorVisible = visible } |
| 144 | |
| 145 | // Style returns the style the next character will be written in. |
| 146 | func (s *Screen) Style() tcell.Style { return s.style } |
| 147 | |
| 148 | // SetStyle sets the style the next characters will be written in. |
| 149 | func (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. |
| 153 | func (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. |
| 158 | func (s *Screen) ApplicationCursor() bool { return s.applicationCursor } |
| 159 | |
| 160 | // SetApplicationCursor turns application cursor keys on or off. |
| 161 | func (s *Screen) SetApplicationCursor(on bool) { s.applicationCursor = on } |
| 162 | |
| 163 | // SetAutoWrap turns wrapping at the right edge on or off. |
| 164 | func (s *Screen) SetAutoWrap(wrap bool) { |
| 165 | s.autoWrap = wrap |
| 166 | s.wrapPending = false |
| 167 | } |
| 168 | |
| 169 | // Alternate reports whether the alternate screen is showing. |
| 170 | func (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. |
| 174 | func (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. |
| 182 | func (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. |
| 193 | func (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. |
| 212 | func (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. |
| 216 | func (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. |
| 227 | func (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. |
| 233 | func (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. |
| 245 | func (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 | } |