Skip to content

Commit 2430d94

Browse files
jbowensannrpom
authored andcommitted
internal/ascii: new package
Add a simple whiteboard package (inspired by datadriven/diagram.Whiteboard) for writing ASCII within a two-dimensional buffer.
1 parent 7d17fe5 commit 2430d94

File tree

3 files changed

+312
-0
lines changed

3 files changed

+312
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
make w=8 h=8
2+
----
3+
4+
write
5+
0 1 hello
6+
1 1 world
7+
----
8+
hello
9+
world
10+
11+
write
12+
0 0 .
13+
1 6 !
14+
----
15+
.hello
16+
world!
17+
18+
write
19+
0 24 1234567890
20+
----
21+
.hello 1234567890
22+
world!
23+
24+
write
25+
9 0 boop!
26+
----
27+
----
28+
.hello 1234567890
29+
world!
30+
31+
32+
33+
34+
35+
36+
37+
boop!
38+
----
39+
----
40+
41+
make w=1 h=1
42+
----
43+
44+
write
45+
1 1 X
46+
0 2 O
47+
0 0 X
48+
2 2 O
49+
----
50+
X O
51+
X
52+
O
53+
54+
write
55+
1 2 X
56+
1 0 O
57+
----
58+
X O
59+
OXX
60+
O

internal/ascii/whiteboard.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
}

internal/ascii/whiteboard_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
"fmt"
9+
"strings"
10+
"testing"
11+
12+
"github.com/cockroachdb/datadriven"
13+
"github.com/cockroachdb/pebble/internal/strparse"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestASCIIBoardDatadriven(t *testing.T) {
18+
var board Board
19+
datadriven.RunTest(t, "testdata/ascii_board", func(t *testing.T, td *datadriven.TestData) string {
20+
switch td.Cmd {
21+
case "make":
22+
var w, h int
23+
td.ScanArgs(t, "w", &w)
24+
td.ScanArgs(t, "h", &h)
25+
board = Make(w, h)
26+
return board.String()
27+
case "write":
28+
for _, line := range strings.Split(td.Input, "\n") {
29+
p := strparse.MakeParser(" ", line)
30+
r := p.Int()
31+
c := p.Int()
32+
board.At(r, c).WriteString(p.Remaining())
33+
}
34+
return board.String()
35+
default:
36+
return fmt.Sprintf("unknown command: %s", td.Cmd)
37+
}
38+
})
39+
}
40+
41+
func TestASCIIBoard(t *testing.T) {
42+
board := Make(10, 2)
43+
board.At(0, 0).Printf("Hello\nworld!")
44+
require.Equal(t, `Hello
45+
world!`, board.String())
46+
47+
board.Reset(10)
48+
cur := board.At(1, 5).SetCarriageReturnPosition().Printf("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n")
49+
require.Equal(t, 11, cur.Row())
50+
require.Equal(t, 5, cur.Column())
51+
require.Equal(t, `
52+
a
53+
b
54+
c
55+
d
56+
e
57+
f
58+
g
59+
h
60+
i
61+
j`, board.String())
62+
}

0 commit comments

Comments
 (0)