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 }