package terminal // WriteRune puts one character at the cursor and moves on. // // The cursor stops in the last column rather than moving past it: only the // next character written wraps to the following line. That is what stops a // character in the bottom-right corner from scrolling the screen by itself, // which is the difference between a terminal that redraws correctly and one // that creeps upwards. func (s *Screen) WriteRune(r rune) { if s.wrapPending && s.autoWrap { s.CarriageReturn() s.LineFeed() } s.wrapPending = false s.lines[s.cursor.Row][s.cursor.Col] = Cell{Rune: r, Style: s.style} if s.cursor.Col == s.width-1 { s.wrapPending = true return } s.cursor.Col++ } // MoveTo puts the cursor at a position, clamped to the screen. func (s *Screen) MoveTo(row, col int) { s.cursor.Row = min(max(row, 0), s.height-1) s.cursor.Col = min(max(col, 0), s.width-1) s.wrapPending = false } // MoveBy moves the cursor relative to where it is, clamped to the screen. func (s *Screen) MoveBy(rows, cols int) { s.MoveTo(s.cursor.Row+rows, s.cursor.Col+cols) } // CarriageReturn takes the cursor to the first column of its line. func (s *Screen) CarriageReturn() { s.cursor.Col = 0 s.wrapPending = false } // LineFeed moves the cursor down a line, scrolling the region when it is // already on the last row of it. func (s *Screen) LineFeed() { s.wrapPending = false if s.cursor.Row == s.bottom { s.ScrollUp(1) return } if s.cursor.Row < s.height-1 { s.cursor.Row++ } } // ReverseLineFeed moves the cursor up a line, scrolling the region down when // it is already on the first row of it. func (s *Screen) ReverseLineFeed() { s.wrapPending = false if s.cursor.Row == s.top { s.ScrollDown(1) return } if s.cursor.Row > 0 { s.cursor.Row-- } } // Backspace moves the cursor one column left, stopping at the first column. // It does not erase: a program that wants the character gone writes a space. func (s *Screen) Backspace() { s.wrapPending = false if s.cursor.Col > 0 { s.cursor.Col-- } } // TabWidth is how far apart the default tab stops are. const TabWidth = 8 // Tab moves the cursor to the next tab stop, or to the last column when there // is no further stop on the line. func (s *Screen) Tab() { s.wrapPending = false next := (s.cursor.Col/TabWidth + 1) * TabWidth s.cursor.Col = min(next, s.width-1) } // SaveCursor remembers the cursor and the current style, as DECSC does. func (s *Screen) SaveCursor() { s.savedCursor, s.savedStyle = s.cursor, s.style } // RestoreCursor puts back what SaveCursor remembered, as DECRC does. func (s *Screen) RestoreCursor() { s.cursor, s.style = s.savedCursor, s.savedStyle s.cursor.Row = min(s.cursor.Row, s.height-1) s.cursor.Col = min(s.cursor.Col, s.width-1) s.wrapPending = false } // SetScrollRegion narrows the rows that scroll to the inclusive range // [top, bottom], and puts the cursor at the top left as DECSTBM does. // // A region that is empty or the wrong way round is ignored, which is what a // terminal does with a malformed request rather than corrupting its own state. func (s *Screen) SetScrollRegion(top, bottom int) { top = min(max(top, 0), s.height-1) bottom = min(max(bottom, 0), s.height-1) if top >= bottom { return } s.top, s.bottom = top, bottom s.MoveTo(0, 0) } // ScrollUp moves the scrolling region up by n lines, blanking the lines that // appear at the bottom. Lines leaving the top of a full-height region are kept // in the scrollback. func (s *Screen) ScrollUp(n int) { rows := s.bottom - s.top + 1 n = min(max(n, 0), rows) if n == 0 { return } for i := range n { s.remember(s.lines[s.top+i]) } copy(s.lines[s.top:s.bottom+1-n], s.lines[s.top+n:s.bottom+1]) for row := s.bottom + 1 - n; row <= s.bottom; row++ { s.lines[row] = s.blankLine() } } // ScrollDown moves the scrolling region down by n lines, blanking the lines // that appear at the top. Nothing is remembered: these lines are not history, // they are being pushed back down. func (s *Screen) ScrollDown(n int) { rows := s.bottom - s.top + 1 n = min(max(n, 0), rows) if n == 0 { return } copy(s.lines[s.top+n:s.bottom+1], s.lines[s.top:s.bottom+1-n]) for row := s.top; row < s.top+n; row++ { s.lines[row] = s.blankLine() } }