File: vida/config.go
   1 package main
   2 
   3 import (
   4     "errors"
   5     "flag"
   6     "io"
   7     "strings"
   8 )
   9 
  10 const (
  11     mb = 1024 * 1024
  12     gb = 1024 * mb
  13 
  14     lineBufferLimit = 8 * gb
  15 )
  16 
  17 const (
  18     // columnGap is how many spaces show between adjacent output-columns
  19     columnGap = 2
  20 
  21     // bullet is the symbol used to show tallies
  22     bullet = `•`
  23 
  24     // emptyTile is used to show empty row values
  25     emptyTile = `○`
  26 
  27     // usualTile is ANSI-styled to show non-empty row values, its color
  28     // telling various kinds of values apart
  29     usualTile = `■`
  30 )
  31 
  32 const (
  33     resetStyleANSI = "\x1b[0m"
  34 
  35     altDigitGroupStyleANSI = "\x1b[38;5;249m"
  36 
  37     swappedStyleANSI = "\x1b[7m"
  38 
  39     // emptyStyleANSI uses a background-color style, and thus forces the
  40     // tile-display logic to reset the style after each tile, to avoid
  41     // this background from carrying over all later tiles
  42     // emptyStyleANSI = "\x1b[48;5;252m"
  43 
  44     emptyStyleANSI = "\x1b[0m"
  45 
  46     paddedStyleANSI = "\x1b[38;5;226m"
  47     normalStyleANSI = "\x1b[38;5;244m"
  48 
  49     positiveDecimalStyleANSI = "\x1b[38;5;29m"
  50     positiveIntegerStyleANSI = "\x1b[38;5;22m"
  51     negativeDecimalStyleANSI = "\x1b[38;5;167m"
  52     negativeIntegerStyleANSI = "\x1b[38;5;88m"
  53     zeroStyleANSI            = "\x1b[38;5;33m"
  54 
  55     // emptyStyleANSI           = "\x1b[48;5;248m"
  56     // positiveIntegerStyleANSI = "\x1b[38;5;2m"
  57     // positiveIntegerStyleANSI = "\x1b[38;5;28m"
  58     // negativeDecimalStyleANSI = "\x1b[38;5;166m"
  59     // negativeIntegerStyleANSI = "\x1b[38;5;168m"
  60     // linkStyleANSI = "\x1b[38;5;75m"
  61 
  62     // linkStyleANSI = "\x1b[38;5;33m"
  63 )
  64 
  65 const (
  66     // swappedClassHTML = ``
  67 
  68     emptyClassHTML = `empty`
  69 
  70     paddedClassHTML = `padded`
  71     normalClassHTML = ``
  72 
  73     positiveDecimalClassHTML = `pos`
  74     positiveIntegerClassHTML = `pos`
  75     negativeDecimalClassHTML = `neg`
  76     negativeIntegerClassHTML = `neg`
  77     zeroClassHTML            = `zero`
  78 )
  79 
  80 const (
  81     // jsonLevelIndent is how many spaces to use for each nesting-level when
  82     // showing JSON data
  83     jsonLevelIndent = 2
  84 
  85     nullStyleANSI   = "\x1b[38;5;243m"
  86     boolStyleANSI   = "\x1b[38;5;74m"
  87     keyStyleANSI    = "\x1b[38;5;99m"
  88     stringStyleANSI = "\x1b[38;5;24m"
  89     syntaxStyleANSI = "\x1b[38;5;249m"
  90 
  91     // stringStyleANSI = resetStyleANSI
  92 )
  93 
  94 const (
  95     inputUsage = `input type: can be csv, tsv, or json; ` +
  96         `only needed when filepath extensions don't help or when reading from stdin`
  97     outputUsage    = `output type: can be ansi, plain, text, html, json, or jsons`
  98     colorsUsage    = `which (if any) column to color-code by unique values`
  99     altDigitsUsage = `style/color name to use for groups of alternating digits`
 100     linksUsage     = `style/color name to use for detected (hyper)links`
 101     bulletsUsage   = `which (if any) column to use to show bullet-talllies`
 102     tilesUsage     = `whether to show tiles for table rows; used for ANSI output`
 103     titleUsage     = `the (optional) title to use for HTML output`
 104 )
 105 
 106 // config is this app's general configuration, parsed from cmd-line args
 107 type config struct {
 108     // From is the input-format to read the output as: it can be `csv`,
 109     // `tsv`, or `json`
 110     From string
 111 
 112     // To is the output-format to use for the output: it can be `plain`,
 113     // `ansi`, or `html`
 114     To string
 115 
 116     // Inputs has all named inputs/files given to this app
 117     Inputs []string
 118 
 119     // Title is used when Output is `html` and isn't empty
 120     Title string
 121 
 122     // BulletColumn is the 1-based index of the column to use (if any), to
 123     // show bullets after each row; negative numbers are allowed and are
 124     // meant to be fixed with the first/header row; 0 means disabled
 125 
 126     // Bullets is the column name/index to use to show bullet-tallies after
 127     // each output row: it can be a column name, a 1-based index, or even a
 128     // negative index, counting backwards from the last column
 129     Bullets string
 130 
 131     // Colors is the column name/index to color-code by unique values: it
 132     // be a column name, a 1-based index, or even a negative index, counting
 133     // backwards from the last column
 134     Colors string
 135 
 136     // DigitsStyle is the name of the style/color to use for alternating
 137     // groups of digits, when rendering long-enough numbers
 138     DigitsStyle string
 139 
 140     // LinksStyle is the name of the style/color to use for detected links
 141     LinksStyle string
 142 
 143     // UseTiles controls whether to show row-tiles for tabular inputs
 144     UseTiles bool
 145 
 146     // NiceNumbers causes output to either alternate ANSI-styles, or put
 147     // separator-commas every 3-digit group
 148     NiceNumbers bool
 149 }
 150 
 151 func parseArgs(usage string) (config, error) {
 152     cfg := config{
 153         From:        `auto`,
 154         To:          `ansi`,
 155         DigitsStyle: `lightgray`,
 156         LinksStyle:  `lightblue`,
 157         UseTiles:    true,
 158         NiceNumbers: true,
 159     }
 160 
 161     flag.Usage = func() {
 162         w := flag.CommandLine.Output()
 163         io.WriteString(w, usage)
 164         io.WriteString(w, "\n\nOptions\n\n")
 165         flag.PrintDefaults()
 166     }
 167 
 168     flag.StringVar(&cfg.From, `from`, cfg.From, inputUsage)
 169     flag.StringVar(&cfg.To, `to`, cfg.To, outputUsage)
 170     flag.StringVar(&cfg.Bullets, `bullets`, cfg.Bullets, bulletsUsage)
 171     flag.StringVar(&cfg.Bullets, `tally`, cfg.Bullets, bulletsUsage)
 172     flag.StringVar(&cfg.Colors, `color`, cfg.Colors, colorsUsage)
 173     flag.StringVar(&cfg.Colors, `colors`, cfg.Colors, colorsUsage)
 174     flag.StringVar(&cfg.DigitsStyle, `digits`, cfg.DigitsStyle, altDigitsUsage)
 175     flag.StringVar(&cfg.LinksStyle, `links`, cfg.LinksStyle, linksUsage)
 176     flag.StringVar(&cfg.Title, `title`, cfg.Title, titleUsage)
 177     flag.BoolVar(&cfg.UseTiles, `tiles`, cfg.UseTiles, tilesUsage)
 178     flag.Parse()
 179 
 180     cfg.From = conform(typeAliases, cfg.From)
 181     cfg.To = conform(typeAliases, cfg.To)
 182 
 183     cfg.Inputs = flag.Args()
 184 
 185     if cfg.From == `` {
 186         return cfg, errors.New(`not given an input-format`)
 187     }
 188     if cfg.To == `` {
 189         return cfg, errors.New(`not given an output-format`)
 190     }
 191 
 192     switch cfg.To {
 193     case `csv`, `tsv`, `json`, `jsons`:
 194         if len(cfg.Inputs) > 1 {
 195             out := strings.ToUpper(cfg.To)
 196             msg := `converting multiple inputs into ` + out + ` isn't allowed`
 197             return cfg, errors.New(msg)
 198         }
 199     }
 200 
 201     return cfg, nil
 202 }
 203 
 204 var typeAliases = map[string]string{
 205     `a`:       `ansi`,
 206     `ansi`:    `ansi`,
 207     `color`:   `ansi`,
 208     `colored`: `ansi`,
 209     `colors`:  `ansi`,
 210     `styled`:  `ansi`,
 211 
 212     `b64`:       `base64`,
 213     `b-64`:      `base64`,
 214     `base64`:    `base64`,
 215     `base-64`:   `base64`,
 216     `datahref`:  `base64`,
 217     `data-href`: `base64`,
 218     `datalink`:  `base64`,
 219     `data-link`: `base64`,
 220     `datauri`:   `base64`,
 221     `data-uri`:  `base64`,
 222     `dataurl`:   `base64`,
 223     `data-url`:  `base64`,
 224     `href`:      `base64`,
 225     `link`:      `base64`,
 226 
 227     `b`:     `bytes`,
 228     `bytes`: `bytes`,
 229 
 230     `,`:                       `csv`,
 231     `c`:                       `csv`,
 232     `cs`:                      `csv`,
 233     `csv`:                     `csv`,
 234     `text/csv`:                `csv`,
 235     `text/csv; charset=ascii`: `csv`,
 236     `text/csv; charset=utf-8`: `csv`,
 237 
 238     `h`:        `html`,
 239     `htm`:      `html`,
 240     `html`:     `html`,
 241     `page`:     `html`,
 242     `w`:        `html`,
 243     `web`:      `html`,
 244     `webpage`:  `html`,
 245     `web-page`: `html`,
 246     `wp`:       `html`,
 247 
 248     `application/json`:                `json`,
 249     `application/json; charset=ascii`: `json`,
 250     `application/json; charset=utf-8`: `json`,
 251     `j`:                               `json`,
 252     `json`:                            `json`,
 253     `text/json`:                       `json`,
 254     `text/json; charset=ascii`:        `json`,
 255     `text/json; charset=utf-8`:        `json`,
 256 
 257     `jl`:    `jsonl`,
 258     `jsonl`: `jsonl`,
 259 
 260     `js`:           `jsons`,
 261     `jsons`:        `jsons`,
 262     `jsonstrings`:  `jsons`,
 263     `json-s`:       `jsons`,
 264     `json-strings`: `jsons`,
 265 
 266     `l`:     `lines`,
 267     `li`:    `lines`,
 268     `lines`: `lines`,
 269     `ln`:    `lines`,
 270 
 271     `p`:                         `plain`,
 272     `pl`:                        `plain`,
 273     `plain`:                     `plain`,
 274     `plaintext`:                 `plain`,
 275     `plain-text`:                `plain`,
 276     `prose`:                     `plain`,
 277     `te`:                        `plain`,
 278     `text`:                      `plain`,
 279     `text/plain`:                `plain`,
 280     `text/plain; charset=ascii`: `plain`,
 281     `text/plain; charset=utf-8`: `plain`,
 282     `tx`:                        `plain`,
 283     `txt`:                       `plain`,
 284 
 285     `n`:       `null`,
 286     `no`:      `null`,
 287     `nothing`: `null`,
 288     `nu`:      `null`,
 289     `null`:    `null`,
 290 
 291     `ta`:                      `tsv`,
 292     `tab`:                     `tsv`,
 293     `table`:                   `tsv`,
 294     `tabs`:                    `tsv`,
 295     `text/tsv`:                `tsv`,
 296     `text/tsv; charset=ascii`: `tsv`,
 297     `text/tsv; charset=utf-8`: `tsv`,
 298     `ts`:                      `tsv`,
 299     `tsv`:                     `tsv`,
 300 }
 301 
 302 // conform tries to mold format-type names into canonical keys this app's
 303 // dispatch tables have entries for, handling many variations letter-casing,
 304 // MIME types, filepath extensions, shortcuts, and aliases can introduce
 305 func conform(aliases map[string]string, k string) string {
 306     k = strings.TrimSpace(k)
 307     k = strings.TrimPrefix(k, `.`)
 308     k = strings.ToLower(k)
 309     if alias, ok := aliases[k]; ok {
 310         return alias
 311     }
 312     return k
 313 }
 314 
 315 var outFormat2handler = map[string]func(c config) outputHandler{
 316     `ansi`:   newOutputHandlerNoWrap,
 317     `binary`: newOutputHandlerNoWrap,
 318     `csv`:    newOutputHandlerNoWrap,
 319     `html`:   newOutputHandlerHTML,
 320     `json`:   newOutputHandlerNoWrap,
 321     `jsons`:  newOutputHandlerNoWrap,
 322     `plain`:  newOutputHandlerNoWrap,
 323     `tsv`:    newOutputHandlerNoWrap,
 324 }
 325 
 326 // formatPair is the lookup-key-type for all converters
 327 type formatPair struct {
 328     From string
 329     To   string
 330 }
 331 
 332 // converter is a general data-format converter: it's so general it includes
 333 // both whole-input-blocking converters, as well as streaming ones
 334 type converter func(w writer, r io.Reader, name string, cfg config) error
 335 
 336 var io2Converter = map[formatPair]converter{
 337     // {`json`, `csv`}
 338     // {`json`, `tsv`}
 339 
 340     {`base64`, `binary`}: decodeBase64,
 341 
 342     {`bytes`, `ansi`}:  viewHex,
 343     {`csv`, `ansi`}:    convertWholeTable,
 344     {`csv`, `html`}:    convertWholeTable,
 345     {`csv`, `json`}:    csv2json,
 346     {`csv`, `jsons`}:   csv2jsons,
 347     {`csv`, `plain`}:   convertWholeTable,
 348     {`csv`, `tsv`}:     csv2tsv,
 349     {`json`, `ansi`}:   renderJSON,
 350     {`json`, `html`}:   renderJSON,
 351     {`json`, `jsons`}:  renderJSON,
 352     {`json`, `plain`}:  renderJSON,
 353     {`jsonl`, `json`}:  jsonl2json,
 354     {`lines`, `ansi`}:  text2ansi,
 355     {`lines`, `html`}:  prose2html,
 356     {`lines`, `json`}:  lines2jsons,
 357     {`lines`, `jsons`}: lines2jsons,
 358     {`null`, `html`}:   null2anything,
 359     {`plain`, `ansi`}:  text2ansi,
 360     {`plain`, `html`}:  prose2html,
 361     {`plain`, `json`}:  text2jsons,
 362     {`plain`, `jsons`}: text2jsons,
 363     {`tsv`, `ansi`}:    convertWholeTable,
 364     {`tsv`, `csv`}:     tsv2csv,
 365     {`tsv`, `html`}:    convertWholeTable,
 366     {`tsv`, `json`}:    tsv2json,
 367     {`tsv`, `jsons`}:   tsv2jsons,
 368     {`tsv`, `plain`}:   convertWholeTable,
 369 }
 370 
 371 func adaptName(s string) string {
 372     if s == `` || s == `-` {
 373         return `<stdin>`
 374     }
 375     return s
 376 }
 377 
 378 var styleAliases = map[string]string{
 379     `dark-gray`:  `darkgray`,
 380     `light-blue`: `lightblue`,
 381     `light-gray`: `lightgray`,
 382 
 383     `l`:       `lightblue`,
 384     `light`:   `lightblue`,
 385     `b`:       `blue`,
 386     `blue`:    `blue`,
 387     `g`:       `green`,
 388     `inv`:     `inverse`,
 389     `invert`:  `inverse`,
 390     `swap`:    `inverse`,
 391     `rev`:     `inverse`,
 392     `reverse`: `inverse`,
 393     `m`:       `magenta`,
 394     `mag`:     `magenta`,
 395     `o`:       `orange`,
 396     `p`:       `purple`,
 397     `r`:       `red`,
 398     `t`:       `teal`,
 399 }
 400 
 401 var ansiStyles = map[string]string{
 402     `blue`:      "\x1b[38;5;26m",
 403     `bold`:      "\x1b[1m",
 404     `darkgray`:  "\x1b[38;5;243m",
 405     `gray`:      "\x1b[38;5;246m",
 406     `green`:     "\x1b[38;5;29m",
 407     `inverse`:   "\x1b[7m",
 408     `lightblue`: "\x1b[38;5;33m",
 409     `lightgray`: "\x1b[38;5;249m",
 410     `magenta`:   "\x1b[38;5;165m",
 411     `orange`:    "\x1b[38;5;166m",
 412     `pink`:      "\x1b[38;5;213m",
 413     `plain`:     "\x1b[0m",
 414     `purple`:    "\x1b[38;5;99m",
 415     `red`:       "\x1b[31m",
 416     `teal`:      "\x1b[38;5;6m",
 417     `underline`: "\x1b[4m",
 418 }
 419 
 420 func lookupStyleANSI(name string) (ansi string, ok bool) {
 421     if alias, ok := styleAliases[name]; ok {
 422         name = alias
 423     }
 424     ansi, ok = ansiStyles[name]
 425     return ansi, ok
 426 }

     File: vida/go.mod
   1 module vida
   2 
   3 go 1.18

     File: vida/hex.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "fmt"
   7     "io"
   8     "math"
   9     "os"
  10     "strconv"
  11     "strings"
  12 )
  13 
  14 // hexViewPadding is the padding/spacing emitted across each output line,
  15 // except for the breather/ruler lines
  16 const hexViewPadding = `  `
  17 
  18 const (
  19     hexUnknownStyle = 0
  20     hexZeroStyle    = 1
  21     hexOtherStyle   = 2
  22     hexStyleASCII   = 3
  23     hexAllOnStyle   = 4
  24 )
  25 
  26 // hexByteStyles turns bytes into one of several distinct visual types, which
  27 // allows quickly telling when ANSI styles codes are repetitive and when
  28 // they're actually needed
  29 var hexByteStyles = [256]int{
  30     hexZeroStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  31     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  32     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  33     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  34     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  35     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  36     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  37     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  38     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  39     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  40     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  41     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  42     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  43     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  44     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  45     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  46     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  47     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  48     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  49     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  50     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  51     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  52     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  53     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  54     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  55     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  56     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  57     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  58     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  59     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  60     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII,
  61     hexStyleASCII, hexStyleASCII, hexStyleASCII, hexOtherStyle,
  62     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  63     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  64     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  65     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  66     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  67     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  68     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  69     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  70     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  71     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  72     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  73     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  74     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  75     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  76     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  77     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  78     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  79     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  80     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  81     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  82     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  83     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  84     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  85     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  86     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  87     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  88     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  89     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  90     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  91     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  92     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle,
  93     hexOtherStyle, hexOtherStyle, hexOtherStyle, hexAllOnStyle,
  94 }
  95 
  96 // hexSymbols is a direct lookup table combining 2 hex digits with either a
  97 // space or a displayable ASCII symbol matching the byte's own ASCII value;
  98 // this table was autogenerated by running the command
  99 //
 100 // seq 0 255 | ./hex-symbols.awk
 101 var hexSymbols = [256]string{
 102     `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
 103     `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
 104     `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
 105     `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
 106     `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
 107     `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
 108     `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
 109     `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
 110     `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
 111     `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
 112     `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
 113     `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
 114     "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
 115     `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
 116     `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
 117     `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
 118     `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
 119     `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
 120     `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
 121     `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
 122     `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
 123     `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
 124     `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
 125     `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
 126     `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
 127     `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
 128     `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
 129     `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
 130     `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
 131     `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
 132     `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
 133     `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
 134 }
 135 
 136 // styledHexBytes is a super-fast direct byte-to-result lookup table, and was
 137 // autogenerated by running the command
 138 //
 139 // seq 0 255 | ./hex-styles.awk
 140 var styledHexBytes = [256]string{
 141     "\x1b[38;5;111m00 ", "\x1b[38;5;246m01 ",
 142     "\x1b[38;5;246m02 ", "\x1b[38;5;246m03 ",
 143     "\x1b[38;5;246m04 ", "\x1b[38;5;246m05 ",
 144     "\x1b[38;5;246m06 ", "\x1b[38;5;246m07 ",
 145     "\x1b[38;5;246m08 ", "\x1b[38;5;246m09 ",
 146     "\x1b[38;5;246m0a ", "\x1b[38;5;246m0b ",
 147     "\x1b[38;5;246m0c ", "\x1b[38;5;246m0d ",
 148     "\x1b[38;5;246m0e ", "\x1b[38;5;246m0f ",
 149     "\x1b[38;5;246m10 ", "\x1b[38;5;246m11 ",
 150     "\x1b[38;5;246m12 ", "\x1b[38;5;246m13 ",
 151     "\x1b[38;5;246m14 ", "\x1b[38;5;246m15 ",
 152     "\x1b[38;5;246m16 ", "\x1b[38;5;246m17 ",
 153     "\x1b[38;5;246m18 ", "\x1b[38;5;246m19 ",
 154     "\x1b[38;5;246m1a ", "\x1b[38;5;246m1b ",
 155     "\x1b[38;5;246m1c ", "\x1b[38;5;246m1d ",
 156     "\x1b[38;5;246m1e ", "\x1b[38;5;246m1f ",
 157     "\x1b[38;5;72m20\x1b[38;5;239m ", "\x1b[38;5;72m21\x1b[38;5;239m!",
 158     "\x1b[38;5;72m22\x1b[38;5;239m\"", "\x1b[38;5;72m23\x1b[38;5;239m#",
 159     "\x1b[38;5;72m24\x1b[38;5;239m$", "\x1b[38;5;72m25\x1b[38;5;239m%",
 160     "\x1b[38;5;72m26\x1b[38;5;239m&", "\x1b[38;5;72m27\x1b[38;5;239m'",
 161     "\x1b[38;5;72m28\x1b[38;5;239m(", "\x1b[38;5;72m29\x1b[38;5;239m)",
 162     "\x1b[38;5;72m2a\x1b[38;5;239m*", "\x1b[38;5;72m2b\x1b[38;5;239m+",
 163     "\x1b[38;5;72m2c\x1b[38;5;239m,", "\x1b[38;5;72m2d\x1b[38;5;239m-",
 164     "\x1b[38;5;72m2e\x1b[38;5;239m.", "\x1b[38;5;72m2f\x1b[38;5;239m/",
 165     "\x1b[38;5;72m30\x1b[38;5;239m0", "\x1b[38;5;72m31\x1b[38;5;239m1",
 166     "\x1b[38;5;72m32\x1b[38;5;239m2", "\x1b[38;5;72m33\x1b[38;5;239m3",
 167     "\x1b[38;5;72m34\x1b[38;5;239m4", "\x1b[38;5;72m35\x1b[38;5;239m5",
 168     "\x1b[38;5;72m36\x1b[38;5;239m6", "\x1b[38;5;72m37\x1b[38;5;239m7",
 169     "\x1b[38;5;72m38\x1b[38;5;239m8", "\x1b[38;5;72m39\x1b[38;5;239m9",
 170     "\x1b[38;5;72m3a\x1b[38;5;239m:", "\x1b[38;5;72m3b\x1b[38;5;239m;",
 171     "\x1b[38;5;72m3c\x1b[38;5;239m<", "\x1b[38;5;72m3d\x1b[38;5;239m=",
 172     "\x1b[38;5;72m3e\x1b[38;5;239m>", "\x1b[38;5;72m3f\x1b[38;5;239m?",
 173     "\x1b[38;5;72m40\x1b[38;5;239m@", "\x1b[38;5;72m41\x1b[38;5;239mA",
 174     "\x1b[38;5;72m42\x1b[38;5;239mB", "\x1b[38;5;72m43\x1b[38;5;239mC",
 175     "\x1b[38;5;72m44\x1b[38;5;239mD", "\x1b[38;5;72m45\x1b[38;5;239mE",
 176     "\x1b[38;5;72m46\x1b[38;5;239mF", "\x1b[38;5;72m47\x1b[38;5;239mG",
 177     "\x1b[38;5;72m48\x1b[38;5;239mH", "\x1b[38;5;72m49\x1b[38;5;239mI",
 178     "\x1b[38;5;72m4a\x1b[38;5;239mJ", "\x1b[38;5;72m4b\x1b[38;5;239mK",
 179     "\x1b[38;5;72m4c\x1b[38;5;239mL", "\x1b[38;5;72m4d\x1b[38;5;239mM",
 180     "\x1b[38;5;72m4e\x1b[38;5;239mN", "\x1b[38;5;72m4f\x1b[38;5;239mO",
 181     "\x1b[38;5;72m50\x1b[38;5;239mP", "\x1b[38;5;72m51\x1b[38;5;239mQ",
 182     "\x1b[38;5;72m52\x1b[38;5;239mR", "\x1b[38;5;72m53\x1b[38;5;239mS",
 183     "\x1b[38;5;72m54\x1b[38;5;239mT", "\x1b[38;5;72m55\x1b[38;5;239mU",
 184     "\x1b[38;5;72m56\x1b[38;5;239mV", "\x1b[38;5;72m57\x1b[38;5;239mW",
 185     "\x1b[38;5;72m58\x1b[38;5;239mX", "\x1b[38;5;72m59\x1b[38;5;239mY",
 186     "\x1b[38;5;72m5a\x1b[38;5;239mZ", "\x1b[38;5;72m5b\x1b[38;5;239m[",
 187     "\x1b[38;5;72m5c\x1b[38;5;239m\\", "\x1b[38;5;72m5d\x1b[38;5;239m]",
 188     "\x1b[38;5;72m5e\x1b[38;5;239m^", "\x1b[38;5;72m5f\x1b[38;5;239m_",
 189     "\x1b[38;5;72m60\x1b[38;5;239m`", "\x1b[38;5;72m61\x1b[38;5;239ma",
 190     "\x1b[38;5;72m62\x1b[38;5;239mb", "\x1b[38;5;72m63\x1b[38;5;239mc",
 191     "\x1b[38;5;72m64\x1b[38;5;239md", "\x1b[38;5;72m65\x1b[38;5;239me",
 192     "\x1b[38;5;72m66\x1b[38;5;239mf", "\x1b[38;5;72m67\x1b[38;5;239mg",
 193     "\x1b[38;5;72m68\x1b[38;5;239mh", "\x1b[38;5;72m69\x1b[38;5;239mi",
 194     "\x1b[38;5;72m6a\x1b[38;5;239mj", "\x1b[38;5;72m6b\x1b[38;5;239mk",
 195     "\x1b[38;5;72m6c\x1b[38;5;239ml", "\x1b[38;5;72m6d\x1b[38;5;239mm",
 196     "\x1b[38;5;72m6e\x1b[38;5;239mn", "\x1b[38;5;72m6f\x1b[38;5;239mo",
 197     "\x1b[38;5;72m70\x1b[38;5;239mp", "\x1b[38;5;72m71\x1b[38;5;239mq",
 198     "\x1b[38;5;72m72\x1b[38;5;239mr", "\x1b[38;5;72m73\x1b[38;5;239ms",
 199     "\x1b[38;5;72m74\x1b[38;5;239mt", "\x1b[38;5;72m75\x1b[38;5;239mu",
 200     "\x1b[38;5;72m76\x1b[38;5;239mv", "\x1b[38;5;72m77\x1b[38;5;239mw",
 201     "\x1b[38;5;72m78\x1b[38;5;239mx", "\x1b[38;5;72m79\x1b[38;5;239my",
 202     "\x1b[38;5;72m7a\x1b[38;5;239mz", "\x1b[38;5;72m7b\x1b[38;5;239m{",
 203     "\x1b[38;5;72m7c\x1b[38;5;239m|", "\x1b[38;5;72m7d\x1b[38;5;239m}",
 204     "\x1b[38;5;72m7e\x1b[38;5;239m~", "\x1b[38;5;246m7f ",
 205     "\x1b[38;5;246m80 ", "\x1b[38;5;246m81 ",
 206     "\x1b[38;5;246m82 ", "\x1b[38;5;246m83 ",
 207     "\x1b[38;5;246m84 ", "\x1b[38;5;246m85 ",
 208     "\x1b[38;5;246m86 ", "\x1b[38;5;246m87 ",
 209     "\x1b[38;5;246m88 ", "\x1b[38;5;246m89 ",
 210     "\x1b[38;5;246m8a ", "\x1b[38;5;246m8b ",
 211     "\x1b[38;5;246m8c ", "\x1b[38;5;246m8d ",
 212     "\x1b[38;5;246m8e ", "\x1b[38;5;246m8f ",
 213     "\x1b[38;5;246m90 ", "\x1b[38;5;246m91 ",
 214     "\x1b[38;5;246m92 ", "\x1b[38;5;246m93 ",
 215     "\x1b[38;5;246m94 ", "\x1b[38;5;246m95 ",
 216     "\x1b[38;5;246m96 ", "\x1b[38;5;246m97 ",
 217     "\x1b[38;5;246m98 ", "\x1b[38;5;246m99 ",
 218     "\x1b[38;5;246m9a ", "\x1b[38;5;246m9b ",
 219     "\x1b[38;5;246m9c ", "\x1b[38;5;246m9d ",
 220     "\x1b[38;5;246m9e ", "\x1b[38;5;246m9f ",
 221     "\x1b[38;5;246ma0 ", "\x1b[38;5;246ma1 ",
 222     "\x1b[38;5;246ma2 ", "\x1b[38;5;246ma3 ",
 223     "\x1b[38;5;246ma4 ", "\x1b[38;5;246ma5 ",
 224     "\x1b[38;5;246ma6 ", "\x1b[38;5;246ma7 ",
 225     "\x1b[38;5;246ma8 ", "\x1b[38;5;246ma9 ",
 226     "\x1b[38;5;246maa ", "\x1b[38;5;246mab ",
 227     "\x1b[38;5;246mac ", "\x1b[38;5;246mad ",
 228     "\x1b[38;5;246mae ", "\x1b[38;5;246maf ",
 229     "\x1b[38;5;246mb0 ", "\x1b[38;5;246mb1 ",
 230     "\x1b[38;5;246mb2 ", "\x1b[38;5;246mb3 ",
 231     "\x1b[38;5;246mb4 ", "\x1b[38;5;246mb5 ",
 232     "\x1b[38;5;246mb6 ", "\x1b[38;5;246mb7 ",
 233     "\x1b[38;5;246mb8 ", "\x1b[38;5;246mb9 ",
 234     "\x1b[38;5;246mba ", "\x1b[38;5;246mbb ",
 235     "\x1b[38;5;246mbc ", "\x1b[38;5;246mbd ",
 236     "\x1b[38;5;246mbe ", "\x1b[38;5;246mbf ",
 237     "\x1b[38;5;246mc0 ", "\x1b[38;5;246mc1 ",
 238     "\x1b[38;5;246mc2 ", "\x1b[38;5;246mc3 ",
 239     "\x1b[38;5;246mc4 ", "\x1b[38;5;246mc5 ",
 240     "\x1b[38;5;246mc6 ", "\x1b[38;5;246mc7 ",
 241     "\x1b[38;5;246mc8 ", "\x1b[38;5;246mc9 ",
 242     "\x1b[38;5;246mca ", "\x1b[38;5;246mcb ",
 243     "\x1b[38;5;246mcc ", "\x1b[38;5;246mcd ",
 244     "\x1b[38;5;246mce ", "\x1b[38;5;246mcf ",
 245     "\x1b[38;5;246md0 ", "\x1b[38;5;246md1 ",
 246     "\x1b[38;5;246md2 ", "\x1b[38;5;246md3 ",
 247     "\x1b[38;5;246md4 ", "\x1b[38;5;246md5 ",
 248     "\x1b[38;5;246md6 ", "\x1b[38;5;246md7 ",
 249     "\x1b[38;5;246md8 ", "\x1b[38;5;246md9 ",
 250     "\x1b[38;5;246mda ", "\x1b[38;5;246mdb ",
 251     "\x1b[38;5;246mdc ", "\x1b[38;5;246mdd ",
 252     "\x1b[38;5;246mde ", "\x1b[38;5;246mdf ",
 253     "\x1b[38;5;246me0 ", "\x1b[38;5;246me1 ",
 254     "\x1b[38;5;246me2 ", "\x1b[38;5;246me3 ",
 255     "\x1b[38;5;246me4 ", "\x1b[38;5;246me5 ",
 256     "\x1b[38;5;246me6 ", "\x1b[38;5;246me7 ",
 257     "\x1b[38;5;246me8 ", "\x1b[38;5;246me9 ",
 258     "\x1b[38;5;246mea ", "\x1b[38;5;246meb ",
 259     "\x1b[38;5;246mec ", "\x1b[38;5;246med ",
 260     "\x1b[38;5;246mee ", "\x1b[38;5;246mef ",
 261     "\x1b[38;5;246mf0 ", "\x1b[38;5;246mf1 ",
 262     "\x1b[38;5;246mf2 ", "\x1b[38;5;246mf3 ",
 263     "\x1b[38;5;246mf4 ", "\x1b[38;5;246mf5 ",
 264     "\x1b[38;5;246mf6 ", "\x1b[38;5;246mf7 ",
 265     "\x1b[38;5;246mf8 ", "\x1b[38;5;246mf9 ",
 266     "\x1b[38;5;246mfa ", "\x1b[38;5;246mfb ",
 267     "\x1b[38;5;246mfc ", "\x1b[38;5;246mfd ",
 268     "\x1b[38;5;246mfe ", "\x1b[38;5;209mff ",
 269 }
 270 
 271 // hexRenderConfig groups several arguments given to the hex-rendering funcs
 272 type hexRenderConfig struct {
 273     // out is writer to send all output to
 274     out *bufio.Writer
 275 
 276     // offset is the byte-offset of the first byte shown on the current output
 277     // line: if shown at all, it's shown at the start the line
 278     offset uint
 279 
 280     // chunks is the 0-based counter for byte-chunks/lines shown so far, which
 281     // indirectly keeps track of when it's time to show a `breather` line
 282     chunks uint
 283 
 284     // ruler is the `ruler` content to show on `breather` lines
 285     ruler []byte
 286 
 287     // perLine is how many hex-encoded bytes are shown per line
 288     perLine uint
 289 
 290     // offsetWidth is the max string-width for the byte-offsets shown at the
 291     // start of output lines, and determines those values' left-padding
 292     offsetWidth uint
 293 
 294     // showOffsets determines whether byte-offsets are shown at all
 295     showOffsets bool
 296 
 297     // showASCII determines whether the ASCII-panels are shown at all
 298     showASCII bool
 299 }
 300 
 301 // viewHex renders bytes as lines/rows of ANSI-styled hex values
 302 func viewHex(w writer, r io.Reader, name string, cfg config) error {
 303     size := -1
 304     if f, ok := r.(*os.File); ok && r != os.Stdin {
 305         if st, err := f.Stat(); err == nil {
 306             size = int(st.Size())
 307         }
 308     }
 309 
 310     if cfg.Title != "" {
 311         fmt.Fprintf(w, "\x1b[4m%s\x1b[0m\n", cfg.Title)
 312         w.WriteString("\n")
 313     }
 314 
 315     if name == `-` {
 316         name = `<stdin>`
 317     }
 318 
 319     if size < 0 {
 320         fmt.Fprintf(w, "• %s\n", name)
 321     } else {
 322         const fs = "• %s  \x1b[38;5;245m(%s bytes)\x1b[0m\n"
 323         fmt.Fprintf(w, fs, name, sprintCommas(size))
 324     }
 325     w.WriteString("\n")
 326 
 327     // when done, emit a new line in case only part of the last line is
 328     // shown, which means no newline was emitted for it
 329     defer w.WriteString("\x1b[0m\n")
 330 
 331     rc := hexRenderConfig{
 332         out:     w,
 333         offset:  0,
 334         chunks:  0,
 335         perLine: 16,
 336         ruler:   nil,
 337 
 338         offsetWidth: 8,
 339         showOffsets: true,
 340         showASCII:   true,
 341     }
 342     rc.ruler = makeRuler(int(rc.perLine))
 343 
 344     // calling func Read directly can sometimes result in chunks shorter
 345     // than the max chunk-size, even when there are plenty of bytes yet
 346     // to read; to avoid that, use a buffered-reader to explicitly fill
 347     // a slice instead
 348     br := bufio.NewReader(r)
 349 
 350     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 351     cur := make([]byte, 0, rc.perLine)
 352     ahead := make([]byte, 0, rc.perLine)
 353 
 354     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 355     // so to speak
 356     cur, err := fillChunk(cur[:0], int(rc.perLine), br)
 357     if len(cur) == 0 {
 358         if err == io.EOF {
 359             err = nil
 360         }
 361         return err
 362     }
 363 
 364     for {
 365         ahead, err := fillChunk(ahead[:0], int(rc.perLine), br)
 366         if err != nil && err != io.EOF {
 367             return err
 368         }
 369 
 370         if len(ahead) == 0 {
 371             // done, maybe except for an extra line of output
 372             break
 373         }
 374 
 375         // show the byte-chunk on its own output line
 376         err = hexWriteBufferANSI(rc, cur, ahead)
 377         if err != nil {
 378             // probably a pipe was closed
 379             return nil
 380         }
 381 
 382         rc.chunks++
 383         rc.offset += uint(len(cur))
 384         cur = cur[:copy(cur, ahead)]
 385     }
 386 
 387     // don't forget the last output line
 388     if rc.chunks > 0 && len(cur) > 0 {
 389         return hexWriteBufferANSI(rc, cur, nil)
 390     }
 391     return nil
 392 }
 393 
 394 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
 395 // handles negatives, even though this app only uses it with non-negatives.
 396 func loopThousandsGroups(n int, fn func(i, n int)) {
 397     // 0 doesn't have a log10
 398     if n == 0 {
 399         fn(0, 0)
 400         return
 401     }
 402 
 403     sign := +1
 404     if n < 0 {
 405         n = -n
 406         sign = -1
 407     }
 408 
 409     intLog1000 := int(math.Log10(float64(n)) / 3)
 410     remBase := int(math.Pow10(3 * intLog1000))
 411 
 412     for i := 0; remBase > 0; i++ {
 413         group := (1000 * n) / remBase / 1000
 414         fn(i, sign*group)
 415         // if original number was negative, ensure only first
 416         // group gives a negative input to the callback
 417         sign = +1
 418 
 419         n %= remBase
 420         remBase /= 1000
 421     }
 422 }
 423 
 424 // hexWriteBufferANSI shows the hex byte-view using ANSI colors/styles
 425 func hexWriteBufferANSI(rc hexRenderConfig, first, second []byte) error {
 426     // show a ruler every few lines to make eye-scanning easier
 427     if rc.chunks%5 == 0 && rc.chunks > 0 {
 428         hexWriteRulerANSI(rc)
 429     }
 430 
 431     return hexWriteLineANSI(rc, first, second)
 432 }
 433 
 434 // hexWriteRulerANSI emits an indented ANSI-styled line showing spaced-out
 435 // dots, so as to help eye-scan items on nearby output lines
 436 func hexWriteRulerANSI(rc hexRenderConfig) {
 437     w := rc.out
 438     if len(rc.ruler) == 0 {
 439         w.WriteByte('\n')
 440         return
 441     }
 442 
 443     w.WriteString("\x1b[38;5;245m")
 444     indent := int(rc.offsetWidth) + len(hexViewPadding)
 445     writeSpaces(w, indent)
 446     w.Write(rc.ruler)
 447     w.WriteString("\x1b[0m\n")
 448 }
 449 
 450 func hexWriteLineANSI(rc hexRenderConfig, first, second []byte) error {
 451     w := rc.out
 452 
 453     // start each line with the byte-offset for the 1st item shown on it
 454     if rc.showOffsets {
 455         writeStyledCounter(w, int(rc.offsetWidth), rc.offset)
 456         w.WriteString(hexViewPadding + "\x1b[48;5;254m")
 457     } else {
 458         w.WriteString(hexViewPadding)
 459     }
 460 
 461     prevStyle := hexUnknownStyle
 462     for _, b := range first {
 463         // using the slow/generic fmt.Fprintf is a performance bottleneck,
 464         // since it's called for each input byte
 465         // w.WriteString(styledHexBytes[b])
 466 
 467         // this more complicated way of emitting output avoids repeating
 468         // ANSI styles when dealing with bytes which aren't displayable
 469         // ASCII symbols, thus emitting fewer bytes when dealing with
 470         // general binary datasets
 471         style := hexByteStyles[b]
 472         if style != prevStyle {
 473             w.WriteString(styledHexBytes[b])
 474             if style == hexStyleASCII {
 475                 // styling displayable ASCII symbols uses multiple different
 476                 // styles each time it happens, always forcing ANSI-style
 477                 // updates
 478                 style = hexUnknownStyle
 479             }
 480         } else {
 481             w.WriteString(hexSymbols[b])
 482         }
 483         prevStyle = style
 484     }
 485 
 486     w.WriteString("\x1b[0m")
 487     if rc.showASCII {
 488         hexWritePlainASCII(w, first, second, int(rc.perLine))
 489     }
 490 
 491     return w.WriteByte('\n')
 492 }
 493 
 494 // sprintCommas turns the non-negative number given into a readable string,
 495 // where digits are grouped-separated by commas
 496 func sprintCommas(n int) string {
 497     var sb strings.Builder
 498     loopThousandsGroups(n, func(i, n int) {
 499         if i == 0 {
 500             var buf [4]byte
 501             sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
 502             return
 503         }
 504         sb.WriteByte(',')
 505         writePad0Sub1000Counter(&sb, uint(n))
 506     })
 507     return sb.String()
 508 }
 509 
 510 func writeStyledCounter(w *bufio.Writer, width int, n uint) {
 511     var buf [32]byte
 512     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 513 
 514     // left-pad the final result with leading spaces
 515     writeSpaces(w, width-len(str))
 516 
 517     var style bool
 518     // emit leading part with 1 or 2 digits unstyled, ensuring the
 519     // rest or the rendered number's string is a multiple of 3 long
 520     if rem := len(str) % 3; rem != 0 {
 521         w.Write(str[:rem])
 522         str = str[rem:]
 523         // next digit-group needs some styling
 524         style = true
 525     } else {
 526         style = false
 527     }
 528 
 529     // alternate between styled/unstyled 3-digit groups
 530     for len(str) >= 3 {
 531         if !style {
 532             w.Write(str[:3])
 533         } else {
 534             // w.WriteString("\x1b[38;5;243m")
 535             w.WriteString("\x1b[38;5;249m")
 536             w.Write(str[:3])
 537             w.WriteString("\x1b[0m")
 538         }
 539 
 540         style = !style
 541         str = str[3:]
 542     }
 543 }
 544 
 545 // hexWritePlainASCII emits the side-panel showing all ASCII runs for each line
 546 func hexWritePlainASCII(w *bufio.Writer, first, second []byte, perline int) {
 547     // prev keeps track of the previous byte, so spaces are added
 548     // when bytes change from non-visible-ASCII to visible-ASCII
 549     prev := byte(0)
 550 
 551     spaces := 3*(perline-len(first)) + len(hexViewPadding)
 552 
 553     for _, b := range first {
 554         if 32 < b && b < 127 {
 555             if !(32 < prev && prev < 127) {
 556                 writeSpaces(w, spaces)
 557                 spaces = 1
 558             }
 559             w.WriteByte(b)
 560         }
 561         prev = b
 562     }
 563 
 564     for _, b := range second {
 565         if 32 < b && b < 127 {
 566             if !(32 < prev && prev < 127) {
 567                 writeSpaces(w, spaces)
 568                 spaces = 1
 569             }
 570             w.WriteByte(b)
 571         }
 572         prev = b
 573     }
 574 }
 575 
 576 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
 577 func writePad0Sub1000Counter(w io.Writer, n uint) {
 578     // precondition is 0...999
 579     if n > 999 {
 580         w.Write([]byte(`???`))
 581         return
 582     }
 583 
 584     var buf [3]byte
 585     buf[0] = byte(n/100) + '0'
 586     n %= 100
 587     buf[1] = byte(n/10) + '0'
 588     buf[2] = byte(n%10) + '0'
 589     w.Write(buf[:])
 590 }
 591 
 592 // makeRuler prerenders a ruler-line, used to make the output lines breathe
 593 func makeRuler(numitems int) []byte {
 594     if n := numitems / 4; n > 0 {
 595         return bytes.Repeat([]byte(`           ·`), n)
 596     }
 597     return nil
 598 }
 599 
 600 // fillChunk tries to read the number of bytes given, appending them to the
 601 // byte-slice given; this func returns an EOF error only when no bytes are
 602 // read, which somewhat simplifies error-handling for the func caller
 603 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 604     // read buffered-bytes up to the max chunk-size
 605     for i := 0; i < n; i++ {
 606         b, err := br.ReadByte()
 607         if err == nil {
 608             chunk = append(chunk, b)
 609             continue
 610         }
 611 
 612         if err == io.EOF && i > 0 {
 613             return chunk, nil
 614         }
 615         return chunk, err
 616     }
 617 
 618     // got the full byte-count asked for
 619     return chunk, nil
 620 }

     File: vida/hex-styles.awk
   1 #!/usr/bin/awk -f
   2 
   3 # all 0 bits
   4 $0 == 0 {
   5     print "\"\\x1b[38;5;111m00 \","
   6     next
   7 }
   8 
   9 # ascii symbol which need backslashing
  10 $0 == 34 || $0 == 92 {
  11     printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m\\%c\",\n", $0 + 0, $0
  12     next
  13 }
  14 
  15 # all other ascii symbol
  16 32 <= $0 && $0 <= 126 {
  17     printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m%c\",\n", $0 + 0, $0
  18     next
  19 }
  20 
  21 # all 1 bits
  22 $0 == 255 {
  23     print "\"\\x1b[38;5;209mff \","
  24     next
  25 }
  26 
  27 # all other bytes
  28 1 {
  29     printf "\"\\x1b[38;5;246m%02x \",\n", $0 + 0
  30     next
  31 }

     File: vida/hex-symbols.awk
   1 #!/usr/bin/awk -f
   2 
   3 # ascii symbol which need backslashing
   4 $0 == 34 || $0 == 92 {
   5     printf "\"%02x\\%c\",\n", $0 + 0, $0
   6     next
   7 }
   8 
   9 # all other ascii symbol
  10 32 <= $0 && $0 <= 126 {
  11     printf "\"%02x%c\",\n", $0 + 0, $0
  12     next
  13 }
  14 
  15 # all other bytes
  16 1 {
  17     printf "\"%02x \",\n", $0 + 0
  18     next
  19 }

     File: vida/html.go
   1 package main
   2 
   3 import (
   4     "fmt"
   5     "html"
   6     "io"
   7     "math"
   8     "strconv"
   9     "strings"
  10 
  11     _ "embed"
  12 )
  13 
  14 //go:embed style.css
  15 var styleCSS string
  16 
  17 const (
  18     htmlViewportContent = `content="width=device-width, initial-scale=1.0"`
  19     htmlViewportTag     = `<meta name="viewport" ` + htmlViewportContent + `>`
  20 )
  21 
  22 type htmlOutputHandler struct {
  23     Title     string
  24     NumInputs int
  25 }
  26 
  27 func newOutputHandlerHTML(c config) outputHandler {
  28     return htmlOutputHandler{
  29         Title:     c.Title,
  30         NumInputs: len(c.Inputs),
  31     }
  32 }
  33 
  34 func (h htmlOutputHandler) Begin(w writer) error {
  35     w.WriteString(`<!DOCTYPE html>` + "\n")
  36     // w.WriteString(`<html lang="en">` + "\n")
  37     w.WriteString(`<html>` + "\n")
  38 
  39     w.WriteString(`<head>` + "\n")
  40     w.WriteString(`    <meta charset="UTF-8">` + "\n")
  41     w.WriteString(`    ` + htmlViewportTag + "\n")
  42     w.WriteString(`    <link rel="icon" href="data:,">` + "\n")
  43     if len(h.Title) > 0 {
  44         w.WriteString(`    <title>`)
  45         w.WriteString(html.EscapeString(h.Title))
  46         w.WriteString(`</title>` + "\n")
  47     }
  48     w.WriteString(`    <style>` + "\n")
  49     w.WriteString(styleCSS)
  50     w.WriteString(`    </style>` + "\n")
  51     w.WriteString(`</head>` + "\n")
  52 
  53     _, err := w.WriteString(`<body>` + "\n")
  54     return adaptWriterError(err)
  55 }
  56 
  57 func (h htmlOutputHandler) Before(w writer, i int, name string) error {
  58     if i > 0 {
  59         return writeString(w, "\n\n")
  60     }
  61 
  62     if h.NumInputs > 1 {
  63         w.WriteString(`<h1>`)
  64         w.WriteString(html.EscapeString(adaptName(name)))
  65         w.WriteString(`</h1>`)
  66         return endLine(w)
  67     }
  68     return nil
  69 }
  70 
  71 func (h htmlOutputHandler) After(w writer, i int, name string) error {
  72     return nil
  73 }
  74 
  75 func (h htmlOutputHandler) End(w writer) error {
  76     w.WriteString(`</body>` + "\n")
  77     w.WriteString(`</html>` + "\n")
  78     return nil
  79 }
  80 
  81 func (h htmlOutputHandler) Error(w writer, err error) error {
  82     if err == nil {
  83         return nil
  84     }
  85 
  86     w.WriteString(`<p class="error">`)
  87     w.WriteString(html.EscapeString(err.Error()))
  88     w.WriteString(`</p>`)
  89     return err
  90 }
  91 
  92 type htmlTableWriter struct {
  93     out writer
  94     cfg rowParams
  95 }
  96 
  97 func (tw htmlTableWriter) BeforeTable() error {
  98     w := tw.out
  99     names := make([]string, 0, len(tw.cfg.Columns))
 100     for _, c := range tw.cfg.Columns {
 101         names = append(names, c.Name)
 102     }
 103 
 104     w.WriteString(`<table>` + "\n")
 105 
 106     w.WriteString(`<thead>` + "\n")
 107     w.WriteString(`<tr>`)
 108     tw.WriteTiles(names)
 109     tw.WriteRowIndex(-1, names)
 110     for _, c := range tw.cfg.Columns {
 111         w.WriteString(`<th>`)
 112         w.WriteString(html.EscapeString(c.Name))
 113         w.WriteString(`</th>`)
 114     }
 115     w.WriteString(`</tr>` + "\n")
 116     w.WriteString(`</thead>` + "\n")
 117 
 118     w.WriteString(`<tbody>` + "\n")
 119     return nil
 120 }
 121 
 122 func (tw htmlTableWriter) AfterTable() error {
 123     w := tw.out
 124     names := make([]string, 0, len(tw.cfg.Columns))
 125     for _, c := range tw.cfg.Columns {
 126         names = append(names, c.Name)
 127     }
 128 
 129     w.WriteString(`</tbody>` + "\n")
 130 
 131     w.WriteString(`<tfoot>` + "\n")
 132     w.WriteString(`<tr>`)
 133     tw.WriteTiles(names)
 134     tw.WriteRowIndex(-1, names)
 135     for _, c := range tw.cfg.Columns {
 136         w.WriteString(`<th>`)
 137         w.WriteString(html.EscapeString(c.Name))
 138         w.WriteString(`</th>`)
 139     }
 140     w.WriteString(`</tr>` + "\n")
 141     w.WriteString(`</tfoot>` + "\n")
 142 
 143     w.WriteString(`</table>` + "\n")
 144     return nil
 145 }
 146 
 147 func (tw htmlTableWriter) WriteHeaderRow(cols []columnMeta) error {
 148     return nil
 149 }
 150 
 151 func (tw htmlTableWriter) WriteTiles(values []string) error {
 152     w := tw.out
 153     w.WriteString(`<th class="t">`)
 154 
 155     for _, s := range values {
 156         style, tile := matchTileHTML(s)
 157         w.WriteString(`<span class="`)
 158         w.WriteString(style)
 159         w.WriteString(`">`)
 160         w.WriteString(tile)
 161         w.WriteString(`</span>`)
 162     }
 163 
 164     w.WriteString(`</th>`)
 165     return nil
 166 }
 167 
 168 func (tw htmlTableWriter) WriteRowIndex(i int, values []string) error {
 169     w := tw.out
 170     if i < 0 {
 171         w.WriteString(`<th class="ri"></th>`)
 172         return nil
 173     }
 174 
 175     var buf [32]byte
 176     s := strconv.AppendInt(buf[:0], int64(i), 10)
 177     w.WriteString(`<th class="ri">`)
 178     w.Write(s)
 179     w.WriteString(`</th>`)
 180     return nil
 181 }
 182 
 183 func (tw htmlTableWriter) WriteRowValues(values []string) error {
 184     w := tw.out
 185 
 186     for i, s := range values {
 187         if f, ok := tryNumeric(s); ok {
 188             style := numeric2class(f)
 189             w.WriteString(`<td class="f `)
 190             w.WriteString(style)
 191             w.WriteString(`" style="`)
 192             c := tw.cfg.Columns[i]
 193             writeNumericStyleHTML(w, f, c.Min, c.Max)
 194             w.WriteString(`">`)
 195             writeFloat64(w, f)
 196             w.WriteString(`</td>`)
 197             continue
 198         }
 199 
 200         if len(s) == 0 {
 201             w.WriteString(`<td class="empty"></td>`)
 202             continue
 203         }
 204 
 205         w.WriteString(`<td>`)
 206 
 207         if ok, err := tryLinkHTML(w, s); ok || err != nil {
 208             w.WriteString(`</td>`)
 209             if err != nil {
 210                 return err
 211             }
 212             continue
 213         }
 214 
 215         if ok, err := tryBase64HTML(w, s); ok || err != nil {
 216             w.WriteString(`</td>`)
 217             if err != nil {
 218                 return err
 219             }
 220             continue
 221         }
 222 
 223         w.WriteString(html.EscapeString(s))
 224         w.WriteString(`</td>`)
 225     }
 226 
 227     return nil
 228 }
 229 
 230 func (w htmlTableWriter) StartRow(values []string) error {
 231     c := w.cfg.Colors
 232     i := w.cfg.CategoryIndex
 233 
 234     if 0 <= i && i < len(values) && len(values[i]) > 0 && c != nil {
 235         cat := w.cfg.matchColor(values[i], paletteHTML)
 236         w.out.WriteString(`<tr class="`)
 237         w.out.WriteString(cat)
 238         w.out.WriteString(`">`)
 239         return nil
 240     }
 241 
 242     w.out.WriteString(`<tr>`)
 243     return nil
 244 }
 245 
 246 func (w htmlTableWriter) EndRow() error {
 247     w.out.WriteString(`</tr>`)
 248     return endLine(w.out)
 249 }
 250 
 251 func writeNumericStyleHTML(w writer, f float64, min, max float64) {
 252     fprintCellBackground(w, f, min, max, `transparent`)
 253 }
 254 
 255 // constants used in func FprintCellBackground
 256 const (
 257     poscol = `#c1dfc1` // #b9dfb9
 258     negcol = `#e7c6c6` // #d8aaaa
 259     bgcol  = `transparent`
 260     left   = 0.0
 261     right  = 100.0
 262 
 263     // format-strings for 2 or 3 decimal digits
 264     fs2 = `background:linear-gradient(to right,%s,%s %.2f%%,%s %.2f%%,%s %.2f%%,%s %.2f%%,%s)`
 265     fs3 = `background:linear-gradient(to right,%s,%s %.3f%%,%s %.3f%%,%s %.3f%%,%s %.3f%%,%s)`
 266 )
 267 
 268 func fprintCellBackground(w io.Writer, x float64, min, max float64, bgColor string) {
 269     // avoid nonsense CSS for nonsense inputs
 270     if math.IsNaN(x) || min >= max {
 271         return
 272     }
 273 
 274     // ensure zero-based proportional bars for values from all-positive domains
 275     if min > 0 {
 276         min = 0
 277     }
 278     // ensure zero-based proportional bars for values from all-negative domains
 279     if max < 0 {
 280         max = 0
 281     }
 282     zero := scale(0, min, max, left, right)
 283     p := scale(x, min, max, left, right)
 284 
 285     if x > 0 {
 286         fmt.Fprintf(w, fs2, bgColor, bgColor, zero, poscol, zero, poscol, p, bgColor, p, bgColor)
 287     }
 288     if x < 0 {
 289         fmt.Fprintf(w, fs2, bgColor, bgColor, p, negcol, p, negcol, zero, bgColor, zero, bgColor)
 290     }
 291     if x == 0 {
 292         fmt.Fprintf(w, `background-color:%s`, bgColor)
 293     }
 294 }
 295 
 296 func scale(x float64, xmin, xmax, ymin, ymax float64) float64 {
 297     k := (x - xmin) / (xmax - xmin)
 298     return (ymax-ymin)*k + ymin
 299 }
 300 
 301 func tryBase64HTML(w writer, s string) (ok bool, err error) {
 302     const (
 303         prefix = `data:`
 304         suffix = `;base64,`
 305     )
 306     if !strings.HasPrefix(s, prefix) {
 307         return false, nil
 308     }
 309 
 310     start := s
 311     if len(start) > 32 {
 312         start = s[:32]
 313     }
 314 
 315     semicolon := strings.Index(start, suffix)
 316     if semicolon < 0 {
 317         return false, nil
 318     }
 319 
 320     slash := strings.IndexByte(start, '/')
 321     if slash < len(prefix) {
 322         return false, nil
 323     }
 324 
 325     for _, b := range s[semicolon+len(suffix):] {
 326         if isValidBase64[b] {
 327             continue
 328         }
 329         return false, nil
 330     }
 331 
 332     switch s[len(prefix):slash] {
 333     case `image`:
 334         w.WriteString(`<img src="`)
 335         w.WriteString(s)
 336         err := writeString(w, `">`)
 337         return true, err
 338 
 339     case `audio`:
 340         w.WriteString(`<audio controls src="`)
 341         w.WriteString(s)
 342         err := writeString(w, `"></audio>`)
 343         return true, err
 344 
 345     case `video`:
 346         w.WriteString(`<video controls src="`)
 347         w.WriteString(s)
 348         err := writeString(w, `"></video>`)
 349         return true, err
 350 
 351     default:
 352         return false, nil
 353     }
 354 }
 355 
 356 func tryLinkHTML(w writer, s string) (ok bool, err error) {
 357     if f := strings.HasPrefix; !f(s, `https://`) && !f(s, `http://`) {
 358         return false, nil
 359     }
 360 
 361     if len(s) == 0 || strings.ContainsRune(s, '"') {
 362         return false, nil
 363     }
 364 
 365     w.WriteString(`<a rel="noopener noreferrer" href="`)
 366     w.WriteString(s)
 367     w.WriteString(`">`)
 368     w.WriteString(html.EscapeString(s))
 369     err = writeString(w, `</a>`)
 370     return true, err
 371 }
 372 
 373 func prose2html(w writer, r io.Reader, name string, cfg config) error {
 374     inParagraph := false
 375 
 376     err := loopLines(r, func(line string) error {
 377         line = strings.TrimSpace(line)
 378         if len(line) == 0 {
 379             if inParagraph {
 380                 inParagraph = false
 381                 return writeString(w, `</p>`+"\n")
 382             }
 383             return nil
 384         }
 385 
 386         if !inParagraph {
 387             inParagraph = true
 388             w.WriteString(`<p>` + "\n")
 389         }
 390 
 391         if ok, err := tryBase64HTML(w, line); ok || err != nil {
 392             return err
 393         }
 394         return proseLine2html(w, line)
 395     })
 396 
 397     if err != nil {
 398         return err
 399     }
 400 
 401     if inParagraph {
 402         w.WriteString(`</p>` + "\n")
 403     }
 404     return nil
 405 }
 406 
 407 func proseLine2html(w writer, line string) error {
 408     for len(line) > 0 {
 409         i, j := indexLink(line)
 410         if i < 0 {
 411             break
 412         }
 413 
 414         w.WriteString(html.EscapeString(line[:i]))
 415 
 416         href := line[i:j]
 417         w.WriteString(`<a rel="noopener noreferrer" href="`)
 418         w.WriteString(href)
 419         w.WriteString(`">`)
 420         w.WriteString(html.EscapeString(href))
 421         w.WriteString(`</a>`)
 422 
 423         line = line[j:]
 424     }
 425 
 426     w.WriteString(html.EscapeString(line))
 427     return writeString(w, `<br>`+"\n")
 428 }

     File: vida/info.txt
   1 vida [options...] [filepath...]
   2 
   3 VIew DAta emits/reformats table-like data or JSON as either ANSI-styled text,
   4 or even self-contained HTML. Output can be embellished/enriched using various
   5 options.
   6 
   7 Some of its supported input-output format permutations are data-converters,
   8 rather than data-viewers.
   9 
  10 Supported input-output format-pairs:
  11 
  12     base64   binary
  13     bytes    ansi
  14     csv      ansi
  15     csv      html
  16     csv      json
  17     csv      jsons
  18     csv      plain
  19     csv      tsv
  20     json     ansi
  21     json     html
  22     json     jsons
  23     json     plain
  24     jsonl    json
  25     lines    json
  26     lines    jsons
  27     null     html
  28     plain    ansi
  29     plain    html
  30     plain    json
  31     tsv      ansi
  32     tsv      csv
  33     tsv      html
  34     tsv      json
  35     tsv      jsons
  36     tsv      plain
  37 
  38 Special formats:
  39 
  40     bytes    input-format which shows colored-hex when output is `ansi`
  41 
  42     jsonl    `JSON Lines` is plain-text lines, each of which is valid JSON
  43 
  44     jsons    `JSON Strings` is JSON where nulls, booleans, and numbers are
  45              always turned into strings; it's an output-only format
  46 
  47     lines    input-format used to convert plain-text lines into JSON arrays
  48              of strings; output-formats `json` and `jsons` give the same
  49              output, in this case
  50 
  51     null     read nothing, or write nothing; filepaths and data are still
  52              opened, read, and checked, when used as an output-format
  53 
  54 Data-format aliases:
  55 
  56     datauri        alias for the `base64` format
  57     jsonlines      alias for the `jsonl` format
  58     jsonstrings    alias for the `jsonl` format
  59     prose          alias for the `plain` input-output format
  60     text           alias for the `plain` input-output format
  61     web            alias for the `html` output-format
  62     webpage        alias for the `html` output-format

     File: vida/io.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "errors"
   7     "io"
   8     "os"
   9     "strconv"
  10     "strings"
  11 )
  12 
  13 type writer = *bufio.Writer
  14 
  15 // errNoMoreOutput is a dummy error which is ignored deliberately: it allows
  16 // quitting this app right away and successfully
  17 var errNoMoreOutput = errors.New(`no more output`)
  18 
  19 // bullets is a cache trying to minimize string (re)allocations when showing
  20 // tallies after data rows, when that option is enabled
  21 var bullets = ``
  22 
  23 func loopLines(r io.Reader, fn func(line string) error) error {
  24     sc := bufio.NewScanner(r)
  25     sc.Buffer(nil, lineBufferLimit)
  26 
  27     for i := 0; sc.Scan(); i++ {
  28         line := sc.Text()
  29         if i == 0 {
  30             line = strings.TrimPrefix(line, utf8BOM)
  31         }
  32         if err := fn(line); err != nil {
  33             return err
  34         }
  35     }
  36 
  37     return sc.Err()
  38 }
  39 
  40 func adaptWriterError(err error) error {
  41     if err == nil {
  42         return nil
  43     }
  44     return errNoMoreOutput
  45 }
  46 
  47 func prefaceError(intro string, err error) error {
  48     if err == nil || err == errNoMoreOutput {
  49         return nil
  50     }
  51     return errors.New(intro + `: ` + err.Error())
  52 }
  53 
  54 func showError(err error) {
  55     if err == nil || err == errNoMoreOutput {
  56         return
  57     }
  58 
  59     os.Stderr.WriteString("\x1b[31m")
  60     os.Stderr.WriteString(err.Error())
  61     os.Stderr.WriteString("\x1b[0m\n")
  62 }
  63 
  64 // writeBullets tries to minimize write-like operations
  65 func writeBullets(w writer, n int) {
  66     if n < 1 {
  67         return
  68     }
  69 
  70     i := len(bullet) * n
  71     if i > len(bullets) {
  72         // round-up to chunk-sizes of multiple kilobytes
  73         c := ceilIntBy(n, (10*1024)/len(bullet))
  74         c = maxInt(c, 1024/len(bullet))
  75         bullets = strings.Repeat(bullet, c)
  76     }
  77 
  78     w.WriteString(bullets[:i])
  79 }
  80 
  81 // writeSpaces tries to minimize write-like operations
  82 func writeSpaces(w writer, n int) {
  83     const spaces = `                                `
  84     if n < 1 {
  85         return
  86     }
  87 
  88     for n >= len(spaces) {
  89         w.WriteString(spaces)
  90         n -= len(spaces)
  91     }
  92     w.WriteString(spaces[:n])
  93 }
  94 
  95 func writeString(w writer, s string) error {
  96     _, err := w.WriteString(s)
  97     return adaptWriterError(err)
  98 }
  99 
 100 func endLine(w writer) error {
 101     return adaptWriterError(w.WriteByte('\n'))
 102 }
 103 
 104 func padWriteInt64(w writer, n int64, pad int) {
 105     var buf [32]byte
 106     s := strconv.AppendInt(buf[:0], n, 10)
 107     writeSpaces(w, pad-len(s))
 108     w.Write(s)
 109 }
 110 
 111 func numeric2style(f float64) string {
 112     if f > 0 {
 113         if float64(int64(f)) != f {
 114             return positiveDecimalStyleANSI
 115         }
 116         return positiveIntegerStyleANSI
 117     }
 118     if f < 0 {
 119         if float64(int64(f)) != f {
 120             return negativeDecimalStyleANSI
 121         }
 122         return negativeIntegerStyleANSI
 123     }
 124     if f == 0 {
 125         return zeroStyleANSI
 126     }
 127     return ``
 128 }
 129 
 130 func numeric2class(f float64) string {
 131     if f > 0 {
 132         if float64(int64(f)) != f {
 133             return positiveDecimalClassHTML
 134         }
 135         return positiveIntegerClassHTML
 136     }
 137     if f < 0 {
 138         if float64(int64(f)) != f {
 139             return negativeDecimalClassHTML
 140         }
 141         return negativeIntegerClassHTML
 142     }
 143     if f == 0 {
 144         return zeroClassHTML
 145     }
 146     return ``
 147 }
 148 
 149 func writeNumericANSI(w writer, f float64, cm columnMeta) {
 150     const alt = altDigitGroupStyleANSI
 151     const reset = resetStyleANSI
 152 
 153     var buf [32]byte
 154     s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64)
 155     pre := cm.MaxWidth - len(s)
 156 
 157     post := 0
 158     if cm.MaxDecimals > 0 {
 159         decs := countDecimals(s)
 160         post = cm.MaxDecimals - decs
 161         pre -= post
 162     }
 163 
 164     // writeSpaces(w, pre)
 165     // w.WriteString(numeric2style(f))
 166     // w.Write(s)
 167     // w.WriteString(resetStyleANSI)
 168     // writeSpaces(w, post)
 169 
 170     style := numeric2style(f)
 171     writeSpaces(w, pre)
 172     w.WriteString(style)
 173     if dot := bytes.IndexByte(s, '.'); dot >= 0 {
 174         alternateDigitGroups(w, s[:dot], alt, reset)
 175         w.WriteString(style)
 176         w.Write(s[dot:])
 177         w.WriteString(reset)
 178     } else {
 179         alternateDigitGroups(w, s, alt, reset)
 180     }
 181     w.WriteString(resetStyleANSI)
 182     writeSpaces(w, post)
 183 }
 184 
 185 func writeFloat64(w writer, f float64) {
 186     var buf [32]byte
 187     s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64)
 188     w.Write(s)
 189 }
 190 
 191 func writeNumericPlain(w writer, f float64, cm columnMeta) {
 192     var buf [32]byte
 193     s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64)
 194     pre := cm.MaxWidth - len(s)
 195 
 196     post := 0
 197     if cm.MaxDecimals > 0 {
 198         decs := countDecimals(s)
 199         post = cm.MaxDecimals - decs
 200         pre -= post
 201     }
 202 
 203     writeSpaces(w, pre)
 204     w.Write(s)
 205     writeSpaces(w, post)
 206 }
 207 
 208 func writeStringANSI(w writer, s string) {
 209     if isPadded(s) {
 210         w.WriteString(paddedStyleANSI)
 211         w.WriteString(s)
 212         w.WriteString(resetStyleANSI)
 213         return
 214     }
 215 
 216     w.WriteString(s)
 217 }
 218 
 219 func alternateDigitGroups(w writer, s []byte, alt string, reset string) {
 220     if len(s) > 0 && s[0] == '-' {
 221         w.WriteByte('-')
 222         s = s[1:]
 223     }
 224 
 225     if len(s) < 4 {
 226         w.Write(s)
 227         return
 228     }
 229 
 230     lead := len(s) % 3
 231     w.Write(s[:lead])
 232     s = s[lead:]
 233 
 234     for style := lead > 0; len(s) > 0; s = s[3:] {
 235         if style {
 236             w.WriteString(alt)
 237             w.Write(s[:3])
 238             w.WriteString(reset)
 239         } else {
 240             w.Write(s[:3])
 241         }
 242         style = !style
 243     }
 244 }
 245 
 246 func alternateDigitGroupsString(w writer, s string, alt string, reset string) {
 247     if len(s) > 0 && s[0] == '-' {
 248         w.WriteByte('-')
 249         s = s[1:]
 250     }
 251 
 252     if len(s) < 4 {
 253         w.WriteString(s)
 254         return
 255     }
 256 
 257     lead := len(s) % 3
 258     w.WriteString(s[:lead])
 259     s = s[lead:]
 260 
 261     for style := lead > 0; len(s) > 0; s = s[3:] {
 262         if style {
 263             w.WriteString(alt)
 264             w.WriteString(s[:3])
 265             w.WriteString(reset)
 266         } else {
 267             w.WriteString(s[:3])
 268         }
 269         style = !style
 270     }
 271 }
 272 
 273 func matchTileANSI(s string) (style, tile string) {
 274     if f, ok := tryNumeric(s); ok {
 275         return numeric2style(f), usualTile
 276     }
 277 
 278     if len(s) == 0 {
 279         return emptyStyleANSI, emptyTile
 280     }
 281 
 282     if isPadded(s) {
 283         return paddedStyleANSI, usualTile
 284     }
 285 
 286     return normalStyleANSI, usualTile
 287 }
 288 
 289 func matchTileHTML(s string) (style, tile string) {
 290     if f, ok := tryNumeric(s); ok {
 291         return numeric2class(f), usualTile
 292     }
 293 
 294     if len(s) == 0 {
 295         return emptyClassHTML, emptyTile
 296     }
 297 
 298     if isPadded(s) {
 299         return paddedClassHTML, usualTile
 300     }
 301 
 302     return normalClassHTML, usualTile
 303 }
 304 
 305 type outputHandler interface {
 306     Begin(w writer) error
 307     Before(w writer, i int, name string) error
 308     After(w writer, i int, name string) error
 309     End(w writer) error
 310     Error(w writer, err error) error
 311 }
 312 
 313 type noWrapOutputHandler struct {
 314     NumInputs int
 315 }
 316 
 317 func newOutputHandlerNoWrap(c config) outputHandler {
 318     return noWrapOutputHandler{
 319         NumInputs: len(c.Inputs),
 320     }
 321 }
 322 
 323 func (h noWrapOutputHandler) Begin(w writer) error            { return nil }
 324 func (h noWrapOutputHandler) End(w writer) error              { return nil }
 325 func (h noWrapOutputHandler) Error(w writer, err error) error { return nil }
 326 
 327 func (h noWrapOutputHandler) Before(w writer, i int, name string) error {
 328     if i > 0 {
 329         return writeString(w, "\n\n")
 330     }
 331 
 332     if h.NumInputs > 1 {
 333         w.WriteString(bullet + ` `)
 334         w.WriteString(adaptName(name))
 335         return endLine(w)
 336     }
 337     return nil
 338 }
 339 
 340 func (h noWrapOutputHandler) After(w writer, i int, name string) error {
 341     return nil
 342 }
 343 
 344 func null2anything(w writer, r io.Reader, name string, cfg config) error {
 345     return nil
 346 }

     File: vida/io_test.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "strings"
   6     "testing"
   7     "unicode/utf8"
   8 )
   9 
  10 func TestWriteBullets(t *testing.T) {
  11     const max = 2_000
  12     var sb strings.Builder
  13     sb.Grow(len(bullet) * max)
  14 
  15     for c := -100; c < max; c++ {
  16         sb.Reset()
  17         w := bufio.NewWriter(&sb)
  18         writeBullets(w, c)
  19         w.Flush()
  20 
  21         got := sb.String()
  22         exp := strings.Repeat(bullet, maxInt(c, 0))
  23         m := utf8.RuneCountInString(exp)
  24         n := utf8.RuneCountInString(got)
  25         if m != n {
  26             const fs = `%d: expected len(%d), instead of len(%d)`
  27             t.Fatalf(fs, c, m, n)
  28             return
  29         }
  30     }
  31 }
  32 
  33 func TestWriteSpaces(t *testing.T) {
  34     const max = 2_000
  35     var sb strings.Builder
  36     sb.Grow(max)
  37 
  38     for c := -100; c < max; c++ {
  39         sb.Reset()
  40         w := bufio.NewWriter(&sb)
  41         writeSpaces(w, c)
  42         w.Flush()
  43 
  44         got := sb.String()
  45         exp := strings.Repeat(` `, maxInt(c, 0))
  46         if len(exp) != len(got) {
  47             const fs = `%d: expected len(%d), instead of len(%d)`
  48             t.Fatalf(fs, c, len(exp), len(got))
  49             return
  50         }
  51     }
  52 }

     File: vida/json.go
   1 package main
   2 
   3 import (
   4     "encoding/json"
   5     "errors"
   6     "html"
   7     "io"
   8     "strconv"
   9     "strings"
  10 )
  11 
  12 var type2jsonWalker = map[string]func(w writer) jsonWalker{
  13     `ansi`:  func(w writer) jsonWalker { return &jsonWalkerANSI{w} },
  14     `html`:  func(w writer) jsonWalker { return &jsonWalkerHTML{w} },
  15     `jsons`: func(w writer) jsonWalker { return &jsonWalkerJSONS{jsonWalkerPlain{w}} },
  16     `plain`: func(w writer) jsonWalker { return &jsonWalkerPlain{w} },
  17     `null`:  func(w writer) jsonWalker { return &jsonWalkerNull{} },
  18 }
  19 
  20 func renderJSON(w writer, r io.Reader, name string, cfg config) error {
  21     makeWalker, ok := type2jsonWalker[cfg.To]
  22     if !ok {
  23         return convTypesError(name, cfg.From, cfg.To)
  24     }
  25 
  26     var h jsonHandler
  27     h.dec = json.NewDecoder(r)
  28     h.dec.UseNumber()
  29     return h.walk(makeWalker(w))
  30 }
  31 
  32 func jsonl2json(w writer, r io.Reader, name string, cfg config) error {
  33     i := 0
  34     var h jsonHandler
  35 
  36     err := loopLines(r, func(line string) error {
  37         line = strings.TrimSpace(line)
  38         if len(line) == 0 || line[0] == '#' || strings.HasPrefix(line, `//`) {
  39             return nil
  40         }
  41 
  42         if err := writeString(w, bool2str(i == 0, "[\n", ",\n")); err != nil {
  43             return err
  44         }
  45 
  46         i++
  47         writeSpaces(w, jsonLevelIndent)
  48         h.dec = json.NewDecoder(strings.NewReader(line))
  49         h.dec.UseNumber()
  50         return h.walk(jsonWalkerSingleLine{jsonWalkerPlain{w}})
  51     })
  52 
  53     if err != nil {
  54         return err
  55     }
  56     return writeString(w, bool2str(i == 0, "[]\n", "\n]\n"))
  57 }
  58 
  59 func lines2jsons(w writer, r io.Reader, name string, cfg config) error {
  60     i := 0
  61 
  62     err := loopLines(r, func(line string) error {
  63         if err := writeString(w, bool2str(i == 0, "[\n", ",\n")); err != nil {
  64             return err
  65         }
  66 
  67         i++
  68         writeSpaces(w, jsonLevelIndent)
  69         w.WriteByte('"')
  70         writeInnerStringJSON(w, line)
  71         w.WriteByte('"')
  72         return nil
  73     })
  74 
  75     if err != nil {
  76         return err
  77     }
  78     return writeString(w, bool2str(i == 0, "[]\n", "\n]\n"))
  79 }
  80 
  81 func text2jsons(w writer, r io.Reader, name string, cfg config) error {
  82     b, err := io.ReadAll(r)
  83     if err != nil {
  84         return err
  85     }
  86 
  87     w.WriteByte('"')
  88     writeInnerStringJSON(w, string(b))
  89     w.WriteByte('"')
  90     return endLine(w)
  91 }
  92 
  93 type jsonWalker interface {
  94     End() error
  95 
  96     Indent(level int) error
  97 
  98     Null() error
  99     Bool(b bool) error
 100     Number(f float64, s string) error
 101     String(s string) error
 102 
 103     ArrayItem(i, level int) error
 104     EndArray(i, level int) error
 105 
 106     Key(i int, k string, level int) error
 107     EndObject(i, level int) error
 108 }
 109 
 110 // jsonHandler recursively digs into JSON input, and renders it according to
 111 // the callbacks/funcs it's configured with; it assumes the json.Decoder it
 112 // uses is in numbers-mode, activated by calling json.Decoder.UseNumber()
 113 type jsonHandler struct {
 114     dec *json.Decoder
 115 
 116     // level is the current nesting-level
 117     level int
 118 }
 119 
 120 func (h *jsonHandler) walk(w jsonWalker) error {
 121     t, err := h.dec.Token()
 122     if err == io.EOF {
 123         return errors.New(`input has no JSON values`)
 124     }
 125     if err != nil {
 126         return err
 127     }
 128 
 129     err = h.token(w, t)
 130     endErr := w.End()
 131 
 132     if err != nil {
 133         return err
 134     }
 135     if endErr != nil {
 136         return endErr
 137     }
 138 
 139     _, err = h.dec.Token()
 140     if err == io.EOF {
 141         return nil
 142     }
 143     return errors.New(`unexpected trailing JSON data`)
 144 }
 145 
 146 // token handles read/parsed tokens; this func doesn't read tokens
 147 func (h *jsonHandler) token(w jsonWalker, t json.Token) error {
 148     switch t := t.(type) {
 149     case nil:
 150         return w.Null()
 151 
 152     case bool:
 153         return w.Bool(t)
 154 
 155     case json.Number:
 156         f, _ := t.Float64()
 157         return w.Number(f, t.String())
 158 
 159     case string:
 160         return w.String(t)
 161 
 162     case json.Delim:
 163         switch t {
 164         case json.Delim('['):
 165             return h.array(w)
 166         case json.Delim('{'):
 167             return h.object(w)
 168         default:
 169             return errors.New(`unsupported JSON delimiter`)
 170         }
 171 
 172     default:
 173         return errors.New(`unsupported JSON token`)
 174     }
 175 }
 176 
 177 // array handles arrays for func token
 178 func (h *jsonHandler) array(w jsonWalker) error {
 179     level := h.level
 180     h.level++
 181     defer func() { h.level = level }()
 182 
 183     for i := 0; true; i++ {
 184         t, err := h.dec.Token()
 185         if err != nil {
 186             return err
 187         }
 188 
 189         if t == json.Delim(']') {
 190             return w.EndArray(i, h.level-1)
 191         }
 192 
 193         if err := w.ArrayItem(i, h.level); err != nil {
 194             return err
 195         }
 196 
 197         if err := h.token(w, t); err != nil {
 198             return err
 199         }
 200     }
 201 
 202     // make the compiler happy
 203     return nil
 204 }
 205 
 206 // object handles objects for func token
 207 func (h *jsonHandler) object(w jsonWalker) error {
 208     level := h.level
 209     h.level++
 210     defer func() { h.level = level }()
 211 
 212     for i := 0; true; i++ {
 213         t, err := h.dec.Token()
 214         if err != nil {
 215             return err
 216         }
 217 
 218         if t == json.Delim('}') {
 219             return w.EndObject(i, level)
 220         }
 221 
 222         // the stdlib JSON decoder is supposed to fail with non-string keys
 223         k, _ := t.(string)
 224         if err := w.Key(i, k, h.level); err != nil {
 225             return err
 226         }
 227 
 228         t, err = h.dec.Token()
 229         if err != nil {
 230             return err
 231         }
 232 
 233         if err := h.token(w, t); err != nil {
 234             return err
 235         }
 236     }
 237 
 238     // make the compiler happy
 239     return nil
 240 }
 241 
 242 type jsonWalkerANSI struct {
 243     out writer
 244 }
 245 
 246 func (w jsonWalkerANSI) End() error {
 247     w.out.WriteByte('\n')
 248     return nil
 249 }
 250 
 251 func (w jsonWalkerANSI) Indent(level int) error {
 252     writeSpaces(w.out, jsonLevelIndent*level)
 253     return nil
 254 }
 255 
 256 func (w jsonWalkerANSI) Null() error {
 257     w.out.WriteString(nullStyleANSI + `null` + resetStyleANSI)
 258     return nil
 259 }
 260 
 261 func (w jsonWalkerANSI) Bool(b bool) error {
 262     if b {
 263         w.out.WriteString(boolStyleANSI + `true` + resetStyleANSI)
 264     } else {
 265         w.out.WriteString(boolStyleANSI + `false` + resetStyleANSI)
 266     }
 267     return nil
 268 }
 269 
 270 func (w jsonWalkerANSI) Number(f float64, s string) error {
 271     style := numeric2style(f)
 272     w.out.WriteString(style)
 273     w.out.WriteString(s)
 274     w.out.WriteString(resetStyleANSI)
 275     return nil
 276 }
 277 
 278 func (w jsonWalkerANSI) String(s string) error {
 279     w.out.WriteString(syntaxStyleANSI + `"` + resetStyleANSI)
 280     w.out.WriteString(stringStyleANSI)
 281     writeInnerStringJSON(w.out, s)
 282     w.out.WriteString(syntaxStyleANSI + `"` + resetStyleANSI)
 283     return nil
 284 }
 285 
 286 func (w jsonWalkerANSI) EndArray(i, level int) error {
 287     if i == 0 {
 288         w.out.WriteString(syntaxStyleANSI + `[]` + resetStyleANSI)
 289     } else {
 290         w.out.WriteByte('\n')
 291         w.Indent(level)
 292         w.out.WriteString(syntaxStyleANSI + `]` + resetStyleANSI)
 293     }
 294     return nil
 295 }
 296 
 297 func (w jsonWalkerANSI) ArrayItem(i, level int) error {
 298     var err error
 299     if i == 0 {
 300         err = writeString(w.out, syntaxStyleANSI+`[`+resetStyleANSI+"\n")
 301     } else {
 302         err = writeString(w.out, syntaxStyleANSI+`,`+resetStyleANSI+"\n")
 303     }
 304 
 305     if err != nil {
 306         return err
 307     }
 308     return w.Indent(level)
 309 }
 310 
 311 func (w jsonWalkerANSI) EndObject(i, level int) error {
 312     if i == 0 {
 313         w.out.WriteString(syntaxStyleANSI + `{}` + resetStyleANSI)
 314     } else {
 315         w.out.WriteByte('\n')
 316         w.Indent(level)
 317         w.out.WriteString(syntaxStyleANSI + `}` + resetStyleANSI)
 318     }
 319     return nil
 320 }
 321 
 322 func (w jsonWalkerANSI) Key(i int, s string, level int) error {
 323     var err error
 324     if i == 0 {
 325         err = writeString(w.out, syntaxStyleANSI+`{`+resetStyleANSI+"\n")
 326     } else {
 327         err = writeString(w.out, syntaxStyleANSI+`,`+resetStyleANSI+"\n")
 328     }
 329 
 330     if err != nil {
 331         return err
 332     }
 333 
 334     w.Indent(level)
 335     w.out.WriteString(syntaxStyleANSI + `"` + keyStyleANSI)
 336     writeInnerStringJSON(w.out, s)
 337     w.out.WriteString(syntaxStyleANSI + `":` + resetStyleANSI + ` `)
 338     return nil
 339 }
 340 
 341 type jsonWalkerJSONS struct {
 342     jsonWalkerPlain
 343 }
 344 
 345 func (w jsonWalkerJSONS) Null() error {
 346     w.out.WriteString(`"null"`)
 347     return nil
 348 }
 349 
 350 func (w jsonWalkerJSONS) Bool(b bool) error {
 351     if b {
 352         w.out.WriteString(`"true"`)
 353     } else {
 354         w.out.WriteString(`"false"`)
 355     }
 356     return nil
 357 }
 358 
 359 func (w jsonWalkerJSONS) Number(f float64, s string) error {
 360     w.out.WriteByte('"')
 361     writeFloat64(w.out, f)
 362     w.out.WriteByte('"')
 363     return nil
 364 }
 365 
 366 type jsonWalkerHTML struct {
 367     out writer
 368 }
 369 
 370 func (w jsonWalkerHTML) End() error {
 371     return nil
 372 }
 373 
 374 func (w jsonWalkerHTML) Indent(level int) error {
 375     return nil
 376 }
 377 
 378 func (w jsonWalkerHTML) Null() error {
 379     return writeString(w.out, `<span class="n">null</span>`)
 380 }
 381 
 382 func (w jsonWalkerHTML) Bool(b bool) error {
 383     if b {
 384         return writeString(w.out, `<span class="b">true</span>`)
 385     } else {
 386         return writeString(w.out, `<span class="b">false</span>`)
 387     }
 388 }
 389 
 390 func (w jsonWalkerHTML) Number(f float64, s string) error {
 391     w.out.WriteString(`<span class="f `)
 392     w.out.WriteString(numeric2class(f))
 393     w.out.WriteString(`">`)
 394     writeFloat64(w.out, f)
 395     return writeString(w.out, `</span>`)
 396 }
 397 
 398 func (w jsonWalkerHTML) String(s string) error {
 399     if len(s) == 0 {
 400         return writeString(w.out, `<span class="empty"></span>`)
 401     }
 402 
 403     w.out.WriteString(`<span>`)
 404     defer w.out.WriteString(`</span>`)
 405 
 406     if ok, err := tryLinkHTML(w.out, s); ok || err != nil {
 407         return err
 408     }
 409 
 410     if ok, err := tryBase64HTML(w.out, s); ok || err != nil {
 411         return err
 412     }
 413 
 414     return writeString(w.out, html.EscapeString(s))
 415 }
 416 
 417 func (w jsonWalkerHTML) EndArray(i, level int) error {
 418     if i == 0 {
 419         w.out.WriteString(`<details open>` + "\n")
 420         w.out.WriteString(`<summary>empty array</summary>` + "\n")
 421         return writeString(w.out, `</details>`+"\n")
 422     }
 423 
 424     w.out.WriteString(`</li>` + "\n")
 425     w.out.WriteString(`</ul>` + "\n")
 426     return writeString(w.out, `</details>`+"\n")
 427 }
 428 
 429 func (w jsonWalkerHTML) ArrayItem(i, level int) error {
 430     if i == 0 {
 431         w.out.WriteString(`<details open>` + "\n")
 432         w.out.WriteString(`<summary>array</summary>` + "\n")
 433         return writeString(w.out, `<ul>`+"\n"+`<li>`)
 434     }
 435 
 436     return writeString(w.out, `</li>`+"\n"+`<li>`)
 437 }
 438 
 439 func (w jsonWalkerHTML) EndObject(i, level int) error {
 440     if i == 0 {
 441         w.out.WriteString(`<details open>` + "\n")
 442         w.out.WriteString(`<summary>empty object</summary>` + "\n")
 443         return writeString(w.out, `</details">`+"\n")
 444     }
 445 
 446     w.out.WriteString(`</div>` + "\n")
 447     return writeString(w.out, `</details>`+"\n")
 448 }
 449 
 450 func (w jsonWalkerHTML) Key(i int, s string, level int) error {
 451     if i == 0 {
 452         w.out.WriteString(`<details open>` + "\n")
 453         w.out.WriteString(`<summary>object</summary>` + "\n")
 454         w.out.WriteString(`<div class="pairs">` + "\n")
 455     }
 456 
 457     w.out.WriteString(`<span class="key">`)
 458     w.out.WriteString(html.EscapeString(s))
 459     return writeString(w.out, `</span>`)
 460 }
 461 
 462 type jsonWalkerNull struct{}
 463 
 464 func (w jsonWalkerNull) End() error                           { return nil }
 465 func (w jsonWalkerNull) Indent(level int) error               { return nil }
 466 func (w jsonWalkerNull) Null() error                          { return nil }
 467 func (w jsonWalkerNull) Bool(b bool) error                    { return nil }
 468 func (w jsonWalkerNull) Number(f float64, s string) error     { return nil }
 469 func (w jsonWalkerNull) String(s string) error                { return nil }
 470 func (w jsonWalkerNull) EndArray(i, level int) error          { return nil }
 471 func (w jsonWalkerNull) ArrayItem(i, level int) error         { return nil }
 472 func (w jsonWalkerNull) EndObject(i, level int) error         { return nil }
 473 func (w jsonWalkerNull) Key(i int, s string, level int) error { return nil }
 474 
 475 type jsonWalkerPlain struct {
 476     out writer
 477 }
 478 
 479 func (w jsonWalkerPlain) End() error {
 480     w.out.WriteByte('\n')
 481     return nil
 482 }
 483 
 484 func (w jsonWalkerPlain) Indent(level int) error {
 485     writeSpaces(w.out, jsonLevelIndent*level)
 486     return nil
 487 }
 488 
 489 func (w jsonWalkerPlain) Null() error {
 490     w.out.WriteString(`null`)
 491     return nil
 492 }
 493 
 494 func (w jsonWalkerPlain) Bool(b bool) error {
 495     if b {
 496         w.out.WriteString(`true`)
 497     } else {
 498         w.out.WriteString(`false`)
 499     }
 500     return nil
 501 }
 502 
 503 func (w jsonWalkerPlain) Number(f float64, s string) error {
 504     writeFloat64(w.out, f)
 505     return nil
 506 }
 507 
 508 func (w jsonWalkerPlain) String(s string) error {
 509     w.out.WriteString(`"`)
 510     writeInnerStringJSON(w.out, s)
 511     w.out.WriteString(`"`)
 512     return nil
 513 }
 514 
 515 func (w jsonWalkerPlain) EndArray(i, level int) error {
 516     if i == 0 {
 517         w.out.WriteString(`[]`)
 518     } else {
 519         w.out.WriteByte('\n')
 520         w.Indent(level)
 521         w.out.WriteString(`]`)
 522     }
 523     return nil
 524 }
 525 
 526 func (w jsonWalkerPlain) ArrayItem(i, level int) error {
 527     var err error
 528     if i == 0 {
 529         err = writeString(w.out, "[\n")
 530     } else {
 531         err = writeString(w.out, ",\n")
 532     }
 533 
 534     if err != nil {
 535         return err
 536     }
 537     return w.Indent(level)
 538 }
 539 
 540 func (w jsonWalkerPlain) EndObject(i, level int) error {
 541     if i == 0 {
 542         w.out.WriteString(`{}`)
 543     } else {
 544         w.out.WriteByte('\n')
 545         w.Indent(level)
 546         w.out.WriteString(`}`)
 547     }
 548     return nil
 549 }
 550 
 551 func (w jsonWalkerPlain) Key(i int, s string, level int) error {
 552     var err error
 553     if i == 0 {
 554         err = writeString(w.out, "{\n")
 555     } else {
 556         err = writeString(w.out, ",\n")
 557     }
 558 
 559     if err != nil {
 560         return err
 561     }
 562 
 563     w.Indent(level)
 564     w.out.WriteString(`"`)
 565     writeInnerStringJSON(w.out, s)
 566     w.out.WriteString(`": `)
 567     return nil
 568 }
 569 
 570 type jsonWalkerSingleLine struct {
 571     jsonWalkerPlain
 572 }
 573 
 574 func (w jsonWalkerSingleLine) End() error {
 575     return nil
 576 }
 577 
 578 func (w jsonWalkerSingleLine) Indent(level int) error {
 579     return nil
 580 }
 581 
 582 func (w jsonWalkerSingleLine) EndArray(i, level int) error {
 583     if i == 0 {
 584         w.out.WriteString(`[]`)
 585     } else {
 586         w.out.WriteString(`]`)
 587     }
 588     return nil
 589 }
 590 
 591 func (w jsonWalkerSingleLine) ArrayItem(i, level int) error {
 592     if i == 0 {
 593         w.out.WriteString(`[`)
 594     } else {
 595         w.out.WriteString(`, `)
 596     }
 597     return nil
 598 }
 599 
 600 func (w jsonWalkerSingleLine) EndObject(i, level int) error {
 601     if i == 0 {
 602         w.out.WriteString(`{}`)
 603     } else {
 604         w.out.WriteString(`}`)
 605     }
 606     return nil
 607 }
 608 
 609 func (w jsonWalkerSingleLine) Key(i int, s string, level int) error {
 610     if i == 0 {
 611         w.out.WriteString(`{`)
 612     } else {
 613         w.out.WriteString(`, `)
 614     }
 615 
 616     w.out.WriteString(`"`)
 617     writeInnerStringJSON(w.out, s)
 618     w.out.WriteString(`": `)
 619     return nil
 620 }
 621 
 622 // writeInnerStringJSON ensures this app's JSON-sourced output has all its
 623 // strings properly escaped: this allows plain-text output to be used as
 624 // valid JSON, along with its ANSI-styled variant, if styles are filtered
 625 // out by other tools
 626 func writeInnerStringJSON(w writer, s string) {
 627     if !strings.ContainsAny(s, "\"\\\t\r\n") {
 628         w.WriteString(s)
 629         return
 630     }
 631 
 632     for _, r := range s {
 633         switch r {
 634         case '"':
 635             w.WriteString(`\"`)
 636         case '\\':
 637             w.WriteString(`\\`)
 638         case '\t':
 639             w.WriteString(`\t`)
 640         case '\r':
 641             w.WriteString(`\r`)
 642         case '\n':
 643             w.WriteString(`\n`)
 644         default:
 645             w.WriteRune(r)
 646         }
 647     }
 648 }
 649 
 650 type kvRowWriter func(w writer, keys, values []string)
 651 
 652 func writeRowJSON(w writer, keys, values []string) {
 653     w.WriteByte('{')
 654 
 655     for i, k := range keys {
 656         // treat missing trailing values as null ones
 657         s := ``
 658         if i < len(values) {
 659             s = values[i]
 660         }
 661 
 662         if i > 0 {
 663             w.WriteString(`, "`)
 664         } else {
 665             w.WriteByte('"')
 666         }
 667 
 668         writeInnerStringJSON(w, k)
 669         w.WriteString(`": `)
 670         if len(s) == 0 {
 671             w.WriteString(`null`)
 672             continue
 673         }
 674 
 675         if f, ok := tryNumeric(s); ok {
 676             var buf [32]byte
 677             s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64)
 678             w.Write(s)
 679             continue
 680         }
 681 
 682         w.WriteByte('"')
 683         writeInnerStringJSON(w, s)
 684         w.WriteByte('"')
 685     }
 686 
 687     // don't forget to treat missing trailing values as null ones
 688     for i := len(values); i < len(keys); i++ {
 689         if i > 0 {
 690             w.WriteString(`, "`)
 691         } else {
 692             w.WriteByte('"')
 693         }
 694 
 695         writeInnerStringJSON(w, keys[i])
 696         w.WriteString(`": null`)
 697     }
 698 
 699     w.WriteByte('}')
 700 }
 701 
 702 func writeRowJSONS(w writer, keys, values []string) {
 703     w.WriteByte('{')
 704 
 705     for i, k := range keys {
 706         // treat missing trailing values as null ones
 707         v := ``
 708         if i < len(values) {
 709             v = values[i]
 710         }
 711 
 712         if i > 0 {
 713             w.WriteString(`, "`)
 714         } else {
 715             w.WriteByte('"')
 716         }
 717 
 718         writeInnerStringJSON(w, k)
 719         w.WriteString(`": "`)
 720         writeInnerStringJSON(w, v)
 721         w.WriteByte('"')
 722     }
 723 
 724     w.WriteByte('}')
 725 }

     File: vida/main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "compress/bzip2"
   6     "compress/gzip"
   7     "errors"
   8     "io"
   9     "os"
  10     "path/filepath"
  11     "strings"
  12 
  13     _ "embed"
  14 )
  15 
  16 //go:embed info.txt
  17 var info string
  18 
  19 func main() {
  20     cfg, err := parseArgs(info)
  21     if err != nil {
  22         showError(err)
  23         os.Exit(1)
  24     }
  25 
  26     err = run(os.Stdout, cfg.Inputs, cfg)
  27     if err != nil && err != errNoMoreOutput {
  28         showError(err)
  29         os.Exit(1)
  30     }
  31 }
  32 
  33 func run(w io.Writer, paths []string, cfg config) error {
  34     if len(paths) == 0 {
  35         paths = []string{`-`}
  36     }
  37 
  38     // refuse to read from stdin more than once
  39     dashes := 0
  40     for _, p := range paths {
  41         if p == `-` {
  42             dashes++
  43         }
  44         if dashes > 1 {
  45             const msg = `can't use stdin/single-dash as input more than once`
  46             return errors.New(msg)
  47         }
  48     }
  49 
  50     bw := bufio.NewWriter(w)
  51     defer bw.Flush()
  52 
  53     makeHandler, ok := outFormat2handler[cfg.To]
  54     if !ok {
  55         return errors.New(cfg.To + ` isn't a supported output-format`)
  56     }
  57 
  58     h := makeHandler(cfg)
  59     if err := h.Begin(bw); err != nil {
  60         return err
  61     }
  62     defer h.End(bw)
  63 
  64     for i, path := range paths {
  65         if err := h.Before(bw, i, path); err != nil {
  66             return err
  67         }
  68         if err := handleInput(bw, path, cfg); err != nil {
  69             return prefaceError(adaptName(path), err)
  70         }
  71         if err := h.After(bw, i, path); err != nil {
  72             return err
  73         }
  74     }
  75     return nil
  76 }
  77 
  78 func convTypesError(path, from, to string) error {
  79     from = strings.TrimSuffix(from, `.gz`)
  80 
  81     if from == `` {
  82         const s = `: name doesn't suggest any supported input-type`
  83         return errors.New(path + s)
  84     }
  85 
  86     msg := `can't convert from ` + from + ` to ` + to
  87     return errors.New(msg)
  88 }
  89 
  90 func open(path string) (rc io.ReadCloser, kind string, err error) {
  91     if path == `-` {
  92         return io.NopCloser(os.Stdin), `tsv`, nil
  93     }
  94 
  95     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
  96     //  resp, err := http.Get(path)
  97     //  if err != nil {
  98     //      return nil, ``, err
  99     //  }
 100     //  kind = resp.Header.Get(`Content-Type`)
 101     //  if i := strings.IndexByte(kind, ';'); i >= 0 {
 102     //      kind = kind[:i]
 103     //      kind = strings.TrimSpace(kind)
 104     //  }
 105     //  return resp.Body, strings.ToLower(kind), err
 106     // }
 107 
 108     ext := fileExtension(path)
 109     kind = strings.TrimPrefix(ext, `.`)
 110     kind = strings.ToLower(kind)
 111 
 112     f, err := os.Open(path)
 113     if err != nil {
 114         return f, kind, errors.New(`can't open file named ` + path)
 115     }
 116     return f, kind, nil
 117 }
 118 
 119 func handleInput(w writer, path string, cfg config) error {
 120     rc, typeHint, err := open(path)
 121     if err != nil {
 122         return err
 123     }
 124     defer rc.Close()
 125 
 126     // the dummy `auto` input-format is meant to be overridden
 127     if cfg.From == `auto` {
 128         cfg.From = typeHint
 129     }
 130     cfg.From = strings.ToLower(cfg.From)
 131 
 132     // auto-handle gzipped input
 133     if strings.HasSuffix(cfg.From, `.gz`) {
 134         cfg.From = strings.TrimSuffix(cfg.From, `.gz`)
 135         dec, err := gzip.NewReader(rc)
 136         if err != nil {
 137             return err
 138         }
 139         defer dec.Close()
 140         return handleReader(w, dec, path, cfg)
 141     }
 142 
 143     // auto-handle bzipped input
 144     ext := filepath.Ext(cfg.From)
 145     if isAnyFold(ext, `.bz`, `.bz2`, `.bzip`, `.bzip2`) {
 146         cfg.From = cfg.From[:len(cfg.From)-len(ext)]
 147         dec := bzip2.NewReader(rc)
 148         return handleReader(w, dec, path, cfg)
 149     }
 150 
 151     return handleReader(w, rc, path, cfg)
 152 }
 153 
 154 func handleReader(w writer, r io.Reader, path string, cfg config) error {
 155     // don't rely on the type-aliases table having keys for file extensions
 156     from := conform(typeAliases, cfg.From)
 157 
 158     if conv, ok := io2Converter[formatPair{from, cfg.To}]; ok {
 159         return conv(w, r, path, cfg)
 160     }
 161     return convTypesError(path, from, cfg.To)
 162 }

     File: vida/math.go
   1 package main
   2 
   3 func maxInt(x, y int) int {
   4     if x > y {
   5         return x
   6     }
   7     return y
   8 }
   9 
  10 // ceilIntBy calculates a sort of `modular ceiling` for integer, given a main
  11 // number and a period; this func expects its 2nd argument to be positive
  12 func ceilIntBy(n int, by int) int {
  13     if rem := n % by; rem > 0 {
  14         return n - rem + by
  15     }
  16     return n
  17 }

     File: vida/mit-license.txt
   1 The MIT License (MIT)
   2 
   3 Copyright © 2024 pacman64
   4 
   5 Permission is hereby granted, free of charge, to any person obtaining a copy of
   6 this software and associated documentation files (the “Software”), to deal
   7 in the Software without restriction, including without limitation the rights to
   8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
   9 of the Software, and to permit persons to whom the Software is furnished to do
  10 so, subject to the following conditions:
  11 
  12 The above copyright notice and this permission notice shall be included in all
  13 copies or substantial portions of the Software.
  14 
  15 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21 SOFTWARE.

     File: vida/strings.go
   1 package main
   2 
   3 import (
   4     "bytes"
   5     "math"
   6     "path/filepath"
   7     "strconv"
   8     "strings"
   9     "unicode"
  10     "unicode/utf8"
  11 )
  12 
  13 // utf8BOM is used by some windows apps to start plain-text output, but it's
  14 // useless as a `byte order mark` (BOM), since the UTF-8 format always has a
  15 // single/known byte-order by design
  16 const utf8BOM = "\xef\xbb\xbf"
  17 
  18 // paletteANSI is constrained in the colors it can use by the colors which
  19 // the tiles-panel uses, to avoid casually associating colored rows with the
  20 // colors of numeric tiles
  21 var paletteANSI = []string{
  22     "\x1b[38;5;166m", // orange
  23     "\x1b[38;5;99m",  // purple
  24     "\x1b[38;5;30m",  // teal
  25     "\x1b[38;5;169m", // pink
  26     "\x1b[38;5;94m",  // brown?
  27     "\x1b[38;5;245m", // gray
  28     "\x1b[38;5;142m", // yellowish
  29 
  30     // green looks like the tile colors for positive numbers, which use
  31     // a simple green colorscale
  32     // "\x1b[38;5;28m",  // green
  33 
  34     // can't use reds or most blues either to avoid looking like any
  35     // colored tiles for zeros or negative numbers
  36 
  37     // "\x1b[38;5;39m",  // cyan
  38     // "\x1b[38;5;207m", // pink
  39 }
  40 
  41 // paletteHTML is the HTML-class analog to the ANSI-values palette
  42 var paletteHTML = []string{
  43     "c1", // orange
  44     "c2", // purple
  45     "c3", // teal
  46     "c4", // pink
  47     "c5", // brown?
  48     "c6", // gray
  49     "c7", // yellowish
  50 }
  51 
  52 var isValidBase64 = [256]bool{
  53     '0': true, '1': true, '2': true, '3': true, '4': true,
  54     '5': true, '6': true, '7': true, '8': true, '9': true,
  55 
  56     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
  57     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
  58     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
  59     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
  60     'Y': true, 'Z': true,
  61 
  62     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
  63     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
  64     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
  65     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
  66     'y': true, 'z': true,
  67 
  68     '+': true, '/': true, '=': true,
  69 }
  70 
  71 // isValidLinkDomainByte helps check the domain part of a web link
  72 var isValidLinkDomainByte = [256]bool{
  73     '0': true, '1': true, '2': true, '3': true, '4': true,
  74     '5': true, '6': true, '7': true, '8': true, '9': true,
  75 
  76     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
  77     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
  78     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
  79     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
  80     'Y': true, 'Z': true,
  81 
  82     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
  83     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
  84     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
  85     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
  86     'y': true, 'z': true,
  87 
  88     '+': true, '-': true, '_': true, '.': true, ':': true, '/': true,
  89     '%': true,
  90 }
  91 
  92 // isValidLinkPathByte helps check bytes in a web link after the domain part;
  93 // among the few differences with isValidLinkDomainByte are semicolons, which
  94 // in this table aren't considered valid: this correctly excludes them from
  95 // links
  96 var isValidLinkPathByte = [256]bool{
  97     '0': true, '1': true, '2': true, '3': true, '4': true,
  98     '5': true, '6': true, '7': true, '8': true, '9': true,
  99 
 100     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
 101     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
 102     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
 103     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
 104     'Y': true, 'Z': true,
 105 
 106     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
 107     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
 108     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
 109     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
 110     'y': true, 'z': true,
 111 
 112     '.': true, '+': true, '-': true, '_': true, '/': true,
 113     '#': true, '%': true, '?': true, '&': true, '=': true,
 114 }
 115 
 116 // appendTSV allows reusing previously-allocated slices, while never allocating
 117 // values on its own
 118 func appendTSV(dest []string, items string) []string {
 119     loopTSV(items, func(i int, s string) {
 120         dest = append(dest, s)
 121     })
 122     return dest
 123 }
 124 
 125 func bool2str(cond bool, yes, no string) string {
 126     if cond {
 127         return yes
 128     } else {
 129         return no
 130     }
 131 }
 132 
 133 func countDecimals(s []byte) int {
 134     if i := bytes.IndexByte(s, '.'); i >= 0 {
 135         return len(s) - i
 136     }
 137     return 0
 138 }
 139 
 140 func countItemsTSV(line string) int {
 141     if len(line) == 0 {
 142         return 0
 143     }
 144 
 145     n := strings.Count(line, "\t")
 146     return n + 1
 147 }
 148 
 149 // loopTSV helps avoid using strings.Split, which both minimizes allocations
 150 // and reduces memory-use overall
 151 func loopTSV(line string, fn func(i int, s string)) {
 152     if len(line) == 0 {
 153         return
 154     }
 155 
 156     for i := 0; true; i++ {
 157         j := strings.IndexByte(line, '\t')
 158         if j < 0 {
 159             fn(i, line)
 160             return
 161         }
 162 
 163         fn(i, line[:j])
 164         line = line[j+1:]
 165     }
 166 }
 167 
 168 func hasPrefixFold(s, prefix string) bool {
 169     n := len(prefix)
 170     return len(s) >= n && strings.EqualFold(s[:n], prefix)
 171 }
 172 
 173 func float64width(f float64) (span, decimals int) {
 174     var buf [32]byte
 175     s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64)
 176     return len(s), countDecimals(s)
 177 }
 178 
 179 func int64width(n int64) int {
 180     var buf [32]byte
 181     s := strconv.AppendInt(buf[:0], n, 10)
 182     return len(s)
 183 }
 184 
 185 func isPadded(s string) bool {
 186     return len(s) > 0 && s[0] == ' ' || s[len(s)-1] == ' '
 187 }
 188 
 189 // stringWidth makes it easy to change how the whole app finds string-widths
 190 func stringWidth(s string) int {
 191     return utf8.RuneCountInString(s)
 192 }
 193 
 194 // tryNumeric checks if a string is useable as a number, returning both the
 195 // parsed value and its success/suitability
 196 func tryNumeric(s string) (f float64, ok bool) {
 197     f, err := strconv.ParseFloat(s, 64)
 198     if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 199         return f, true
 200     }
 201     return 0, false
 202 }
 203 
 204 func fileExtension(path string) string {
 205     ext := filepath.Ext(path)
 206     if !isAnyFold(ext, `.gz`, `.bz`, `.bz2`, `.bzip`, `.bzip2`) {
 207         return ext
 208     }
 209 
 210     // handle gzip double-extension
 211     j := len(path) - len(ext)
 212     if i := strings.LastIndexByte(path[:j], '.'); i >= 0 {
 213         return path[i:]
 214     }
 215 
 216     // extension is just .gz
 217     return ext
 218 }
 219 
 220 // isAnyFold generalizes func strings.EqualFold to multiple values, which are
 221 // all compared to the first string given
 222 func isAnyFold(s string, options ...string) bool {
 223     for _, t := range options {
 224         if strings.EqualFold(s, t) {
 225             return true
 226         }
 227     }
 228     return false
 229 }
 230 
 231 // indexLinkOrDigits is alternative to calling both indexLink and indexDigits,
 232 // having to figure out if these overlap, which comes first when both occur,
 233 // and implying multiple loops, which slow processing down unnecessarily
 234 func indexLinkOrDigits(s string) (isLink bool, i, j int) {
 235     for k := 0; k < len(s); k++ {
 236         switch s[k] {
 237         case 'h':
 238             i, j = indexLink(s[k:])
 239             if i >= 0 {
 240                 return true, k + i, k + j
 241             }
 242             // not a link, so just keep going
 243 
 244         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 245             n := indexNonDigit(s[k:])
 246             if n >= 0 {
 247                 return false, k, k + n
 248             }
 249             return false, k, len(s)
 250         }
 251     }
 252 
 253     return false, -1, -1
 254 }
 255 
 256 // indexLink tries to find a slice for the next (if any) detected web link in
 257 // the string given; lack of detected links results in negative indices
 258 func indexLink(s string) (i, j int) {
 259     i, k := indexWebProtocol(s)
 260     if i < 0 || k == len(s) {
 261         return -1, -1
 262     }
 263 
 264     r, _ := utf8.DecodeRuneInString(s[k:])
 265     if !unicode.IsLetter(r) {
 266         return -1, -1
 267     }
 268 
 269     isValidByte := isValidLinkDomainByte
 270 
 271     for j = k + 1; j < len(s); j++ {
 272         b := s[j]
 273 
 274         // byte-checking logic changes with the first non-protocol slash
 275         if b == '/' {
 276             isValidByte = isValidLinkPathByte
 277         }
 278 
 279         if isValidByte[b] {
 280             continue
 281         }
 282 
 283         // found end of the link
 284         break
 285     }
 286 
 287     // exclude trailing dots, which presumably end sentences with trailing
 288     // links in them
 289     if s[j-1] == '.' {
 290         j--
 291     }
 292 
 293     // just a protocol, even if followed by a dot, isn't a valid link
 294     if k == j {
 295         return -1, -1
 296     }
 297 
 298     // found what seems a valid link
 299     return i, j
 300 }
 301 
 302 // indexWebProtocol simplifies the control-flow of func indexLink
 303 func indexWebProtocol(s string) (i, j int) {
 304     i = strings.Index(s, `http`)
 305     if i < 0 {
 306         return -1, -1
 307     }
 308 
 309     t := s[i+len(`http`):]
 310     if strings.HasPrefix(t, `s://`) {
 311         return i, i + len(`https://`)
 312     }
 313     if strings.HasPrefix(t, `://`) {
 314         return i, i + len(`http://`)
 315     }
 316     return -1, -1
 317 }
 318 
 319 func indexNonDigit(s string) int {
 320     for i := 0; i < len(s); i++ {
 321         switch s[i] {
 322         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 323             continue
 324         default:
 325             return i
 326         }
 327     }
 328 
 329     return -1
 330 }

     File: vida/strings_test.go
   1 package main
   2 
   3 import (
   4     "strings"
   5     "testing"
   6 )
   7 
   8 func TestLoopTSV(t *testing.T) {
   9     var tests = []struct {
  10         Input    string
  11         Expected []string
  12     }{
  13         {``, nil},
  14         {` `, []string{}},
  15         {"abc\tdef", []string{`abc`, `def`}},
  16         {"abc\tdef\t", []string{`abc`, `def`, ``}},
  17         {"\t\tabc\tdef\t", []string{``, ``, `abc`, `def`, ``}},
  18     }
  19 
  20     for _, tc := range tests {
  21         t.Run(tc.Input, func(t *testing.T) {
  22             var got []string
  23             var expected []string
  24             if tc.Input != `` {
  25                 expected = strings.Split(tc.Input, "\t")
  26             }
  27 
  28             loopTSV(tc.Input, func(i int, s string) {
  29                 if exp := expected[i]; expected[i] != s {
  30                     const fs = `item @ %d: expected %q, instead of %q`
  31                     t.Fatalf(fs, i, exp, s)
  32                 }
  33                 got = append(got, s)
  34             })
  35 
  36             if len(got) != len(expected) {
  37                 const fs = `expected %d pieces, instead of %d`
  38                 t.Fatalf(fs, len(expected), len(got))
  39             }
  40         })
  41     }
  42 }
  43 
  44 func TestIndexLink(t *testing.T) {
  45     var tests = []struct {
  46         Input string
  47         Start int
  48         Stop  int
  49     }{
  50         {``, -1, -1},
  51         {` `, -1, -1},
  52         {`abc, 123`, -1, -1},
  53         {`abc https:`, -1, -1},
  54         {
  55             `https://www.duckduckgo.com/, blah blah`,
  56             0,
  57             len(`https://www.duckduckgo.com/`),
  58         },
  59         {
  60             `a link to https://www.duckduckgo.com/, blah blah`,
  61             len(`a link to `),
  62             len(`a link to https://www.duckduckgo.com/`),
  63         },
  64         {
  65             `Here's a link to https://www.duckduckgo.com/.`,
  66             len(`Here's a link to `),
  67             len(`Here's a link to https://www.duckduckgo.com/`),
  68         },
  69         {
  70             `Here's a link to https://www.duckduckgo.com/: enjoy.`,
  71             len(`Here's a link to `),
  72             len(`Here's a link to https://www.duckduckgo.com/`),
  73         },
  74         {
  75             `Here's a link to http://localhost:1234/: try it.`,
  76             len(`Here's a link to `),
  77             len(`Here's a link to http://localhost:1234/`),
  78         },
  79     }
  80 
  81     for _, tc := range tests {
  82         t.Run(tc.Input, func(t *testing.T) {
  83             i, j := indexLink(tc.Input)
  84             if i != tc.Start || j != tc.Stop {
  85                 const fs = `expected %d and %d, instead of %d and %d`
  86                 t.Fatalf(fs, tc.Start, tc.Stop, i, j)
  87             }
  88         })
  89     }
  90 }

     File: vida/style.css
   1 body {
   2     font-size: 15px;
   3     font-family: system-ui, -apple-system, sans-serif;
   4     line-height: 1.6rem;
   5 }
   6 
   7 p {
   8     margin: auto;
   9     margin-top: 1rem;
  10     margin-bottom: 1rem;
  11 
  12     max-width: 85ch;
  13 }
  14 
  15 a {
  16     color: steelblue;
  17     text-decoration: none;
  18 }
  19 
  20 audio,
  21 video {
  22     height: fit-content;
  23 }
  24 
  25 ul,
  26 ol {
  27     margin: 0.25rem;
  28     line-height: 1.45rem;
  29 }
  30 
  31 li {
  32     padding-right: 1rem;
  33 }
  34 
  35 pre {
  36     margin: auto;
  37 }
  38 
  39 section {
  40     width: fit-content;
  41     margin: 0.5rem auto 0.5rem auto;
  42 }
  43 
  44 details {
  45     margin: auto;
  46 
  47     min-width: 10ch;
  48     max-width: fit-content;
  49     border: solid thin #ddd;
  50 }
  51 
  52 li>details {
  53     margin-bottom: 1rem;
  54 }
  55 
  56 summary {
  57     min-width: 10ch;
  58     padding: 0 0.5rem;
  59     background-color: #eee;
  60 }
  61 
  62 td {
  63     padding: 0.25rem;
  64     vertical-align: top;
  65     border-bottom: solid thin transparent;
  66 }
  67 
  68 tbody tr:nth-child(5n) td {
  69     border-bottom: solid thin lightgray;
  70 }
  71 
  72 .pairs {
  73     display: grid;
  74     padding: 0.5rem;
  75     grid-template-columns: max-content max-content;
  76 }
  77 
  78 .inline {
  79     margin: 0.25rem;
  80     display: inline-block;
  81     vertical-align: text-top;
  82 }
  83 
  84 /* img.inline {
  85     vertical-align: middle;
  86 } */
  87 
  88 .array {
  89     border: solid thin #ddd;
  90 }
  91 
  92 .object {
  93     border: solid thin #e0e0e0;
  94 }
  95 
  96 .key {
  97     color: blueviolet;
  98 }
  99 
 100 .key+* {
 101     margin-left: 1rem;
 102 }
 103 
 104 .padded {
 105     /* color: cyan; */
 106     background-color: yellow;
 107 }
 108 
 109 .null {
 110     color: darkred;
 111 }
 112 
 113 .nothing {
 114     color: #777;
 115     font-style: italic;
 116 }
 117 
 118 .i,
 119 .integer {
 120     color: royalblue;
 121 }
 122 
 123 .f,
 124 .float64 {
 125     color: darkgreen;
 126 }
 127 
 128 /* t is a tile cell used in tables */
 129 .t {
 130     font-size: small;
 131     background-color: white;
 132 }
 133 
 134 /* ri is a row-index cell used in tables */
 135 .ri {
 136     padding: 0.25rem;
 137     padding-right: 0.5rem;
 138     padding-left: 1rem;
 139     text-align: right;
 140     vertical-align: top;
 141     border-bottom: solid thin transparent;
 142 }
 143 
 144 td>span.i,
 145 td>span.f,
 146 td>span.integer,
 147 td>span.float64 {
 148     width: 100%;
 149     text-align: right;
 150     display: inline-block;
 151 }
 152 
 153 td.i,
 154 td.f {
 155     text-align: right;
 156 }
 157 
 158 .bigint,
 159 .fraction {
 160     color: darkcyan;
 161 }
 162 
 163 abbr {
 164     color: #444;
 165 }
 166 
 167 section a {
 168     /* padding: 0.35rem 0.8rem; */
 169     padding: 0rem 0.8rem;
 170 }
 171 
 172 img {
 173     display: block;
 174     margin: auto;
 175 }
 176 
 177 hr {
 178     margin: auto;
 179     color: #eee;
 180 }
 181 
 182 
 183 .spacer {
 184     width: 5ch;
 185     display: inline-block;
 186 }
 187 
 188 /* center-aligned */
 189 .c {
 190     text-align: center;
 191 }
 192 
 193 /* right-aligned: style for td cells */
 194 .r {
 195     text-align: right;
 196 }
 197 
 198 /* wide: style for thead th items above numeric columns */
 199 .w {
 200     width: 15ch;
 201 }
 202 
 203 /* tight container */
 204 .t {
 205     padding: 0;
 206 }
 207 
 208 /* trendline cells */
 209 .trend {
 210     /* border: solid thin #f0f0f0; */
 211     padding: 0;
 212 }
 213 
 214 /* give consistent width to columns with counts in them */
 215 .count {
 216     width: 7ch;
 217 }
 218 
 219 .dim {
 220     color: #bbb;
 221 }
 222 
 223 .pos {
 224     color: green;
 225 }
 226 
 227 .zero {
 228     color: darkcyan;
 229     background-color: #dcf8f8;
 230     /* lighter than lightcyan */
 231     background-color: #eaf7f7;
 232     /* lighter than lightcyan */
 233 }
 234 
 235 .neg {
 236     color: indianred;
 237 }
 238 
 239 td.neg {
 240     color: firebrick;
 241     /* color: tomato; */
 242 }
 243 
 244 .index {
 245     color: #aaa;
 246     background-color: white;
 247 }
 248 
 249 .container {
 250     padding-left: 0.2rem;
 251     background-color: #eee;
 252     border: dotted solid black;
 253 }
 254 
 255 /* don't let the left side of summaries touch any dotted borders */
 256 /* details {
 257     width: 100%;
 258     padding: 0.5rem;
 259 } */
 260 
 261 .general-options,
 262 .bubbleplot-options,
 263 .dotplot-options {
 264     gap: 1ch;
 265     display: grid;
 266     grid-template-columns: 2fr repeat(3, 3fr);
 267 }
 268 
 269 table {
 270     margin: 3rem auto;
 271     border-collapse: collapse;
 272 }
 273 
 274 thead th {
 275     top: 0;
 276     position: sticky;
 277     background-color: white;
 278 
 279     text-align: center;
 280     font-weight: bolder;
 281     padding: 0.1rem 0.5rem;
 282 }
 283 
 284 tfoot th {
 285     text-align: center;
 286     font-weight: bolder;
 287     padding: 0.1rem 0.5rem;
 288     user-select: none;
 289 }
 290 
 291 /* allow multiline cells */
 292 th {
 293     white-space: pre-wrap;
 294 }
 295 
 296 td {
 297     max-width: 85ch;
 298     border: solid thin #f0f0f0;
 299 }
 300 
 301 tr:first-child td {
 302     border-top: solid medium #bbb;
 303 }
 304 
 305 tr:nth-child(5n) td {
 306     border-bottom: solid thin #bbb;
 307 }
 308 
 309 td.inv {
 310     background-color: #ddd;
 311 }
 312 
 313 thead th.cat1 {
 314     color: #eee;
 315     background-color: #444;
 316 }
 317 
 318 thead th.cat2 {
 319     font-weight: 900;
 320 }
 321 
 322 tbody th {
 323     background-color: white;
 324 }
 325 
 326 .error {
 327     padding: 1.5rem;
 328     color: whitesmoke;
 329     background-color: firebrick;
 330 }
 331 
 332 .divider {
 333     color: #ccc;
 334 }
 335 
 336 .n,
 337 .nothing {
 338     font-style: italic;
 339     color: #ccc;
 340 }
 341 
 342 .b {
 343     font-style: italic;
 344     color: cornflowerblue;
 345 }
 346 
 347 .i,
 348 .integer {
 349     color: cornflowerblue;
 350 }
 351 
 352 .f,
 353 .float64 {
 354     color: forestgreen;
 355 }
 356 
 357 .k,
 358 .key {
 359     color: mediumpurple;
 360 }
 361 
 362 .kv {
 363     margin: 0;
 364 }
 365 
 366 .kv thead {
 367     display: none;
 368 }
 369 
 370 tr.c1 td {
 371     background-color: #ffa50040;
 372 }
 373 
 374 tr.c1 th.ri {
 375     color: #ffa500;
 376 }
 377 
 378 tr.c2 td {
 379     background-color: #4b008230;
 380 }
 381 
 382 tr.c2 th.ri {
 383     color: #4b008280;
 384 }
 385 
 386 tr.c3 td {
 387     background-color: #5f9ea040;
 388 }
 389 
 390 tr.c3 th.ri {
 391     color: #5f9ea0;
 392 }
 393 
 394 tr.c4 td {
 395     background-color: #ffc0cb40;
 396 }
 397 
 398 tr.c4 th.ri {
 399     color: #ffc0cb;
 400 }
 401 
 402 tr.c5 td {
 403     background-color: #8b451340;
 404 }
 405 
 406 tr.c5 th.ri {
 407     color: #8b4513;
 408 }
 409 
 410 tr.c6 td {
 411     background-color: #c3c3c340;
 412 }
 413 
 414 tr.c6 th.ri {
 415     color: #c3c3c3;
 416 }
 417 
 418 tr.c7 td {
 419     background-color: #ffff0040;
 420 }
 421 
 422 tr.c7 th.ri {
 423     color: #ffff0080;
 424 }

     File: vida/tables.go
   1 package main
   2 
   3 import (
   4     "encoding/csv"
   5     "errors"
   6     "io"
   7     "math"
   8     "strconv"
   9     "strings"
  10 )
  11 
  12 type columnMeta struct {
  13     // Name is the column's name, taken from the first row/line
  14     Name string
  15 
  16     // Empty is how many items in the column are empty/missing
  17     Empty int
  18 
  19     // Padded is how many items in the column start/end with spaces
  20     Padded int
  21 
  22     // Numeric is how many number-like values the column has
  23     Numeric int
  24 
  25     // MaxDecimals is used to right-pad the column's number-like values
  26     MaxDecimals int
  27 
  28     // MaxWidth is the column's max width in runes, and is used to
  29     // pad the column's values
  30     MaxWidth int
  31 
  32     Min float64
  33     Max float64
  34     Sum float64
  35 }
  36 
  37 // newColumnMeta is columnMeta's constructor, which is needed as zero-like
  38 // values are the wrong way to start some of its fields
  39 func newColumnMeta() columnMeta {
  40     return columnMeta{
  41         Min: math.Inf(+1),
  42         Max: math.Inf(-1),
  43     }
  44 }
  45 
  46 // update is used to update the column's stats; don't use with the 1st row
  47 func (cm *columnMeta) update(s string) {
  48     if len(s) == 0 {
  49         cm.Empty++
  50         return
  51     }
  52 
  53     if isPadded(s) {
  54         cm.Padded++
  55     }
  56 
  57     n := stringWidth(s)
  58     cm.MaxWidth = maxInt(cm.MaxWidth, n)
  59 
  60     if f, ok := tryNumeric(s); ok {
  61         cm.Sum += f
  62         cm.Min = math.Min(cm.Min, f)
  63         cm.Max = math.Max(cm.Max, f)
  64         cm.Numeric++
  65 
  66         // recheck widths in case regular (non-exponential) number-notation
  67         // is wider than the original string
  68         span, decs := float64width(f)
  69         cm.MaxWidth = maxInt(cm.MaxWidth, span)
  70         cm.MaxDecimals = maxInt(cm.MaxDecimals, decs)
  71     }
  72 }
  73 
  74 // matchColumn tries to find which column a string may refer to, using various
  75 // fairly-reliable heuristics, which are used when there are no exact matches
  76 func matchColumn(k string, cols []columnMeta) int {
  77     // empty strings match as prefixes to any string, according to both func
  78     // strings.HasPrefix and func hasPrefixFold
  79     if len(k) == 0 {
  80         return -1
  81     }
  82 
  83     // try non-zero number indices
  84     if i, err := strconv.ParseInt(k, 10, 64); err == nil {
  85         return matchColumnIndex(int(i), cols)
  86     }
  87 
  88     // try exact matches
  89     for i, c := range cols {
  90         if k == c.Name {
  91             return i
  92         }
  93     }
  94 
  95     // try case-insensitive matches
  96     for i, c := range cols {
  97         if strings.EqualFold(k, c.Name) {
  98             return i
  99         }
 100     }
 101 
 102     // try exact prefix matches
 103     for i, c := range cols {
 104         if strings.HasPrefix(c.Name, k) {
 105             return i
 106         }
 107     }
 108 
 109     // try case-insensitive prefix matches
 110     for i, c := range cols {
 111         if hasPrefixFold(c.Name, k) {
 112             return i
 113         }
 114     }
 115 
 116     return -1
 117 }
 118 
 119 func matchColumnIndex(i int, cols []columnMeta) int {
 120     if 1 <= i && i <= len(cols) {
 121         return i - 1
 122     }
 123     if -len(cols) <= i && i < 0 {
 124         return i + len(cols)
 125     }
 126     return -1
 127 }
 128 
 129 type visualTallier func(w writer, row []string)
 130 
 131 func (rp *rowParams) configBullets(k string) {
 132     if k == `` {
 133         rp.WriteBullets = func(w writer, row []string) {}
 134         rp.BulletsIndex = -1
 135         return
 136     }
 137 
 138     cols := rp.Columns
 139     i := matchColumn(k, cols)
 140     if i < 0 {
 141         rp.WriteBullets = func(w writer, row []string) {}
 142         rp.BulletsIndex = -1
 143         return
 144     }
 145 
 146     c := cols[i]
 147     if c.Numeric == 0 || c.Max <= 0 {
 148         rp.WriteBullets = func(w writer, row []string) {}
 149         rp.BulletsIndex = -1
 150         return
 151     }
 152 
 153     div := 1.0
 154     if k := 25.0; c.Max > k {
 155         log := math.Log10(c.Max)
 156         div = math.Pow10(int(log)) / k
 157     }
 158 
 159     maxWidthLast := cols[len(cols)-1].MaxWidth
 160     needPadding := rp.CategoryIndex != len(cols)-1
 161 
 162     rp.BulletsIndex = i
 163     rp.WriteBullets = func(w writer, row []string) {
 164         if i >= len(row) {
 165             return
 166         }
 167 
 168         f, ok := tryNumeric(row[i])
 169         if !ok || f <= 0 || math.IsNaN(f) || math.IsInf(f, 0) {
 170             return
 171         }
 172 
 173         n := 0
 174         if needPadding {
 175             n = stringWidth(row[len(row)-1])
 176             n = maxInt(maxWidthLast-n, 0)
 177         }
 178 
 179         writeSpaces(w, n+columnGap)
 180         writeBullets(w, int(f/div))
 181     }
 182 }
 183 
 184 func writeTilesANSI(w writer, row []string) {
 185     for _, s := range row {
 186         style, tile := matchTileANSI(s)
 187         w.WriteString(style)
 188         w.WriteString(tile)
 189     }
 190 
 191     w.WriteString(resetStyleANSI)
 192     writeSpaces(w, columnGap)
 193 }
 194 
 195 type rowParams struct {
 196     // Columns has metadata for all table columns
 197     Columns []columnMeta
 198 
 199     RowCountMaxWidth int
 200 
 201     // CategoryIndex marks which column to color-code, if any
 202     CategoryIndex int
 203 
 204     BulletsIndex int
 205 
 206     // Colors keeps track of which color-table index to use for which unique
 207     // value
 208     Colors map[string]int
 209 
 210     WriteBullets visualTallier
 211 }
 212 
 213 func newRowParams(nrows int, cols []columnMeta, cfg config) rowParams {
 214     var rp rowParams
 215     rp.Columns = cols
 216     rp.CategoryIndex = matchColumn(cfg.Colors, cols)
 217     rp.Colors = make(map[string]int)
 218     rp.RowCountMaxWidth = int64width(int64(nrows))
 219     rp.configBullets(cfg.Bullets)
 220     return rp
 221 }
 222 
 223 func (rp rowParams) matchColor(s string, with []string) string {
 224     n, ok := rp.Colors[s]
 225     if !ok {
 226         n = len(rp.Colors) % len(with)
 227         rp.Colors[s] = n
 228     }
 229     return with[n]
 230 }
 231 
 232 func writeRowValuesANSI(w writer, row []string, p rowParams) {
 233     gapDue := 0
 234 
 235     for i, s := range row {
 236         col := p.Columns[i]
 237         writeSpaces(w, columnGap+gapDue)
 238 
 239         if i == p.CategoryIndex && p.Colors != nil {
 240             w.WriteString(p.matchColor(s, paletteANSI))
 241             w.WriteString(s)
 242             writeSpaces(w, col.MaxWidth-stringWidth(s))
 243             w.WriteString(resetStyleANSI)
 244             gapDue = 0
 245             continue
 246         }
 247 
 248         if f, ok := tryNumeric(s); ok {
 249             writeNumericANSI(w, f, col)
 250             gapDue = 0
 251             continue
 252         }
 253 
 254         if len(s) == 0 {
 255             w.WriteString(emptyStyleANSI)
 256             writeSpaces(w, col.MaxWidth)
 257             w.WriteString(resetStyleANSI)
 258             gapDue = 0
 259             continue
 260         }
 261 
 262         writeStringANSI(w, s)
 263         n := stringWidth(s)
 264         gapDue = col.MaxWidth - n
 265     }
 266 }
 267 
 268 func writeRowValuesPlain(w writer, row []string, p rowParams) {
 269     gapDue := 0
 270 
 271     for i, s := range row {
 272         col := p.Columns[i]
 273         writeSpaces(w, columnGap+gapDue)
 274 
 275         if f, ok := tryNumeric(s); ok {
 276             writeNumericPlain(w, f, col)
 277             gapDue = 0
 278             continue
 279         }
 280 
 281         w.WriteString(s)
 282         n := stringWidth(s)
 283         gapDue = col.MaxWidth - n
 284     }
 285 }
 286 
 287 func newReaderCSV(r io.Reader) *csv.Reader {
 288     rr := csv.NewReader(r)
 289     rr.Comma = ','
 290     rr.LazyQuotes = true
 291     rr.ReuseRecord = true
 292     rr.FieldsPerRecord = -1
 293     return rr
 294 }
 295 
 296 func newWriterCSV(w io.Writer) *csv.Writer {
 297     rw := csv.NewWriter(w)
 298     rw.UseCRLF = false
 299     rw.Comma = ','
 300     return rw
 301 }
 302 
 303 func csv2tsv(w writer, r io.Reader, name string, cfg config) error {
 304     rr := newReaderCSV(r)
 305 
 306     for {
 307         row, err := rr.Read()
 308         if err == io.EOF {
 309             return nil
 310         }
 311         if err != nil {
 312             return err
 313         }
 314 
 315         for i, s := range row {
 316             if i > 0 {
 317                 w.WriteByte('\t')
 318             }
 319             w.WriteString(s)
 320         }
 321 
 322         if err := endLine(w); err != nil {
 323             return err
 324         }
 325     }
 326 }
 327 
 328 func csv2json(w writer, r io.Reader, name string, cfg config) error {
 329     return csv2jsonImpl(w, r, writeRowJSON)
 330 }
 331 
 332 func csv2jsons(w writer, r io.Reader, name string, cfg config) error {
 333     return csv2jsonImpl(w, r, writeRowJSONS)
 334 }
 335 
 336 func csv2jsonImpl(w writer, r io.Reader, fn kvRowWriter) error {
 337     var keys []string
 338     rr := newReaderCSV(r)
 339 
 340     for i := 0; true; i++ {
 341         row, err := rr.Read()
 342         if err == io.EOF {
 343             if i > 0 {
 344                 w.WriteString("\n]\n")
 345             } else {
 346                 w.WriteString("[]\n")
 347             }
 348             return nil
 349         }
 350         if err != nil {
 351             return err
 352         }
 353 
 354         if i == 0 {
 355             keys = append(keys, row...)
 356             continue
 357         }
 358 
 359         if i > 1 {
 360             _, err = w.WriteString(",\n")
 361             writeSpaces(w, jsonLevelIndent)
 362         } else {
 363             _, err = w.WriteString("[\n")
 364             writeSpaces(w, jsonLevelIndent)
 365         }
 366 
 367         if err = adaptWriterError(err); err != nil {
 368             return err
 369         }
 370 
 371         fn(w, keys, row)
 372     }
 373 
 374     // make the compiler happy
 375     return nil
 376 }
 377 
 378 func tsv2csv(w writer, r io.Reader, name string, cfg config) error {
 379     var row []string
 380     rw := newWriterCSV(w)
 381     defer rw.Flush()
 382 
 383     return loopLines(r, func(line string) error {
 384         if len(line) == 0 {
 385             return nil
 386         }
 387 
 388         row = appendTSV(row[:0], line)
 389         return rw.Write(row)
 390     })
 391 }
 392 
 393 func tsv2json(w writer, r io.Reader, name string, cfg config) error {
 394     return tsv2jsonImpl(w, r, writeRowJSON)
 395 }
 396 
 397 func tsv2jsons(w writer, r io.Reader, name string, cfg config) error {
 398     return tsv2jsonImpl(w, r, writeRowJSONS)
 399 }
 400 
 401 func tsv2jsonImpl(w writer, r io.Reader, fn kvRowWriter) error {
 402     var row []string
 403     var keys []string
 404 
 405     rows := 0
 406     err := loopLines(r, func(line string) error {
 407         if len(line) == 0 {
 408             return nil
 409         }
 410 
 411         row = appendTSV(row[:0], line)
 412         if len(keys) == 0 {
 413             keys = append(keys, row...)
 414             return nil
 415         }
 416 
 417         var err error
 418         if rows > 0 {
 419             _, err = w.WriteString(",\n")
 420             writeSpaces(w, jsonLevelIndent)
 421         } else {
 422             _, err = w.WriteString("[\n")
 423             writeSpaces(w, jsonLevelIndent)
 424         }
 425 
 426         if err = adaptWriterError(err); err != nil {
 427             return err
 428         }
 429 
 430         rows++
 431         fn(w, keys, row)
 432         return nil
 433     })
 434 
 435     if rows > 0 {
 436         w.WriteString("\n]\n")
 437     } else {
 438         w.WriteString("[]\n")
 439     }
 440     return err
 441 }
 442 
 443 func makeWholeTableReader(cfg config) (wholeTableReader, error) {
 444     from := strings.TrimSuffix(cfg.From, `.gz`)
 445 
 446     switch from {
 447     case `csv`:
 448         return &wholeReaderCSV{}, nil
 449     case `tsv`:
 450         return &wholeReaderTSV{}, nil
 451     default:
 452         msg := `internal bug: invalid table input-type ` + from
 453         return nil, errors.New(msg)
 454     }
 455 }
 456 
 457 func makeTableWriter(w writer, rp rowParams, cfg config) (tableWriter, error) {
 458     switch cfg.To {
 459     case `ansi`:
 460         return &ansiTableWriter{out: w, cfg: rp}, nil
 461 
 462     case `plain`:
 463         return &plainTableWriter{out: w, cfg: rp}, nil
 464 
 465     case `html`:
 466         return &htmlTableWriter{out: w, cfg: rp}, nil
 467 
 468     default:
 469         msg := `internal bug: invalid output-type ` + cfg.From
 470         return nil, errors.New(msg)
 471     }
 472 }
 473 
 474 func convertWholeTable(w writer, r io.Reader, name string, cfg config) error {
 475     wtr, err := makeWholeTableReader(cfg)
 476     if err != nil {
 477         return err
 478     }
 479 
 480     nrows, cols, err := wtr.Read(r)
 481     if err != nil {
 482         return err
 483     }
 484 
 485     rp := newRowParams(nrows, cols, cfg)
 486 
 487     tw, err := makeTableWriter(w, rp, cfg)
 488     if err != nil {
 489         return err
 490     }
 491 
 492     tw.BeforeTable()
 493     defer tw.AfterTable()
 494 
 495     tw.WriteHeaderRow(cols)
 496     if err := endLine(w); err != nil {
 497         return err
 498     }
 499 
 500     return wtr.Loop(func(i int, row []string) error {
 501         tw.StartRow(row)
 502         tw.WriteTiles(row)
 503         tw.WriteRowIndex(i+1, row)
 504         tw.WriteRowValues(row)
 505         rp.WriteBullets(w, row)
 506         return tw.EndRow()
 507     })
 508 }
 509 
 510 type wholeTableReader interface {
 511     Read(r io.Reader) (nrows int, cols []columnMeta, err error)
 512     Loop(fn func(i int, row []string) error) error
 513 }
 514 
 515 type wholeReaderCSV struct {
 516     // dataRows has all split-rows, excluding the first/header row
 517     dataRows [][]string
 518 }
 519 
 520 func (wrc *wholeReaderCSV) Read(r io.Reader) (nrows int, cols []columnMeta, err error) {
 521     rr := newReaderCSV(r)
 522 
 523     lines, err := rr.ReadAll()
 524     if err != nil {
 525         return -1, nil, err
 526     }
 527     if len(lines) == 0 {
 528         return -1, nil, nil
 529     }
 530 
 531     header := lines[0]
 532     cols = make([]columnMeta, 0, len(header))
 533     for _, s := range header {
 534         cm := newColumnMeta()
 535         cm.Name = s
 536         cm.MaxWidth = stringWidth(s)
 537         cols = append(cols, cm)
 538     }
 539 
 540     rows := lines[1:]
 541     for _, row := range lines {
 542         for j, s := range row {
 543             if j == len(cols) {
 544                 cols = append(cols, newColumnMeta())
 545             }
 546             cols[j].update(s)
 547         }
 548     }
 549     nrows = len(rows)
 550 
 551     if len(cols) == 0 {
 552         return nrows, cols, errors.New(`empty table`)
 553     }
 554 
 555     wrc.dataRows = rows
 556     return nrows, cols, err
 557 }
 558 
 559 func (wrc wholeReaderCSV) Loop(fn func(i int, row []string) error) error {
 560     for i, row := range wrc.dataRows {
 561         if err := fn(i, row); err != nil {
 562             return err
 563         }
 564     }
 565     return nil
 566 }
 567 
 568 type wholeReaderTSV struct {
 569     // lines has all unsplit lines, except the first one
 570     lines []string
 571 }
 572 
 573 func (wrt *wholeReaderTSV) Read(r io.Reader) (nrows int, cols []columnMeta, err error) {
 574     var lines []string
 575     err = loopLines(r, func(line string) error {
 576         if len(line) == 0 {
 577             return nil
 578         }
 579 
 580         if len(cols) == 0 {
 581             n := countItemsTSV(line)
 582             cols = make([]columnMeta, 0, n)
 583             loopTSV(line, func(i int, s string) {
 584                 cm := newColumnMeta()
 585                 cm.Name = s
 586                 cm.MaxWidth = stringWidth(s)
 587                 cols = append(cols, cm)
 588             })
 589             return nil
 590         }
 591 
 592         loopTSV(line, func(i int, s string) {
 593             if i == len(cols) {
 594                 cols = append(cols, newColumnMeta())
 595             }
 596             cols[i].update(s)
 597         })
 598 
 599         lines = append(lines, line)
 600         return nil
 601     })
 602 
 603     nrows = len(lines)
 604     if err != nil {
 605         return nrows, cols, err
 606     }
 607 
 608     if len(cols) == 0 {
 609         return nrows, cols, errors.New(`empty table`)
 610     }
 611     wrt.lines = lines
 612     return nrows, cols, nil
 613 }
 614 
 615 func (wrt wholeReaderTSV) Loop(fn func(i int, row []string) error) error {
 616     var items []string
 617     for i, row := range wrt.lines {
 618         items = appendTSV(items[:0], row)
 619         if err := fn(i, items); err != nil {
 620             return err
 621         }
 622     }
 623     return nil
 624 }
 625 
 626 type tableWriter interface {
 627     BeforeTable() error
 628     AfterTable() error
 629     WriteHeaderRow(cols []columnMeta) error
 630     WriteTiles(values []string) error
 631     WriteRowIndex(i int, values []string) error
 632     WriteRowValues(values []string) error
 633 
 634     StartRow(values []string) error
 635     EndRow() error
 636 }
 637 
 638 type ansiTableWriter struct {
 639     out writer
 640     cfg rowParams
 641 }
 642 
 643 func (w ansiTableWriter) BeforeTable() error {
 644     return nil
 645 }
 646 
 647 func (w ansiTableWriter) AfterTable() error {
 648     return nil
 649 }
 650 
 651 func (tw ansiTableWriter) WriteHeaderRow(cols []columnMeta) error {
 652     names := make([]string, 0, len(cols))
 653     for _, c := range cols {
 654         names = append(names, c.Name)
 655     }
 656     tw.WriteTiles(names)
 657     tw.WriteRowIndex(-1, nil)
 658     return writeHeaderRowANSI(tw.out, tw.cfg)
 659 }
 660 
 661 func (w ansiTableWriter) WriteTiles(values []string) error {
 662     writeTilesANSI(w.out, values)
 663     return nil
 664 }
 665 
 666 func (w ansiTableWriter) WriteRowIndex(i int, values []string) error {
 667     if i < 0 {
 668         writeSpaces(w.out, w.cfg.RowCountMaxWidth)
 669         return nil
 670     }
 671 
 672     j := w.cfg.CategoryIndex
 673     ok := w.cfg.Colors != nil && 0 <= j && j < len(values)
 674 
 675     if !ok || len(values[j]) == 0 {
 676         padWriteInt64(w.out, int64(i), w.cfg.RowCountMaxWidth)
 677         return nil
 678     }
 679 
 680     style := w.cfg.matchColor(values[j], paletteANSI)
 681     w.out.WriteString(style)
 682     padWriteInt64(w.out, int64(i), w.cfg.RowCountMaxWidth)
 683     w.out.WriteString(resetStyleANSI)
 684     return nil
 685 }
 686 
 687 func (w ansiTableWriter) WriteRowValues(values []string) error {
 688     writeRowValuesANSI(w.out, values, w.cfg)
 689     return nil
 690 }
 691 
 692 func (w ansiTableWriter) StartRow(values []string) error {
 693     return nil
 694 }
 695 
 696 func (w ansiTableWriter) EndRow() error {
 697     return endLine(w.out)
 698 }
 699 
 700 type plainTableWriter struct {
 701     out writer
 702     cfg rowParams
 703 }
 704 
 705 func (w plainTableWriter) BeforeTable() error {
 706     return nil
 707 }
 708 
 709 func (w plainTableWriter) AfterTable() error {
 710     return nil
 711 }
 712 
 713 func (tw plainTableWriter) WriteHeaderRow(cols []columnMeta) error {
 714     names := make([]string, 0, len(cols))
 715     for _, c := range cols {
 716         names = append(names, c.Name)
 717     }
 718     tw.WriteTiles(names)
 719     tw.WriteRowIndex(-1, nil)
 720     return writeHeaderRowPlain(tw.out, tw.cfg)
 721 }
 722 
 723 func (tw plainTableWriter) WriteTiles(values []string) error {
 724     w := tw.out
 725 
 726     for _, s := range values {
 727         if len(s) == 0 {
 728             w.WriteString(emptyTile)
 729             continue
 730         }
 731 
 732         if f, ok := tryNumeric(s); ok {
 733             if f > 0 {
 734                 w.WriteByte('+')
 735                 continue
 736             }
 737             if f < 0 {
 738                 w.WriteByte('-')
 739                 continue
 740             }
 741             w.WriteByte('0')
 742             continue
 743         }
 744 
 745         w.WriteString(usualTile)
 746     }
 747 
 748     writeSpaces(w, columnGap)
 749     return nil
 750 }
 751 
 752 func (w plainTableWriter) WriteRowIndex(i int, values []string) error {
 753     if i >= 0 {
 754         padWriteInt64(w.out, int64(i), w.cfg.RowCountMaxWidth)
 755         return nil
 756     }
 757     writeSpaces(w.out, w.cfg.RowCountMaxWidth)
 758     return nil
 759 }
 760 
 761 func (w plainTableWriter) WriteRowValues(values []string) error {
 762     writeRowValuesPlain(w.out, values, w.cfg)
 763     return nil
 764 }
 765 
 766 func (w plainTableWriter) StartRow(values []string) error {
 767     return nil
 768 }
 769 
 770 func (w plainTableWriter) EndRow() error {
 771     return endLine(w.out)
 772 }
 773 
 774 func writeHeaderRowANSI(w writer, cfg rowParams) error {
 775     gapDue := 0
 776     cols := cfg.Columns
 777 
 778     for i, c := range cols {
 779         s := c.Name
 780         writeSpaces(w, columnGap+gapDue)
 781 
 782         if i == cfg.CategoryIndex {
 783             w.WriteString(swappedStyleANSI)
 784             w.WriteString(s)
 785             n := stringWidth(s)
 786             writeSpaces(w, c.MaxWidth-n)
 787             w.WriteString(resetStyleANSI)
 788             gapDue = 0
 789             continue
 790         }
 791 
 792         w.WriteString(s)
 793         n := stringWidth(s)
 794         gapDue = c.MaxWidth - n
 795     }
 796 
 797     extraCol := cfg.BulletsIndex
 798     if len(cols) > 0 && 0 <= extraCol && extraCol < len(cols) {
 799         n := 0
 800         if cfg.CategoryIndex != len(cols)-1 {
 801             last := cols[len(cols)-1]
 802             n = last.MaxWidth - stringWidth(last.Name)
 803         }
 804         writeSpaces(w, columnGap+maxInt(n, 0))
 805         w.WriteString(cols[extraCol].Name)
 806     }
 807     return nil
 808 }
 809 
 810 func writeHeaderRowPlain(w writer, cfg rowParams) error {
 811     cfg.CategoryIndex = -1
 812     return writeHeaderRowANSI(w, cfg)
 813 }

     File: vida/text.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "encoding/base64"
   7     "errors"
   8     "io"
   9 )
  10 
  11 func decodeBase64(w writer, r io.Reader, name string, cfg config) error {
  12     var chunk [4096]byte
  13     n, err := r.Read(chunk[:])
  14     if err != nil {
  15         return err
  16     }
  17     read := chunk[:n]
  18 
  19     // handle direct base64 input, instead of prefixed base64 data-URIs
  20     if !bytes.HasPrefix(read, []byte{'d', 'a', 't', 'a', ':'}) {
  21         r = io.MultiReader(bytes.NewReader(read), r)
  22         dec := base64.NewDecoder(base64.RawStdEncoding, r)
  23         _, err := io.Copy(w, dec)
  24         return err
  25     }
  26 
  27     // don't bother searching for base64-markers far from the start
  28     start := read
  29     if len(start) > 32 {
  30         start = read[:32]
  31     }
  32 
  33     var marker = []byte{';', 'b', 'a', 's', 'e', '6', '4', ','}
  34     i := bytes.Index(start, marker)
  35     if i < 0 {
  36         return errors.New(`data-URI doesn't have base64-encoding marker`)
  37     }
  38 
  39     // handle data-URIs by skipping their leading metadata
  40     r = io.MultiReader(bytes.NewReader(read[i+len(marker):]), r)
  41     dec := base64.NewDecoder(base64.RawStdEncoding, r)
  42     _, err = io.Copy(w, dec)
  43     return err
  44 }
  45 
  46 func text2ansi(w writer, r io.Reader, name string, cfg config) error {
  47     const reset = resetStyleANSI
  48 
  49     var digitsStyle string
  50     s, ok := lookupStyleANSI(cfg.DigitsStyle)
  51     if !ok {
  52         return errors.New(`unsupported style name ` + cfg.DigitsStyle)
  53     }
  54     digitsStyle = s
  55 
  56     var linksStyle string
  57     s, ok = lookupStyleANSI(cfg.LinksStyle)
  58     if !ok {
  59         return errors.New(`unsupported style name ` + cfg.LinksStyle)
  60     }
  61     linksStyle = s
  62 
  63     if digitsStyle == reset {
  64         digitsStyle = ``
  65     }
  66     if linksStyle == reset {
  67         linksStyle = ``
  68     }
  69 
  70     if len(digitsStyle) == 0 && len(linksStyle) == 0 {
  71         return loopLines(r, func(line string) error {
  72             w.WriteString(line)
  73             return endLine(w)
  74         })
  75     }
  76 
  77     return loopLines(r, func(line string) error {
  78         return line2ansi(w, line, digitsStyle, linksStyle)
  79     })
  80 }
  81 
  82 func line2ansi(w *bufio.Writer, line string, digits, links string) error {
  83     const reset = resetStyleANSI
  84 
  85     for len(line) > 0 {
  86         isLink, i, j := indexLinkOrDigits(line)
  87         if i < 0 {
  88             w.WriteString(line)
  89             return endLine(w)
  90         }
  91 
  92         if isLink {
  93             if len(links) != 0 {
  94                 w.WriteString(line[:i])
  95                 w.WriteString(links)
  96                 w.WriteString(line[i:j])
  97                 w.WriteString(reset)
  98             } else {
  99                 w.WriteString(line[:j])
 100             }
 101         } else {
 102             if len(digits) != 0 {
 103                 w.WriteString(line[:i])
 104                 alternateDigitGroupsString(w, line[i:j], digits, reset)
 105             } else {
 106                 w.WriteString(line[:j])
 107             }
 108         }
 109 
 110         line = line[j:]
 111     }
 112 
 113     return endLine(w)
 114 }