package terminal import "github.com/gdamore/tcell/v2" // Resize changes the screen's size, keeping as much of what is on it as fits. // // Lines that no longer fit at the bottom go into the scrollback rather than // being dropped, which is what makes shrinking a window and growing it again // leave the session's history intact. Columns beyond the new width are lost; // no terminal reflows them, and pretending otherwise would move text the // program believes it has already placed. func (s *Screen) Resize(width, height int) { width, height = max(width, 1), max(height, 1) if width == s.width && height == s.height { return } previous := s.lines s.width, s.height = width, height dropped := rowsToDrop(len(previous), height, s.cursor.Row) for i := range dropped { s.remember(fitLine(previous[i], width, s.style)) } s.lines = make([]Line, height) for row := range height { if source := row + dropped; source < len(previous) { s.lines[row] = fitLine(previous[source], width, s.style) } else { s.lines[row] = s.blankLine() } } s.cursor.Row -= dropped s.resetScrollRegion() s.fitCursor() s.resizePrimary(width, height) } // resizePrimary keeps the grid put aside for the alternate screen the same // size as the visible one, so that leaving the alternate screen does not // restore a grid of the wrong shape. func (s *Screen) resizePrimary(width, height int) { if !s.alternate { return } lines := make([]Line, height) for row := range height { if row < len(s.primaryLines) { lines[row] = fitLine(s.primaryLines[row], width, s.primaryStyle) } else { lines[row] = blankLineOf(width, s.primaryStyle) } } s.primaryLines = lines s.primaryCursor.Row = min(s.primaryCursor.Row, height-1) s.primaryCursor.Col = min(s.primaryCursor.Col, width-1) } // fitLine returns a copy of a line at a new width, padded or cut. // rowsToDrop returns how many rows must come off the top when a screen shrinks. // // Only as many as it takes to keep the cursor on screen — never more. // // Dropping every row that no longer fits is the obvious rule and is wrong for // the common case: a command that prints one line and exits leaves the cursor // near the top and the rest of the screen blank, and dropping from the top then // throws the output away while keeping the blank rows below it. That is how // `echo TADA` in a tool window came out empty, the window having been created // at 80×24 and resized to fit its frame a moment later. // // A screen that really is full behaves as before, because a cursor at the // bottom needs exactly the rows that no longer fit to be dropped. func rowsToDrop(previous, height, cursorRow int) int { needed := cursorRow - height + 1 return min(max(needed, 0), max(previous-height, 0)) } func fitLine(line Line, width int, style tcell.Style) Line { out := blankLineOf(width, style) copy(out, line[:min(len(line), width)]) return out } // blankLineOf returns an empty line of a given width. func blankLineOf(width int, style tcell.Style) Line { line := make(Line, width) for i := range line { line[i] = blank(style) } return line }