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 }