github.com/elves/elvish@v0.15.0/pkg/cli/term/buffer.go (about)

     1  package term
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/elves/elvish/pkg/wcwidth"
     8  )
     9  
    10  // Cell is an indivisible unit on the screen. It is not necessarily 1 column
    11  // wide.
    12  type Cell struct {
    13  	Text  string
    14  	Style string
    15  }
    16  
    17  // Pos is a line/column position.
    18  type Pos struct {
    19  	Line, Col int
    20  }
    21  
    22  // CellsWidth returns the total width of a Cell slice.
    23  func CellsWidth(cs []Cell) int {
    24  	w := 0
    25  	for _, c := range cs {
    26  		w += wcwidth.Of(c.Text)
    27  	}
    28  	return w
    29  }
    30  
    31  // CompareCells returns whether two Cell slices are equal, and when they are
    32  // not, the first index at which they differ.
    33  func CompareCells(r1, r2 []Cell) (bool, int) {
    34  	for i, c := range r1 {
    35  		if i >= len(r2) || c != r2[i] {
    36  			return false, i
    37  		}
    38  	}
    39  	if len(r1) < len(r2) {
    40  		return false, len(r1)
    41  	}
    42  	return true, 0
    43  }
    44  
    45  // Buffer reflects a continuous range of lines on the terminal.
    46  //
    47  // The Unix terminal API provides only awkward ways of querying the terminal
    48  // Buffer, so we keep an internal reflection and do one-way synchronizations
    49  // (Buffer -> terminal, and not the other way around). This requires us to
    50  // exactly match the terminal's idea of the width of characters (wcwidth) and
    51  // where to insert soft carriage returns, so there could be bugs.
    52  type Buffer struct {
    53  	Width int
    54  	// Lines the content of the buffer.
    55  	Lines Lines
    56  	// Dot is what the user perceives as the cursor.
    57  	Dot Pos
    58  }
    59  
    60  // Lines stores multiple lines.
    61  type Lines [][]Cell
    62  
    63  // Line stores a single line.
    64  type Line []Cell
    65  
    66  // NewBuffer builds a new buffer, with one empty line.
    67  func NewBuffer(width int) *Buffer {
    68  	return &Buffer{Width: width, Lines: [][]Cell{make([]Cell, 0, width)}}
    69  }
    70  
    71  // Col returns the column the cursor is in.
    72  func (b *Buffer) Col() int {
    73  	return CellsWidth(b.Lines[len(b.Lines)-1])
    74  }
    75  
    76  // Cursor returns the current position of the cursor.
    77  func (b *Buffer) Cursor() Pos {
    78  	return Pos{len(b.Lines) - 1, b.Col()}
    79  }
    80  
    81  // BuffersHeight computes the combined height of a number of buffers.
    82  func BuffersHeight(bufs ...*Buffer) (l int) {
    83  	for _, buf := range bufs {
    84  		if buf != nil {
    85  			l += len(buf.Lines)
    86  		}
    87  	}
    88  	return
    89  }
    90  
    91  // TrimToLines trims a buffer to the lines [low, high).
    92  func (b *Buffer) TrimToLines(low, high int) {
    93  	if low < 0 {
    94  		low = 0
    95  	}
    96  	if high > len(b.Lines) {
    97  		high = len(b.Lines)
    98  	}
    99  	for i := 0; i < low; i++ {
   100  		b.Lines[i] = nil
   101  	}
   102  	for i := high; i < len(b.Lines); i++ {
   103  		b.Lines[i] = nil
   104  	}
   105  	b.Lines = b.Lines[low:high]
   106  	b.Dot.Line -= low
   107  	if b.Dot.Line < 0 {
   108  		b.Dot.Line = 0
   109  	}
   110  }
   111  
   112  // Extend adds all lines from b2 to the bottom of this buffer. If moveDot is
   113  // true, the dot is updated to match the dot of b2.
   114  func (b *Buffer) Extend(b2 *Buffer, moveDot bool) {
   115  	if b2 != nil && b2.Lines != nil {
   116  		if moveDot {
   117  			b.Dot.Line = b2.Dot.Line + len(b.Lines)
   118  			b.Dot.Col = b2.Dot.Col
   119  		}
   120  		b.Lines = append(b.Lines, b2.Lines...)
   121  	}
   122  }
   123  
   124  // ExtendRight extends bb to the right. It pads each line in b to be b.Width and
   125  // appends the corresponding line in b2 to it, making new lines when b2 has more
   126  // lines than bb.
   127  func (b *Buffer) ExtendRight(b2 *Buffer) {
   128  	i := 0
   129  	w := b.Width
   130  	b.Width += b2.Width
   131  	for ; i < len(b.Lines) && i < len(b2.Lines); i++ {
   132  		if w0 := CellsWidth(b.Lines[i]); w0 < w {
   133  			b.Lines[i] = append(b.Lines[i], makeSpacing(w-w0)...)
   134  		}
   135  		b.Lines[i] = append(b.Lines[i], b2.Lines[i]...)
   136  	}
   137  	for ; i < len(b2.Lines); i++ {
   138  		row := append(makeSpacing(w), b2.Lines[i]...)
   139  		b.Lines = append(b.Lines, row)
   140  	}
   141  }
   142  
   143  // Buffer returns itself.
   144  func (b *Buffer) Buffer() *Buffer { return b }
   145  
   146  // TTYString returns a string for representing the buffer on the terminal.
   147  func (b *Buffer) TTYString() string {
   148  	if b == nil {
   149  		return "nil"
   150  	}
   151  	sb := new(strings.Builder)
   152  	fmt.Fprintf(sb, "Width = %d, Dot = (%d, %d)\n", b.Width, b.Dot.Line, b.Dot.Col)
   153  	// Top border
   154  	sb.WriteString("┌" + strings.Repeat("─", b.Width) + "┐\n")
   155  	for _, line := range b.Lines {
   156  		// Left border
   157  		sb.WriteRune('│')
   158  		// Content
   159  		lastStyle := ""
   160  		usedWidth := 0
   161  		for _, cell := range line {
   162  			if cell.Style != lastStyle {
   163  				switch {
   164  				case lastStyle == "":
   165  					sb.WriteString("\033[" + cell.Style + "m")
   166  				case cell.Style == "":
   167  					sb.WriteString("\033[m")
   168  				default:
   169  					sb.WriteString("\033[;" + cell.Style + "m")
   170  				}
   171  				lastStyle = cell.Style
   172  			}
   173  			sb.WriteString(cell.Text)
   174  			usedWidth += wcwidth.Of(cell.Text)
   175  		}
   176  		if lastStyle != "" {
   177  			sb.WriteString("\033[m")
   178  		}
   179  		if usedWidth < b.Width {
   180  			sb.WriteString("$" + strings.Repeat(" ", b.Width-usedWidth-1))
   181  		}
   182  		// Right border and newline
   183  		sb.WriteString("│\n")
   184  	}
   185  	// Bottom border
   186  	sb.WriteString("└" + strings.Repeat("─", b.Width) + "┘\n")
   187  	return sb.String()
   188  }