| 🛟 Updated. 28d5985 k33g 15h ago | 1 | package terminal |
| 2 | |
| 3 | // WriteRune puts one character at the cursor and moves on. |
| 4 | // |
| 5 | // The cursor stops in the last column rather than moving past it: only the |
| 6 | // next character written wraps to the following line. That is what stops a |
| 7 | // character in the bottom-right corner from scrolling the screen by itself, |
| 8 | // which is the difference between a terminal that redraws correctly and one |
| 9 | // that creeps upwards. |
| 10 | func (s *Screen) WriteRune(r rune) { |
| 11 | if s.wrapPending && s.autoWrap { |
| 12 | s.CarriageReturn() |
| 13 | s.LineFeed() |
| 14 | } |
| 15 | s.wrapPending = false |
| 16 | |
| 17 | s.lines[s.cursor.Row][s.cursor.Col] = Cell{Rune: r, Style: s.style} |
| 18 | |
| 19 | if s.cursor.Col == s.width-1 { |
| 20 | s.wrapPending = true |
| 21 | return |
| 22 | } |
| 23 | s.cursor.Col++ |
| 24 | } |
| 25 | |
| 26 | // MoveTo puts the cursor at a position, clamped to the screen. |
| 27 | func (s *Screen) MoveTo(row, col int) { |
| 28 | s.cursor.Row = min(max(row, 0), s.height-1) |
| 29 | s.cursor.Col = min(max(col, 0), s.width-1) |
| 30 | s.wrapPending = false |
| 31 | } |
| 32 | |
| 33 | // MoveBy moves the cursor relative to where it is, clamped to the screen. |
| 34 | func (s *Screen) MoveBy(rows, cols int) { |
| 35 | s.MoveTo(s.cursor.Row+rows, s.cursor.Col+cols) |
| 36 | } |
| 37 | |
| 38 | // CarriageReturn takes the cursor to the first column of its line. |
| 39 | func (s *Screen) CarriageReturn() { |
| 40 | s.cursor.Col = 0 |
| 41 | s.wrapPending = false |
| 42 | } |
| 43 | |
| 44 | // LineFeed moves the cursor down a line, scrolling the region when it is |
| 45 | // already on the last row of it. |
| 46 | func (s *Screen) LineFeed() { |
| 47 | s.wrapPending = false |
| 48 | |
| 49 | if s.cursor.Row == s.bottom { |
| 50 | s.ScrollUp(1) |
| 51 | return |
| 52 | } |
| 53 | if s.cursor.Row < s.height-1 { |
| 54 | s.cursor.Row++ |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | // ReverseLineFeed moves the cursor up a line, scrolling the region down when |
| 59 | // it is already on the first row of it. |
| 60 | func (s *Screen) ReverseLineFeed() { |
| 61 | s.wrapPending = false |
| 62 | |
| 63 | if s.cursor.Row == s.top { |
| 64 | s.ScrollDown(1) |
| 65 | return |
| 66 | } |
| 67 | if s.cursor.Row > 0 { |
| 68 | s.cursor.Row-- |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // Backspace moves the cursor one column left, stopping at the first column. |
| 73 | // It does not erase: a program that wants the character gone writes a space. |
| 74 | func (s *Screen) Backspace() { |
| 75 | s.wrapPending = false |
| 76 | if s.cursor.Col > 0 { |
| 77 | s.cursor.Col-- |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | // TabWidth is how far apart the default tab stops are. |
| 82 | const TabWidth = 8 |
| 83 | |
| 84 | // Tab moves the cursor to the next tab stop, or to the last column when there |
| 85 | // is no further stop on the line. |
| 86 | func (s *Screen) Tab() { |
| 87 | s.wrapPending = false |
| 88 | next := (s.cursor.Col/TabWidth + 1) * TabWidth |
| 89 | s.cursor.Col = min(next, s.width-1) |
| 90 | } |
| 91 | |
| 92 | // SaveCursor remembers the cursor and the current style, as DECSC does. |
| 93 | func (s *Screen) SaveCursor() { |
| 94 | s.savedCursor, s.savedStyle = s.cursor, s.style |
| 95 | } |
| 96 | |
| 97 | // RestoreCursor puts back what SaveCursor remembered, as DECRC does. |
| 98 | func (s *Screen) RestoreCursor() { |
| 99 | s.cursor, s.style = s.savedCursor, s.savedStyle |
| 100 | s.cursor.Row = min(s.cursor.Row, s.height-1) |
| 101 | s.cursor.Col = min(s.cursor.Col, s.width-1) |
| 102 | s.wrapPending = false |
| 103 | } |
| 104 | |
| 105 | // SetScrollRegion narrows the rows that scroll to the inclusive range |
| 106 | // [top, bottom], and puts the cursor at the top left as DECSTBM does. |
| 107 | // |
| 108 | // A region that is empty or the wrong way round is ignored, which is what a |
| 109 | // terminal does with a malformed request rather than corrupting its own state. |
| 110 | func (s *Screen) SetScrollRegion(top, bottom int) { |
| 111 | top = min(max(top, 0), s.height-1) |
| 112 | bottom = min(max(bottom, 0), s.height-1) |
| 113 | if top >= bottom { |
| 114 | return |
| 115 | } |
| 116 | |
| 117 | s.top, s.bottom = top, bottom |
| 118 | s.MoveTo(0, 0) |
| 119 | } |
| 120 | |
| 121 | // ScrollUp moves the scrolling region up by n lines, blanking the lines that |
| 122 | // appear at the bottom. Lines leaving the top of a full-height region are kept |
| 123 | // in the scrollback. |
| 124 | func (s *Screen) ScrollUp(n int) { |
| 125 | rows := s.bottom - s.top + 1 |
| 126 | n = min(max(n, 0), rows) |
| 127 | if n == 0 { |
| 128 | return |
| 129 | } |
| 130 | |
| 131 | for i := range n { |
| 132 | s.remember(s.lines[s.top+i]) |
| 133 | } |
| 134 | |
| 135 | copy(s.lines[s.top:s.bottom+1-n], s.lines[s.top+n:s.bottom+1]) |
| 136 | for row := s.bottom + 1 - n; row <= s.bottom; row++ { |
| 137 | s.lines[row] = s.blankLine() |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // ScrollDown moves the scrolling region down by n lines, blanking the lines |
| 142 | // that appear at the top. Nothing is remembered: these lines are not history, |
| 143 | // they are being pushed back down. |
| 144 | func (s *Screen) ScrollDown(n int) { |
| 145 | rows := s.bottom - s.top + 1 |
| 146 | n = min(max(n, 0), rows) |
| 147 | if n == 0 { |
| 148 | return |
| 149 | } |
| 150 | |
| 151 | copy(s.lines[s.top+n:s.bottom+1], s.lines[s.top:s.bottom+1-n]) |
| 152 | for row := s.top; row < s.top+n; row++ { |
| 153 | s.lines[row] = s.blankLine() |
| 154 | } |
| 155 | } |