// 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 }