| 🛟 Updated. 28d5985 k33g 21h ago | 1 | package terminal |
| 2 | |
| 3 | import "github.com/gdamore/tcell/v2" |
| 4 | |
| 5 | // EraseMode says how much of the display or the line an erase covers. |
| 6 | type EraseMode int |
| 7 | |
| 8 | // The three erase modes the protocol defines. |
| 9 | const ( |
| 10 | // EraseToEnd clears from the cursor to the end, cursor included. |
| 11 | EraseToEnd EraseMode = iota |
| 12 | // EraseToStart clears from the start to the cursor, cursor included. |
| 13 | EraseToStart |
| 14 | // EraseAll clears the whole display or line. |
| 15 | EraseAll |
| 16 | ) |
| 17 | |
| 18 | // EraseDisplay clears part or all of the screen, as ED does. The cursor does |
| 19 | // not move: a program that wants it elsewhere says so separately. |
| 20 | func (s *Screen) EraseDisplay(mode EraseMode) { |
| 21 | switch mode { |
| 22 | case EraseToEnd: |
| 23 | s.eraseLineRange(s.cursor.Row, s.cursor.Col, s.width-1) |
| 24 | s.blankRows(s.cursor.Row+1, s.height-1) |
| 25 | case EraseToStart: |
| 26 | s.blankRows(0, s.cursor.Row-1) |
| 27 | s.eraseLineRange(s.cursor.Row, 0, s.cursor.Col) |
| 28 | case EraseAll: |
| 29 | s.blankRows(0, s.height-1) |
| 30 | } |
| 31 | s.wrapPending = false |
| 32 | } |
| 33 | |
| 34 | // EraseLine clears part or all of the cursor's line, as EL does. |
| 35 | func (s *Screen) EraseLine(mode EraseMode) { |
| 36 | switch mode { |
| 37 | case EraseToEnd: |
| 38 | s.eraseLineRange(s.cursor.Row, s.cursor.Col, s.width-1) |
| 39 | case EraseToStart: |
| 40 | s.eraseLineRange(s.cursor.Row, 0, s.cursor.Col) |
| 41 | case EraseAll: |
| 42 | s.eraseLineRange(s.cursor.Row, 0, s.width-1) |
| 43 | } |
| 44 | s.wrapPending = false |
| 45 | } |
| 46 | |
| 47 | // EraseChars blanks n characters from the cursor rightwards without moving |
| 48 | // anything along, as ECH does. |
| 49 | func (s *Screen) EraseChars(n int) { |
| 50 | s.eraseLineRange(s.cursor.Row, s.cursor.Col, s.cursor.Col+max(n, 1)-1) |
| 51 | s.wrapPending = false |
| 52 | } |
| 53 | |
| 54 | // blankRows clears whole rows, inclusive, ignoring any outside the screen. |
| 55 | func (s *Screen) blankRows(from, to int) { |
| 56 | for row := max(from, 0); row <= min(to, s.height-1); row++ { |
| 57 | s.lines[row] = s.blankLine() |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // eraseLineRange blanks the columns [from, to] of a row, inclusive. |
| 62 | func (s *Screen) eraseLineRange(row, from, to int) { |
| 63 | if row < 0 || row >= s.height { |
| 64 | return |
| 65 | } |
| 66 | for col := max(from, 0); col <= min(to, s.width-1); col++ { |
| 67 | s.lines[row][col] = blank(s.style) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // InsertLines opens n blank lines at the cursor's row, pushing the rest of the |
| 72 | // scrolling region down and off its bottom, as IL does. |
| 73 | // |
| 74 | // It does nothing when the cursor is outside the scrolling region, which is |
| 75 | // what the protocol requires and what stops a program with a status line from |
| 76 | // pushing that line about. |
| 77 | func (s *Screen) InsertLines(n int) { |
| 78 | if !s.cursorInRegion() { |
| 79 | return |
| 80 | } |
| 81 | s.scrollRegionFrom(s.cursor.Row, max(n, 1), down) |
| 82 | } |
| 83 | |
| 84 | // DeleteLines removes n lines at the cursor's row, pulling the rest of the |
| 85 | // scrolling region up and blanking its bottom, as DL does. |
| 86 | func (s *Screen) DeleteLines(n int) { |
| 87 | if !s.cursorInRegion() { |
| 88 | return |
| 89 | } |
| 90 | s.scrollRegionFrom(s.cursor.Row, max(n, 1), up) |
| 91 | } |
| 92 | |
| 93 | // direction says which way a partial scroll moves the lines. |
| 94 | type direction int |
| 95 | |
| 96 | const ( |
| 97 | up direction = iota |
| 98 | down |
| 99 | ) |
| 100 | |
| 101 | // scrollRegionFrom scrolls the part of the scrolling region that starts at a |
| 102 | // row, which is what insert-lines and delete-lines both do. |
| 103 | func (s *Screen) scrollRegionFrom(from, n int, dir direction) { |
| 104 | rows := s.bottom - from + 1 |
| 105 | n = min(n, rows) |
| 106 | |
| 107 | if dir == up { |
| 108 | copy(s.lines[from:s.bottom+1-n], s.lines[from+n:s.bottom+1]) |
| 109 | s.blankRows(s.bottom+1-n, s.bottom) |
| 110 | } else { |
| 111 | copy(s.lines[from+n:s.bottom+1], s.lines[from:s.bottom+1-n]) |
| 112 | s.blankRows(from, from+n-1) |
| 113 | } |
| 114 | s.wrapPending = false |
| 115 | } |
| 116 | |
| 117 | // cursorInRegion reports whether the cursor is inside the scrolling region. |
| 118 | func (s *Screen) cursorInRegion() bool { |
| 119 | return s.cursor.Row >= s.top && s.cursor.Row <= s.bottom |
| 120 | } |
| 121 | |
| 122 | // InsertChars opens n blank cells at the cursor, pushing the rest of the line |
| 123 | // right and off its end, as ICH does. |
| 124 | func (s *Screen) InsertChars(n int) { |
| 125 | line := s.lines[s.cursor.Row] |
| 126 | n = min(max(n, 1), s.width-s.cursor.Col) |
| 127 | |
| 128 | copy(line[s.cursor.Col+n:], line[s.cursor.Col:s.width-n]) |
| 129 | s.eraseLineRange(s.cursor.Row, s.cursor.Col, s.cursor.Col+n-1) |
| 130 | s.wrapPending = false |
| 131 | } |
| 132 | |
| 133 | // DeleteChars removes n cells at the cursor, pulling the rest of the line left |
| 134 | // and blanking its end, as DCH does. |
| 135 | func (s *Screen) DeleteChars(n int) { |
| 136 | line := s.lines[s.cursor.Row] |
| 137 | n = min(max(n, 1), s.width-s.cursor.Col) |
| 138 | |
| 139 | copy(line[s.cursor.Col:s.width-n], line[s.cursor.Col+n:]) |
| 140 | s.eraseLineRange(s.cursor.Row, s.width-n, s.width-1) |
| 141 | s.wrapPending = false |
| 142 | } |
| 143 | |
| 144 | // UseAlternate switches to the alternate screen and back. |
| 145 | // |
| 146 | // The alternate screen is a blank scratch surface with no scrollback: a |
| 147 | // full-screen program asks for it so that whatever the terminal showed before |
| 148 | // comes back untouched when the program exits. |
| 149 | func (s *Screen) UseAlternate(alternate bool) { |
| 150 | if alternate == s.alternate { |
| 151 | return |
| 152 | } |
| 153 | |
| 154 | if alternate { |
| 155 | s.primaryLines, s.primaryCursor = s.lines, s.cursor |
| 156 | s.primaryStyle, s.primaryHistory = s.style, s.scrollback |
| 157 | |
| 158 | s.alternate = true |
| 159 | s.lines = s.blankLines(s.height) |
| 160 | s.scrollback = nil |
| 161 | s.MoveTo(0, 0) |
| 162 | s.resetScrollRegion() |
| 163 | return |
| 164 | } |
| 165 | |
| 166 | s.alternate = false |
| 167 | s.lines, s.cursor = s.primaryLines, s.primaryCursor |
| 168 | s.style, s.scrollback = s.primaryStyle, s.primaryHistory |
| 169 | s.primaryLines, s.primaryHistory = nil, nil |
| 170 | s.resetScrollRegion() |
| 171 | s.fitCursor() |
| 172 | } |
| 173 | |
| 174 | // Reset puts the screen back to how it started, as RIS does. |
| 175 | func (s *Screen) Reset() { |
| 176 | s.alternate = false |
| 177 | s.primaryLines, s.primaryHistory = nil, nil |
| 178 | |
| 179 | s.style = tcell.StyleDefault |
| 180 | s.savedStyle = tcell.StyleDefault |
| 181 | s.lines = s.blankLines(s.height) |
| 182 | s.scrollback = nil |
| 183 | |
| 184 | s.cursor, s.savedCursor = Cursor{}, Cursor{} |
| 185 | s.cursorVisible, s.autoWrap, s.wrapPending = true, true, false |
| 186 | s.applicationCursor = false |
| 187 | s.resetScrollRegion() |
| 188 | } |
| 189 | |
| 190 | // fitCursor pulls the cursor back onto the screen after a resize or a swap. |
| 191 | func (s *Screen) fitCursor() { |
| 192 | s.cursor.Row = min(max(s.cursor.Row, 0), s.height-1) |
| 193 | s.cursor.Col = min(max(s.cursor.Col, 0), s.width-1) |
| 194 | s.wrapPending = false |
| 195 | } |