148 lines
3.2 KiB
Go
148 lines
3.2 KiB
Go
|
// Package colwriter provides a write filter that formats
|
||
|
// input lines in multiple columns.
|
||
|
//
|
||
|
// The package is a straightforward translation from
|
||
|
// /src/cmd/draw/mc.c in Plan 9 from User Space.
|
||
|
package colwriter
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"io"
|
||
|
"unicode/utf8"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
tab = 4
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// Print each input line ending in a colon ':' separately.
|
||
|
BreakOnColon uint = 1 << iota
|
||
|
)
|
||
|
|
||
|
// A Writer is a filter that arranges input lines in as many columns as will
|
||
|
// fit in its width. Tab '\t' chars in the input are translated to sequences
|
||
|
// of spaces ending at multiples of 4 positions.
|
||
|
//
|
||
|
// If BreakOnColon is set, each input line ending in a colon ':' is written
|
||
|
// separately.
|
||
|
//
|
||
|
// The Writer assumes that all Unicode code points have the same width; this
|
||
|
// may not be true in some fonts.
|
||
|
type Writer struct {
|
||
|
w io.Writer
|
||
|
buf []byte
|
||
|
width int
|
||
|
flag uint
|
||
|
}
|
||
|
|
||
|
// NewWriter allocates and initializes a new Writer writing to w.
|
||
|
// Parameter width controls the total number of characters on each line
|
||
|
// across all columns.
|
||
|
func NewWriter(w io.Writer, width int, flag uint) *Writer {
|
||
|
return &Writer{
|
||
|
w: w,
|
||
|
width: width,
|
||
|
flag: flag,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Write writes p to the writer w. The only errors returned are ones
|
||
|
// encountered while writing to the underlying output stream.
|
||
|
func (w *Writer) Write(p []byte) (n int, err error) {
|
||
|
var linelen int
|
||
|
var lastWasColon bool
|
||
|
for i, c := range p {
|
||
|
w.buf = append(w.buf, c)
|
||
|
linelen++
|
||
|
if c == '\t' {
|
||
|
w.buf[len(w.buf)-1] = ' '
|
||
|
for linelen%tab != 0 {
|
||
|
w.buf = append(w.buf, ' ')
|
||
|
linelen++
|
||
|
}
|
||
|
}
|
||
|
if w.flag&BreakOnColon != 0 && c == ':' {
|
||
|
lastWasColon = true
|
||
|
} else if lastWasColon {
|
||
|
if c == '\n' {
|
||
|
pos := bytes.LastIndex(w.buf[:len(w.buf)-1], []byte{'\n'})
|
||
|
if pos < 0 {
|
||
|
pos = 0
|
||
|
}
|
||
|
line := w.buf[pos:]
|
||
|
w.buf = w.buf[:pos]
|
||
|
if err = w.columnate(); err != nil {
|
||
|
if len(line) < i {
|
||
|
return i - len(line), err
|
||
|
}
|
||
|
return 0, err
|
||
|
}
|
||
|
if n, err := w.w.Write(line); err != nil {
|
||
|
if r := len(line) - n; r < i {
|
||
|
return i - r, err
|
||
|
}
|
||
|
return 0, err
|
||
|
}
|
||
|
}
|
||
|
lastWasColon = false
|
||
|
}
|
||
|
if c == '\n' {
|
||
|
linelen = 0
|
||
|
}
|
||
|
}
|
||
|
return len(p), nil
|
||
|
}
|
||
|
|
||
|
// Flush should be called after the last call to Write to ensure that any data
|
||
|
// buffered in the Writer is written to output.
|
||
|
func (w *Writer) Flush() error {
|
||
|
return w.columnate()
|
||
|
}
|
||
|
|
||
|
func (w *Writer) columnate() error {
|
||
|
words := bytes.Split(w.buf, []byte{'\n'})
|
||
|
w.buf = nil
|
||
|
if len(words[len(words)-1]) == 0 {
|
||
|
words = words[:len(words)-1]
|
||
|
}
|
||
|
maxwidth := 0
|
||
|
for _, wd := range words {
|
||
|
if n := utf8.RuneCount(wd); n > maxwidth {
|
||
|
maxwidth = n
|
||
|
}
|
||
|
}
|
||
|
maxwidth++ // space char
|
||
|
wordsPerLine := w.width / maxwidth
|
||
|
if wordsPerLine <= 0 {
|
||
|
wordsPerLine = 1
|
||
|
}
|
||
|
nlines := (len(words) + wordsPerLine - 1) / wordsPerLine
|
||
|
for i := 0; i < nlines; i++ {
|
||
|
col := 0
|
||
|
endcol := 0
|
||
|
for j := i; j < len(words); j += nlines {
|
||
|
endcol += maxwidth
|
||
|
_, err := w.w.Write(words[j])
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
col += utf8.RuneCount(words[j])
|
||
|
if j+nlines < len(words) {
|
||
|
for col < endcol {
|
||
|
_, err := w.w.Write([]byte{' '})
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
col++
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
_, err := w.w.Write([]byte{'\n'})
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|