github.com/seeker-insurance/kit@v0.0.13/pretty/text/colwriter/column.go (about)

     1  // Package colwriter provides a write filter that formats
     2  // input lines in multiple columns.
     3  //
     4  // The package is a straightforward translation from
     5  // /src/cmd/draw/mc.c in Plan 9 from User Space.
     6  package colwriter
     7  
     8  import (
     9  	"bytes"
    10  	"io"
    11  	"unicode/utf8"
    12  )
    13  
    14  const (
    15  	tab = 4
    16  )
    17  
    18  const (
    19  	// Print each input line ending in a colon ':' separately.
    20  	BreakOnColon uint = 1 << iota
    21  )
    22  
    23  // A Writer is a filter that arranges input lines in as many columns as will
    24  // fit in its width. Tab '\t' chars in the input are translated to sequences
    25  // of spaces ending at multiples of 4 positions.
    26  //
    27  // If BreakOnColon is set, each input line ending in a colon ':' is written
    28  // separately.
    29  //
    30  // The Writer assumes that all Unicode code points have the same width; this
    31  // may not be true in some fonts.
    32  type Writer struct {
    33  	w     io.Writer
    34  	buf   []byte
    35  	width int
    36  	flag  uint
    37  }
    38  
    39  // NewWriter allocates and initializes a new Writer writing to w.
    40  // Parameter width controls the total number of characters on each line
    41  // across all columns.
    42  func NewWriter(w io.Writer, width int, flag uint) *Writer {
    43  	return &Writer{
    44  		w:     w,
    45  		width: width,
    46  		flag:  flag,
    47  	}
    48  }
    49  
    50  // Write writes p to the writer w. The only errors returned are ones
    51  // encountered while writing to the underlying output stream.
    52  func (w *Writer) Write(p []byte) (n int, err error) {
    53  	var linelen int
    54  	var lastWasColon bool
    55  	for i, c := range p {
    56  		w.buf = append(w.buf, c)
    57  		linelen++
    58  		if c == '\t' {
    59  			w.buf[len(w.buf)-1] = ' '
    60  			for linelen%tab != 0 {
    61  				w.buf = append(w.buf, ' ')
    62  				linelen++
    63  			}
    64  		}
    65  		if w.flag&BreakOnColon != 0 && c == ':' {
    66  			lastWasColon = true
    67  		} else if lastWasColon {
    68  			if c == '\n' {
    69  				pos := bytes.LastIndex(w.buf[:len(w.buf)-1], []byte{'\n'})
    70  				if pos < 0 {
    71  					pos = 0
    72  				}
    73  				line := w.buf[pos:]
    74  				w.buf = w.buf[:pos]
    75  				if err = w.columnate(); err != nil {
    76  					if len(line) < i {
    77  						return i - len(line), err
    78  					}
    79  					return 0, err
    80  				}
    81  				if n, err := w.w.Write(line); err != nil {
    82  					if r := len(line) - n; r < i {
    83  						return i - r, err
    84  					}
    85  					return 0, err
    86  				}
    87  			}
    88  			lastWasColon = false
    89  		}
    90  		if c == '\n' {
    91  			linelen = 0
    92  		}
    93  	}
    94  	return len(p), nil
    95  }
    96  
    97  // Flush should be called after the last call to Write to ensure that any data
    98  // buffered in the Writer is written to output.
    99  func (w *Writer) Flush() error {
   100  	return w.columnate()
   101  }
   102  
   103  func (w *Writer) columnate() error {
   104  	words := bytes.Split(w.buf, []byte{'\n'})
   105  	w.buf = nil
   106  	if len(words[len(words)-1]) == 0 {
   107  		words = words[:len(words)-1]
   108  	}
   109  	maxwidth := 0
   110  	for _, wd := range words {
   111  		if n := utf8.RuneCount(wd); n > maxwidth {
   112  			maxwidth = n
   113  		}
   114  	}
   115  	maxwidth++ // space char
   116  	wordsPerLine := w.width / maxwidth
   117  	if wordsPerLine <= 0 {
   118  		wordsPerLine = 1
   119  	}
   120  	nlines := (len(words) + wordsPerLine - 1) / wordsPerLine
   121  	for i := 0; i < nlines; i++ {
   122  		col := 0
   123  		endcol := 0
   124  		for j := i; j < len(words); j += nlines {
   125  			endcol += maxwidth
   126  			_, err := w.w.Write(words[j])
   127  			if err != nil {
   128  				return err
   129  			}
   130  			col += utf8.RuneCount(words[j])
   131  			if j+nlines < len(words) {
   132  				for col < endcol {
   133  					_, err := w.w.Write([]byte{' '})
   134  					if err != nil {
   135  						return err
   136  					}
   137  					col++
   138  				}
   139  			}
   140  		}
   141  		_, err := w.w.Write([]byte{'\n'})
   142  		if err != nil {
   143  			return err
   144  		}
   145  	}
   146  	return nil
   147  }