305 lines
7.3 KiB
Go
305 lines
7.3 KiB
Go
|
package pretty
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
|
||
|
"github.com/fatih/color"
|
||
|
)
|
||
|
|
||
|
// NB(directxman12): this isn't particularly elegant, but it's also
|
||
|
// sufficiently simple as to be maintained here. Man (roff) would've
|
||
|
// probably worked, but it's not necessarily on Windows by default.
|
||
|
|
||
|
// Span is a chunk of content that is writable to an output, but knows how to
|
||
|
// calculate its apparent visual "width" on the terminal (not to be confused
|
||
|
// with the raw length, which may include zero-width coloring sequences).
|
||
|
type Span interface {
|
||
|
// VisualLength reports the "width" as perceived by the user on the terminal
|
||
|
// (i.e. widest line, ignoring ANSI escape characters).
|
||
|
VisualLength() int
|
||
|
// WriteTo writes the full span contents to the given writer.
|
||
|
WriteTo(io.Writer) error
|
||
|
}
|
||
|
|
||
|
// Table is a Span that writes its data in table form, with sizing controlled
|
||
|
// by the given table calculator. Rows are started with StartRow, followed by
|
||
|
// some calls to Column, followed by a call to EndRow. Once all rows are
|
||
|
// added, the table can be used as a Span.
|
||
|
type Table struct {
|
||
|
Sizing *TableCalculator
|
||
|
|
||
|
cellsByRow [][]Span
|
||
|
colSizes []int
|
||
|
}
|
||
|
|
||
|
// StartRow starts a new row.
|
||
|
// It must eventually be followed by EndRow.
|
||
|
func (t *Table) StartRow() {
|
||
|
t.cellsByRow = append(t.cellsByRow, []Span(nil))
|
||
|
}
|
||
|
|
||
|
// EndRow ends the currently started row.
|
||
|
func (t *Table) EndRow() {
|
||
|
lastRow := t.cellsByRow[len(t.cellsByRow)-1]
|
||
|
sizes := make([]int, len(lastRow))
|
||
|
for i, cell := range lastRow {
|
||
|
sizes[i] = cell.VisualLength()
|
||
|
}
|
||
|
t.Sizing.AddRowSizes(sizes...)
|
||
|
}
|
||
|
|
||
|
// Column adds the given span as a new column to the current row.
|
||
|
func (t *Table) Column(contents Span) {
|
||
|
currentRowInd := len(t.cellsByRow) - 1
|
||
|
t.cellsByRow[currentRowInd] = append(t.cellsByRow[currentRowInd], contents)
|
||
|
}
|
||
|
|
||
|
// SkipRow prints a span without having it contribute to the table calculation.
|
||
|
func (t *Table) SkipRow(contents Span) {
|
||
|
t.cellsByRow = append(t.cellsByRow, []Span{contents})
|
||
|
}
|
||
|
|
||
|
func (t *Table) WriteTo(out io.Writer) error {
|
||
|
if t.colSizes == nil {
|
||
|
t.colSizes = t.Sizing.ColumnWidths()
|
||
|
}
|
||
|
|
||
|
for _, cells := range t.cellsByRow {
|
||
|
currentPosition := 0
|
||
|
for colInd, cell := range cells {
|
||
|
colSize := t.colSizes[colInd]
|
||
|
diff := colSize - cell.VisualLength()
|
||
|
|
||
|
if err := cell.WriteTo(out); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if diff > 0 {
|
||
|
if err := writePadding(out, columnPadding, diff); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
currentPosition += colSize
|
||
|
}
|
||
|
|
||
|
if _, err := fmt.Fprint(out, "\n"); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (t *Table) VisualLength() int {
|
||
|
if t.colSizes == nil {
|
||
|
t.colSizes = t.Sizing.ColumnWidths()
|
||
|
}
|
||
|
|
||
|
res := 0
|
||
|
for _, colSize := range t.colSizes {
|
||
|
res += colSize
|
||
|
}
|
||
|
return res
|
||
|
}
|
||
|
|
||
|
// Text is a span that simply contains raw text. It's a good starting point.
|
||
|
type Text string
|
||
|
|
||
|
func (t Text) VisualLength() int { return len(t) }
|
||
|
func (t Text) WriteTo(w io.Writer) error {
|
||
|
_, err := w.Write([]byte(t))
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// indented is a span that indents all lines by the given number of tabs.
|
||
|
type indented struct {
|
||
|
Amount int
|
||
|
Content Span
|
||
|
}
|
||
|
|
||
|
func (i *indented) VisualLength() int { return i.Content.VisualLength() }
|
||
|
func (i *indented) WriteTo(w io.Writer) error {
|
||
|
var out bytes.Buffer
|
||
|
if err := i.Content.WriteTo(&out); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
lines := bytes.Split(out.Bytes(), []byte("\n"))
|
||
|
for lineInd, line := range lines {
|
||
|
if lineInd != 0 {
|
||
|
if _, err := w.Write([]byte("\n")); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
if len(line) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if err := writePadding(w, indentPadding, i.Amount); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if _, err := w.Write(line); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Indented returns a span that indents all lines by the given number of tabs.
|
||
|
func Indented(amt int, content Span) Span {
|
||
|
return &indented{Amount: amt, Content: content}
|
||
|
}
|
||
|
|
||
|
// fromWriter is a span that takes content from a function expecting a Writer.
|
||
|
type fromWriter struct {
|
||
|
cache []byte
|
||
|
cacheError error
|
||
|
run func(io.Writer) error
|
||
|
}
|
||
|
|
||
|
func (f *fromWriter) VisualLength() int {
|
||
|
if f.cache == nil {
|
||
|
var buf bytes.Buffer
|
||
|
if err := f.run(&buf); err != nil {
|
||
|
f.cacheError = err
|
||
|
}
|
||
|
f.cache = buf.Bytes()
|
||
|
}
|
||
|
return len(f.cache)
|
||
|
}
|
||
|
func (f *fromWriter) WriteTo(w io.Writer) error {
|
||
|
if f.cache != nil {
|
||
|
if f.cacheError != nil {
|
||
|
return f.cacheError
|
||
|
}
|
||
|
_, err := w.Write(f.cache)
|
||
|
return err
|
||
|
}
|
||
|
return f.run(w)
|
||
|
}
|
||
|
|
||
|
// FromWriter returns a span that takes content from a function expecting a Writer.
|
||
|
func FromWriter(run func(io.Writer) error) Span {
|
||
|
return &fromWriter{run: run}
|
||
|
}
|
||
|
|
||
|
// Decoration represents a terminal decoration.
|
||
|
type Decoration color.Color
|
||
|
|
||
|
// Containing returns a Span that has the given decoration applied.
|
||
|
func (d Decoration) Containing(contents Span) Span {
|
||
|
return &decorated{
|
||
|
Contents: contents,
|
||
|
Attributes: color.Color(d),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// decorated is a span that has some terminal decoration applied.
|
||
|
type decorated struct {
|
||
|
Contents Span
|
||
|
Attributes color.Color
|
||
|
}
|
||
|
|
||
|
func (d *decorated) VisualLength() int { return d.Contents.VisualLength() }
|
||
|
func (d *decorated) WriteTo(w io.Writer) error {
|
||
|
oldOut := color.Output
|
||
|
color.Output = w
|
||
|
defer func() { color.Output = oldOut }()
|
||
|
|
||
|
d.Attributes.Set()
|
||
|
defer color.Unset()
|
||
|
|
||
|
return d.Contents.WriteTo(w)
|
||
|
}
|
||
|
|
||
|
// SpanWriter is a span that contains multiple sub-spans.
|
||
|
type SpanWriter struct {
|
||
|
contents []Span
|
||
|
}
|
||
|
|
||
|
func (m *SpanWriter) VisualLength() int {
|
||
|
res := 0
|
||
|
for _, span := range m.contents {
|
||
|
res += span.VisualLength()
|
||
|
}
|
||
|
return res
|
||
|
}
|
||
|
func (m *SpanWriter) WriteTo(w io.Writer) error {
|
||
|
for _, span := range m.contents {
|
||
|
if err := span.WriteTo(w); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Print adds a new span to this SpanWriter.
|
||
|
func (m *SpanWriter) Print(s Span) {
|
||
|
m.contents = append(m.contents, s)
|
||
|
}
|
||
|
|
||
|
// lines is a span that adds some newlines, optionally followed by some content.
|
||
|
type lines struct {
|
||
|
content Span
|
||
|
amountBefore int
|
||
|
}
|
||
|
|
||
|
func (l *lines) VisualLength() int {
|
||
|
if l.content == nil {
|
||
|
return 0
|
||
|
}
|
||
|
return l.content.VisualLength()
|
||
|
}
|
||
|
func (l *lines) WriteTo(w io.Writer) error {
|
||
|
if err := writePadding(w, linesPadding, l.amountBefore); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if l.content != nil {
|
||
|
if err := l.content.WriteTo(w); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Newlines returns a span just containing some newlines.
|
||
|
func Newlines(amt int) Span {
|
||
|
return &lines{amountBefore: amt}
|
||
|
}
|
||
|
|
||
|
// Line returns a span that emits a newline, followed by the given content.
|
||
|
func Line(content Span) Span {
|
||
|
return &lines{amountBefore: 1, content: content}
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
columnPadding = []byte(" ")
|
||
|
indentPadding = []byte("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t")
|
||
|
linesPadding = []byte("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")
|
||
|
)
|
||
|
|
||
|
// writePadding writes out padding of the given type in the given amount to the writer.
|
||
|
// Each byte in the padding buffer contributes 1 to the amount -- the padding being
|
||
|
// a buffer is just for efficiency.
|
||
|
func writePadding(out io.Writer, typ []byte, amt int) error {
|
||
|
if amt <= len(typ) {
|
||
|
_, err := out.Write(typ[:amt])
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
num := amt / len(typ)
|
||
|
rem := amt % len(typ)
|
||
|
for i := 0; i < num; i++ {
|
||
|
if _, err := out.Write(typ); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if _, err := out.Write(typ[:rem]); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|