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