|
| 1 | +// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use |
| 2 | +// of this source code is governed by a BSD-style license that can be found in |
| 3 | +// the LICENSE file. |
| 4 | + |
| 5 | +package ascii |
| 6 | + |
| 7 | +import ( |
| 8 | + "bytes" |
| 9 | + "fmt" |
| 10 | + "strings" |
| 11 | +) |
| 12 | + |
| 13 | +// Board is a simple ASCII-based board for rendering ASCII text diagrams. |
| 14 | +type Board struct { |
| 15 | + buf []byte |
| 16 | + width int |
| 17 | +} |
| 18 | + |
| 19 | +// Make returns a new Board with the given initial width and height. |
| 20 | +func Make(w, h int) Board { |
| 21 | + buf := make([]byte, 0, w*h) |
| 22 | + return Board{buf: buf, width: w} |
| 23 | +} |
| 24 | + |
| 25 | +// At returns a position at the given coordinates. |
| 26 | +func (b *Board) At(r, c int) Cursor { |
| 27 | + if r >= b.lines() { |
| 28 | + b.buf = append(b.buf, bytes.Repeat([]byte{' '}, (r-b.lines()+1)*b.width)...) |
| 29 | + } |
| 30 | + return Cursor{b: b, r: r, c: c} |
| 31 | +} |
| 32 | + |
| 33 | +// NewLine appends a new line to the board and returns a position at the |
| 34 | +// beginning of the line. |
| 35 | +func (b *Board) NewLine() Cursor { |
| 36 | + return b.At(b.lines(), 0) |
| 37 | +} |
| 38 | + |
| 39 | +// String returns the Board as a string. |
| 40 | +func (b *Board) String() string { |
| 41 | + return b.Render("") |
| 42 | +} |
| 43 | + |
| 44 | +// Render returns the Board as a string, with every line prefixed by |
| 45 | +// indent. |
| 46 | +func (b *Board) Render(indent string) string { |
| 47 | + var buf bytes.Buffer |
| 48 | + for r := 0; r < b.lines(); r++ { |
| 49 | + if r > 0 { |
| 50 | + buf.WriteByte('\n') |
| 51 | + } |
| 52 | + buf.WriteString(indent) |
| 53 | + buf.Write(bytes.TrimRight(b.row(r), " ")) |
| 54 | + } |
| 55 | + return buf.String() |
| 56 | +} |
| 57 | + |
| 58 | +// Reset resets the board to the given width and clears the contents. |
| 59 | +func (b *Board) Reset(w int) { |
| 60 | + b.buf = b.buf[:0] |
| 61 | + b.width = w |
| 62 | +} |
| 63 | + |
| 64 | +func (b *Board) write(r, c int, s string) { |
| 65 | + if c+len(s) > b.width { |
| 66 | + b.growWidth(c + len(s)) |
| 67 | + } |
| 68 | + row := b.row(r) |
| 69 | + for i := 0; i < len(s); i++ { |
| 70 | + row[c+i] = s[i] |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +func (b *Board) repeat(r, c int, n int, ch byte) { |
| 75 | + if c+n > b.width { |
| 76 | + b.growWidth(c + n) |
| 77 | + } |
| 78 | + row := b.row(r) |
| 79 | + for i := 0; i < n; i++ { |
| 80 | + row[c+i] = ch |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +func (b *Board) growWidth(w int) { |
| 85 | + buf := bytes.Repeat([]byte{' '}, w*b.lines()) |
| 86 | + for i := 0; i < b.lines(); i++ { |
| 87 | + copy(buf[i*w:(i+1)*w], b.buf[i*b.width:(i+1)*b.width]) |
| 88 | + } |
| 89 | + b.buf = buf |
| 90 | + b.width = w |
| 91 | +} |
| 92 | + |
| 93 | +func (b *Board) lines() int { |
| 94 | + return len(b.buf) / b.width |
| 95 | +} |
| 96 | + |
| 97 | +func (b *Board) row(r int) []byte { |
| 98 | + if sz := (r + 1) * b.width; sz > len(b.buf) { |
| 99 | + b.buf = append(b.buf, bytes.Repeat([]byte{' '}, sz-len(b.buf))...) |
| 100 | + } |
| 101 | + return b.buf[r*b.width : (r+1)*b.width] |
| 102 | +} |
| 103 | + |
| 104 | +// Cursor is a position on a Board. |
| 105 | +type Cursor struct { |
| 106 | + b *Board |
| 107 | + r, c int |
| 108 | + // carriageReturnCol is the column to which newlines will return. It is set |
| 109 | + // by SetCarriageReturnPosition. It's used when writing text in a column, |
| 110 | + // and newlines should return to the same column position. |
| 111 | + carriageReturnCol int |
| 112 | +} |
| 113 | + |
| 114 | +// Offset returns a new cursor with the given offset from the current cursor. |
| 115 | +func (c Cursor) Offset(dr, dc int) Cursor { |
| 116 | + c.r += dr |
| 117 | + c.c += dc |
| 118 | + return c |
| 119 | +} |
| 120 | + |
| 121 | +// SetCarriageReturnPosition returns a copy of the cursor, but with a carriage |
| 122 | +// return position set so that newlines written to the resulting Cursor will |
| 123 | +// return to the current column. |
| 124 | +func (c Cursor) SetCarriageReturnPosition() Cursor { |
| 125 | + c.carriageReturnCol = c.c |
| 126 | + return c |
| 127 | +} |
| 128 | + |
| 129 | +// Row returns the row of the current position. |
| 130 | +func (c Cursor) Row() int { |
| 131 | + return c.r |
| 132 | +} |
| 133 | + |
| 134 | +// Column returns the column of the current position. |
| 135 | +func (c Cursor) Column() int { |
| 136 | + return c.c |
| 137 | +} |
| 138 | + |
| 139 | +// SetRow returns a copy of the cursor, but with the row set to the given value. |
| 140 | +func (c Cursor) SetRow(row int) Cursor { |
| 141 | + c.r = row |
| 142 | + return c |
| 143 | +} |
| 144 | + |
| 145 | +// SetColumn returns a copy of the cursor, but with the column set to the given |
| 146 | +// value. |
| 147 | +func (c Cursor) SetColumn(col int) Cursor { |
| 148 | + c.c = col |
| 149 | + return c |
| 150 | +} |
| 151 | + |
| 152 | +// Printf writes the formatted string to cursor, returning a cursor where the |
| 153 | +// written text ends. |
| 154 | +func (c Cursor) Printf(format string, args ...interface{}) Cursor { |
| 155 | + return c.WriteString(fmt.Sprintf(format, args...)) |
| 156 | +} |
| 157 | + |
| 158 | +// WriteString writes the provided string starting at the cursor, returning a |
| 159 | +// cursor where the written text ends. Newlines in the string break to the next |
| 160 | +// row, with the column reset to the cursor's carriage return column. |
| 161 | +func (c Cursor) WriteString(s string) Cursor { |
| 162 | + for len(s) > 0 { |
| 163 | + i := strings.IndexByte(s, '\n') |
| 164 | + if i >= 0 { |
| 165 | + c.b.write(c.r, c.c, s[:i]) |
| 166 | + c = c.NewlineReturn() |
| 167 | + s = s[i+1:] |
| 168 | + } else { |
| 169 | + c.b.write(c.r, c.c, s) |
| 170 | + c.c += len(s) |
| 171 | + break |
| 172 | + } |
| 173 | + } |
| 174 | + return c |
| 175 | +} |
| 176 | + |
| 177 | +// RepeatByte writes the given byte n times starting at the cursor, returning a |
| 178 | +// cursor where the written bytes end. |
| 179 | +func (c Cursor) RepeatByte(n int, b byte) Cursor { |
| 180 | + c.b.repeat(c.r, c.c, n, b) |
| 181 | + return c.Offset(0, n) |
| 182 | +} |
| 183 | + |
| 184 | +// NewlineReturn returns a cursor at the next line, with the column set to the |
| 185 | +// cursor's carriage return column. |
| 186 | +func (c Cursor) NewlineReturn() Cursor { |
| 187 | + c.r += 1 |
| 188 | + c.c = c.carriageReturnCol |
| 189 | + return c |
| 190 | +} |
0 commit comments