package terminal import "github.com/gdamore/tcell/v2" // EraseMode says how much of the display or the line an erase covers. type EraseMode int // The three erase modes the protocol defines. const ( // EraseToEnd clears from the cursor to the end, cursor included. EraseToEnd EraseMode = iota // EraseToStart clears from the start to the cursor, cursor included. EraseToStart // EraseAll clears the whole display or line. EraseAll ) // EraseDisplay clears part or all of the screen, as ED does. The cursor does // not move: a program that wants it elsewhere says so separately. func (s *Screen) EraseDisplay(mode EraseMode) { switch mode { case EraseToEnd: s.eraseLineRange(s.cursor.Row, s.cursor.Col, s.width-1) s.blankRows(s.cursor.Row+1, s.height-1) case EraseToStart: s.blankRows(0, s.cursor.Row-1) s.eraseLineRange(s.cursor.Row, 0, s.cursor.Col) case EraseAll: s.blankRows(0, s.height-1) } s.wrapPending = false } // EraseLine clears part or all of the cursor's line, as EL does. func (s *Screen) EraseLine(mode EraseMode) { switch mode { case EraseToEnd: s.eraseLineRange(s.cursor.Row, s.cursor.Col, s.width-1) case EraseToStart: s.eraseLineRange(s.cursor.Row, 0, s.cursor.Col) case EraseAll: s.eraseLineRange(s.cursor.Row, 0, s.width-1) } s.wrapPending = false } // EraseChars blanks n characters from the cursor rightwards without moving // anything along, as ECH does. func (s *Screen) EraseChars(n int) { s.eraseLineRange(s.cursor.Row, s.cursor.Col, s.cursor.Col+max(n, 1)-1) s.wrapPending = false } // blankRows clears whole rows, inclusive, ignoring any outside the screen. func (s *Screen) blankRows(from, to int) { for row := max(from, 0); row <= min(to, s.height-1); row++ { s.lines[row] = s.blankLine() } } // eraseLineRange blanks the columns [from, to] of a row, inclusive. func (s *Screen) eraseLineRange(row, from, to int) { if row < 0 || row >= s.height { return } for col := max(from, 0); col <= min(to, s.width-1); col++ { s.lines[row][col] = blank(s.style) } } // InsertLines opens n blank lines at the cursor's row, pushing the rest of the // scrolling region down and off its bottom, as IL does. // // It does nothing when the cursor is outside the scrolling region, which is // what the protocol requires and what stops a program with a status line from // pushing that line about. func (s *Screen) InsertLines(n int) { if !s.cursorInRegion() { return } s.scrollRegionFrom(s.cursor.Row, max(n, 1), down) } // DeleteLines removes n lines at the cursor's row, pulling the rest of the // scrolling region up and blanking its bottom, as DL does. func (s *Screen) DeleteLines(n int) { if !s.cursorInRegion() { return } s.scrollRegionFrom(s.cursor.Row, max(n, 1), up) } // direction says which way a partial scroll moves the lines. type direction int const ( up direction = iota down ) // scrollRegionFrom scrolls the part of the scrolling region that starts at a // row, which is what insert-lines and delete-lines both do. func (s *Screen) scrollRegionFrom(from, n int, dir direction) { rows := s.bottom - from + 1 n = min(n, rows) if dir == up { copy(s.lines[from:s.bottom+1-n], s.lines[from+n:s.bottom+1]) s.blankRows(s.bottom+1-n, s.bottom) } else { copy(s.lines[from+n:s.bottom+1], s.lines[from:s.bottom+1-n]) s.blankRows(from, from+n-1) } s.wrapPending = false } // cursorInRegion reports whether the cursor is inside the scrolling region. func (s *Screen) cursorInRegion() bool { return s.cursor.Row >= s.top && s.cursor.Row <= s.bottom } // InsertChars opens n blank cells at the cursor, pushing the rest of the line // right and off its end, as ICH does. func (s *Screen) InsertChars(n int) { line := s.lines[s.cursor.Row] n = min(max(n, 1), s.width-s.cursor.Col) copy(line[s.cursor.Col+n:], line[s.cursor.Col:s.width-n]) s.eraseLineRange(s.cursor.Row, s.cursor.Col, s.cursor.Col+n-1) s.wrapPending = false } // DeleteChars removes n cells at the cursor, pulling the rest of the line left // and blanking its end, as DCH does. func (s *Screen) DeleteChars(n int) { line := s.lines[s.cursor.Row] n = min(max(n, 1), s.width-s.cursor.Col) copy(line[s.cursor.Col:s.width-n], line[s.cursor.Col+n:]) s.eraseLineRange(s.cursor.Row, s.width-n, s.width-1) s.wrapPending = false } // UseAlternate switches to the alternate screen and back. // // The alternate screen is a blank scratch surface with no scrollback: a // full-screen program asks for it so that whatever the terminal showed before // comes back untouched when the program exits. func (s *Screen) UseAlternate(alternate bool) { if alternate == s.alternate { return } if alternate { s.primaryLines, s.primaryCursor = s.lines, s.cursor s.primaryStyle, s.primaryHistory = s.style, s.scrollback s.alternate = true s.lines = s.blankLines(s.height) s.scrollback = nil s.MoveTo(0, 0) s.resetScrollRegion() return } s.alternate = false s.lines, s.cursor = s.primaryLines, s.primaryCursor s.style, s.scrollback = s.primaryStyle, s.primaryHistory s.primaryLines, s.primaryHistory = nil, nil s.resetScrollRegion() s.fitCursor() } // Reset puts the screen back to how it started, as RIS does. func (s *Screen) Reset() { s.alternate = false s.primaryLines, s.primaryHistory = nil, nil s.style = tcell.StyleDefault s.savedStyle = tcell.StyleDefault s.lines = s.blankLines(s.height) s.scrollback = nil s.cursor, s.savedCursor = Cursor{}, Cursor{} s.cursorVisible, s.autoWrap, s.wrapPending = true, true, false s.applicationCursor = false s.resetScrollRegion() } // fitCursor pulls the cursor back onto the screen after a resize or a swap. func (s *Screen) fitCursor() { s.cursor.Row = min(max(s.cursor.Row, 0), s.height-1) s.cursor.Col = min(max(s.cursor.Col, 0), s.width-1) s.wrapPending = false }