File: ./avoid/avoid.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package avoid
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 avoid [options...] [regular expressions...]
  38 
  39 Avoid/ignore lines which match any of the extended-mode regular expressions
  40 given. When not given any regex, all empty lines are ignored by default.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help     show this help message
  45     -i, -ins      match regexes case-insensitively
  46     -q, -quiet    never emit output: exit code still succeeds/fails
  47 `
  48 
  49 func Main() {
  50     nerr := 0
  51     buffered := false
  52     sensitive := true
  53     quiet := false
  54     args := os.Args[1:]
  55 
  56     for len(args) > 0 {
  57         switch args[0] {
  58         case `-b`, `--b`, `-buffered`, `--buffered`:
  59             buffered = true
  60             args = args[1:]
  61             continue
  62 
  63         case `-h`, `--h`, `-help`, `--help`:
  64             os.Stdout.WriteString(info[1:])
  65             return
  66 
  67         case `-i`, `--i`, `-ins`, `--ins`:
  68             sensitive = false
  69             args = args[1:]
  70             continue
  71 
  72         case `-q`, `--q`, `-quiet`, `--quiet`:
  73             quiet = true
  74             args = args[1:]
  75             continue
  76         }
  77 
  78         if s := args[0]; s == `-` || s == `--` || !strings.HasPrefix(s, `-`) {
  79             break
  80         }
  81 
  82         for s := args[0][1:]; len(s) > 0; s = s[1:] {
  83             switch s[0] {
  84             case 'b':
  85                 buffered = true
  86                 continue
  87             case 'h':
  88                 os.Stdout.WriteString(info[1:])
  89                 return
  90             case 'i':
  91                 sensitive = false
  92                 continue
  93             case 'q':
  94                 quiet = true
  95                 continue
  96             default:
  97                 os.Stderr.WriteString(`avoid: unsupported option `)
  98                 os.Stderr.WriteString(args[0])
  99                 os.Stderr.WriteString("\n")
 100                 os.Exit(1)
 101             }
 102         }
 103 
 104         args = args[1:]
 105     }
 106 
 107     if len(args) > 0 && args[0] == `--` {
 108         args = args[1:]
 109     }
 110 
 111     liveLines := false
 112     if !quiet {
 113         liveLines = !buffered
 114         if !buffered {
 115             if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 116                 liveLines = false
 117             }
 118         }
 119     }
 120 
 121     if len(args) == 0 {
 122         args = []string{`^$`}
 123     }
 124 
 125     exprs := make([]*regexp.Regexp, 0, len(args))
 126 
 127     for _, src := range args {
 128         var err error
 129         var exp *regexp.Regexp
 130         if !sensitive {
 131             exp, err = regexp.Compile(`(?i)` + src)
 132         } else {
 133             exp, err = regexp.Compile(src)
 134         }
 135 
 136         if err != nil {
 137             os.Stderr.WriteString(`avoid: `)
 138             os.Stderr.WriteString(err.Error())
 139             os.Stderr.WriteString("\n")
 140             nerr++
 141         }
 142 
 143         exprs = append(exprs, exp)
 144     }
 145 
 146     if nerr > 0 {
 147         os.Exit(1)
 148         return
 149     }
 150 
 151     var buf []byte
 152     sc := bufio.NewScanner(os.Stdin)
 153     sc.Buffer(nil, 8*1024*1024*1024)
 154     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 155 
 156     ok := false
 157 
 158     for i := 0; sc.Scan(); i++ {
 159         line := sc.Bytes()
 160         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 161             line = line[3:]
 162         }
 163 
 164         s := line
 165         if bytes.IndexByte(s, '\x1b') >= 0 {
 166             buf = plain(buf[:0], s)
 167             s = buf
 168         }
 169 
 170         if !match(s, exprs) {
 171             ok = true
 172             if quiet {
 173                 continue
 174             }
 175 
 176             bw.Write(line)
 177             bw.WriteByte('\n')
 178 
 179             if liveLines && bw.Flush() != nil {
 180                 break
 181             }
 182         }
 183     }
 184 
 185     if !ok {
 186         os.Exit(1)
 187         return
 188     }
 189 }
 190 
 191 func match(what []byte, with []*regexp.Regexp) bool {
 192     for _, e := range with {
 193         if e.Match(what) {
 194             return true
 195         }
 196     }
 197     return false
 198 }
 199 
 200 func plain(dst []byte, src []byte) []byte {
 201     for len(src) > 0 {
 202         i, j := indexEscapeSequence(src)
 203         if i < 0 {
 204             dst = append(dst, src...)
 205             break
 206         }
 207         if j < 0 {
 208             j = len(src)
 209         }
 210 
 211         if i > 0 {
 212             dst = append(dst, src[:i]...)
 213         }
 214 
 215         src = src[j:]
 216     }
 217 
 218     return dst
 219 }
 220 
 221 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 222 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 223 // indices which can be independently negative when either the start/end of
 224 // a sequence isn't found; given their fairly-common use, even the hyperlink
 225 // ESC]8 sequences are supported
 226 func indexEscapeSequence(s []byte) (int, int) {
 227     var prev byte
 228 
 229     for i, b := range s {
 230         if prev == '\x1b' && b == '[' {
 231             j := indexLetter(s[i+1:])
 232             if j < 0 {
 233                 return i, -1
 234             }
 235             return i - 1, i + 1 + j + 1
 236         }
 237 
 238         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 239             j := indexPair(s[i+1:], '\x1b', '\\')
 240             if j < 0 {
 241                 return i, -1
 242             }
 243             return i - 1, i + 1 + j + 2
 244         }
 245 
 246         prev = b
 247     }
 248 
 249     return -1, -1
 250 }
 251 
 252 func indexLetter(s []byte) int {
 253     for i, b := range s {
 254         upper := b &^ 32
 255         if 'A' <= upper && upper <= 'Z' {
 256             return i
 257         }
 258     }
 259 
 260     return -1
 261 }
 262 
 263 func indexPair(s []byte, x byte, y byte) int {
 264     var prev byte
 265 
 266     for i, b := range s {
 267         if prev == x && b == y && i > 0 {
 268             return i
 269         }
 270         prev = b
 271     }
 272 
 273     return -1
 274 }
     File: ./backsort/backsort.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package backsort
  26 
  27 import (
  28     "bufio"
  29     "math"
  30     "os"
  31     "sort"
  32     "strconv"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 backsort [options...] [columns/indices...]
  38 
  39 BACKward-SORT (numerically) rows using the columns given, in order. Arguments
  40 are matched to columns, first exactly, failing that case-insensitively, and
  41 failing even that, as 1-based indices, with negative numbers counting backward
  42 from the last column.
  43 
  44 Input is TSV (tab-separated values) lines from the standard input, where the
  45 first/header line has all the column names.
  46 
  47 The options are, available both in single and double-dash versions
  48 
  49     -h, -help    show this help message
  50 `
  51 
  52 func Main() {
  53     args := os.Args[1:]
  54 
  55     if len(args) > 0 {
  56         switch args[0] {
  57         case `-h`, `--h`, `-help`, `--help`:
  58             os.Stdout.WriteString(info[1:])
  59             return
  60         }
  61     }
  62 
  63     if len(args) > 0 && args[0] == `--` {
  64         args = args[1:]
  65     }
  66 
  67     // if len(args) == 0 {
  68     //  os.Stderr.WriteString(info[1:])
  69     //  os.Exit(1)
  70     //  return
  71     // }
  72 
  73     var header string
  74     var rows []string
  75     indices := make([]int, 0, len(args))
  76 
  77     sc := bufio.NewScanner(os.Stdin)
  78     sc.Buffer(nil, 8*1024*1024*1024)
  79 
  80     for i := 0; sc.Scan(); i++ {
  81         line := sc.Text()
  82 
  83         if i == 0 {
  84             if strings.HasPrefix(line, "\xef\xbb\xbf") {
  85                 line = line[3:]
  86             }
  87 
  88             header = line
  89 
  90             for _, s := range args {
  91                 if j := findColumn(header, s); j >= 0 {
  92                     indices = append(indices, j)
  93                 }
  94             }
  95             continue
  96         }
  97 
  98         rows = append(rows, line)
  99     }
 100 
 101     sort.SliceStable(rows, func(i, j int) bool {
 102         return compareRows(rows[i], rows[j], indices)
 103     })
 104 
 105     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 106     defer bw.Flush()
 107 
 108     bw.WriteString(header)
 109     if bw.WriteByte('\n') != nil {
 110         return
 111     }
 112 
 113     for _, s := range rows {
 114         bw.WriteString(s)
 115         if bw.WriteByte('\n') != nil {
 116             return
 117         }
 118     }
 119 }
 120 
 121 // findColumn tries to find the 0-based column-index, given the first/header
 122 // row, and a name/integer-index
 123 func findColumn(line string, name string) int {
 124     // try exact match
 125     s := line
 126     for i := 0; len(s) > 0; i++ {
 127         item, rest, ok := sliceItem(s, 0)
 128         if !ok {
 129             break
 130         }
 131         if item == name {
 132             return i
 133         }
 134         s = rest
 135     }
 136 
 137     // try case-insensitive match
 138     s = line
 139     for i := 0; len(s) > 0; i++ {
 140         item, rest, ok := sliceItem(s, 0)
 141         if !ok {
 142             break
 143         }
 144         if strings.EqualFold(item, name) {
 145             return i
 146         }
 147         s = rest
 148     }
 149 
 150     // try a non-zero numeric index
 151     i, err := strconv.Atoi(name)
 152     if err != nil || i == 0 {
 153         return -1
 154     }
 155 
 156     // make indices 0-based, and correct negative indices
 157     if i > 0 {
 158         i--
 159     } else if i < 0 {
 160         n := countItems(line)
 161         i += n
 162     }
 163 
 164     // see if numeric index is in range
 165     if _, _, ok := sliceItem(line, i); !ok {
 166         return -1
 167     }
 168 
 169     return i
 170 }
 171 
 172 // countItems is 1 more than the number of tabs, or 0 for empty strings
 173 func countItems(line string) int {
 174     if len(line) == 0 {
 175         return 0
 176     }
 177 
 178     tabs := 0
 179 
 180     for len(line) > 0 {
 181         i := strings.IndexByte(line, '\t')
 182         if i < 0 {
 183             break
 184         }
 185 
 186         line = line[i+1:]
 187         tabs++
 188     }
 189 
 190     return tabs + 1
 191 }
 192 
 193 // sliceItem tries to split a TSV item from the row given, using a 0-based
 194 // index, returning both the item and the rest of the TSV line
 195 func sliceItem(line string, i int) (item string, rest string, ok bool) {
 196     s := line
 197     for len(s) > 0 && i > 0 {
 198         j := strings.IndexByte(s, '\t')
 199         if j < 0 {
 200             return ``, ``, false
 201         }
 202 
 203         s = s[j+1:]
 204         i--
 205     }
 206 
 207     if len(s) == 0 || i != 0 {
 208         return ``, ``, false
 209     }
 210 
 211     j := strings.IndexByte(s, '\t')
 212     if j < 0 {
 213         return s, ``, true
 214     }
 215     return s[:j], s[j+1:], true
 216 }
 217 
 218 // compareRows reverse-sorts 2 rows numerically, using the indices given
 219 func compareRows(x, y string, indices []int) bool {
 220     for _, k := range indices {
 221         rank := compareItems(x, y, k)
 222         if rank == 0 {
 223             continue
 224         }
 225         return rank < 0
 226     }
 227 
 228     return false
 229 }
 230 
 231 // compareItems picks the same-indexed item from 2 rows, and numerically
 232 // compares those picks reverse-numerically
 233 func compareItems(x, y string, i int) int {
 234     item1, _, got1 := sliceItem(x, i)
 235     item2, _, got2 := sliceItem(y, i)
 236 
 237     n1, ok1 := parseNumeric(item1)
 238     n2, ok2 := parseNumeric(item2)
 239 
 240     ok1 = ok1 && got1
 241     ok2 = ok2 && got2
 242 
 243     if !ok1 && !ok2 {
 244         return 0
 245     }
 246 
 247     if !ok1 {
 248         return -1
 249     }
 250     if !ok2 {
 251         return +1
 252     }
 253 
 254     if n1 < n2 {
 255         return +1
 256     }
 257     if n1 > n2 {
 258         return -1
 259     }
 260     return 0
 261 }
 262 
 263 // parseNumeric rejects NaNs and Infinities
 264 func parseNumeric(s string) (f float64, ok bool) {
 265     f, err := strconv.ParseFloat(s, 64)
 266     ok = err == nil && !math.IsNaN(f) && !math.IsInf(f, 0)
 267     return f, ok
 268 }
     File: ./base64/base64.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package base64
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "encoding/base64"
  31     "errors"
  32     "io"
  33     "os"
  34 )
  35 
  36 const info = `
  37 base64 [options...] [files...]
  38 
  39 Encode/decode bytes into/from the base-64 format.
  40 
  41 Options
  42 
  43     -d        decode away from base-64, instead of encoding into base-64
  44     --help    show this help message
  45 `
  46 
  47 func Main() {
  48     args := os.Args[1:]
  49     handle := encode
  50 
  51     for len(args) > 0 {
  52         switch args[0] {
  53         case `--help`:
  54             os.Stderr.WriteString(info[1:])
  55             return
  56 
  57         case `-d`:
  58             handle = decode
  59             args = args[1:]
  60             continue
  61         }
  62 
  63         break
  64     }
  65 
  66     if len(args) > 0 && args[0] == `--` {
  67         args = args[1:]
  68     }
  69 
  70     if err := run(os.Stdout, args, handle); err != nil && err != io.EOF {
  71         os.Stderr.WriteString(`base64: `)
  72         os.Stderr.WriteString(err.Error())
  73         os.Stderr.WriteString("\n")
  74         os.Exit(1)
  75         return
  76     }
  77 }
  78 
  79 type handler func(w io.Writer, r io.Reader) error
  80 
  81 func run(w io.Writer, paths []string, handle handler) error {
  82     for _, path := range paths {
  83         if err := handleFile(os.Stdout, path, handle); err != nil {
  84             return err
  85         }
  86     }
  87 
  88     if len(paths) == 0 {
  89         if err := handle(os.Stdout, os.Stdin); err != nil {
  90             return err
  91         }
  92     }
  93 
  94     return nil
  95 }
  96 
  97 func handleFile(w io.Writer, path string, handle handler) error {
  98     f, err := os.Open(path)
  99     if err != nil {
 100         return err
 101     }
 102     defer f.Close()
 103     return handle(w, f)
 104 }
 105 
 106 func decode(w io.Writer, r io.Reader) error {
 107     return debase64(w, r)
 108 }
 109 
 110 func encode(w io.Writer, r io.Reader) error {
 111     enc := base64.NewEncoder(base64.StdEncoding, w)
 112     _, err := io.Copy(enc, r)
 113     enc.Close()
 114 
 115     if err == nil {
 116         w.Write([]byte{'\n'})
 117     }
 118     return err
 119 }
 120 
 121 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
 122 // apart from output-writing ones
 123 func debase64(w io.Writer, r io.Reader) error {
 124     br := bufio.NewReaderSize(r, 32*1024)
 125     start, err := br.Peek(64)
 126     if err != nil && err != io.EOF {
 127         return err
 128     }
 129 
 130     skip, err := skipIntroDataURI(start)
 131     if err != nil {
 132         return err
 133     }
 134 
 135     if skip > 0 {
 136         br.Discard(skip)
 137     }
 138 
 139     dec := base64.NewDecoder(base64.StdEncoding, br)
 140     _, err = io.Copy(w, dec)
 141     return err
 142 }
 143 
 144 func skipIntroDataURI(chunk []byte) (skip int, err error) {
 145     if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 146         chunk = chunk[3:]
 147         skip += 3
 148     }
 149 
 150     if !bytes.HasPrefix(chunk, []byte(`data:`)) {
 151         return skip, nil
 152     }
 153 
 154     const l = len(`data:,`)
 155     if len(chunk) == l && chunk[l-1] == ',' {
 156         return l, nil
 157     }
 158 
 159     start := chunk
 160     if len(start) > 64 {
 161         start = start[:64]
 162     }
 163 
 164     i := bytes.Index(start, []byte(`;base64,`))
 165     if i < 0 {
 166         return skip, errors.New(`invalid data URI`)
 167     }
 168 
 169     skip += i + len(`;base64,`)
 170     return skip, nil
 171 }
     File: ./bitdump/bitdump.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package bitdump
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strconv"
  33 )
  34 
  35 const info = `
  36 bitdump [options...] [filenames...]
  37 
  38 
  39 Show all bits for all input bytes, starting each output line with the
  40 leading byte's offset.
  41 
  42 All (optional) leading options start with either single or double-dash:
  43 
  44     -h, -help                  show this help message
  45     -no-offset, -no-offsets    don't start lines with current byte offsets
  46     -tab, -tabs                separate items with tabs instead of spaces
  47 `
  48 
  49 const itemsPerLine = 8
  50 
  51 /*
  52 tlp = '(f"{bin(v)[2:]:>08}" for v in range(256))' | lineup 6 |
  53 gsub '\t' '`, `' | tlp 'f"\t`{l}`,"'
  54 */
  55 var bits = [256]string{
  56     `00000000`, `00000001`, `00000010`, `00000011`, `00000100`, `00000101`,
  57     `00000110`, `00000111`, `00001000`, `00001001`, `00001010`, `00001011`,
  58     `00001100`, `00001101`, `00001110`, `00001111`, `00010000`, `00010001`,
  59     `00010010`, `00010011`, `00010100`, `00010101`, `00010110`, `00010111`,
  60     `00011000`, `00011001`, `00011010`, `00011011`, `00011100`, `00011101`,
  61     `00011110`, `00011111`, `00100000`, `00100001`, `00100010`, `00100011`,
  62     `00100100`, `00100101`, `00100110`, `00100111`, `00101000`, `00101001`,
  63     `00101010`, `00101011`, `00101100`, `00101101`, `00101110`, `00101111`,
  64     `00110000`, `00110001`, `00110010`, `00110011`, `00110100`, `00110101`,
  65     `00110110`, `00110111`, `00111000`, `00111001`, `00111010`, `00111011`,
  66     `00111100`, `00111101`, `00111110`, `00111111`, `01000000`, `01000001`,
  67     `01000010`, `01000011`, `01000100`, `01000101`, `01000110`, `01000111`,
  68     `01001000`, `01001001`, `01001010`, `01001011`, `01001100`, `01001101`,
  69     `01001110`, `01001111`, `01010000`, `01010001`, `01010010`, `01010011`,
  70     `01010100`, `01010101`, `01010110`, `01010111`, `01011000`, `01011001`,
  71     `01011010`, `01011011`, `01011100`, `01011101`, `01011110`, `01011111`,
  72     `01100000`, `01100001`, `01100010`, `01100011`, `01100100`, `01100101`,
  73     `01100110`, `01100111`, `01101000`, `01101001`, `01101010`, `01101011`,
  74     `01101100`, `01101101`, `01101110`, `01101111`, `01110000`, `01110001`,
  75     `01110010`, `01110011`, `01110100`, `01110101`, `01110110`, `01110111`,
  76     `01111000`, `01111001`, `01111010`, `01111011`, `01111100`, `01111101`,
  77     `01111110`, `01111111`, `10000000`, `10000001`, `10000010`, `10000011`,
  78     `10000100`, `10000101`, `10000110`, `10000111`, `10001000`, `10001001`,
  79     `10001010`, `10001011`, `10001100`, `10001101`, `10001110`, `10001111`,
  80     `10010000`, `10010001`, `10010010`, `10010011`, `10010100`, `10010101`,
  81     `10010110`, `10010111`, `10011000`, `10011001`, `10011010`, `10011011`,
  82     `10011100`, `10011101`, `10011110`, `10011111`, `10100000`, `10100001`,
  83     `10100010`, `10100011`, `10100100`, `10100101`, `10100110`, `10100111`,
  84     `10101000`, `10101001`, `10101010`, `10101011`, `10101100`, `10101101`,
  85     `10101110`, `10101111`, `10110000`, `10110001`, `10110010`, `10110011`,
  86     `10110100`, `10110101`, `10110110`, `10110111`, `10111000`, `10111001`,
  87     `10111010`, `10111011`, `10111100`, `10111101`, `10111110`, `10111111`,
  88     `11000000`, `11000001`, `11000010`, `11000011`, `11000100`, `11000101`,
  89     `11000110`, `11000111`, `11001000`, `11001001`, `11001010`, `11001011`,
  90     `11001100`, `11001101`, `11001110`, `11001111`, `11010000`, `11010001`,
  91     `11010010`, `11010011`, `11010100`, `11010101`, `11010110`, `11010111`,
  92     `11011000`, `11011001`, `11011010`, `11011011`, `11011100`, `11011101`,
  93     `11011110`, `11011111`, `11100000`, `11100001`, `11100010`, `11100011`,
  94     `11100100`, `11100101`, `11100110`, `11100111`, `11101000`, `11101001`,
  95     `11101010`, `11101011`, `11101100`, `11101101`, `11101110`, `11101111`,
  96     `11110000`, `11110001`, `11110010`, `11110011`, `11110100`, `11110101`,
  97     `11110110`, `11110111`, `11111000`, `11111001`, `11111010`, `11111011`,
  98     `11111100`, `11111101`, `11111110`, `11111111`,
  99 }
 100 
 101 func Main() {
 102     emitOffsets := true
 103     separator := byte(' ')
 104     args := os.Args[1:]
 105 
 106     for len(args) > 0 {
 107         switch args[0] {
 108         case `-h`, `--h`, `-help`, `--help`:
 109             os.Stdout.WriteString(info[1:])
 110             return
 111 
 112         case `-no-offset`, `--no-offset`, `-no-offsets`, `--no-offsets`:
 113             emitOffsets = false
 114             args = args[1:]
 115             continue
 116 
 117         case `-tab`, `--tab`, `-tabs`, `--tabs`:
 118             separator = '\t'
 119             args = args[1:]
 120             continue
 121         }
 122 
 123         break
 124     }
 125 
 126     if len(args) > 0 && args[0] == `--` {
 127         args = args[1:]
 128     }
 129 
 130     var p params
 131     p.emitOffset = emitNoOffset
 132     if emitOffsets {
 133         p.emitOffset = emitDecimalOffset
 134     }
 135     p.separator = separator
 136 
 137     if err := run(os.Stdout, p, args); err != nil && err != io.EOF {
 138         os.Stderr.WriteString(`bitdump: `)
 139         os.Stderr.WriteString(err.Error())
 140         os.Stderr.WriteString("\n")
 141         os.Exit(1)
 142         return
 143     }
 144 }
 145 
 146 type params struct {
 147     offset     *int64
 148     emitOffset func(w *bufio.Writer, offset int64, sep byte)
 149     separator  byte
 150 }
 151 
 152 func run(w io.Writer, p params, args []string) error {
 153     offset := int64(0)
 154     p.offset = &offset
 155     bw := bufio.NewWriterSize(w, 32*1024)
 156     defer func() {
 157         if offset%itemsPerLine == 0 && offset > 0 {
 158             bw.WriteByte('\n')
 159         }
 160         bw.Flush()
 161     }()
 162 
 163     if len(args) == 0 {
 164         return bitdump(bw, os.Stdin, p)
 165     }
 166 
 167     for _, name := range args {
 168         if err := handleFile(bw, name, p); err != nil {
 169             return err
 170         }
 171     }
 172     return nil
 173 }
 174 
 175 func handleFile(w *bufio.Writer, name string, p params) error {
 176     if name == `` || name == `-` {
 177         return bitdump(w, os.Stdin, p)
 178     }
 179 
 180     f, err := os.Open(name)
 181     if err != nil {
 182         return errors.New(`can't read from file named "` + name + `"`)
 183     }
 184     defer f.Close()
 185 
 186     return bitdump(w, f, p)
 187 }
 188 
 189 func bitdump(w *bufio.Writer, r io.Reader, p params) error {
 190     var buf [32 * 1024]byte
 191     defer w.Flush()
 192 
 193     for {
 194         n, err := r.Read(buf[:])
 195         if n < 1 && err == io.EOF {
 196             return nil
 197         }
 198 
 199         if err != nil && err != io.EOF {
 200             return err
 201         }
 202 
 203         chunk := buf[:n]
 204 
 205         for len(chunk) >= itemsPerLine {
 206             if err := emitChunk(w, chunk[:itemsPerLine], p); err != nil {
 207                 return err
 208             }
 209 
 210             chunk = chunk[itemsPerLine:]
 211             *p.offset += itemsPerLine
 212         }
 213 
 214         if len(chunk) > 0 {
 215             if err := emitChunk(w, chunk, p); err != nil {
 216                 return err
 217             }
 218 
 219             *p.offset += int64(len(chunk))
 220         }
 221     }
 222 }
 223 
 224 func emitDecimalOffset(w *bufio.Writer, offset int64, sep byte) {
 225     const pad = `00000000`
 226     var str [24]byte
 227 
 228     s := strconv.AppendInt(str[:0], offset, 10)
 229     // pad small offsets with leading zeros
 230     if len(s) < len(pad) {
 231         w.WriteString(pad[len(s):])
 232     }
 233     w.Write(s)
 234     w.WriteByte(sep)
 235 }
 236 
 237 func emitNoOffset(w *bufio.Writer, offset int64, sep byte) {
 238     // deliberately does nothing
 239 }
 240 
 241 func emitChunk(w *bufio.Writer, chunk []byte, p params) error {
 242     p.emitOffset(w, *p.offset, p.separator)
 243     for i, b := range chunk {
 244         if i > 0 {
 245             w.WriteByte(p.separator)
 246         }
 247         w.WriteString(bits[b])
 248     }
 249 
 250     if w.WriteByte('\n') != nil {
 251         // assume a write error is the consequence of stdout
 252         // being closed, perhaps by another app along a pipe
 253         return io.EOF
 254     }
 255     return nil
 256 }
     File: ./breakdown/breakdown.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package breakdown
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33 )
  34 
  35 const info = `
  36 breakdown [options...] [regular expressions...]
  37 
  38 Break each input line into multiple output lines using the first matching
  39 regexes given. All regexes are tried in the order given starting from the
  40 first, until each line is split completely.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help     show this help message
  45     -i, -ins      match regexes case-insensitively
  46 `
  47 
  48 func Main() {
  49     nerr := 0
  50     buffered := false
  51     sensitive := true
  52     args := os.Args[1:]
  53 
  54     for len(args) > 0 {
  55         switch args[0] {
  56         case `-b`, `--b`, `-buffered`, `--buffered`:
  57             buffered = true
  58             args = args[1:]
  59             continue
  60 
  61         case `-h`, `--h`, `-help`, `--help`:
  62             os.Stdout.WriteString(info[1:])
  63             return
  64 
  65         case `-i`, `--i`, `-ins`, `--ins`:
  66             sensitive = false
  67             args = args[1:]
  68             continue
  69         }
  70 
  71         break
  72     }
  73 
  74     if len(args) > 0 && args[0] == `--` {
  75         args = args[1:]
  76     }
  77 
  78     if len(args) == 0 {
  79         os.Stderr.WriteString(info[1:])
  80         return
  81     }
  82 
  83     liveLines := !buffered
  84     if !buffered {
  85         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  86             liveLines = false
  87         }
  88     }
  89 
  90     exprs := make([]*regexp.Regexp, 0, len(args))
  91 
  92     for _, src := range args {
  93         var err error
  94         var exp *regexp.Regexp
  95         if !sensitive {
  96             exp, err = regexp.Compile(`(?i)` + src)
  97         } else {
  98             exp, err = regexp.Compile(src)
  99         }
 100 
 101         if err != nil {
 102             os.Stderr.WriteString(`breakdown: `)
 103             os.Stderr.WriteString(err.Error())
 104             os.Stderr.WriteString("\n")
 105             nerr++
 106         }
 107 
 108         exprs = append(exprs, exp)
 109     }
 110 
 111     if nerr > 0 {
 112         os.Exit(1)
 113         return
 114     }
 115 
 116     var buf []byte
 117     sc := bufio.NewScanner(os.Stdin)
 118     sc.Buffer(nil, 8*1024*1024*1024)
 119     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 120 
 121     for i := 0; sc.Scan(); i++ {
 122         line := sc.Bytes()
 123         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 124             line = line[3:]
 125         }
 126 
 127         s := line
 128         if bytes.IndexByte(s, '\x1b') >= 0 {
 129             buf = plain(buf[:0], s)
 130             s = buf
 131         }
 132 
 133         for len(s) > 0 {
 134             start, end := findEarliest(s, exprs)
 135             if start < 0 {
 136                 if err := emit(bw, s, liveLines); err != nil {
 137                     return
 138                 }
 139                 break
 140             }
 141 
 142             if err := emit(bw, s[:start], liveLines); err != nil {
 143                 return
 144             }
 145             s = s[end:]
 146         }
 147     }
 148 }
 149 
 150 func findEarliest(s []byte, exprs []*regexp.Regexp) (start int, end int) {
 151     start = -1
 152     end = -1
 153 
 154     for _, e := range exprs {
 155         m := e.FindIndex(s)
 156         if len(m) == 2 && m[0] != m[1] && (m[0] < start || start < 0) {
 157             start = m[0]
 158             end = m[1]
 159         }
 160     }
 161 
 162     return start, end
 163 }
 164 
 165 func emit(w *bufio.Writer, line []byte, live bool) error {
 166     w.Write(line)
 167     w.WriteByte('\n')
 168 
 169     if live && w.Flush() != nil {
 170         return io.EOF
 171     }
 172     return nil
 173 }
 174 
 175 func plain(dst []byte, src []byte) []byte {
 176     for len(src) > 0 {
 177         i, j := indexEscapeSequence(src)
 178         if i < 0 {
 179             dst = append(dst, src...)
 180             break
 181         }
 182         if j < 0 {
 183             j = len(src)
 184         }
 185 
 186         if i > 0 {
 187             dst = append(dst, src[:i]...)
 188         }
 189 
 190         src = src[j:]
 191     }
 192 
 193     return dst
 194 }
 195 
 196 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 197 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 198 // indices which can be independently negative when either the start/end of
 199 // a sequence isn't found; given their fairly-common use, even the hyperlink
 200 // ESC]8 sequences are supported
 201 func indexEscapeSequence(s []byte) (int, int) {
 202     var prev byte
 203 
 204     for i, b := range s {
 205         if prev == '\x1b' && b == '[' {
 206             j := indexLetter(s[i+1:])
 207             if j < 0 {
 208                 return i, -1
 209             }
 210             return i - 1, i + 1 + j + 1
 211         }
 212 
 213         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 214             j := indexPair(s[i+1:], '\x1b', '\\')
 215             if j < 0 {
 216                 return i, -1
 217             }
 218             return i - 1, i + 1 + j + 2
 219         }
 220 
 221         prev = b
 222     }
 223 
 224     return -1, -1
 225 }
 226 
 227 func indexLetter(s []byte) int {
 228     for i, b := range s {
 229         upper := b &^ 32
 230         if 'A' <= upper && upper <= 'Z' {
 231             return i
 232         }
 233     }
 234 
 235     return -1
 236 }
 237 
 238 func indexPair(s []byte, x byte, y byte) int {
 239     var prev byte
 240 
 241     for i, b := range s {
 242         if prev == x && b == y && i > 0 {
 243             return i
 244         }
 245         prev = b
 246     }
 247 
 248     return -1
 249 }
     File: ./bytedump/bytedump.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package bytedump
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "io"
  31     "math"
  32     "os"
  33     "strconv"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 bytedump [options...] [filenames...]
  39 
  40 Show bytes as hexadecimal and ascii on the side.
  41 
  42 Each line shows the starting offset for the bytes shown, 16 of the bytes
  43 themselves in base-16 notation, and any ASCII codes when the byte values
  44 are in the typical ASCII range.
  45 
  46 The ASCII codes always include 2 rows, which makes the output more 'grep'
  47 friendly, since strings up to 32 items can't be accidentally missed. The
  48 offsets shown are base-16.
  49 `
  50 
  51 const perLine = 16
  52 
  53 // hexSymbols is a direct lookup table combining 2 hex digits with either a
  54 // space or a displayable ASCII symbol matching the byte's own ASCII value;
  55 // this table was autogenerated by running the command
  56 //
  57 // seq 0 255 | ./hex-symbols.awk
  58 var hexSymbols = [256]string{
  59     `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
  60     `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
  61     `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
  62     `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
  63     `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
  64     `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
  65     `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
  66     `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
  67     `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
  68     `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
  69     `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
  70     `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
  71     "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
  72     `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
  73     `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
  74     `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
  75     `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
  76     `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
  77     `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
  78     `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
  79     `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
  80     `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
  81     `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
  82     `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
  83     `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
  84     `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
  85     `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
  86     `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
  87     `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
  88     `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
  89     `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
  90     `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
  91 }
  92 
  93 func Main() {
  94     args := os.Args[1:]
  95 
  96     if len(args) > 0 {
  97         switch args[0] {
  98         case `-h`, `--h`, `-help`, `--help`:
  99             os.Stdout.WriteString(info[1:])
 100             return
 101         }
 102     }
 103 
 104     if len(args) > 0 && args[0] == `--` {
 105         args = args[1:]
 106     }
 107 
 108     if err := run(args); err != nil {
 109         os.Stdout.WriteString(err.Error())
 110         os.Stdout.WriteString("\n")
 111         os.Exit(1)
 112         return
 113     }
 114 }
 115 
 116 func run(args []string) error {
 117     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 118     defer w.Flush()
 119 
 120     // with no filenames given, handle stdin and quit
 121     if len(args) == 0 {
 122         return handle(w, os.Stdin, `<stdin>`, -1)
 123     }
 124 
 125     for i, fname := range args {
 126         if i > 0 {
 127             w.WriteString("\n")
 128             w.WriteString("\n")
 129         }
 130 
 131         if err := handleFile(w, fname); err != nil {
 132             return err
 133         }
 134     }
 135 
 136     return nil
 137 }
 138 
 139 func handleFile(w *bufio.Writer, fname string) error {
 140     f, err := os.Open(fname)
 141     if err != nil {
 142         return err
 143     }
 144     defer f.Close()
 145 
 146     stat, err := f.Stat()
 147     if err != nil {
 148         return handle(w, f, fname, -1)
 149     }
 150 
 151     fsize := int(stat.Size())
 152     return handle(w, f, fname, fsize)
 153 }
 154 
 155 // handle shows some messages related to the input and the cmd-line options
 156 // used, and then follows them by the hexadecimal byte-view
 157 func handle(w *bufio.Writer, r io.Reader, name string, size int) error {
 158     owidth10 := -1
 159     owidth16 := -1
 160     if size > 0 {
 161         w10 := math.Log10(float64(size))
 162         w10 = math.Max(math.Ceil(w10), 1)
 163         w16 := math.Log2(float64(size)) / 4
 164         w16 = math.Max(math.Ceil(w16), 1)
 165         owidth10 = int(w10)
 166         owidth16 = int(w16)
 167     }
 168 
 169     if owidth10 < 0 {
 170         owidth10 = 8
 171     }
 172     if owidth16 < 0 {
 173         owidth16 = 8
 174     }
 175 
 176     rc := rendererConfig{
 177         out:           w,
 178         offsetWidth10: max(owidth10, 8),
 179         offsetWidth16: max(owidth16, 8),
 180     }
 181 
 182     if size < 0 {
 183         fmt.Fprintf(w, "• %s\n", name)
 184     } else {
 185         const fs = "• %s  (%s bytes)\n"
 186         fmt.Fprintf(w, fs, name, sprintCommas(size))
 187     }
 188     w.WriteByte('\n')
 189 
 190     // calling func Read directly can sometimes result in chunks shorter
 191     // than the max chunk-size, even when there are plenty of bytes yet
 192     // to read; to avoid that, use a buffered-reader to explicitly fill
 193     // a slice instead
 194     br := bufio.NewReader(r)
 195 
 196     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 197     cur := make([]byte, 0, perLine)
 198     ahead := make([]byte, 0, perLine)
 199 
 200     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 201     // so to speak
 202     cur, err := fillChunk(cur[:0], perLine, br)
 203     if len(cur) == 0 {
 204         if err == io.EOF {
 205             err = nil
 206         }
 207         return err
 208     }
 209 
 210     for {
 211         ahead, err := fillChunk(ahead[:0], perLine, br)
 212         if err != nil && err != io.EOF {
 213             return err
 214         }
 215 
 216         if len(ahead) == 0 {
 217             // done, maybe except for an extra line of output
 218             break
 219         }
 220 
 221         // show the byte-chunk on its own output line
 222         if err := writeChunk(rc, cur, ahead); err != nil {
 223             return io.EOF
 224         }
 225 
 226         rc.offset += uint(len(cur))
 227         cur = cur[:copy(cur, ahead)]
 228     }
 229 
 230     // don't forget the last output line
 231     if len(cur) > 0 {
 232         return writeChunk(rc, cur, nil)
 233     }
 234     return nil
 235 }
 236 
 237 // fillChunk tries to read the number of bytes given, appending them to the
 238 // byte-slice given; this func returns an EOF error only when no bytes are
 239 // read, which somewhat simplifies error-handling for the func caller
 240 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 241     // read buffered-bytes up to the max chunk-size
 242     for i := 0; i < n; i++ {
 243         b, err := br.ReadByte()
 244         if err == nil {
 245             chunk = append(chunk, b)
 246             continue
 247         }
 248 
 249         if err == io.EOF && i > 0 {
 250             return chunk, nil
 251         }
 252         return chunk, err
 253     }
 254 
 255     // got the full byte-count asked for
 256     return chunk, nil
 257 }
 258 
 259 // rendererConfig groups several arguments given to any of the rendering funcs
 260 type rendererConfig struct {
 261     // out is writer to send all output to
 262     out *bufio.Writer
 263 
 264     // offset is the byte-offset of the first byte shown on the current output
 265     // line: if shown at all, it's shown at the start the line
 266     offset uint
 267 
 268     // offsetWidth10 is the max string-width for the base-10 byte-offsets
 269     // shown at the start of output lines, and determines those values'
 270     // left-padding
 271     offsetWidth10 int
 272 
 273     // offsetWidth16 is the max string-width for the base-16 byte-offsets
 274     // shown at the start of output lines, and determines those values'
 275     // left-padding
 276     offsetWidth16 int
 277 }
 278 
 279 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
 280 // handles negatives, even though this app only uses it with non-negatives.
 281 func loopThousandsGroups(n int, fn func(i, n int)) {
 282     // 0 doesn't have a log10
 283     if n == 0 {
 284         fn(0, 0)
 285         return
 286     }
 287 
 288     sign := +1
 289     if n < 0 {
 290         n = -n
 291         sign = -1
 292     }
 293 
 294     intLog1000 := int(math.Log10(float64(n)) / 3)
 295     remBase := int(math.Pow10(3 * intLog1000))
 296 
 297     for i := 0; remBase > 0; i++ {
 298         group := (1000 * n) / remBase / 1000
 299         fn(i, sign*group)
 300         // if original number was negative, ensure only first
 301         // group gives a negative input to the callback
 302         sign = +1
 303 
 304         n %= remBase
 305         remBase /= 1000
 306     }
 307 }
 308 
 309 // sprintCommas turns the non-negative number given into a readable string,
 310 // where digits are grouped-separated by commas
 311 func sprintCommas(n int) string {
 312     var sb strings.Builder
 313     loopThousandsGroups(n, func(i, n int) {
 314         if i == 0 {
 315             var buf [4]byte
 316             sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
 317             return
 318         }
 319         sb.WriteByte(',')
 320         writePad0Sub1000Counter(&sb, uint(n))
 321     })
 322     return sb.String()
 323 }
 324 
 325 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
 326 func writePad0Sub1000Counter(w io.Writer, n uint) {
 327     // precondition is 0...999
 328     if n > 999 {
 329         w.Write([]byte(`???`))
 330         return
 331     }
 332 
 333     var buf [3]byte
 334     buf[0] = byte(n/100) + '0'
 335     n %= 100
 336     buf[1] = byte(n/10) + '0'
 337     buf[2] = byte(n%10) + '0'
 338     w.Write(buf[:])
 339 }
 340 
 341 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
 342 // matters because it's called for every byte of input which isn't
 343 // all 0s or all 1s
 344 func writeHex(w *bufio.Writer, b byte) {
 345     const hexDigits = `0123456789abcdef`
 346     w.WriteByte(hexDigits[b>>4])
 347     w.WriteByte(hexDigits[b&0x0f])
 348 }
 349 
 350 // padding is the padding/spacing emitted across each output line
 351 const padding = 2
 352 
 353 func writeChunk(rc rendererConfig, first, second []byte) error {
 354     w := rc.out
 355 
 356     // start each line with the byte-offset for the 1st item shown on it
 357     // writeDecimalCounter(w, rc.offsetWidth10, rc.offset)
 358     // w.WriteByte(' ')
 359 
 360     // start each line with the byte-offset for the 1st item shown on it
 361     writeHexadecimalCounter(w, rc.offsetWidth16, rc.offset)
 362     w.WriteByte(' ')
 363 
 364     for _, b := range first {
 365         // fmt.Fprintf(w, ` %02x`, b)
 366         //
 367         // the commented part above was a performance bottleneck, since
 368         // the slow/generic fmt.Fprintf was called for each input byte
 369         w.WriteByte(' ')
 370         writeHex(w, b)
 371     }
 372 
 373     writeASCII(w, first, second, perLine)
 374     return w.WriteByte('\n')
 375 }
 376 
 377 // writeDecimalCounter just emits a left-padded number
 378 func writeDecimalCounter(w *bufio.Writer, width int, n uint) {
 379     var buf [24]byte
 380     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 381     writeSpaces(w, width-len(str))
 382     w.Write(str)
 383 }
 384 
 385 // writeHexadecimalCounter just emits a zero-padded base-16 number
 386 func writeHexadecimalCounter(w *bufio.Writer, width int, n uint) {
 387     var buf [24]byte
 388     str := strconv.AppendUint(buf[:0], uint64(n), 16)
 389     // writeSpaces(w, width-len(str))
 390     for i := 0; i < width-len(str); i++ {
 391         w.WriteByte('0')
 392     }
 393     w.Write(str)
 394 }
 395 
 396 // writeSpaces bulk-emits the number of spaces given
 397 func writeSpaces(w *bufio.Writer, n int) {
 398     const spaces = `                                `
 399     for ; n > len(spaces); n -= len(spaces) {
 400         w.WriteString(spaces)
 401     }
 402     if n > 0 {
 403         w.WriteString(spaces[:n])
 404     }
 405 }
 406 
 407 // writeASCII emits the side-panel showing all ASCII runs for each line
 408 func writeASCII(w *bufio.Writer, first, second []byte, perline int) {
 409     spaces := padding + 3*(perline-len(first))
 410 
 411     for _, b := range first {
 412         if 32 < b && b < 127 {
 413             writeSpaces(w, spaces)
 414             w.WriteByte(b)
 415             spaces = 0
 416         } else {
 417             spaces++
 418         }
 419     }
 420 
 421     for _, b := range second {
 422         if 32 < b && b < 127 {
 423             writeSpaces(w, spaces)
 424             w.WriteByte(b)
 425             spaces = 0
 426         } else {
 427             spaces++
 428         }
 429     }
 430 }
     File: ./calc/calc.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package calc
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "go/ast"
  31     "go/parser"
  32     "go/token"
  33     "io"
  34     "math"
  35     "math/big"
  36     "os"
  37     "strings"
  38 )
  39 
  40 const info = `
  41 ca [options...] [go expressions...]
  42 
  43 
  44 CAlculate arbitrary-size fractions, using go expressions. For convenience,
  45 function names are case-insensitive, numeric literals can have underscores,
  46 and square brackets are treated the same as (round) parentheses.
  47 
  48 Treating square brackets as parentheses extends the convenience of unquoted
  49 arguments without spaces to also call functions, as parentheses are part of
  50 command-line syntax, and would require quoting to pass as part of literal
  51 command-line arguments otherwise.
  52 
  53 Functions with only 1-input can also be called as fake-properties, using a
  54 dot between the value and the function name: due to a quirk of the syntax,
  55 integer literals require 2 dots, instead of 1 dot.
  56 
  57 Several functions are available, along with their aliases:
  58 
  59     abs(x)
  60     bits(x)
  61     c(n, k)          choose, com, comb, combinations
  62     ceil(x)          ceiling
  63     dbin(x, n, p)    dbinom
  64     den(x)           denom, denominator
  65     digits(x)
  66     f(x)             fac, fact, factorial
  67     floor(x)
  68     isprime(n)       prime
  69     gcd(x, y)        gcf
  70     lcm(x, y)
  71     num(x)           numer, numerator
  72     p(n, k)          per, perm, permutations
  73     pow(x, y)        power
  74     pow2(x)          power2
  75     pow10(x)         power10
  76     rem(x, y)        remainder
  77     sgn(x)           sign
  78 
  79     avg(...)         mean
  80     max(...)
  81     min(...)
  82     polyval(x, ...)  horner
  83 
  84 Note: when the exponent given to the pow/power functions isn't an integer,
  85 the result is a double-precision floating-point approximation.
  86 
  87 All (optional) leading options start with either single or double-dash:
  88 
  89     -d, -decs, -decimals    show decimal digits, instead of fractions
  90     -h, -help               show this help message
  91     -v, -verbose            show a styled banner for each expression being run
  92 `
  93 
  94 type config struct {
  95     showFractions bool
  96     verbose       bool
  97 }
  98 
  99 func Main() {
 100     args := os.Args[1:]
 101 
 102     var cfg config
 103     cfg.verbose = false
 104     cfg.showFractions = true
 105 
 106     for len(args) > 0 {
 107         switch args[0] {
 108         case `-h`, `--h`, `-help`, `--help`:
 109             os.Stdout.WriteString(info[1:])
 110             return
 111 
 112         case `-d`, `--d`, `-decs`, `--decs`, `-decimals`, `--decimals`:
 113             cfg.showFractions = false
 114             args = args[1:]
 115             continue
 116 
 117         case `-v`, `--v`, `-verbose`, `--verbose`:
 118             cfg.verbose = true
 119             args = args[1:]
 120             continue
 121         }
 122 
 123         break
 124     }
 125 
 126     if len(args) > 0 && args[0] == `--` {
 127         args = args[1:]
 128     }
 129 
 130     if len(args) == 0 {
 131         os.Stderr.WriteString(info[1:])
 132         os.Exit(1)
 133         return
 134     }
 135 
 136     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 137         os.Stderr.WriteString(`calc: `)
 138         os.Stderr.WriteString(err.Error())
 139         os.Stderr.WriteString("\n")
 140         os.Exit(1)
 141         return
 142     }
 143 }
 144 
 145 func run(w io.Writer, args []string, cfg config) error {
 146     for _, src := range args {
 147         src = strings.ToLower(src)
 148 
 149         // treat square brackets like parentheses, for convenience
 150         src = strings.Replace(src, `[`, `(`, -1)
 151         src = strings.Replace(src, `]`, `)`, -1)
 152 
 153         if cfg.verbose {
 154             fmt.Fprintf(os.Stderr, "\x1b[7m%s\x1b[0m\n", src)
 155         }
 156 
 157         expr, err := parser.ParseExpr(src)
 158         if err != nil {
 159             return fmt.Errorf("%s: %s", src, err.Error())
 160         }
 161 
 162         n, err := eval(expr)
 163         if err != nil {
 164             return err
 165         }
 166 
 167         // only show the numerator, when the denominator is 1; when showing
 168         // results as numbers with decimals, all trailing zero decimals are
 169         // ignored
 170         s := ``
 171         if n.IsInt() {
 172             s = n.Num().String()
 173         } else if cfg.showFractions {
 174             s = n.String()
 175         } else {
 176             s = trimDecimals(n.FloatString(100))
 177         }
 178 
 179         io.WriteString(w, s)
 180         _, err = io.WriteString(w, "\n")
 181 
 182         if err != nil {
 183             break
 184         }
 185     }
 186 
 187     return nil
 188 }
 189 
 190 // trimDecimals ignores excessive trailing decimal zeros, if any, as well as
 191 // the decimal dot itself, if all decimals turn out to be zeros; integers are
 192 // returned as given
 193 func trimDecimals(s string) string {
 194     // with no decimals, keep all/any trailing zeros
 195     if strings.IndexByte(s, '.') < 0 {
 196         return s
 197     }
 198 
 199     // ignore all trailing zero decimals
 200     for len(s) > 0 && s[len(s)-1] == '0' {
 201         s = s[:len(s)-1]
 202     }
 203     // ignore trailing decimal
 204     if len(s) > 0 && s[len(s)-1] == '.' {
 205         s = s[:len(s)-1]
 206     }
 207     return s
 208 }
 209 
 210 func eval(expr ast.Expr) (*big.Rat, error) {
 211     switch expr := expr.(type) {
 212     case *ast.BasicLit:
 213         return evalLit(expr)
 214     case *ast.ParenExpr:
 215         return eval(expr.X)
 216     case *ast.UnaryExpr:
 217         return evalUnary(expr)
 218     case *ast.SelectorExpr:
 219         return evalSelector(expr)
 220     case *ast.BinaryExpr:
 221         return evalBinary(expr)
 222     case *ast.CallExpr:
 223         return evalCall(expr)
 224     case *ast.Ident:
 225         return evalIdent(expr)
 226     }
 227 
 228     return nil, errors.New(`unsupported expression type`)
 229 }
 230 
 231 func evalLit(expr *ast.BasicLit) (*big.Rat, error) {
 232     switch expr.Kind {
 233     case token.INT, token.FLOAT:
 234         n := big.NewRat(0, 1)
 235         n, _ = n.SetString(expr.Value)
 236         return n, nil
 237     }
 238 
 239     return nil, errors.New(`unsupported literal type`)
 240 }
 241 
 242 func evalSelector(expr *ast.SelectorExpr) (*big.Rat, error) {
 243     name := expr.Sel.Name
 244     call := ast.CallExpr{
 245         Fun:  ast.NewIdent(name),
 246         Args: []ast.Expr{expr.X},
 247     }
 248     return evalCall1(name, &call)
 249 }
 250 
 251 func evalUnary(expr *ast.UnaryExpr) (*big.Rat, error) {
 252     switch expr.Op {
 253     case token.ADD:
 254         return eval(expr.X)
 255 
 256     case token.SUB:
 257         n, err := eval(expr.X)
 258         if n != nil {
 259             n = n.Neg(n)
 260         }
 261         return n, err
 262 
 263     case token.NOT:
 264         return eval(&ast.CallExpr{
 265             Fun:  ast.NewIdent(`factorial`),
 266             Args: []ast.Expr{expr.X},
 267         })
 268     }
 269 
 270     return nil, errors.New(`unsupported unary operation ` + expr.Op.String())
 271 }
 272 
 273 func evalBinary(expr *ast.BinaryExpr) (*big.Rat, error) {
 274     x, err := eval(expr.X)
 275     if err != nil {
 276         return nil, err
 277     }
 278 
 279     y, err := eval(expr.Y)
 280     if err != nil {
 281         return nil, err
 282     }
 283 
 284     z := big.NewRat(0, 1)
 285 
 286     switch expr.Op {
 287     case token.ADD:
 288         z = z.Add(x, y)
 289         return z, nil
 290 
 291     case token.SUB:
 292         z = z.Sub(x, y)
 293         return z, nil
 294 
 295     case token.MUL:
 296         z = z.Mul(x, y)
 297         return z, nil
 298 
 299     case token.QUO:
 300         if y.Sign() == 0 {
 301             return nil, errors.New(`can't divide by zero`)
 302         }
 303         z = z.Quo(x, y)
 304         return z, nil
 305 
 306     case token.REM:
 307         return remainder(x, y)
 308     }
 309 
 310     return nil, errors.New(`unsupported binary operation ` + expr.Op.String())
 311 }
 312 
 313 func evalCall(expr *ast.CallExpr) (*big.Rat, error) {
 314     ident, ok := expr.Fun.(*ast.Ident)
 315     if !ok {
 316         return nil, errors.New(`unsupported function type`)
 317     }
 318     s := ident.Name
 319 
 320     if _, ok := varFuncs[s]; ok {
 321         return evalVarCall(s, expr)
 322     }
 323 
 324     switch len(expr.Args) {
 325     case 1:
 326         return evalCall1(s, expr)
 327     case 2:
 328         return evalCall2(s, expr)
 329     case 3:
 330         return evalCall3(s, expr)
 331     }
 332 
 333     return nil, errors.New(`function '` + s + `' not available`)
 334 }
 335 
 336 func evalIdent(expr *ast.Ident) (*big.Rat, error) {
 337     s := strings.ToLower(expr.Name)
 338     if v, ok := values[s]; ok {
 339         if f, ok := big.NewRat(0, 1).SetString(v); ok {
 340             return f, nil
 341         }
 342         return nil, errors.New(`value '` + s + `' isn't a valid number`)
 343     }
 344     return nil, errors.New(`value '` + s + `' not available`)
 345 }
 346 
 347 func copyFrac(x *big.Rat) *big.Rat {
 348     y := big.NewRat(0, 1)
 349     y = y.Add(y, x)
 350     return y
 351 }
 352 
 353 var values = map[string]string{
 354     `kb`:  `1024`,
 355     `mb`:  `1048576`,
 356     `gb`:  `1073741824`,
 357     `tb`:  `1099511627776`,
 358     `pb`:  `1125899906842624`,
 359     `kib`: `1024`,
 360     `mib`: `1048576`,
 361     `gib`: `1073741824`,
 362     `tib`: `1099511627776`,
 363     `pib`: `1125899906842624`,
 364 
 365     `hour`: `3600`,
 366     `hr`:   `3600`,
 367     `day`:  `86400`,
 368     `week`: `604800`,
 369     `wk`:   `604800`,
 370 
 371     `mol`:  `602214076000000000000000`,
 372     `mole`: `602214076000000000000000`,
 373 }
 374 
 375 var funcs1 = map[string]func(*big.Rat) (*big.Rat, error){
 376     `abs`:         abs,
 377     `bits`:        bits,
 378     `ceil`:        ceiling,
 379     `ceiling`:     ceiling,
 380     `den`:         denominator,
 381     `denom`:       denominator,
 382     `denominator`: denominator,
 383     `digits`:      digits,
 384     `f`:           factorial,
 385     `fac`:         factorial,
 386     `fact`:        factorial,
 387     `factorial`:   factorial,
 388     `floor`:       floor,
 389     `isprime`:     isPrime,
 390     `prime`:       isPrime,
 391     `num`:         numerator,
 392     `numer`:       numerator,
 393     `numerator`:   numerator,
 394     `pow2`:        power2,
 395     `power2`:      power2,
 396     `pow10`:       power10,
 397     `power10`:     power10,
 398     `sgn`:         sign,
 399     `sign`:        sign,
 400 }
 401 
 402 func evalCall1(name string, expr *ast.CallExpr) (*big.Rat, error) {
 403     x, err := eval(expr.Args[0])
 404     if err != nil {
 405         return nil, err
 406     }
 407 
 408     fn, ok := funcs1[name]
 409     if !ok {
 410         return nil, errors.New(`function '` + name + `' not available`)
 411     }
 412 
 413     return fn(x)
 414 }
 415 
 416 var funcs2 = map[string]func(*big.Rat, *big.Rat) (*big.Rat, error){
 417     `c`:            combinations,
 418     `com`:          combinations,
 419     `comb`:         combinations,
 420     `combinations`: combinations,
 421     `choose`:       combinations,
 422     `gcd`:          gcd,
 423     `gcf`:          gcd,
 424     `lcm`:          lcm,
 425     `p`:            permutations,
 426     `per`:          permutations,
 427     `perm`:         permutations,
 428     `permutations`: permutations,
 429     `pow`:          power,
 430     `power`:        power,
 431     `rem`:          remainder,
 432     `remainder`:    remainder,
 433 }
 434 
 435 func evalCall2(name string, expr *ast.CallExpr) (*big.Rat, error) {
 436     x, err := eval(expr.Args[0])
 437     if err != nil {
 438         return nil, err
 439     }
 440 
 441     y, err := eval(expr.Args[1])
 442     if err != nil {
 443         return nil, err
 444     }
 445 
 446     fn, ok := funcs2[name]
 447     if !ok {
 448         return nil, errors.New(`function '` + name + `' not available`)
 449     }
 450 
 451     return fn(x, y)
 452 }
 453 
 454 var funcs3 = map[string]func(*big.Rat, *big.Rat, *big.Rat) (*big.Rat, error){
 455     `db`:     dbinom,
 456     `dbin`:   dbinom,
 457     `dbinom`: dbinom,
 458 }
 459 
 460 func evalCall3(name string, expr *ast.CallExpr) (*big.Rat, error) {
 461     x, err := eval(expr.Args[0])
 462     if err != nil {
 463         return nil, err
 464     }
 465 
 466     y, err := eval(expr.Args[1])
 467     if err != nil {
 468         return nil, err
 469     }
 470 
 471     z, err := eval(expr.Args[2])
 472     if err != nil {
 473         return nil, err
 474     }
 475 
 476     fn, ok := funcs3[name]
 477     if !ok {
 478         return nil, errors.New(`function '` + name + `' not available`)
 479     }
 480 
 481     return fn(x, y, z)
 482 }
 483 
 484 var varFuncs = map[string]func(...*big.Rat) (*big.Rat, error){
 485     `avg`:     avgNum,
 486     `horner`:  polyval,
 487     `max`:     maxNum,
 488     `mean`:    avgNum,
 489     `min`:     minNum,
 490     `polyval`: polyval,
 491     `sum`:     sumNum,
 492 }
 493 
 494 func evalVarCall(name string, expr *ast.CallExpr) (*big.Rat, error) {
 495     fn, ok := varFuncs[name]
 496     if !ok {
 497         return nil, errors.New(`function '` + name + `' not available`)
 498     }
 499 
 500     inputs := make([]*big.Rat, 0, len(expr.Args))
 501     for _, a := range expr.Args {
 502         v, err := eval(a)
 503         if err != nil {
 504             return nil, err
 505         }
 506         inputs = append(inputs, v)
 507     }
 508 
 509     return fn(inputs...)
 510 }
 511 
 512 func abs(n *big.Rat) (*big.Rat, error) {
 513     n = n.Abs(n)
 514     return n, nil
 515 }
 516 
 517 func avgNum(values ...*big.Rat) (*big.Rat, error) {
 518     if len(values) == 0 {
 519         return nil, errors.New(`mean: no numbers given`)
 520     }
 521 
 522     res := big.NewRat(0, 1)
 523     for _, v := range values {
 524         res = res.Add(res, v)
 525     }
 526     res = res.Quo(res, big.NewRat(int64(len(values)), 1))
 527     return res, nil
 528 }
 529 
 530 func bits(n *big.Rat) (*big.Rat, error) {
 531     if !n.IsInt() {
 532         return nil, errors.New(`function 'bits' only works with integers`)
 533     }
 534 
 535     bits := big.NewRat(0, 1)
 536     bits.SetInt64(int64(n.Num().BitLen()))
 537     return bits, nil
 538 }
 539 
 540 func ceiling(n *big.Rat) (*big.Rat, error) {
 541     if n.IsInt() {
 542         return n, nil
 543     }
 544 
 545     v := big.NewInt(0)
 546     v = v.Quo(n.Num(), n.Denom())
 547     if n.Sign() >= 0 {
 548         v = v.Add(v, big.NewInt(1))
 549     }
 550     n = n.SetInt(v)
 551     return n, nil
 552 }
 553 
 554 func combinations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
 555     if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
 556         const msg = `combinations are defined only for non-negative integers`
 557         return nil, errors.New(msg)
 558     }
 559 
 560     v, err := permutations(n, k)
 561     if err != nil {
 562         return v, err
 563     }
 564 
 565     f, err := factorial(k)
 566     if err != nil {
 567         return nil, err
 568     }
 569 
 570     if f.Sign() <= 0 {
 571         return nil, errors.New(`combinations: factorial isn't positive`)
 572     }
 573     return v.Quo(v, f), nil
 574 }
 575 
 576 func dbinom(x *big.Rat, n *big.Rat, p *big.Rat) (*big.Rat, error) {
 577     a, err := combinations(copyFrac(n), copyFrac(x))
 578     if err != nil {
 579         return nil, err
 580     }
 581 
 582     b, err := power(copyFrac(p), copyFrac(x))
 583     if err != nil {
 584         return nil, err
 585     }
 586 
 587     // c = (1 - p) ** (n - x)
 588     y := big.NewRat(1, 1)
 589     y = y.Sub(y, p)
 590     z := copyFrac(n)
 591     z = z.Sub(z, x)
 592     c, err := power(y, z)
 593     if err != nil {
 594         return nil, err
 595     }
 596 
 597     // return combinations(n, x) * (p ** x) * ((1 - p) ** (n - x))
 598     d := big.NewRat(0, 1)
 599     d = d.Add(d, a)
 600     d = d.Mul(d, b)
 601     d = d.Mul(d, c)
 602     return d, nil
 603 }
 604 
 605 func denominator(n *big.Rat) (*big.Rat, error) {
 606     return big.NewRat(0, 1).SetFrac(n.Denom(), big.NewInt(1)), nil
 607 }
 608 
 609 func digits(n *big.Rat) (*big.Rat, error) {
 610     if !n.IsInt() {
 611         return nil, errors.New(`function 'digits' only works with integers`)
 612     }
 613 
 614     digits := big.NewRat(0, 1)
 615     digits.SetInt64(int64(len(n.Num().String())))
 616     return digits, nil
 617 }
 618 
 619 func factorial(n *big.Rat) (*big.Rat, error) {
 620     sign := n.Sign()
 621     if sign < 0 {
 622         return nil, errors.New(`factorials aren't defined for negatives`)
 623     }
 624     if sign == 0 {
 625         return big.NewRat(1, 1), nil
 626     }
 627 
 628     f := big.NewRat(1, 1)
 629     for one := big.NewRat(1, 1); n.Sign() > 0; n = n.Sub(n, one) {
 630         f = f.Mul(f, n)
 631     }
 632     return f, nil
 633 }
 634 
 635 func floor(n *big.Rat) (*big.Rat, error) {
 636     if n.IsInt() {
 637         return n, nil
 638     }
 639 
 640     v := big.NewInt(0)
 641     v = v.Quo(n.Num(), n.Denom())
 642     if n.Sign() < 0 {
 643         v = v.Sub(v, big.NewInt(1))
 644     }
 645     n = n.SetInt(v)
 646     return n, nil
 647 }
 648 
 649 func gcd(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 650     if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
 651         const msg = `gcd are defined only for positive integers`
 652         return nil, errors.New(msg)
 653     }
 654 
 655     gcd := big.NewRat(0, 1)
 656     gcd = gcd.Add(gcd, x)
 657     gcd = gcd.Mul(gcd, y)
 658 
 659     lcm, err := lcm(x, y)
 660     if err != nil {
 661         return nil, err
 662     }
 663     if lcm.Sign() <= 0 {
 664         return nil, errors.New(`gcd: lcm isn't positive`)
 665     }
 666 
 667     gcd = gcd.Quo(gcd, lcm)
 668     return gcd, nil
 669 }
 670 
 671 func isPrime(n *big.Rat) (*big.Rat, error) {
 672     if !n.IsInt() {
 673         return nil, errors.New(`function 'isprime' only works with integers`)
 674     }
 675 
 676     if n.Sign() <= 0 {
 677         return big.NewRat(0, 1), nil
 678     }
 679 
 680     v := n.Num()
 681     if v.IsInt64() {
 682         n := v.Int64()
 683         if n == 2 {
 684             return big.NewRat(1, 1), nil
 685         }
 686         if n < 2 || n%2 == 0 {
 687             return big.NewRat(0, 1), nil
 688         }
 689     }
 690 
 691     two := big.NewInt(2)
 692     max := big.NewInt(1).Sqrt(v)
 693     mod := big.NewInt(0)
 694     for i := big.NewInt(3); i.Cmp(max) <= 0; i = i.Add(i, two) {
 695         mod = mod.Rem(v, i)
 696         if mod.Sign() == 0 {
 697             return big.NewRat(0, 1), nil
 698         }
 699     }
 700     return big.NewRat(1, 1), nil
 701 }
 702 
 703 func lcm(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 704     if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
 705         const msg = `lcm is defined only for positive integers`
 706         return nil, errors.New(msg)
 707     }
 708 
 709     // a = min(x, y)
 710     // b = max(x, y)
 711     var a, b *big.Int
 712     if x.Cmp(y) < 0 {
 713         a = x.Num()
 714         b = y.Num()
 715     } else {
 716         a = y.Num()
 717         b = x.Num()
 718     }
 719 
 720     // c = b
 721     c := big.NewInt(0)
 722     c = c.Add(c, b)
 723 
 724     // while (c % a > 0) c += b
 725     for r := big.NewInt(1); r.Sign() > 0; r = r.Rem(c, a) {
 726         c = c.Add(c, b)
 727     }
 728 
 729     // return c
 730     return big.NewRat(0, 1).SetFrac(c, big.NewInt(1)), nil
 731 }
 732 
 733 func maxNum(values ...*big.Rat) (*big.Rat, error) {
 734     if len(values) == 0 {
 735         return nil, errors.New(`max: no numbers given`)
 736     }
 737 
 738     var max *big.Rat
 739     for i, v := range values {
 740         if i == 0 || max.Cmp(v) < 0 {
 741             max = v
 742         }
 743     }
 744     return max, nil
 745 }
 746 
 747 func minNum(values ...*big.Rat) (*big.Rat, error) {
 748     if len(values) == 0 {
 749         return nil, errors.New(`min: no numbers given`)
 750     }
 751 
 752     var min *big.Rat
 753     for i, v := range values {
 754         if i == 0 || min.Cmp(v) < 0 {
 755             min = v
 756         }
 757     }
 758     return min, nil
 759 }
 760 
 761 func numerator(n *big.Rat) (*big.Rat, error) {
 762     return big.NewRat(0, 1).SetFrac(n.Num(), big.NewInt(1)), nil
 763 }
 764 
 765 func permutations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
 766     if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
 767         const msg = `permutations are defined only for non-negative integers`
 768         return nil, errors.New(msg)
 769     }
 770 
 771     one := big.NewRat(1, 1)
 772     perm := big.NewRat(1, 1)
 773     // end = n - k + 1
 774     end := big.NewRat(1, 1).Set(n)
 775     end = end.Sub(end, k)
 776     end = end.Add(end, one)
 777 
 778     for v := big.NewRat(1, 1).Set(n); v.Cmp(end) >= 0; v = v.Sub(v, one) {
 779         perm = perm.Mul(perm, v)
 780     }
 781     return perm, nil
 782 }
 783 
 784 // polyval evaluates a polynomial using Horner's algorithm: the first number is
 785 // the x value to evaulate the polynomial with, followed by all the polynomial
 786 // coefficients in textbook order, from the highest power down to the final
 787 // constant
 788 func polyval(values ...*big.Rat) (*big.Rat, error) {
 789     if len(values) == 0 {
 790         // return big.NewRat(0, 1), nil
 791         return nil, errors.New(`polyval: no numbers given`)
 792     }
 793 
 794     x0 := values[0]
 795     values = values[1:]
 796 
 797     x := big.NewRat(1, 1)
 798     y := big.NewRat(0, 1)
 799     prod := big.NewRat(0, 1)
 800 
 801     for i := len(values) - 1; i >= 0; i-- {
 802         prod = prod.Mul(values[i], x)
 803         y = y.Add(y, prod)
 804         x = x.Mul(x, x0)
 805     }
 806 
 807     return y, nil
 808 }
 809 
 810 func power(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 811     // if !y.IsInt() {
 812     //  return nil, errors.New(`only integer exponents are supported`)
 813     // }
 814 
 815     if !y.IsInt() {
 816         a, _ := x.Float64()
 817         b, _ := y.Float64()
 818         c := math.Pow(a, b)
 819         if math.IsNaN(c) || math.IsInf(c, 0) {
 820             return nil, errors.New(`can't calculate/approximate power given`)
 821         }
 822         z := big.NewRat(0, 1)
 823         z = z.SetFloat64(c)
 824         return z, nil
 825     }
 826 
 827     if x.Sign() == 0 && y.Sign() == 0 {
 828         return nil, errors.New(`zero to the zero power isn't defined`)
 829     }
 830 
 831     if x.Sign() == 0 {
 832         return big.NewRat(0, 1), nil
 833     }
 834     if y.Sign() == 0 {
 835         return big.NewRat(1, 1), nil
 836     }
 837 
 838     return powFractionInPlace(x, y.Num())
 839 }
 840 
 841 // powFractionInPlace calculates values in place: since bignums are pointers
 842 // to their representations, this means the original values will change
 843 func powFractionInPlace(x *big.Rat, y *big.Int) (*big.Rat, error) {
 844     xsign := x.Sign()
 845     ysign := y.Sign()
 846 
 847     // 0 ** 0 is undefined
 848     if xsign == 0 && ysign == 0 {
 849         const msg = `0 to the 0 doesn't make sense`
 850         return nil, errors.New(msg)
 851     }
 852 
 853     // otherwise x ** 0 is 1
 854     if ysign == 0 {
 855         return big.NewRat(1, 1), nil
 856     }
 857 
 858     // x ** (y < 0) is like (1/x) ** -y
 859     if ysign < 0 {
 860         inv := big.NewRat(1, 1).Inv(x)
 861         neg := big.NewInt(1).Neg(y)
 862         return powFractionInPlace(inv, neg)
 863     }
 864 
 865     // 0 ** (y > 0) is 0
 866     if xsign == 0 {
 867         return x, nil
 868     }
 869 
 870     // x ** 0 is 0
 871     if ysign == 0 {
 872         return big.NewRat(0, 1), nil
 873     }
 874 
 875     // x ** 1 is x
 876     if y.IsInt64() && y.Int64() == 1 {
 877         return x, nil
 878     }
 879 
 880     return _powFractionRec(x, y), nil
 881 }
 882 
 883 func _powFractionRec(x *big.Rat, y *big.Int) *big.Rat {
 884     switch y.Sign() {
 885     case -1:
 886         return big.NewRat(0, 1)
 887     case 0:
 888         return big.NewRat(1, 1)
 889     case 1:
 890         if y.IsInt64() && y.Int64() == 1 {
 891             return x
 892         }
 893     }
 894 
 895     yhalf := big.NewInt(0)
 896     oddrem := big.NewInt(0)
 897     yhalf.QuoRem(y, big.NewInt(2), oddrem)
 898 
 899     if oddrem.Sign() == 0 {
 900         xsquare := big.NewRat(0, 1)
 901         return _powFractionRec(xsquare.Mul(x, x), yhalf)
 902     }
 903     prevpow := _powFractionRec(x, y.Sub(y, big.NewInt(1)))
 904     return prevpow.Mul(prevpow, x)
 905 }
 906 
 907 func power2(x *big.Rat) (*big.Rat, error) {
 908     return power(big.NewRat(2, 1), x)
 909 }
 910 
 911 func power10(x *big.Rat) (*big.Rat, error) {
 912     return power(big.NewRat(10, 1), x)
 913 }
 914 
 915 func remainder(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 916     if !x.IsInt() || !y.IsInt() {
 917         return nil, errors.New(`remainder only works with 2 integers`)
 918     }
 919 
 920     if y.Sign() == 0 {
 921         return nil, errors.New(`can't divide by 0`)
 922     }
 923 
 924     a := x.Num()
 925     b := y.Num()
 926     c := big.NewInt(0)
 927     c = c.Rem(a, b)
 928     rem := big.NewRat(0, 1)
 929     rem = rem.SetInt(c)
 930     return rem, nil
 931 }
 932 
 933 func sign(n *big.Rat) (*big.Rat, error) {
 934     sign := n.Sign()
 935     if sign > 0 {
 936         n = big.NewRat(1, 1)
 937     } else if sign < 0 {
 938         n = big.NewRat(-1, 1)
 939     } else {
 940         n = big.NewRat(0, 1)
 941     }
 942     return n, nil
 943 }
 944 
 945 func sumNum(values ...*big.Rat) (*big.Rat, error) {
 946     sum := big.NewRat(0, 1)
 947     for _, v := range values {
 948         sum = sum.Add(sum, v)
 949     }
 950     return sum, nil
 951 }
     File: ./cat/cat.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package cat
  26 
  27 import (
  28     "io"
  29     "os"
  30 )
  31 
  32 const info = `
  33 cat [options...] [files...]
  34 
  35 Concatenate files to the standard output.
  36 
  37 Options
  38 
  39     --help    show this help message
  40 `
  41 
  42 func Main() {
  43     args := os.Args[1:]
  44     for len(args) > 0 {
  45         switch args[0] {
  46         case `--help`:
  47             os.Stderr.WriteString(info[1:])
  48             return
  49         }
  50 
  51         break
  52     }
  53 
  54     if len(args) > 0 && args[0] == `--` {
  55         args = args[1:]
  56     }
  57 
  58     for _, path := range args {
  59         if err := handleFile(os.Stdout, path); err != nil {
  60             if err == io.EOF {
  61                 break
  62             }
  63 
  64             os.Stderr.WriteString(`cat: `)
  65             os.Stderr.WriteString(err.Error())
  66             os.Stderr.WriteString("\n")
  67             os.Exit(1)
  68             return
  69         }
  70     }
  71 
  72     if len(args) == 0 {
  73         cat(os.Stdout, os.Stdin)
  74     }
  75 }
  76 
  77 func handleFile(w io.Writer, path string) error {
  78     f, err := os.Open(path)
  79     if err != nil {
  80         return err
  81     }
  82     defer f.Close()
  83     return cat(w, f)
  84 }
  85 
  86 func cat(w io.Writer, r io.Reader) error {
  87     var buf [32 * 1024]byte
  88 
  89     for {
  90         got, err := r.Read(buf[:])
  91         if err == io.EOF {
  92             if got > 0 {
  93                 w.Write(buf[:got])
  94             }
  95             break
  96         }
  97 
  98         if err != nil {
  99             return err
 100         }
 101 
 102         if _, err := w.Write(buf[:got]); err != nil {
 103             return io.EOF
 104         }
 105     }
 106 
 107     return nil
 108 }
     File: ./catl/catl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package catl
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 catl [options...] [file...]
  37 
  38 
  39 Unlike "cat", conCATenate Lines ensures lines across inputs are never joined
  40 by accident, when an input's last line doesn't end with a line-feed.
  41 
  42 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  43 feeds. Leading BOM (byte-order marks) on first lines are also ignored.
  44 
  45 All (optional) leading options start with either single or double-dash:
  46 
  47     -h, -help    show this help message
  48     -0, -null    turn null-byte-delimited chunks into proper lines
  49 `
  50 
  51 type config struct {
  52     null      bool
  53     liveLines bool
  54 }
  55 
  56 func Main() {
  57     var cfg config
  58     cfg.liveLines = true
  59     args := os.Args[1:]
  60 
  61     for len(args) > 0 {
  62         switch args[0] {
  63         case `-0`, `--0`, `-null`, `--null`:
  64             cfg.null = true
  65             args = args[1:]
  66             continue
  67 
  68         case `-b`, `--b`, `-buffered`, `--buffered`:
  69             cfg.liveLines = false
  70             args = args[1:]
  71             continue
  72 
  73         case `-h`, `--h`, `-help`, `--help`:
  74             os.Stdout.WriteString(info[1:])
  75             return
  76         }
  77 
  78         break
  79     }
  80 
  81     if len(args) > 0 && args[0] == `--` {
  82         args = args[1:]
  83     }
  84 
  85     if cfg.liveLines {
  86         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  87             cfg.liveLines = false
  88         }
  89     }
  90 
  91     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  92         os.Stderr.WriteString(`catl: `)
  93         os.Stderr.WriteString(err.Error())
  94         os.Stderr.WriteString("\n")
  95         os.Exit(1)
  96         return
  97     }
  98 }
  99 
 100 func run(w io.Writer, args []string, cfg config) error {
 101     bw := bufio.NewWriterSize(w, 32*1024)
 102     defer bw.Flush()
 103 
 104     dashes := 0
 105     for _, name := range args {
 106         if name == `-` {
 107             dashes++
 108         }
 109         if dashes > 1 {
 110             break
 111         }
 112     }
 113 
 114     if len(args) == 0 {
 115         return catl(bw, os.Stdin, cfg)
 116     }
 117 
 118     var stdin []byte
 119     gotStdin := false
 120 
 121     for _, name := range args {
 122         if name == `-` {
 123             if dashes == 1 {
 124                 if err := catl(bw, os.Stdin, cfg); err != nil {
 125                     return err
 126                 }
 127                 continue
 128             }
 129 
 130             if !gotStdin {
 131                 data, err := io.ReadAll(os.Stdin)
 132                 if err != nil {
 133                     return err
 134                 }
 135                 stdin = data
 136                 gotStdin = true
 137             }
 138 
 139             bw.Write(stdin)
 140             if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
 141                 bw.WriteByte('\n')
 142             }
 143 
 144             if cfg.liveLines && bw.Flush() != nil {
 145                 return io.EOF
 146             }
 147 
 148             continue
 149         }
 150 
 151         if err := handleFile(bw, name, cfg); err != nil {
 152             return err
 153         }
 154     }
 155     return nil
 156 }
 157 
 158 func handleFile(w *bufio.Writer, name string, cfg config) error {
 159     if name == `` || name == `-` {
 160         return catl(w, os.Stdin, cfg)
 161     }
 162 
 163     f, err := os.Open(name)
 164     if err != nil {
 165         return errors.New(`can't read from file named "` + name + `"`)
 166     }
 167     defer f.Close()
 168 
 169     return catl(w, f, cfg)
 170 }
 171 
 172 func catl(w *bufio.Writer, r io.Reader, cfg config) error {
 173     if !cfg.liveLines {
 174         return catlFast(w, r, cfg.null)
 175     }
 176 
 177     const gb = 1024 * 1024 * 1024
 178     sc := bufio.NewScanner(r)
 179     sc.Buffer(nil, 8*gb)
 180     if cfg.null {
 181         sc.Split(splitNull)
 182     }
 183 
 184     for i := 0; sc.Scan(); i++ {
 185         s := sc.Bytes()
 186         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 187             s = s[3:]
 188         }
 189 
 190         w.Write(s)
 191         if w.WriteByte('\n') != nil {
 192             return io.EOF
 193         }
 194 
 195         if w.Flush() != nil {
 196             return io.EOF
 197         }
 198     }
 199 
 200     return sc.Err()
 201 }
 202 
 203 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
 204     var buf [32 * 1024]byte
 205     var last byte = '\n'
 206 
 207     for i := 0; true; i++ {
 208         n, err := r.Read(buf[:])
 209         if n > 0 && err == io.EOF {
 210             err = nil
 211         }
 212         if err == io.EOF {
 213             if last != '\n' {
 214                 w.WriteByte('\n')
 215             }
 216             return nil
 217         }
 218 
 219         if err != nil {
 220             return err
 221         }
 222 
 223         chunk := buf[:n]
 224         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 225             chunk = chunk[3:]
 226         }
 227 
 228         // change nulls into line-feeds to handle null-terminated lines
 229         if null {
 230             for i, b := range chunk {
 231                 if b == 0 {
 232                     chunk[i] = '\n'
 233                 }
 234             }
 235         }
 236 
 237         if len(chunk) >= 1 {
 238             if _, err := w.Write(chunk); err != nil {
 239                 return io.EOF
 240             }
 241             last = chunk[len(chunk)-1]
 242         }
 243     }
 244 
 245     return nil
 246 }
 247 
 248 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
 249 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
 250     // handle leading null-terminated line, if found in the current chunk
 251     if i := bytes.IndexByte(data, 0); i >= 0 {
 252         return i + 1, data[:i], nil
 253     }
 254 
 255     // request more data, in case there's a null coming up later
 256     if !atEOF {
 257         return 0, nil, nil
 258     }
 259 
 260     // handle non-empty non-terminated last chunk
 261     if len(data) > 0 {
 262         return len(data), data, bufio.ErrFinalToken
 263     }
 264 
 265     // handle empty non-terminated last chunk
 266     return 0, nil, bufio.ErrFinalToken
 267 }
     File: ./cbl/cbl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package cbl
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "io/fs"
  33     "os"
  34     "path/filepath"
  35     "runtime"
  36     "strconv"
  37     "sync"
  38 )
  39 
  40 const info = `
  41 cbl [options...] [files/folders...]
  42 
  43 
  44 Count Bytes and Lines finds out these simple byte-related stats: the latter
  45 stat (line-count) assumes files are text-based, and thus isn't meaningful
  46 for general binary data.
  47 
  48 The output is TSV (tab-separated values) lines, where the first line has
  49 all the column names.
  50 
  51 When no filepaths are given, the standard input is used by default. All
  52 folder names given expand recursively into all filenames in them. A mix
  53 of files/folders is supported for convenience.
  54 
  55 The only option available is to show this help message, using any of
  56 "-h", "--h", "-help", or "--help", without the quotes.
  57 `
  58 
  59 // event has what the output-reporting task needs to show the results of a
  60 // task which has just completed, perhaps unsuccessfully
  61 type event struct {
  62     // Index points to the task's entry in the results-slice
  63     Index int
  64 
  65     // Stats has all the byte-related stats
  66     Stats stats
  67 
  68     // Err is the completed task's error, or lack of
  69     Err error
  70 }
  71 
  72 func Main() {
  73     args := os.Args[1:]
  74 
  75     if len(args) > 0 {
  76         switch args[0] {
  77         case `-h`, `--h`, `-help`, `--help`:
  78             os.Stdout.WriteString(info[1:])
  79             return
  80 
  81         case `--`:
  82             args = args[1:]
  83         }
  84     }
  85 
  86     // show first/heading line right away, to let users know things are
  87     // happening
  88     os.Stdout.WriteString("name\tbytes\tlines")
  89 
  90     // assume an error means later stages/apps in a pipe had enough input and
  91     // quit successfully, so quit successfully too
  92     _, err := os.Stdout.WriteString("\n")
  93     if err != nil {
  94         return
  95     }
  96 
  97     // names has all filepaths given, ignoring repetitions
  98     names, ok := findAllFiles(args)
  99     if !ok {
 100         os.Exit(1)
 101         return
 102     }
 103     if len(names) == 0 {
 104         names = []string{`-`}
 105     }
 106 
 107     events := make(chan event)
 108     go handleInputs(names, events)
 109     if !handleOutput(os.Stdout, len(names), events) {
 110         os.Exit(1)
 111         return
 112     }
 113 }
 114 
 115 // handleInputs launches all the tasks which do the actual work, limiting how
 116 // many inputs are being worked on at the same time
 117 func handleInputs(names []string, events chan<- event) {
 118     defer close(events) // allow the output-reporter task to end
 119 
 120     var tasks sync.WaitGroup
 121     // the number of tasks is always known in advance
 122     tasks.Add(len(names))
 123 
 124     // permissions is buffered to limit concurrency to the core-count
 125     permissions := make(chan struct{}, runtime.NumCPU())
 126     defer close(permissions)
 127 
 128     for i, name := range names {
 129         // wait until some concurrency-room is available, before proceeding
 130         permissions <- struct{}{}
 131 
 132         go func(i int, name string) {
 133             defer tasks.Done()
 134 
 135             res, err := handleInput(name)
 136             <-permissions
 137             events <- event{Index: i, Stats: res, Err: err}
 138         }(i, name)
 139     }
 140 
 141     // wait for all inputs, before closing the `events` channel, which in turn
 142     // would quit the whole app right away
 143     tasks.Wait()
 144 }
 145 
 146 // handleInput handles each work-item for func handleInputs
 147 func handleInput(path string) (stats, error) {
 148     var res stats
 149     res.name = path
 150 
 151     if path == `-` {
 152         err := res.updateStats(os.Stdin)
 153         return res, err
 154     }
 155 
 156     f, err := os.Open(path)
 157     if err != nil {
 158         res.result = resultError
 159         // on windows, file-not-found error messages may mention `CreateFile`,
 160         // even when trying to open files in read-only mode
 161         return res, errors.New(`can't open file named ` + path)
 162     }
 163     defer f.Close()
 164 
 165     err = res.updateStats(f)
 166     return res, err
 167 }
 168 
 169 // handleOutput asynchronously updates output as results are known, whether
 170 // it's errors or successful results; returns whether it succeeded, which
 171 // means no errors happened
 172 func handleOutput(w io.Writer, inputs int, events <-chan event) (ok bool) {
 173     bw := bufio.NewWriterSize(w, 32*1024)
 174     defer bw.Flush()
 175 
 176     ok = true
 177     results := make([]stats, inputs)
 178 
 179     // keep track of which tasks are over, so that on each event all leading
 180     // results which are ready are shown: all of this ensures prompt output
 181     // updates as soon as results come in, while keeping the original order
 182     // of the names/filepaths given
 183     resultsLeft := results
 184 
 185     for v := range events {
 186         results[v.Index] = v.Stats
 187         if v.Err != nil {
 188             ok = false
 189             bw.Flush()
 190             showError(v.Err)
 191 
 192             // stay in the current loop, in case this failure was keeping
 193             // previous successes from showing up
 194         }
 195 
 196         for len(resultsLeft) > 0 {
 197             if resultsLeft[0].result == resultPending {
 198                 break
 199             }
 200 
 201             if err := showResult(bw, resultsLeft[0]); err != nil {
 202                 // assume later stages/apps in a pipe had enough input
 203                 return ok
 204             }
 205             resultsLeft = resultsLeft[1:]
 206         }
 207 
 208         // show leading results immediately, if any
 209         bw.Flush()
 210     }
 211 
 212     return ok
 213 }
 214 
 215 func showError(err error) {
 216     os.Stderr.WriteString(`cbl: `)
 217     os.Stderr.WriteString(err.Error())
 218     os.Stderr.WriteString("\n")
 219 }
 220 
 221 // showResult shows a TSV line for results marked as successful, doing nothing
 222 // when given other types of results
 223 func showResult(w *bufio.Writer, s stats) error {
 224     if s.result != resultSuccess {
 225         return nil
 226     }
 227 
 228     var buf [24]byte
 229     w.WriteString(s.name)
 230     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.bytes), 10))
 231     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lines), 10))
 232     return w.WriteByte('\n')
 233 }
 234 
 235 // findAllFiles can be given a mix of file/folder paths, finding all files
 236 // recursively in folders, avoiding duplicates
 237 func findAllFiles(paths []string) (files []string, success bool) {
 238     walk := filepath.WalkDir
 239     got := make(map[string]struct{})
 240     success = true
 241 
 242     for _, path := range paths {
 243         if _, ok := got[path]; ok {
 244             continue
 245         }
 246         got[path] = struct{}{}
 247 
 248         // a dash means standard input
 249         if path == `-` {
 250             files = append(files, path)
 251             continue
 252         }
 253 
 254         info, err := os.Stat(path)
 255         if os.IsNotExist(err) {
 256             // on windows, file-not-found messages may mention `CreateFile`,
 257             // even when trying to open files in read-only mode
 258             err = errors.New(`can't find file/folder named ` + path)
 259         }
 260 
 261         if err != nil {
 262             showError(err)
 263             success = false
 264             continue
 265         }
 266 
 267         if !info.IsDir() {
 268             files = append(files, path)
 269             continue
 270         }
 271 
 272         err = walk(path, func(path string, info fs.DirEntry, err error) error {
 273             path, err = filepath.Abs(path)
 274             if err != nil {
 275                 showError(err)
 276                 success = false
 277                 return err
 278             }
 279 
 280             if _, ok := got[path]; ok {
 281                 if info.IsDir() {
 282                     return fs.SkipDir
 283                 }
 284                 return nil
 285             }
 286             got[path] = struct{}{}
 287 
 288             if err != nil {
 289                 showError(err)
 290                 success = false
 291                 return err
 292             }
 293 
 294             if info.IsDir() {
 295                 return nil
 296             }
 297 
 298             files = append(files, path)
 299             return nil
 300         })
 301 
 302         if err != nil {
 303             showError(err)
 304             success = false
 305         }
 306     }
 307 
 308     return files, success
 309 }
 310 
 311 // counter makes it easy to change the int-size of almost all counters
 312 type counter uint64
 313 
 314 // statResult constrains possible result-states/values in type stats
 315 type statResult int
 316 
 317 const (
 318     // resultPending is the default not-yet-ready result-status
 319     resultPending = statResult(0)
 320 
 321     // resultError means result should show as an error, instead of data
 322     resultError = statResult(1)
 323 
 324     // resultSuccess means a result's stats are ready to show
 325     resultSuccess = statResult(2)
 326 )
 327 
 328 // bomType is the type for the byte-order-mark enumeration
 329 type bomType int
 330 
 331 const (
 332     noBOM      = bomType(0)
 333     utf8BOM    = bomType(1)
 334     utf16leBOM = bomType(2)
 335     utf16beBOM = bomType(3)
 336     utf32leBOM = bomType(4)
 337     utf32beBOM = bomType(5)
 338 )
 339 
 340 // bomLegend has the string-equivalents of the bomType constants
 341 var bomLegend = []string{
 342     ``,
 343     `UTF-8`,
 344     `UTF-16 LE`,
 345     `UTF-16 BE`,
 346     `UTF-32 LE`,
 347     `UTF-32 BE`,
 348 }
 349 
 350 // stats has all the size-stats for some input, as well as a way to
 351 // skip showing results, in case of an error such as `file not found`
 352 type stats struct {
 353     // bytes counts all bytes read
 354     bytes counter
 355 
 356     // lines counts lines, and is 0 only when the byte-count is also 0
 357     lines counter
 358 
 359     // name is the filepath of the file/source these stats are about
 360     name string
 361 
 362     // results keeps track of whether results are valid and/or ready
 363     result statResult
 364 }
 365 
 366 // updateStats does what it says, reading everything from a reader
 367 func (res *stats) updateStats(r io.Reader) error {
 368     err := res.updateUsing(r)
 369     if err == io.EOF {
 370         err = nil
 371     }
 372 
 373     if err == nil {
 374         res.result = resultSuccess
 375     } else {
 376         res.result = resultError
 377     }
 378     return err
 379 }
 380 
 381 // updateUsing helps func updateStats do its job
 382 func (res *stats) updateUsing(r io.Reader) error {
 383     var buf [32 * 1024]byte
 384 
 385     var prev byte
 386 
 387     for {
 388         n, err := r.Read(buf[:])
 389         if n < 1 {
 390             if err == io.EOF {
 391                 if prev != '\n' {
 392                     res.lines++
 393                 }
 394             }
 395             return err
 396         }
 397 
 398         prev = buf[n-1]
 399         res.bytes += counter(n)
 400         res.lines += counter(bytes.Count(buf[:n], []byte{'\n'}))
 401     }
 402 }
     File: ./chopleads/chopleads.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package chopleads
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 chopleads [options...] [files...]
  37 
  38 Ignore the longest common leading parts of all non-empty lines. A typical
  39 use case is when dealing with lines starting with full filenames, where
  40 all files come from the same subfolder, as getting rid of the long common
  41 starts can vastly reduce the clutter.
  42 
  43 The options are, available both in single and double-dash versions
  44 
  45     -h, -help              show this help message
  46 `
  47 
  48 type config struct {
  49     state
  50 
  51     liveLines bool
  52 }
  53 
  54 func (cfg config) maybeFlush(w *bufio.Writer) error {
  55     if cfg.liveLines && w.Flush() != nil {
  56         return io.EOF
  57     }
  58     return nil
  59 }
  60 
  61 type state struct {
  62     // lines remembers all input lines so far, since finding common leads
  63     // may require knowning all input lines, in general
  64     lines []string
  65 
  66     // lead is the current (leading) common-lead among lines seen so far
  67     lead string
  68 
  69     // nothing remembers if there's no common lead for sure
  70     nothing bool
  71 }
  72 
  73 // dump emits all lines kept so far, then forgets about them
  74 func (s *state) dump(w *bufio.Writer) error {
  75     for _, s := range s.lines {
  76         w.WriteString(s)
  77         if w.WriteByte('\n') != nil {
  78             return io.EOF
  79         }
  80     }
  81 
  82     // forgetting the lines may save some memory, with enough lines
  83     s.lines = nil
  84 
  85     return nil
  86 }
  87 
  88 func Main() {
  89     var cfg config
  90     cfg.liveLines = true
  91     args := os.Args[1:]
  92 
  93     for len(args) > 0 {
  94         switch args[0] {
  95         case `-b`, `--b`, `-buffered`, `--buffered`:
  96             cfg.liveLines = false
  97             args = args[1:]
  98             continue
  99 
 100         case `-h`, `--h`, `-help`, `--help`:
 101             os.Stdout.WriteString(info[1:])
 102             return
 103         }
 104 
 105         break
 106     }
 107 
 108     if len(args) > 0 && args[0] == `--` {
 109         args = args[1:]
 110     }
 111 
 112     if cfg.liveLines {
 113         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 114             cfg.liveLines = false
 115         }
 116     }
 117 
 118     if err := run(args, &cfg); err != nil && err != io.EOF {
 119         os.Stderr.WriteString(`chopleads: `)
 120         os.Stderr.WriteString(err.Error())
 121         os.Stderr.WriteString("\n")
 122         os.Exit(1)
 123         return
 124     }
 125 }
 126 
 127 func run(paths []string, cfg *config) error {
 128     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 129     defer bw.Flush()
 130 
 131     for _, p := range paths {
 132         if err := handleFile(bw, p, cfg); err != nil {
 133             return err
 134         }
 135     }
 136 
 137     if len(paths) == 0 {
 138         if err := handleReader(bw, os.Stdin, cfg); err != nil {
 139             return err
 140         }
 141     }
 142 
 143     if cfg.nothing {
 144         return nil
 145     }
 146 
 147     skip := len(cfg.lead)
 148     // with only 1 line of input, avoid emitting a single empty line for sure
 149     if len(cfg.lines) == 1 {
 150         skip = 0
 151     }
 152 
 153     for _, s := range cfg.lines {
 154         if len(s) > 0 {
 155             bw.WriteString(s[skip:])
 156         }
 157         if bw.WriteByte('\n') != nil {
 158             return io.EOF
 159         }
 160     }
 161     return nil
 162 }
 163 
 164 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 165     f, err := os.Open(path)
 166     if err != nil {
 167         // on windows, file-not-found error messages may mention `CreateFile`,
 168         // even when trying to open files in read-only mode
 169         return errors.New(`can't open file named ` + path)
 170     }
 171     defer f.Close()
 172     return handleReader(w, f, cfg)
 173 }
 174 
 175 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 176     const gb = 1024 * 1024 * 1024
 177     sc := bufio.NewScanner(r)
 178     sc.Buffer(nil, 8*gb)
 179 
 180     for i := 0; sc.Scan(); i++ {
 181         s := sc.Text()
 182         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 183             s = s[3:]
 184         }
 185 
 186         // when no common lead is possible, show lines as they come and as
 187         // they are; also don't bother remembering them, thus saving memory
 188         if cfg.nothing {
 189             w.WriteString(s)
 190             if w.WriteByte('\n') != nil {
 191                 return io.EOF
 192             }
 193 
 194             if err := cfg.maybeFlush(w); err != nil {
 195                 return err
 196             }
 197             continue
 198         }
 199 
 200         cfg.lines = append(cfg.lines, s)
 201 
 202         if len(s) == 0 {
 203             // empty lines don't count for common leads
 204             continue
 205         }
 206 
 207         // the full first non-empty line automatically becomes the lead
 208         if len(cfg.lead) == 0 {
 209             cfg.lead = s
 210         }
 211 
 212         // the common lead can only be as long as the shortest non-empty line
 213         if len(cfg.lead) > len(s) {
 214             cfg.lead = cfg.lead[:len(s)]
 215         }
 216 
 217         // avoid lead-shrink loop in case leading byte is different, which
 218         // by itself implies there's no common lead
 219         if len(s) > 0 && len(cfg.lead) > 0 && s[0] != cfg.lead[0] {
 220             if err := cfg.dump(w); err != nil {
 221                 return err
 222             }
 223             if err := cfg.maybeFlush(w); err != nil {
 224                 return err
 225             }
 226             cfg.nothing = true
 227             continue
 228         }
 229 
 230         // shrink common lead from the end
 231         for len(cfg.lead) > 0 && !strings.HasPrefix(s, cfg.lead) {
 232             cfg.lead = cfg.lead[:len(cfg.lead)-1]
 233             continue
 234         }
 235 
 236         // if previous loop ends with an empty string: there's no common lead
 237         if len(cfg.lead) == 0 {
 238             if err := cfg.dump(w); err != nil {
 239                 return err
 240             }
 241             if err := cfg.maybeFlush(w); err != nil {
 242                 return err
 243             }
 244             cfg.nothing = true
 245             continue
 246         }
 247     }
 248 
 249     return sc.Err()
 250 }
     File: ./clock/clock.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package clock
  26 
  27 import (
  28     "os"
  29     "time"
  30 )
  31 
  32 const (
  33     spaces = `                `
  34 
  35     // clear has enough spaces in it to cover any date-time output
  36     clear = "\r" + spaces + spaces + spaces + "\r"
  37 
  38     dateTime = `Mon Jan 02  15:04`
  39     timeDate = `15:04  Mon Jan 02`
  40 
  41     shortFormat = dateTime
  42     longFormat  = `2006-01-02  ` + shortFormat
  43 )
  44 
  45 const info = `
  46 clock [options...]
  47 
  48 Keep showing the current time, updating every new minute until force-quit.
  49 
  50 All (optional) leading options start with either single or double-dash:
  51 
  52     -f, -full, -l, -long    also show year-month-day
  53     -h, -help               show this help message
  54 `
  55 
  56 func Main() {
  57     args := os.Args[1:]
  58     clockFormat := shortFormat
  59 
  60     if len(args) > 0 {
  61         switch args[0] {
  62         case `-h`, `--h`, `-help`, `--help`:
  63             os.Stdout.WriteString(info[1:])
  64             return
  65 
  66         case `-f`, `--f`, `-full`, `--full`, `-l`, `--l`, `-long`, `--long`:
  67             clockFormat = longFormat
  68             args = args[1:]
  69         }
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     if len(args) > 0 {
  77         os.Stderr.WriteString(info[1:])
  78         os.Exit(1)
  79         return
  80     }
  81 
  82     var buf [128]byte
  83 
  84     for {
  85         now := time.Now()
  86 
  87         s := buf[:0]
  88         s = append(s, clear...)
  89         s = now.AppendFormat(s, clockFormat)
  90         os.Stderr.Write(s)
  91 
  92         time.Sleep(time.Second * time.Duration(60-now.Second()))
  93     }
  94 }
     File: ./coby/coby.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package coby
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "io/fs"
  32     "os"
  33     "path/filepath"
  34     "runtime"
  35     "strconv"
  36     "sync"
  37 )
  38 
  39 const info = `
  40 coby [options...] [files/folders...]
  41 
  42 
  43 COunt BYtes finds out some simple byte-related stats, counting
  44 
  45     - bytes
  46     - lines
  47     - how many lines have trailing spaces (trails)
  48     - how many lines end with a CRLF pair
  49     - all-bits-off (null) bytes
  50     - all-bits-on (full) bytes
  51     - top-bit-on (high) bytes
  52     - which unicode byte-order-mark (bom) sequence the data start with
  53 
  54 Some of these stats (lines, CRLFs, BOMs) only make sense for plain-text
  55 data, and thus may not be meaningful for general binary data.
  56 
  57 The output is TSV (tab-separated values) lines, where the first line has
  58 all the column names.
  59 
  60 When no filepaths are given, the standard input is used by default. All
  61 folder names given expand recursively into all filenames in them. A mix
  62 of files/folders is supported for convenience.
  63 
  64 The only option available is to show this help message, using any of
  65 "-h", "--h", "-help", or "--help", without the quotes.
  66 `
  67 
  68 // header has all the values for the first output line
  69 var header = []string{
  70     `name`,
  71     `bytes`,
  72     `lines`,
  73     `lf`,
  74     `crlf`,
  75     `spaces`,
  76     `tabs`,
  77     `trails`,
  78     `nulls`,
  79     `fulls`,
  80     `highs`,
  81     `bom`,
  82 }
  83 
  84 // event has what the output-reporting task needs to show the results of a
  85 // task which has just completed, perhaps unsuccessfully
  86 type event struct {
  87     // Index points to the task's entry in the results-slice
  88     Index int
  89 
  90     // Stats has all the byte-related stats
  91     Stats stats
  92 
  93     // Err is the completed task's error, or lack of
  94     Err error
  95 }
  96 
  97 func Main() {
  98     args := os.Args[1:]
  99 
 100     if len(args) > 0 {
 101         switch args[0] {
 102         case `-h`, `--h`, `-help`, `--help`:
 103             os.Stdout.WriteString(info[1:])
 104             return
 105 
 106         case `--`:
 107             args = args[1:]
 108         }
 109     }
 110 
 111     // show first/heading line right away, to let users know things are
 112     // happening
 113     for i, s := range header {
 114         if i > 0 {
 115             os.Stdout.WriteString("\t")
 116         }
 117         os.Stdout.WriteString(s)
 118     }
 119     // assume an error means later stages/apps in a pipe had enough input and
 120     // quit successfully, so quit successfully too
 121     _, err := os.Stdout.WriteString("\n")
 122     if err != nil {
 123         return
 124     }
 125 
 126     // names has all filepaths given, ignoring repetitions
 127     names, ok := findAllFiles(args)
 128     if !ok {
 129         os.Exit(1)
 130         return
 131     }
 132     if len(names) == 0 {
 133         names = []string{`-`}
 134     }
 135 
 136     events := make(chan event)
 137     go handleInputs(names, events)
 138     if !handleOutput(os.Stdout, len(names), events) {
 139         os.Exit(1)
 140         return
 141     }
 142 }
 143 
 144 // handleInputs launches all the tasks which do the actual work, limiting how
 145 // many inputs are being worked on at the same time
 146 func handleInputs(names []string, events chan<- event) {
 147     defer close(events) // allow the output-reporter task to end
 148 
 149     var tasks sync.WaitGroup
 150     // the number of tasks is always known in advance
 151     tasks.Add(len(names))
 152 
 153     // permissions is buffered to limit concurrency to the core-count
 154     permissions := make(chan struct{}, runtime.NumCPU())
 155     defer close(permissions)
 156 
 157     for i, name := range names {
 158         // wait until some concurrency-room is available, before proceeding
 159         permissions <- struct{}{}
 160 
 161         go func(i int, name string) {
 162             defer tasks.Done()
 163 
 164             res, err := handleInput(name)
 165             <-permissions
 166             events <- event{Index: i, Stats: res, Err: err}
 167         }(i, name)
 168     }
 169 
 170     // wait for all inputs, before closing the `events` channel, which in turn
 171     // would quit the whole app right away
 172     tasks.Wait()
 173 }
 174 
 175 // handleInput handles each work-item for func handleInputs
 176 func handleInput(path string) (stats, error) {
 177     var res stats
 178     res.name = path
 179 
 180     if path == `-` {
 181         err := res.updateStats(os.Stdin)
 182         return res, err
 183     }
 184 
 185     f, err := os.Open(path)
 186     if err != nil {
 187         res.result = resultError
 188         // on windows, file-not-found error messages may mention `CreateFile`,
 189         // even when trying to open files in read-only mode
 190         return res, errors.New(`can't open file named ` + path)
 191     }
 192     defer f.Close()
 193 
 194     err = res.updateStats(f)
 195     return res, err
 196 }
 197 
 198 // handleOutput asynchronously updates output as results are known, whether
 199 // it's errors or successful results; returns whether it succeeded, which
 200 // means no errors happened
 201 func handleOutput(w io.Writer, inputs int, events <-chan event) (ok bool) {
 202     bw := bufio.NewWriterSize(w, 32*1024)
 203     defer bw.Flush()
 204 
 205     ok = true
 206     results := make([]stats, inputs)
 207 
 208     // keep track of which tasks are over, so that on each event all leading
 209     // results which are ready are shown: all of this ensures prompt output
 210     // updates as soon as results come in, while keeping the original order
 211     // of the names/filepaths given
 212     resultsLeft := results
 213 
 214     for v := range events {
 215         results[v.Index] = v.Stats
 216         if v.Err != nil {
 217             ok = false
 218             bw.Flush()
 219             showError(v.Err)
 220 
 221             // stay in the current loop, in case this failure was keeping
 222             // previous successes from showing up
 223         }
 224 
 225         for len(resultsLeft) > 0 {
 226             if resultsLeft[0].result == resultPending {
 227                 break
 228             }
 229 
 230             if err := showResult(bw, resultsLeft[0]); err != nil {
 231                 // assume later stages/apps in a pipe had enough input
 232                 return ok
 233             }
 234             resultsLeft = resultsLeft[1:]
 235         }
 236 
 237         // show leading results immediately, if any
 238         bw.Flush()
 239     }
 240 
 241     return ok
 242 }
 243 
 244 func showError(err error) {
 245     os.Stderr.WriteString(`coby: `)
 246     os.Stderr.WriteString(err.Error())
 247     os.Stderr.WriteString("\n")
 248 }
 249 
 250 // showResult shows a TSV line for results marked as successful, doing nothing
 251 // when given other types of results
 252 func showResult(w *bufio.Writer, s stats) error {
 253     if s.result != resultSuccess {
 254         return nil
 255     }
 256 
 257     var buf [24]byte
 258     w.WriteString(s.name)
 259     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.bytes), 10))
 260     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lines), 10))
 261     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lf), 10))
 262     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.crlf), 10))
 263     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.spaces), 10))
 264     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.tabs), 10))
 265     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.trailing), 10))
 266     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.nulls), 10))
 267     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.fulls), 10))
 268     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.highs), 10))
 269     w.WriteByte('\t')
 270     w.WriteString(bomLegend[s.bom])
 271     return w.WriteByte('\n')
 272 }
 273 
 274 // findAllFiles can be given a mix of file/folder paths, finding all files
 275 // recursively in folders, avoiding duplicates
 276 func findAllFiles(paths []string) (files []string, success bool) {
 277     walk := filepath.WalkDir
 278     got := make(map[string]struct{})
 279     success = true
 280 
 281     for _, path := range paths {
 282         if _, ok := got[path]; ok {
 283             continue
 284         }
 285         got[path] = struct{}{}
 286 
 287         // a dash means standard input
 288         if path == `-` {
 289             files = append(files, path)
 290             continue
 291         }
 292 
 293         info, err := os.Stat(path)
 294         if os.IsNotExist(err) {
 295             // on windows, file-not-found messages may mention `CreateFile`,
 296             // even when trying to open files in read-only mode
 297             err = errors.New(`can't find file/folder named ` + path)
 298         }
 299 
 300         if err != nil {
 301             showError(err)
 302             success = false
 303             continue
 304         }
 305 
 306         if !info.IsDir() {
 307             files = append(files, path)
 308             continue
 309         }
 310 
 311         err = walk(path, func(path string, info fs.DirEntry, err error) error {
 312             path, err = filepath.Abs(path)
 313             if err != nil {
 314                 showError(err)
 315                 success = false
 316                 return err
 317             }
 318 
 319             if _, ok := got[path]; ok {
 320                 if info.IsDir() {
 321                     return fs.SkipDir
 322                 }
 323                 return nil
 324             }
 325             got[path] = struct{}{}
 326 
 327             if err != nil {
 328                 showError(err)
 329                 success = false
 330                 return err
 331             }
 332 
 333             if info.IsDir() {
 334                 return nil
 335             }
 336 
 337             files = append(files, path)
 338             return nil
 339         })
 340 
 341         if err != nil {
 342             showError(err)
 343             success = false
 344         }
 345     }
 346 
 347     return files, success
 348 }
 349 
 350 // counter makes it easy to change the int-size of almost all counters
 351 type counter uint64
 352 
 353 // statResult constrains possible result-states/values in type stats
 354 type statResult int
 355 
 356 const (
 357     // resultPending is the default not-yet-ready result-status
 358     resultPending = statResult(0)
 359 
 360     // resultError means result should show as an error, instead of data
 361     resultError = statResult(1)
 362 
 363     // resultSuccess means a result's stats are ready to show
 364     resultSuccess = statResult(2)
 365 )
 366 
 367 // bomType is the type for the byte-order-mark enumeration
 368 type bomType int
 369 
 370 const (
 371     noBOM      = bomType(0)
 372     utf8BOM    = bomType(1)
 373     utf16leBOM = bomType(2)
 374     utf16beBOM = bomType(3)
 375     utf32leBOM = bomType(4)
 376     utf32beBOM = bomType(5)
 377 )
 378 
 379 // bomLegend has the string-equivalents of the bomType constants
 380 var bomLegend = []string{
 381     ``,
 382     `UTF-8`,
 383     `UTF-16 LE`,
 384     `UTF-16 BE`,
 385     `UTF-32 LE`,
 386     `UTF-32 BE`,
 387 }
 388 
 389 // stats has all the size-stats for some input, as well as a way to
 390 // skip showing results, in case of an error such as `file not found`
 391 type stats struct {
 392     // bytes counts all bytes read
 393     bytes counter
 394 
 395     // lines counts lines, and is 0 only when the byte-count is also 0
 396     lines counter
 397 
 398     // maxWidth is maximum byte-width of lines, excluding carriage-returns
 399     // and/or line-feeds
 400     maxWidth counter
 401 
 402     // nulls counts all-bits-off bytes
 403     nulls counter
 404 
 405     // fulls counts all-bits-on bytes
 406     fulls counter
 407 
 408     // highs counts bytes with their `top` (highest-order) bit on
 409     highs counter
 410 
 411     // spaces counts ASCII spaces
 412     spaces counter
 413 
 414     // tabs counts ASCII tabs
 415     tabs counter
 416 
 417     // trailing counts lines with trailing spaces in them
 418     trailing counter
 419 
 420     // lf counts ASCII line-feeds as their own byte-values: this means its
 421     // value will always be at least the same as field `crlf`
 422     lf counter
 423 
 424     // crlf counts ASCII CRLF byte-pairs
 425     crlf counter
 426 
 427     // the type of byte-order mark detected
 428     bom bomType
 429 
 430     // name is the filepath of the file/source these stats are about
 431     name string
 432 
 433     // results keeps track of whether results are valid and/or ready
 434     result statResult
 435 }
 436 
 437 // updateStats does what it says, reading everything from a reader
 438 func (res *stats) updateStats(r io.Reader) error {
 439     err := res.updateUsing(r)
 440     if err == io.EOF {
 441         err = nil
 442     }
 443 
 444     if err == nil {
 445         res.result = resultSuccess
 446     } else {
 447         res.result = resultError
 448     }
 449     return err
 450 }
 451 
 452 func checkBOM(data []byte) bomType {
 453     d := data
 454     l := len(data)
 455 
 456     if l >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
 457         return utf8BOM
 458     }
 459     if l >= 4 && d[0] == 0xff && d[1] == 0xfe && d[2] == 0 && d[3] == 0 {
 460         return utf32leBOM
 461     }
 462     if l >= 4 && d[0] == 0 && d[1] == 0 && d[2] == 0xfe && d[3] == 0xff {
 463         return utf32beBOM
 464     }
 465     if l >= 2 && data[0] == 0xff && data[1] == 0xfe {
 466         return utf16leBOM
 467     }
 468     if l >= 2 && data[0] == 0xfe && data[1] == 0xff {
 469         return utf16beBOM
 470     }
 471 
 472     return noBOM
 473 }
 474 
 475 // updateUsing helps func updateStats do its job
 476 func (res *stats) updateUsing(r io.Reader) error {
 477     var buf [32 * 1024]byte
 478     var tallies [256]uint64
 479 
 480     var width counter
 481     var prev1, prev2 byte
 482 
 483     for {
 484         n, err := r.Read(buf[:])
 485         if n < 1 {
 486             res.lines = counter(tallies['\n'])
 487             res.tabs = counter(tallies['\t'])
 488             res.spaces = counter(tallies[' '])
 489             res.lf = counter(tallies['\n'])
 490             res.nulls = counter(tallies[0])
 491             res.fulls = counter(tallies[255])
 492             for i := 128; i < len(tallies); i++ {
 493                 res.highs += counter(tallies[i])
 494             }
 495 
 496             if err == io.EOF {
 497                 return res.handleEnd(width, prev1, prev2)
 498             }
 499             return err
 500         }
 501 
 502         chunk := buf[:n]
 503         if res.bytes == 0 {
 504             res.bom = checkBOM(chunk)
 505         }
 506         res.bytes += counter(n)
 507 
 508         for _, b := range chunk {
 509             // count values without branching, because it's fun
 510             tallies[b]++
 511 
 512             if b != '\n' {
 513                 prev2 = prev1
 514                 prev1 = b
 515                 width++
 516                 continue
 517             }
 518 
 519             // handle line-feeds
 520 
 521             crlf := count(prev1, '\r')
 522             res.crlf += crlf
 523 
 524             // count lines with trailing spaces, whether these end with
 525             // a CRLF byte-pair or just a line-feed byte
 526             if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
 527                 res.trailing++
 528             }
 529 
 530             // exclude any CR from the current line's width-count
 531             width -= crlf
 532             if res.maxWidth < width {
 533                 res.maxWidth = width
 534             }
 535 
 536             prev2 = prev1
 537             prev1 = b
 538             width = 0
 539         }
 540     }
 541 }
 542 
 543 // handleEnd fixes/finalizes stats when input data end; this func is only
 544 // meant to be used by func updateStats, since it takes some of the latter's
 545 // local variables
 546 func (res *stats) handleEnd(width counter, prev1, prev2 byte) error {
 547     if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
 548         res.trailing++
 549     }
 550 
 551     if res.maxWidth < width {
 552         res.maxWidth = width
 553     }
 554 
 555     // avoid reporting 0 lines with a non-0 byte-count: this is unlike the
 556     // standard cmd-line tool `wc`
 557     if res.bytes > 0 && prev1 != '\n' {
 558         res.lines++
 559     }
 560 
 561     return nil
 562 }
 563 
 564 // count checks if 2 bytes are the same, returning either 0 or 1, which can
 565 // be added directly/branchlessly to totals
 566 func count(x, y byte) counter {
 567     var c counter
 568     if x == y {
 569         c = 1
 570     } else {
 571         c = 0
 572     }
 573     return c
 574 }
     File: ./colorplus/datatables.go
   1 package colorplus
   2 
   3 // I'm using data straight from the original implementation of Viridis
   4 // by Nathaniel Smith & Stefan van der Walt:
   5 // https://github.com/BIDS/colormap/blob/master/option_d.py
   6 var viridisData = [...]float64{
   7     0.26700401, 0.00487433, 0.32941519,
   8     0.26851048, 0.00960483, 0.33542652,
   9     0.26994384, 0.01462494, 0.34137895,
  10     0.27130489, 0.01994186, 0.34726862,
  11     0.27259384, 0.02556309, 0.35309303,
  12     0.27380934, 0.03149748, 0.35885256,
  13     0.27495242, 0.03775181, 0.36454323,
  14     0.27602238, 0.04416723, 0.37016418,
  15     0.2770184, 0.05034437, 0.37571452,
  16     0.27794143, 0.05632444, 0.38119074,
  17     0.27879067, 0.06214536, 0.38659204,
  18     0.2795655, 0.06783587, 0.39191723,
  19     0.28026658, 0.07341724, 0.39716349,
  20     0.28089358, 0.07890703, 0.40232944,
  21     0.28144581, 0.0843197, 0.40741404,
  22     0.28192358, 0.08966622, 0.41241521,
  23     0.28232739, 0.09495545, 0.41733086,
  24     0.28265633, 0.10019576, 0.42216032,
  25     0.28291049, 0.10539345, 0.42690202,
  26     0.28309095, 0.11055307, 0.43155375,
  27     0.28319704, 0.11567966, 0.43611482,
  28     0.28322882, 0.12077701, 0.44058404,
  29     0.28318684, 0.12584799, 0.44496,
  30     0.283072, 0.13089477, 0.44924127,
  31     0.28288389, 0.13592005, 0.45342734,
  32     0.28262297, 0.14092556, 0.45751726,
  33     0.28229037, 0.14591233, 0.46150995,
  34     0.28188676, 0.15088147, 0.46540474,
  35     0.28141228, 0.15583425, 0.46920128,
  36     0.28086773, 0.16077132, 0.47289909,
  37     0.28025468, 0.16569272, 0.47649762,
  38     0.27957399, 0.17059884, 0.47999675,
  39     0.27882618, 0.1754902, 0.48339654,
  40     0.27801236, 0.18036684, 0.48669702,
  41     0.27713437, 0.18522836, 0.48989831,
  42     0.27619376, 0.19007447, 0.49300074,
  43     0.27519116, 0.1949054, 0.49600488,
  44     0.27412802, 0.19972086, 0.49891131,
  45     0.27300596, 0.20452049, 0.50172076,
  46     0.27182812, 0.20930306, 0.50443413,
  47     0.27059473, 0.21406899, 0.50705243,
  48     0.26930756, 0.21881782, 0.50957678,
  49     0.26796846, 0.22354911, 0.5120084,
  50     0.26657984, 0.2282621, 0.5143487,
  51     0.2651445, 0.23295593, 0.5165993,
  52     0.2636632, 0.23763078, 0.51876163,
  53     0.26213801, 0.24228619, 0.52083736,
  54     0.26057103, 0.2469217, 0.52282822,
  55     0.25896451, 0.25153685, 0.52473609,
  56     0.25732244, 0.2561304, 0.52656332,
  57     0.25564519, 0.26070284, 0.52831152,
  58     0.25393498, 0.26525384, 0.52998273,
  59     0.25219404, 0.26978306, 0.53157905,
  60     0.25042462, 0.27429024, 0.53310261,
  61     0.24862899, 0.27877509, 0.53455561,
  62     0.2468114, 0.28323662, 0.53594093,
  63     0.24497208, 0.28767547, 0.53726018,
  64     0.24311324, 0.29209154, 0.53851561,
  65     0.24123708, 0.29648471, 0.53970946,
  66     0.23934575, 0.30085494, 0.54084398,
  67     0.23744138, 0.30520222, 0.5419214,
  68     0.23552606, 0.30952657, 0.54294396,
  69     0.23360277, 0.31382773, 0.54391424,
  70     0.2316735, 0.3181058, 0.54483444,
  71     0.22973926, 0.32236127, 0.54570633,
  72     0.22780192, 0.32659432, 0.546532,
  73     0.2258633, 0.33080515, 0.54731353,
  74     0.22392515, 0.334994, 0.54805291,
  75     0.22198915, 0.33916114, 0.54875211,
  76     0.22005691, 0.34330688, 0.54941304,
  77     0.21812995, 0.34743154, 0.55003755,
  78     0.21620971, 0.35153548, 0.55062743,
  79     0.21429757, 0.35561907, 0.5511844,
  80     0.21239477, 0.35968273, 0.55171011,
  81     0.2105031, 0.36372671, 0.55220646,
  82     0.20862342, 0.36775151, 0.55267486,
  83     0.20675628, 0.37175775, 0.55311653,
  84     0.20490257, 0.37574589, 0.55353282,
  85     0.20306309, 0.37971644, 0.55392505,
  86     0.20123854, 0.38366989, 0.55429441,
  87     0.1994295, 0.38760678, 0.55464205,
  88     0.1976365, 0.39152762, 0.55496905,
  89     0.19585993, 0.39543297, 0.55527637,
  90     0.19410009, 0.39932336, 0.55556494,
  91     0.19235719, 0.40319934, 0.55583559,
  92     0.19063135, 0.40706148, 0.55608907,
  93     0.18892259, 0.41091033, 0.55632606,
  94     0.18723083, 0.41474645, 0.55654717,
  95     0.18555593, 0.4185704, 0.55675292,
  96     0.18389763, 0.42238275, 0.55694377,
  97     0.18225561, 0.42618405, 0.5571201,
  98     0.18062949, 0.42997486, 0.55728221,
  99     0.17901879, 0.43375572, 0.55743035,
 100     0.17742298, 0.4375272, 0.55756466,
 101     0.17584148, 0.44128981, 0.55768526,
 102     0.17427363, 0.4450441, 0.55779216,
 103     0.17271876, 0.4487906, 0.55788532,
 104     0.17117615, 0.4525298, 0.55796464,
 105     0.16964573, 0.45626209, 0.55803034,
 106     0.16812641, 0.45998802, 0.55808199,
 107     0.1666171, 0.46370813, 0.55811913,
 108     0.16511703, 0.4674229, 0.55814141,
 109     0.16362543, 0.47113278, 0.55814842,
 110     0.16214155, 0.47483821, 0.55813967,
 111     0.16066467, 0.47853961, 0.55811466,
 112     0.15919413, 0.4822374, 0.5580728,
 113     0.15772933, 0.48593197, 0.55801347,
 114     0.15626973, 0.4896237, 0.557936,
 115     0.15481488, 0.49331293, 0.55783967,
 116     0.15336445, 0.49700003, 0.55772371,
 117     0.1519182, 0.50068529, 0.55758733,
 118     0.15047605, 0.50436904, 0.55742968,
 119     0.14903918, 0.50805136, 0.5572505,
 120     0.14760731, 0.51173263, 0.55704861,
 121     0.14618026, 0.51541316, 0.55682271,
 122     0.14475863, 0.51909319, 0.55657181,
 123     0.14334327, 0.52277292, 0.55629491,
 124     0.14193527, 0.52645254, 0.55599097,
 125     0.14053599, 0.53013219, 0.55565893,
 126     0.13914708, 0.53381201, 0.55529773,
 127     0.13777048, 0.53749213, 0.55490625,
 128     0.1364085, 0.54117264, 0.55448339,
 129     0.13506561, 0.54485335, 0.55402906,
 130     0.13374299, 0.54853458, 0.55354108,
 131     0.13244401, 0.55221637, 0.55301828,
 132     0.13117249, 0.55589872, 0.55245948,
 133     0.1299327, 0.55958162, 0.55186354,
 134     0.12872938, 0.56326503, 0.55122927,
 135     0.12756771, 0.56694891, 0.55055551,
 136     0.12645338, 0.57063316, 0.5498411,
 137     0.12539383, 0.57431754, 0.54908564,
 138     0.12439474, 0.57800205, 0.5482874,
 139     0.12346281, 0.58168661, 0.54744498,
 140     0.12260562, 0.58537105, 0.54655722,
 141     0.12183122, 0.58905521, 0.54562298,
 142     0.12114807, 0.59273889, 0.54464114,
 143     0.12056501, 0.59642187, 0.54361058,
 144     0.12009154, 0.60010387, 0.54253043,
 145     0.11973756, 0.60378459, 0.54139999,
 146     0.11951163, 0.60746388, 0.54021751,
 147     0.11942341, 0.61114146, 0.53898192,
 148     0.11948255, 0.61481702, 0.53769219,
 149     0.11969858, 0.61849025, 0.53634733,
 150     0.12008079, 0.62216081, 0.53494633,
 151     0.12063824, 0.62582833, 0.53348834,
 152     0.12137972, 0.62949242, 0.53197275,
 153     0.12231244, 0.63315277, 0.53039808,
 154     0.12344358, 0.63680899, 0.52876343,
 155     0.12477953, 0.64046069, 0.52706792,
 156     0.12632581, 0.64410744, 0.52531069,
 157     0.12808703, 0.64774881, 0.52349092,
 158     0.13006688, 0.65138436, 0.52160791,
 159     0.13226797, 0.65501363, 0.51966086,
 160     0.13469183, 0.65863619, 0.5176488,
 161     0.13733921, 0.66225157, 0.51557101,
 162     0.14020991, 0.66585927, 0.5134268,
 163     0.14330291, 0.66945881, 0.51121549,
 164     0.1466164, 0.67304968, 0.50893644,
 165     0.15014782, 0.67663139, 0.5065889,
 166     0.15389405, 0.68020343, 0.50417217,
 167     0.15785146, 0.68376525, 0.50168574,
 168     0.16201598, 0.68731632, 0.49912906,
 169     0.1663832, 0.69085611, 0.49650163,
 170     0.1709484, 0.69438405, 0.49380294,
 171     0.17570671, 0.6978996, 0.49103252,
 172     0.18065314, 0.70140222, 0.48818938,
 173     0.18578266, 0.70489133, 0.48527326,
 174     0.19109018, 0.70836635, 0.48228395,
 175     0.19657063, 0.71182668, 0.47922108,
 176     0.20221902, 0.71527175, 0.47608431,
 177     0.20803045, 0.71870095, 0.4728733,
 178     0.21400015, 0.72211371, 0.46958774,
 179     0.22012381, 0.72550945, 0.46622638,
 180     0.2263969, 0.72888753, 0.46278934,
 181     0.23281498, 0.73224735, 0.45927675,
 182     0.2393739, 0.73558828, 0.45568838,
 183     0.24606968, 0.73890972, 0.45202405,
 184     0.25289851, 0.74221104, 0.44828355,
 185     0.25985676, 0.74549162, 0.44446673,
 186     0.26694127, 0.74875084, 0.44057284,
 187     0.27414922, 0.75198807, 0.4366009,
 188     0.28147681, 0.75520266, 0.43255207,
 189     0.28892102, 0.75839399, 0.42842626,
 190     0.29647899, 0.76156142, 0.42422341,
 191     0.30414796, 0.76470433, 0.41994346,
 192     0.31192534, 0.76782207, 0.41558638,
 193     0.3198086, 0.77091403, 0.41115215,
 194     0.3277958, 0.77397953, 0.40664011,
 195     0.33588539, 0.7770179, 0.40204917,
 196     0.34407411, 0.78002855, 0.39738103,
 197     0.35235985, 0.78301086, 0.39263579,
 198     0.36074053, 0.78596419, 0.38781353,
 199     0.3692142, 0.78888793, 0.38291438,
 200     0.37777892, 0.79178146, 0.3779385,
 201     0.38643282, 0.79464415, 0.37288606,
 202     0.39517408, 0.79747541, 0.36775726,
 203     0.40400101, 0.80027461, 0.36255223,
 204     0.4129135, 0.80304099, 0.35726893,
 205     0.42190813, 0.80577412, 0.35191009,
 206     0.43098317, 0.80847343, 0.34647607,
 207     0.44013691, 0.81113836, 0.3409673,
 208     0.44936763, 0.81376835, 0.33538426,
 209     0.45867362, 0.81636288, 0.32972749,
 210     0.46805314, 0.81892143, 0.32399761,
 211     0.47750446, 0.82144351, 0.31819529,
 212     0.4870258, 0.82392862, 0.31232133,
 213     0.49661536, 0.82637633, 0.30637661,
 214     0.5062713, 0.82878621, 0.30036211,
 215     0.51599182, 0.83115784, 0.29427888,
 216     0.52577622, 0.83349064, 0.2881265,
 217     0.5356211, 0.83578452, 0.28190832,
 218     0.5455244, 0.83803918, 0.27562602,
 219     0.55548397, 0.84025437, 0.26928147,
 220     0.5654976, 0.8424299, 0.26287683,
 221     0.57556297, 0.84456561, 0.25641457,
 222     0.58567772, 0.84666139, 0.24989748,
 223     0.59583934, 0.84871722, 0.24332878,
 224     0.60604528, 0.8507331, 0.23671214,
 225     0.61629283, 0.85270912, 0.23005179,
 226     0.62657923, 0.85464543, 0.22335258,
 227     0.63690157, 0.85654226, 0.21662012,
 228     0.64725685, 0.85839991, 0.20986086,
 229     0.65764197, 0.86021878, 0.20308229,
 230     0.66805369, 0.86199932, 0.19629307,
 231     0.67848868, 0.86374211, 0.18950326,
 232     0.68894351, 0.86544779, 0.18272455,
 233     0.69941463, 0.86711711, 0.17597055,
 234     0.70989842, 0.86875092, 0.16925712,
 235     0.72039115, 0.87035015, 0.16260273,
 236     0.73088902, 0.87191584, 0.15602894,
 237     0.74138803, 0.87344918, 0.14956101,
 238     0.75188414, 0.87495143, 0.14322828,
 239     0.76237342, 0.87642392, 0.13706449,
 240     0.77285183, 0.87786808, 0.13110864,
 241     0.78331535, 0.87928545, 0.12540538,
 242     0.79375994, 0.88067763, 0.12000532,
 243     0.80418159, 0.88204632, 0.11496505,
 244     0.81457634, 0.88339329, 0.11034678,
 245     0.82494028, 0.88472036, 0.10621724,
 246     0.83526959, 0.88602943, 0.1026459,
 247     0.84556056, 0.88732243, 0.09970219,
 248     0.8558096, 0.88860134, 0.09745186,
 249     0.86601325, 0.88986815, 0.09595277,
 250     0.87616824, 0.89112487, 0.09525046,
 251     0.88627146, 0.89237353, 0.09537439,
 252     0.89632002, 0.89361614, 0.09633538,
 253     0.90631121, 0.89485467, 0.09812496,
 254     0.91624212, 0.89609127, 0.1007168,
 255     0.92610579, 0.89732977, 0.10407067,
 256     0.93590444, 0.8985704, 0.10813094,
 257     0.94563626, 0.899815, 0.11283773,
 258     0.95529972, 0.90106534, 0.11812832,
 259     0.96489353, 0.90232311, 0.12394051,
 260     0.97441665, 0.90358991, 0.13021494,
 261     0.98386829, 0.90486726, 0.13689671,
 262     0.99324789, 0.90615657, 0.1439362,
 263 }
 264 
 265 // I'm using data straight from the original implementation of Magma
 266 // by Nathaniel Smith & Stefan van der Walt:
 267 // https://github.com/BIDS/colormap/blob/master/option_a.py
 268 var magmaData = [...]float64{
 269     1.46159096e-03, 4.66127766e-04, 1.38655200e-02,
 270     2.25764007e-03, 1.29495431e-03, 1.83311461e-02,
 271     3.27943222e-03, 2.30452991e-03, 2.37083291e-02,
 272     4.51230222e-03, 3.49037666e-03, 2.99647059e-02,
 273     5.94976987e-03, 4.84285000e-03, 3.71296695e-02,
 274     7.58798550e-03, 6.35613622e-03, 4.49730774e-02,
 275     9.42604390e-03, 8.02185006e-03, 5.28443561e-02,
 276     1.14654337e-02, 9.82831486e-03, 6.07496380e-02,
 277     1.37075706e-02, 1.17705913e-02, 6.86665843e-02,
 278     1.61557566e-02, 1.38404966e-02, 7.66026660e-02,
 279     1.88153670e-02, 1.60262753e-02, 8.45844897e-02,
 280     2.16919340e-02, 1.83201254e-02, 9.26101050e-02,
 281     2.47917814e-02, 2.07147875e-02, 1.00675555e-01,
 282     2.81228154e-02, 2.32009284e-02, 1.08786954e-01,
 283     3.16955304e-02, 2.57651161e-02, 1.16964722e-01,
 284     3.55204468e-02, 2.83974570e-02, 1.25209396e-01,
 285     3.96084872e-02, 3.10895652e-02, 1.33515085e-01,
 286     4.38295350e-02, 3.38299885e-02, 1.41886249e-01,
 287     4.80616391e-02, 3.66066101e-02, 1.50326989e-01,
 288     5.23204388e-02, 3.94066020e-02, 1.58841025e-01,
 289     5.66148978e-02, 4.21598925e-02, 1.67445592e-01,
 290     6.09493930e-02, 4.47944924e-02, 1.76128834e-01,
 291     6.53301801e-02, 4.73177796e-02, 1.84891506e-01,
 292     6.97637296e-02, 4.97264666e-02, 1.93735088e-01,
 293     7.42565152e-02, 5.20167766e-02, 2.02660374e-01,
 294     7.88150034e-02, 5.41844801e-02, 2.11667355e-01,
 295     8.34456313e-02, 5.62249365e-02, 2.20755099e-01,
 296     8.81547730e-02, 5.81331465e-02, 2.29921611e-01,
 297     9.29486914e-02, 5.99038167e-02, 2.39163669e-01,
 298     9.78334770e-02, 6.15314414e-02, 2.48476662e-01,
 299     1.02814972e-01, 6.30104053e-02, 2.57854400e-01,
 300     1.07898679e-01, 6.43351102e-02, 2.67288933e-01,
 301     1.13094451e-01, 6.54920358e-02, 2.76783978e-01,
 302     1.18405035e-01, 6.64791593e-02, 2.86320656e-01,
 303     1.23832651e-01, 6.72946449e-02, 2.95879431e-01,
 304     1.29380192e-01, 6.79349264e-02, 3.05442931e-01,
 305     1.35053322e-01, 6.83912798e-02, 3.14999890e-01,
 306     1.40857952e-01, 6.86540710e-02, 3.24537640e-01,
 307     1.46785234e-01, 6.87382323e-02, 3.34011109e-01,
 308     1.52839217e-01, 6.86368599e-02, 3.43404450e-01,
 309     1.59017511e-01, 6.83540225e-02, 3.52688028e-01,
 310     1.65308131e-01, 6.79108689e-02, 3.61816426e-01,
 311     1.71713033e-01, 6.73053260e-02, 3.70770827e-01,
 312     1.78211730e-01, 6.65758073e-02, 3.79497161e-01,
 313     1.84800877e-01, 6.57324381e-02, 3.87972507e-01,
 314     1.91459745e-01, 6.48183312e-02, 3.96151969e-01,
 315     1.98176877e-01, 6.38624166e-02, 4.04008953e-01,
 316     2.04934882e-01, 6.29066192e-02, 4.11514273e-01,
 317     2.11718061e-01, 6.19917876e-02, 4.18646741e-01,
 318     2.18511590e-01, 6.11584918e-02, 4.25391816e-01,
 319     2.25302032e-01, 6.04451843e-02, 4.31741767e-01,
 320     2.32076515e-01, 5.98886855e-02, 4.37694665e-01,
 321     2.38825991e-01, 5.95170384e-02, 4.43255999e-01,
 322     2.45543175e-01, 5.93524384e-02, 4.48435938e-01,
 323     2.52220252e-01, 5.94147119e-02, 4.53247729e-01,
 324     2.58857304e-01, 5.97055998e-02, 4.57709924e-01,
 325     2.65446744e-01, 6.02368754e-02, 4.61840297e-01,
 326     2.71994089e-01, 6.09935552e-02, 4.65660375e-01,
 327     2.78493300e-01, 6.19778136e-02, 4.69190328e-01,
 328     2.84951097e-01, 6.31676261e-02, 4.72450879e-01,
 329     2.91365817e-01, 6.45534486e-02, 4.75462193e-01,
 330     2.97740413e-01, 6.61170432e-02, 4.78243482e-01,
 331     3.04080941e-01, 6.78353452e-02, 4.80811572e-01,
 332     3.10382027e-01, 6.97024767e-02, 4.83186340e-01,
 333     3.16654235e-01, 7.16895272e-02, 4.85380429e-01,
 334     3.22899126e-01, 7.37819504e-02, 4.87408399e-01,
 335     3.29114038e-01, 7.59715081e-02, 4.89286796e-01,
 336     3.35307503e-01, 7.82361045e-02, 4.91024144e-01,
 337     3.41481725e-01, 8.05635079e-02, 4.92631321e-01,
 338     3.47635742e-01, 8.29463512e-02, 4.94120923e-01,
 339     3.53773161e-01, 8.53726329e-02, 4.95501096e-01,
 340     3.59897941e-01, 8.78311772e-02, 4.96778331e-01,
 341     3.66011928e-01, 9.03143031e-02, 4.97959963e-01,
 342     3.72116205e-01, 9.28159917e-02, 4.99053326e-01,
 343     3.78210547e-01, 9.53322947e-02, 5.00066568e-01,
 344     3.84299445e-01, 9.78549106e-02, 5.01001964e-01,
 345     3.90384361e-01, 1.00379466e-01, 5.01864236e-01,
 346     3.96466670e-01, 1.02902194e-01, 5.02657590e-01,
 347     4.02547663e-01, 1.05419865e-01, 5.03385761e-01,
 348     4.08628505e-01, 1.07929771e-01, 5.04052118e-01,
 349     4.14708664e-01, 1.10431177e-01, 5.04661843e-01,
 350     4.20791157e-01, 1.12920210e-01, 5.05214935e-01,
 351     4.26876965e-01, 1.15395258e-01, 5.05713602e-01,
 352     4.32967001e-01, 1.17854987e-01, 5.06159754e-01,
 353     4.39062114e-01, 1.20298314e-01, 5.06555026e-01,
 354     4.45163096e-01, 1.22724371e-01, 5.06900806e-01,
 355     4.51270678e-01, 1.25132484e-01, 5.07198258e-01,
 356     4.57385535e-01, 1.27522145e-01, 5.07448336e-01,
 357     4.63508291e-01, 1.29892998e-01, 5.07651812e-01,
 358     4.69639514e-01, 1.32244819e-01, 5.07809282e-01,
 359     4.75779723e-01, 1.34577500e-01, 5.07921193e-01,
 360     4.81928997e-01, 1.36891390e-01, 5.07988509e-01,
 361     4.88088169e-01, 1.39186217e-01, 5.08010737e-01,
 362     4.94257673e-01, 1.41462106e-01, 5.07987836e-01,
 363     5.00437834e-01, 1.43719323e-01, 5.07919772e-01,
 364     5.06628929e-01, 1.45958202e-01, 5.07806420e-01,
 365     5.12831195e-01, 1.48179144e-01, 5.07647570e-01,
 366     5.19044825e-01, 1.50382611e-01, 5.07442938e-01,
 367     5.25269968e-01, 1.52569121e-01, 5.07192172e-01,
 368     5.31506735e-01, 1.54739247e-01, 5.06894860e-01,
 369     5.37755194e-01, 1.56893613e-01, 5.06550538e-01,
 370     5.44015371e-01, 1.59032895e-01, 5.06158696e-01,
 371     5.50287252e-01, 1.61157816e-01, 5.05718782e-01,
 372     5.56570783e-01, 1.63269149e-01, 5.05230210e-01,
 373     5.62865867e-01, 1.65367714e-01, 5.04692365e-01,
 374     5.69172368e-01, 1.67454379e-01, 5.04104606e-01,
 375     5.75490107e-01, 1.69530062e-01, 5.03466273e-01,
 376     5.81818864e-01, 1.71595728e-01, 5.02776690e-01,
 377     5.88158375e-01, 1.73652392e-01, 5.02035167e-01,
 378     5.94508337e-01, 1.75701122e-01, 5.01241011e-01,
 379     6.00868399e-01, 1.77743036e-01, 5.00393522e-01,
 380     6.07238169e-01, 1.79779309e-01, 4.99491999e-01,
 381     6.13617209e-01, 1.81811170e-01, 4.98535746e-01,
 382     6.20005032e-01, 1.83839907e-01, 4.97524075e-01,
 383     6.26401108e-01, 1.85866869e-01, 4.96456304e-01,
 384     6.32804854e-01, 1.87893468e-01, 4.95331769e-01,
 385     6.39215638e-01, 1.89921182e-01, 4.94149821e-01,
 386     6.45632778e-01, 1.91951556e-01, 4.92909832e-01,
 387     6.52055535e-01, 1.93986210e-01, 4.91611196e-01,
 388     6.58483116e-01, 1.96026835e-01, 4.90253338e-01,
 389     6.64914668e-01, 1.98075202e-01, 4.88835712e-01,
 390     6.71349279e-01, 2.00133166e-01, 4.87357807e-01,
 391     6.77785975e-01, 2.02202663e-01, 4.85819154e-01,
 392     6.84223712e-01, 2.04285721e-01, 4.84219325e-01,
 393     6.90661380e-01, 2.06384461e-01, 4.82557941e-01,
 394     6.97097796e-01, 2.08501100e-01, 4.80834678e-01,
 395     7.03531700e-01, 2.10637956e-01, 4.79049270e-01,
 396     7.09961888e-01, 2.12797337e-01, 4.77201121e-01,
 397     7.16387038e-01, 2.14981693e-01, 4.75289780e-01,
 398     7.22805451e-01, 2.17193831e-01, 4.73315708e-01,
 399     7.29215521e-01, 2.19436516e-01, 4.71278924e-01,
 400     7.35615545e-01, 2.21712634e-01, 4.69179541e-01,
 401     7.42003713e-01, 2.24025196e-01, 4.67017774e-01,
 402     7.48378107e-01, 2.26377345e-01, 4.64793954e-01,
 403     7.54736692e-01, 2.28772352e-01, 4.62508534e-01,
 404     7.61077312e-01, 2.31213625e-01, 4.60162106e-01,
 405     7.67397681e-01, 2.33704708e-01, 4.57755411e-01,
 406     7.73695380e-01, 2.36249283e-01, 4.55289354e-01,
 407     7.79967847e-01, 2.38851170e-01, 4.52765022e-01,
 408     7.86212372e-01, 2.41514325e-01, 4.50183695e-01,
 409     7.92426972e-01, 2.44242250e-01, 4.47543155e-01,
 410     7.98607760e-01, 2.47039798e-01, 4.44848441e-01,
 411     8.04751511e-01, 2.49911350e-01, 4.42101615e-01,
 412     8.10854841e-01, 2.52861399e-01, 4.39304963e-01,
 413     8.16914186e-01, 2.55894550e-01, 4.36461074e-01,
 414     8.22925797e-01, 2.59015505e-01, 4.33572874e-01,
 415     8.28885740e-01, 2.62229049e-01, 4.30643647e-01,
 416     8.34790818e-01, 2.65539703e-01, 4.27671352e-01,
 417     8.40635680e-01, 2.68952874e-01, 4.24665620e-01,
 418     8.46415804e-01, 2.72473491e-01, 4.21631064e-01,
 419     8.52126490e-01, 2.76106469e-01, 4.18572767e-01,
 420     8.57762870e-01, 2.79856666e-01, 4.15496319e-01,
 421     8.63320397e-01, 2.83729003e-01, 4.12402889e-01,
 422     8.68793368e-01, 2.87728205e-01, 4.09303002e-01,
 423     8.74176342e-01, 2.91858679e-01, 4.06205397e-01,
 424     8.79463944e-01, 2.96124596e-01, 4.03118034e-01,
 425     8.84650824e-01, 3.00530090e-01, 4.00047060e-01,
 426     8.89731418e-01, 3.05078817e-01, 3.97001559e-01,
 427     8.94700194e-01, 3.09773445e-01, 3.93994634e-01,
 428     8.99551884e-01, 3.14616425e-01, 3.91036674e-01,
 429     9.04281297e-01, 3.19609981e-01, 3.88136889e-01,
 430     9.08883524e-01, 3.24755126e-01, 3.85308008e-01,
 431     9.13354091e-01, 3.30051947e-01, 3.82563414e-01,
 432     9.17688852e-01, 3.35500068e-01, 3.79915138e-01,
 433     9.21884187e-01, 3.41098112e-01, 3.77375977e-01,
 434     9.25937102e-01, 3.46843685e-01, 3.74959077e-01,
 435     9.29845090e-01, 3.52733817e-01, 3.72676513e-01,
 436     9.33606454e-01, 3.58764377e-01, 3.70540883e-01,
 437     9.37220874e-01, 3.64929312e-01, 3.68566525e-01,
 438     9.40687443e-01, 3.71224168e-01, 3.66761699e-01,
 439     9.44006448e-01, 3.77642889e-01, 3.65136328e-01,
 440     9.47179528e-01, 3.84177874e-01, 3.63701130e-01,
 441     9.50210150e-01, 3.90819546e-01, 3.62467694e-01,
 442     9.53099077e-01, 3.97562894e-01, 3.61438431e-01,
 443     9.55849237e-01, 4.04400213e-01, 3.60619076e-01,
 444     9.58464079e-01, 4.11323666e-01, 3.60014232e-01,
 445     9.60949221e-01, 4.18323245e-01, 3.59629789e-01,
 446     9.63310281e-01, 4.25389724e-01, 3.59469020e-01,
 447     9.65549351e-01, 4.32518707e-01, 3.59529151e-01,
 448     9.67671128e-01, 4.39702976e-01, 3.59810172e-01,
 449     9.69680441e-01, 4.46935635e-01, 3.60311120e-01,
 450     9.71582181e-01, 4.54210170e-01, 3.61030156e-01,
 451     9.73381238e-01, 4.61520484e-01, 3.61964652e-01,
 452     9.75082439e-01, 4.68860936e-01, 3.63111292e-01,
 453     9.76690494e-01, 4.76226350e-01, 3.64466162e-01,
 454     9.78209957e-01, 4.83612031e-01, 3.66024854e-01,
 455     9.79645181e-01, 4.91013764e-01, 3.67782559e-01,
 456     9.81000291e-01, 4.98427800e-01, 3.69734157e-01,
 457     9.82279159e-01, 5.05850848e-01, 3.71874301e-01,
 458     9.83485387e-01, 5.13280054e-01, 3.74197501e-01,
 459     9.84622298e-01, 5.20712972e-01, 3.76698186e-01,
 460     9.85692925e-01, 5.28147545e-01, 3.79370774e-01,
 461     9.86700017e-01, 5.35582070e-01, 3.82209724e-01,
 462     9.87646038e-01, 5.43015173e-01, 3.85209578e-01,
 463     9.88533173e-01, 5.50445778e-01, 3.88365009e-01,
 464     9.89363341e-01, 5.57873075e-01, 3.91670846e-01,
 465     9.90138201e-01, 5.65296495e-01, 3.95122099e-01,
 466     9.90871208e-01, 5.72706259e-01, 3.98713971e-01,
 467     9.91558165e-01, 5.80106828e-01, 4.02441058e-01,
 468     9.92195728e-01, 5.87501706e-01, 4.06298792e-01,
 469     9.92784669e-01, 5.94891088e-01, 4.10282976e-01,
 470     9.93325561e-01, 6.02275297e-01, 4.14389658e-01,
 471     9.93834412e-01, 6.09643540e-01, 4.18613221e-01,
 472     9.94308514e-01, 6.16998953e-01, 4.22949672e-01,
 473     9.94737698e-01, 6.24349657e-01, 4.27396771e-01,
 474     9.95121854e-01, 6.31696376e-01, 4.31951492e-01,
 475     9.95480469e-01, 6.39026596e-01, 4.36607159e-01,
 476     9.95809924e-01, 6.46343897e-01, 4.41360951e-01,
 477     9.96095703e-01, 6.53658756e-01, 4.46213021e-01,
 478     9.96341406e-01, 6.60969379e-01, 4.51160201e-01,
 479     9.96579803e-01, 6.68255621e-01, 4.56191814e-01,
 480     9.96774784e-01, 6.75541484e-01, 4.61314158e-01,
 481     9.96925427e-01, 6.82827953e-01, 4.66525689e-01,
 482     9.97077185e-01, 6.90087897e-01, 4.71811461e-01,
 483     9.97186253e-01, 6.97348991e-01, 4.77181727e-01,
 484     9.97253982e-01, 7.04610791e-01, 4.82634651e-01,
 485     9.97325180e-01, 7.11847714e-01, 4.88154375e-01,
 486     9.97350983e-01, 7.19089119e-01, 4.93754665e-01,
 487     9.97350583e-01, 7.26324415e-01, 4.99427972e-01,
 488     9.97341259e-01, 7.33544671e-01, 5.05166839e-01,
 489     9.97284689e-01, 7.40771893e-01, 5.10983331e-01,
 490     9.97228367e-01, 7.47980563e-01, 5.16859378e-01,
 491     9.97138480e-01, 7.55189852e-01, 5.22805996e-01,
 492     9.97019342e-01, 7.62397883e-01, 5.28820775e-01,
 493     9.96898254e-01, 7.69590975e-01, 5.34892341e-01,
 494     9.96726862e-01, 7.76794860e-01, 5.41038571e-01,
 495     9.96570645e-01, 7.83976508e-01, 5.47232992e-01,
 496     9.96369065e-01, 7.91167346e-01, 5.53498939e-01,
 497     9.96162309e-01, 7.98347709e-01, 5.59819643e-01,
 498     9.95932448e-01, 8.05527126e-01, 5.66201824e-01,
 499     9.95680107e-01, 8.12705773e-01, 5.72644795e-01,
 500     9.95423973e-01, 8.19875302e-01, 5.79140130e-01,
 501     9.95131288e-01, 8.27051773e-01, 5.85701463e-01,
 502     9.94851089e-01, 8.34212826e-01, 5.92307093e-01,
 503     9.94523666e-01, 8.41386618e-01, 5.98982818e-01,
 504     9.94221900e-01, 8.48540474e-01, 6.05695903e-01,
 505     9.93865767e-01, 8.55711038e-01, 6.12481798e-01,
 506     9.93545285e-01, 8.62858846e-01, 6.19299300e-01,
 507     9.93169558e-01, 8.70024467e-01, 6.26189463e-01,
 508     9.92830963e-01, 8.77168404e-01, 6.33109148e-01,
 509     9.92439881e-01, 8.84329694e-01, 6.40099465e-01,
 510     9.92089454e-01, 8.91469549e-01, 6.47116021e-01,
 511     9.91687744e-01, 8.98627050e-01, 6.54201544e-01,
 512     9.91331929e-01, 9.05762748e-01, 6.61308839e-01,
 513     9.90929685e-01, 9.12915010e-01, 6.68481201e-01,
 514     9.90569914e-01, 9.20048699e-01, 6.75674592e-01,
 515     9.90174637e-01, 9.27195612e-01, 6.82925602e-01,
 516     9.89814839e-01, 9.34328540e-01, 6.90198194e-01,
 517     9.89433736e-01, 9.41470354e-01, 6.97518628e-01,
 518     9.89077438e-01, 9.48604077e-01, 7.04862519e-01,
 519     9.88717064e-01, 9.55741520e-01, 7.12242232e-01,
 520     9.88367028e-01, 9.62878026e-01, 7.19648627e-01,
 521     9.88032885e-01, 9.70012413e-01, 7.27076773e-01,
 522     9.87690702e-01, 9.77154231e-01, 7.34536205e-01,
 523     9.87386827e-01, 9.84287561e-01, 7.42001547e-01,
 524     9.87052509e-01, 9.91437853e-01, 7.49504188e-01,
 525 }
 526 
 527 // I'm using data straight from
 528 // https://github.com/BIDS/colormap/blob/master/parula.py
 529 var parulaData = [...]float64{
 530     0.2081, 0.1663, 0.5292, 0.2116238095, 0.1897809524, 0.5776761905,
 531     0.212252381, 0.2137714286, 0.6269714286, 0.2081, 0.2386, 0.6770857143,
 532     0.1959047619, 0.2644571429, 0.7279, 0.1707285714, 0.2919380952,
 533     0.779247619, 0.1252714286, 0.3242428571, 0.8302714286,
 534     0.0591333333, 0.3598333333, 0.8683333333, 0.0116952381, 0.3875095238,
 535     0.8819571429, 0.0059571429, 0.4086142857, 0.8828428571,
 536     0.0165142857, 0.4266, 0.8786333333, 0.032852381, 0.4430428571,
 537     0.8719571429, 0.0498142857, 0.4585714286, 0.8640571429,
 538     0.0629333333, 0.4736904762, 0.8554380952, 0.0722666667, 0.4886666667,
 539     0.8467, 0.0779428571, 0.5039857143, 0.8383714286,
 540     0.079347619, 0.5200238095, 0.8311809524, 0.0749428571, 0.5375428571,
 541     0.8262714286, 0.0640571429, 0.5569857143, 0.8239571429,
 542     0.0487714286, 0.5772238095, 0.8228285714, 0.0343428571, 0.5965809524,
 543     0.819852381, 0.0265, 0.6137, 0.8135, 0.0238904762, 0.6286619048,
 544     0.8037619048, 0.0230904762, 0.6417857143, 0.7912666667,
 545     0.0227714286, 0.6534857143, 0.7767571429, 0.0266619048, 0.6641952381,
 546     0.7607190476, 0.0383714286, 0.6742714286, 0.743552381,
 547     0.0589714286, 0.6837571429, 0.7253857143,
 548     0.0843, 0.6928333333, 0.7061666667, 0.1132952381, 0.7015, 0.6858571429,
 549     0.1452714286, 0.7097571429, 0.6646285714, 0.1801333333, 0.7176571429,
 550     0.6424333333, 0.2178285714, 0.7250428571, 0.6192619048,
 551     0.2586428571, 0.7317142857, 0.5954285714, 0.3021714286, 0.7376047619,
 552     0.5711857143, 0.3481666667, 0.7424333333, 0.5472666667,
 553     0.3952571429, 0.7459, 0.5244428571, 0.4420095238, 0.7480809524,
 554     0.5033142857, 0.4871238095, 0.7490619048, 0.4839761905,
 555     0.5300285714, 0.7491142857, 0.4661142857, 0.5708571429, 0.7485190476,
 556     0.4493904762, 0.609852381, 0.7473142857, 0.4336857143,
 557     0.6473, 0.7456, 0.4188, 0.6834190476, 0.7434761905, 0.4044333333,
 558     0.7184095238, 0.7411333333, 0.3904761905,
 559     0.7524857143, 0.7384, 0.3768142857, 0.7858428571, 0.7355666667,
 560     0.3632714286, 0.8185047619, 0.7327333333, 0.3497904762,
 561     0.8506571429, 0.7299, 0.3360285714, 0.8824333333, 0.7274333333, 0.3217,
 562     0.9139333333, 0.7257857143, 0.3062761905, 0.9449571429, 0.7261142857,
 563     0.2886428571, 0.9738952381, 0.7313952381, 0.266647619,
 564     0.9937714286, 0.7454571429, 0.240347619, 0.9990428571, 0.7653142857,
 565     0.2164142857, 0.9955333333, 0.7860571429, 0.196652381,
 566     0.988, 0.8066, 0.1793666667, 0.9788571429, 0.8271428571, 0.1633142857,
 567     0.9697, 0.8481380952, 0.147452381, 0.9625857143, 0.8705142857, 0.1309,
 568     0.9588714286, 0.8949, 0.1132428571, 0.9598238095, 0.9218333333,
 569     0.0948380952, 0.9661, 0.9514428571, 0.0755333333,
 570     0.9763, 0.9831, 0.0538,
 571 }
 572 
 573 // I'm using data straight from
 574 // https://github.com/matplotlib/cmocean/blob/master/cmocean/rgb/haline-rgb.txt
 575 var halineData = [...]float64{
 576     1.629529545569048110e-01, 9.521591660747855124e-02, 4.225729247643043585e-01,
 577     1.648101130638113809e-01, 9.635115909727909322e-02, 4.318459659833655540e-01,
 578     1.666161667445505146e-01, 9.744967053737302320e-02, 4.412064832719169161e-01,
 579     1.683662394047173716e-01, 9.851521320092249123e-02, 4.506510991070378780e-01,
 580     1.700547063176806595e-01, 9.955275459284393391e-02, 4.601751103492678907e-01,
 581     1.716750780810941956e-01, 1.005687314559364776e-01, 4.697722208210775574e-01,
 582     1.732198670017069397e-01, 1.015713570251385311e-01, 4.794342308257477092e-01,
 583     1.746804342417165035e-01, 1.025709733421875103e-01, 4.891506793097686878e-01,
 584     1.760433654254164593e-01, 1.035658402770499587e-01, 4.989416012077843576e-01,
 585     1.772982333235153807e-01, 1.045802467658180357e-01, 5.087715885336102639e-01,
 586     1.784322966250933284e-01, 1.056380265564063059e-01, 5.186108302832771466e-01,
 587     1.794226692010022772e-01, 1.067416562108134404e-01, 5.284836071020164727e-01,
 588     1.802542327126359922e-01, 1.079356346679062328e-01, 5.383245681077661882e-01,
 589     1.808975365813079994e-01, 1.092386640641496154e-01, 5.481352134375515606e-01,
 590     1.813298273265454008e-01, 1.107042924622455293e-01, 5.578435355461390799e-01,
 591     1.815069308605478937e-01, 1.123613365530294061e-01, 5.674471854200233700e-01,
 592     1.813959559086370799e-01, 1.142804413027345978e-01, 5.768505865319291104e-01,
 593     1.809499433760710929e-01, 1.165251530113385336e-01, 5.859821014031293407e-01,
 594     1.801166524094891808e-01, 1.191682999758127970e-01, 5.947494236872948870e-01,
 595     1.788419557731087683e-01, 1.222886104999623413e-01, 6.030366129604394221e-01,
 596     1.770751344832933727e-01, 1.259620672997293078e-01, 6.107077426144936760e-01,
 597     1.747764954226868062e-01, 1.302486445940692350e-01, 6.176174300439590814e-01,
 598     1.719255883800615836e-01, 1.351768519397535118e-01, 6.236290832033221099e-01,
 599     1.685302279919113078e-01, 1.407308818346016399e-01, 6.286357211183263294e-01,
 600     1.646373543798159977e-01, 1.468433194330099889e-01, 6.325796572366660930e-01,
 601     1.603141656593721487e-01, 1.534074847391770635e-01, 6.354701889106297852e-01,
 602     1.556539455727427579e-01, 1.602911795924207572e-01, 6.373742153046678682e-01,
 603     1.507373567977903506e-01, 1.673688895313445446e-01, 6.383989700654711941e-01,
 604     1.456427577979826360e-01, 1.745293312408868480e-01, 6.386687569056349600e-01,
 605     1.404368075255880977e-01, 1.816841459042554952e-01, 6.383089542091028301e-01,
 606     1.351726504089350855e-01, 1.887688275072176014e-01, 6.374350053971095109e-01,
 607     1.298906561807787186e-01, 1.957398580438490798e-01, 6.361469852044080442e-01,
 608     1.246205125693149729e-01, 2.025703385486158914e-01, 6.345282558695404251e-01,
 609     1.193859004780570554e-01, 2.092446623034395214e-01, 6.326478270215730726e-01,
 610     1.142294912197052703e-01, 2.157456284251405010e-01, 6.305768690676523125e-01,
 611     1.091404911375367659e-01, 2.220831206181900774e-01, 6.283455167242665285e-01,
 612     1.041438584244326337e-01, 2.282546518282705383e-01, 6.259979600528258192e-01,
 613     9.926304855671816418e-02, 2.342609767388125763e-01, 6.235717761795677161e-01,
 614     9.449512580805050077e-02, 2.401139958170242505e-01, 6.210816676451920149e-01,
 615     8.986951574154733446e-02, 2.458147193889331228e-01, 6.185591936304666305e-01,
 616     8.539285840535987271e-02, 2.513729453557033144e-01, 6.160166295227810229e-01,
 617     8.106756674391193962e-02, 2.567997050291829786e-01, 6.134596708213713168e-01,
 618     7.694418932732069449e-02, 2.620915612909199277e-01, 6.109238301118911085e-01,
 619     7.300703739422578775e-02, 2.672655035330154250e-01, 6.083965011432549419e-01,
 620     6.927650669442811382e-02, 2.723273008096985248e-01, 6.058873223830183452e-01,
 621     6.578801445169751849e-02, 2.772789491245566951e-01, 6.034141752438300088e-01,
 622     6.255595479554787453e-02, 2.821282390686886132e-01, 6.009787922718963227e-01,
 623     5.959205181913470456e-02, 2.868831717247524726e-01, 5.985797542127682114e-01,
 624     5.691772151374491912e-02, 2.915488716281217640e-01, 5.962214962176209943e-01,
 625     5.455347307306369214e-02, 2.961302536799313989e-01, 5.939076044300871660e-01,
 626     5.251889627870443694e-02, 3.006318409387543356e-01, 5.916414807294642086e-01,
 627     5.083877347247430650e-02, 3.050562292020492783e-01, 5.894315315373579445e-01,
 628     4.951454037014189208e-02, 3.094102218220906031e-01, 5.872714908845412252e-01,
 629     4.855490408104565919e-02, 3.136977658751046727e-01, 5.851627302038014955e-01,
 630     4.796369156225028380e-02, 3.179225992994973993e-01, 5.831062484926557987e-01,
 631     4.773946305380068894e-02, 3.220882581897950847e-01, 5.811027291021745311e-01,
 632     4.787545181415154422e-02, 3.261980852298422273e-01, 5.791525884087008746e-01,
 633     4.835984720504159923e-02, 3.302552388254387794e-01, 5.772560174768572860e-01,
 634     4.917638757411300215e-02, 3.342627026038174076e-01, 5.754130176762238813e-01,
 635     5.030518671680884318e-02, 3.382232950310170572e-01, 5.736234310853615126e-01,
 636     5.172369283691292258e-02, 3.421396789632700219e-01, 5.718869664041401624e-01,
 637     5.340767549062541003e-02, 3.460143709989857430e-01, 5.702032209969470911e-01,
 638     5.533215100674954839e-02, 3.498497505368459159e-01, 5.685716996038682192e-01,
 639     5.747218306369886870e-02, 3.536480684754851334e-01, 5.669918301827847618e-01,
 640     5.980352430527090951e-02, 3.574114555130643578e-01, 5.654629772812437283e-01,
 641     6.230309069250609261e-02, 3.611419300223894235e-01, 5.639844532816007394e-01,
 642     6.494927925026378057e-02, 3.648414054902228698e-01, 5.625555278152573058e-01,
 643     6.772215122553368327e-02, 3.685116975190721456e-01, 5.611754356007422340e-01,
 644     7.060350747784929770e-02, 3.721545303967777607e-01, 5.598433829250933913e-01,
 645     7.357840396611611822e-02, 3.757710087912914942e-01, 5.585612898846836760e-01,
 646     7.663101364217325684e-02, 3.793629991309988569e-01, 5.573268773673928367e-01,
 647     7.974739830752586300e-02, 3.829322364932883360e-01, 5.561380319387883020e-01,
 648     8.291580548873803136e-02, 3.864801413799028307e-01, 5.549938581786431069e-01,
 649     8.612580955679122185e-02, 3.900080689665953448e-01, 5.538934528024703763e-01,
 650     8.936817980172057085e-02, 3.935173134989205512e-01, 5.528359060062189023e-01,
 651     9.263475301781654014e-02, 3.970091123550867351e-01, 5.518203023495191761e-01,
 652     9.591831391237073956e-02, 4.004846497947374129e-01, 5.508457212473490960e-01,
 653     9.921323508992196949e-02, 4.039446967979293257e-01, 5.499133654313189679e-01,
 654     1.025139614653171327e-01, 4.073902646845309894e-01, 5.490228475752887416e-01,
 655     1.058144853522852702e-01, 4.108228877965505177e-01, 5.481703859285301794e-01,
 656     1.091104131658914012e-01, 4.142435690118807523e-01, 5.473550236322454188e-01,
 657     1.123978495089298091e-01, 4.176532725696484594e-01, 5.465757987301745890e-01,
 658     1.156733362160069500e-01, 4.210529263321469151e-01, 5.458317432175192607e-01,
 659     1.189341701380315364e-01, 4.244432161011251203e-01, 5.451231873866256850e-01,
 660     1.221781892974011241e-01, 4.278246691926565481e-01, 5.444513010684173260e-01,
 661     1.254018943745988379e-01, 4.311987106338182607e-01, 5.438113725374379426e-01,
 662     1.286031296249826039e-01, 4.345661396549306832e-01, 5.432023919026625070e-01,
 663     1.317799711665625373e-01, 4.379277275711909168e-01, 5.426233385794481112e-01,
 664     1.349307019221451520e-01, 4.412842189714309415e-01, 5.420731800699918335e-01,
 665     1.380549051543558114e-01, 4.446356900078314855e-01, 5.415550926551251365e-01,
 666     1.411501069188544899e-01, 4.479834819017672332e-01, 5.410638066180113448e-01,
 667     1.442150210041465985e-01, 4.513283116704150388e-01, 5.405979268760919831e-01,
 668     1.472485820103837661e-01, 4.546708245755168298e-01, 5.401563599016087069e-01,
 669     1.502499341655997578e-01, 4.580115969309521140e-01, 5.397383042732707414e-01,
 670     1.532192022679761678e-01, 4.613507130792227628e-01, 5.393460827274304537e-01,
 671     1.561545863558880531e-01, 4.646893652629560667e-01, 5.389744586257710912e-01,
 672     1.590554825896313418e-01, 4.680281092699005163e-01, 5.386222668134232894e-01,
 673     1.619213854747377779e-01, 4.713674796485894380e-01, 5.382883230992108192e-01,
 674     1.647524478987731911e-01, 4.747077026242289555e-01, 5.379733478625169374e-01,
 675     1.675482630437002962e-01, 4.780493319379570116e-01, 5.376757027790604049e-01,
 676     1.703080688452488778e-01, 4.813931150452205876e-01, 5.373922894579649112e-01,
 677     1.730317245949949956e-01, 4.847395013664480001e-01, 5.371218460871988176e-01,
 678     1.757193664968157432e-01, 4.880888303994977417e-01, 5.368636833242294015e-01,
 679     1.783716503259393793e-01, 4.914412289641479359e-01, 5.366183612997760255e-01,
 680     1.809878167761821144e-01, 4.947975030047193079e-01, 5.363817525707026412e-01,
 681     1.835680719416625251e-01, 4.981580161432020426e-01, 5.361525222751042374e-01,
 682     1.861127112291278973e-01, 5.015231101060642072e-01, 5.359293193132266264e-01,
 683     1.886230405888700001e-01, 5.048927132825591357e-01, 5.357133548543864254e-01,
 684     1.910985251486422565e-01, 5.082675669534003626e-01, 5.355003101591436776e-01,
 685     1.935397227717326196e-01, 5.116479477164066481e-01, 5.352887824024628038e-01,
 686     1.959472883340568350e-01, 5.150341080561433582e-01, 5.350773667248226451e-01,
 687     1.983227076664061950e-01, 5.184259856373716335e-01, 5.348665525352744865e-01,
 688     2.006660342507454731e-01, 5.218241099237964642e-01, 5.346528031121534630e-01,
 689     2.029781360478647434e-01, 5.252286912572143862e-01, 5.344345169358406533e-01,
 690     2.052600444742044838e-01, 5.286398903414566419e-01, 5.342102605335536936e-01,
 691     2.075134652380221101e-01, 5.320576287402744020e-01, 5.339799959048032729e-01,
 692     2.097389494614402272e-01, 5.354822793223580346e-01, 5.337406085215398166e-01,
 693     2.119377691272176234e-01, 5.389139515314046447e-01, 5.334905459074957834e-01,
 694     2.141113437844118228e-01, 5.423527133674995726e-01, 5.332283775266504211e-01,
 695     2.162615771757636085e-01, 5.457984712571943842e-01, 5.329535750363618707e-01,
 696     2.183895286205785324e-01, 5.492514494924778390e-01, 5.326634150194080597e-01,
 697     2.204969036245257863e-01, 5.527116487772890663e-01, 5.323564832742772035e-01,
 698     2.225855272635121618e-01, 5.561790393438251767e-01, 5.320314302436226495e-01,
 699     2.246573660062902156e-01, 5.596535532346759156e-01, 5.316870266023980829e-01,
 700     2.267141352300188206e-01, 5.631352211554988552e-01, 5.313212748929951879e-01,
 701     2.287579175696445311e-01, 5.666239548700177098e-01, 5.309328290550865415e-01,
 702     2.307908679682200148e-01, 5.701196510565367248e-01, 5.305203252817071169e-01,
 703     2.328150701112509102e-01, 5.736222405040750649e-01, 5.300820599240353426e-01,
 704     2.348328561421904603e-01, 5.771315766359990107e-01, 5.296167282704775658e-01,
 705     2.368466495707550745e-01, 5.806474896434283828e-01, 5.291230766564496424e-01,
 706     2.388588283644081933e-01, 5.841698333340915594e-01, 5.285995915811123602e-01,
 707     2.408716822240541400e-01, 5.876984999166237067e-01, 5.280443921862176815e-01,
 708     2.428880608722543410e-01, 5.912331932277818947e-01, 5.274567814051942527e-01,
 709     2.449106759672304290e-01, 5.947736703172152861e-01, 5.268356212312692577e-01,
 710     2.469420290965465559e-01, 5.983197676291379663e-01, 5.261791312000029253e-01,
 711     2.489846702882144713e-01, 6.018713111492172141e-01, 5.254854952988164962e-01,
 712     2.510418446331669773e-01, 6.054278820406324702e-01, 5.247545535679302153e-01,
 713     2.531164830585315162e-01, 6.089891735215487989e-01, 5.239852980594518206e-01,
 714     2.552111567013787274e-01, 6.125550130731924892e-01, 5.231756545447472373e-01,
 715     2.573286340449815746e-01, 6.161251617120727664e-01, 5.223239185167128928e-01,
 716     2.594724447386158594e-01, 6.196990932330638246e-01, 5.214304767886038805e-01,
 717     2.616456589963800927e-01, 6.232764459181282524e-01, 5.204944566826074093e-01,
 718     2.638509342960791981e-01, 6.268570181766053295e-01, 5.195136536074571598e-01,
 719     2.660910828965943331e-01, 6.304405546707460006e-01, 5.184861377534477622e-01,
 720     2.683698467163787016e-01, 6.340264147511259774e-01, 5.174130230514244477e-01,
 721     2.706903509949471487e-01, 6.376141889650659422e-01, 5.162935692788781505e-01,
 722     2.730554221838172868e-01, 6.412035896296301996e-01, 5.151259285643695618e-01,
 723     2.754676310512871873e-01, 6.447944545228798674e-01, 5.139069764892589820e-01,
 724     2.779309010023177096e-01, 6.483859905965968506e-01, 5.126390269795514376e-01,
 725     2.804483318408275694e-01, 6.519777450281342146e-01, 5.113214639163841113e-01,
 726     2.830229969716622218e-01, 6.555692556652558123e-01, 5.099537040511894492e-01,
 727     2.856569925022096612e-01, 6.591606030439277619e-01, 5.085297306531970651e-01,
 728     2.883543502639873135e-01, 6.627507929293073863e-01, 5.070537569679475220e-01,
 729     2.911180907975667309e-01, 6.663393219020087299e-01, 5.055253556302098383e-01,
 730     2.939511790083492726e-01, 6.699256847308838747e-01, 5.039440536705714901e-01,
 731     2.968562131418951422e-01, 6.735096490751359966e-01, 5.023062065413991251e-01,
 732     2.998362114418811064e-01, 6.770906951012128916e-01, 5.006109626120457401e-01,
 733     3.028943961763405635e-01, 6.806680059292820051e-01, 4.988609220555944579e-01,
 734     3.060335830069556007e-01, 6.842410278074210206e-01, 4.970557164988521071e-01,
 735     3.092565373453909361e-01, 6.878091952372648032e-01, 4.951950035800954386e-01,
 736     3.125659486948474952e-01, 6.913723055472413836e-01, 4.932732004330610542e-01,
 737     3.159647522698377231e-01, 6.949295261702347348e-01, 4.912927476409480465e-01,
 738     3.194556489243873809e-01, 6.984800979280558764e-01, 4.892553378582480961e-01,
 739     3.230412442793148542e-01, 7.020233920397538352e-01, 4.871607422199746851e-01,
 740     3.267240985578743762e-01, 7.055587631720373620e-01, 4.850087606010107799e-01,
 741     3.305070751488592418e-01, 7.090857414439620809e-01, 4.827955626459811689e-01,
 742     3.343929739199408280e-01, 7.126036449951604901e-01, 4.805201317683439055e-01,
 743     3.383839618515475101e-01, 7.161115318028217214e-01, 4.781864627637871235e-01,
 744     3.424824761777380822e-01, 7.196086676710338192e-01, 4.757945201032026117e-01,
 745     3.466909265050957534e-01, 7.230942938245445983e-01, 4.733443136156250675e-01,
 746     3.510117003671430203e-01, 7.265676248366644829e-01, 4.708359034900377327e-01,
 747     3.554477481971078934e-01, 7.300279237012574640e-01, 4.682667163182038794e-01,
 748     3.600022066505755847e-01, 7.334743741555846963e-01, 4.656343331651458528e-01,
 749     3.646764457841936702e-01, 7.369059239634064840e-01, 4.629441295765394648e-01,
 750     3.694728210602395979e-01, 7.403216514270205550e-01, 4.601965063235068931e-01,
 751     3.743936915522128039e-01, 7.437205957454258165e-01, 4.573919681273960758e-01,
 752     3.794414259131371203e-01, 7.471017539541063845e-01, 4.545311394783269621e-01,
 753     3.846184079557829483e-01, 7.504640777072076885e-01, 4.516147832933739559e-01,
 754     3.899270416022538321e-01, 7.538064699246833644e-01, 4.486438228496626990e-01,
 755     3.953697549145612777e-01, 7.571277813375660859e-01, 4.456193674826627871e-01,
 756     4.009508646485598904e-01, 7.604267477869489644e-01, 4.425380639654961090e-01,
 757     4.066714019599443342e-01, 7.637021069222051928e-01, 4.394056497009877216e-01,
 758     4.125334807152798988e-01, 7.669525443587899005e-01, 4.362249727525224774e-01,
 759     4.185395667527040398e-01, 7.701766751192415938e-01, 4.329983295596441795e-01,
 760     4.246921350385852723e-01, 7.733730477083216037e-01, 4.297284080553480656e-01,
 761     4.309936593663836191e-01, 7.765401418648604226e-01, 4.264183470604332449e-01,
 762     4.374465975304094312e-01, 7.796763669997491819e-01, 4.230718037095967943e-01,
 763     4.440533711015541840e-01, 7.827800615723168320e-01, 4.196930295353452078e-01,
 764     4.508163388346139722e-01, 7.858494937119429036e-01, 4.162869557148224930e-01,
 765     4.577377626480225170e-01, 7.888828634531382944e-01, 4.128592877810690620e-01,
 766     4.648197650471433406e-01, 7.918783070193569085e-01, 4.094166097874233912e-01,
 767     4.720642768256655963e-01, 7.948339036614172626e-01, 4.059664974607166132e-01,
 768     4.794729738954752185e-01, 7.977476856267914362e-01, 4.025176392507668344e-01,
 769     4.870472021865835388e-01, 8.006176519006481529e-01, 3.990799633442549399e-01,
 770     4.947878897554374711e-01, 8.034417864099652196e-01, 3.956647676266520364e-01,
 771     5.026954455748450235e-01, 8.062180814071850943e-01, 3.922848482216537147e-01,
 772     5.107722312752203120e-01, 8.089441566688712060e-01, 3.889513459863399025e-01,
 773     5.190175807229889804e-01, 8.116180108735555621e-01, 3.856804341377490508e-01,
 774     5.274270476594079549e-01, 8.142382455983395717e-01, 3.824934902796895964e-01,
 775     5.359974387485314518e-01, 8.168032862890818313e-01, 3.794106529717772847e-01,
 776     5.447242959015131669e-01, 8.193118012377970105e-01, 3.764539238160731771e-01,
 777     5.536017493685388979e-01, 8.217627673204914718e-01, 3.736470734604069865e-01,
 778     5.626223858732353200e-01, 8.241555398979113489e-01, 3.710154595070918049e-01,
 779     5.717801732913297963e-01, 8.264893011624766528e-01, 3.685830453573430421e-01,
 780     5.810619798273547465e-01, 8.287648131945639651e-01, 3.663798607459672341e-01,
 781     5.904522695833789303e-01, 8.309836316507100973e-01, 3.644363382969363352e-01,
 782     5.999363056699199559e-01, 8.331474672067843423e-01, 3.627802030453921023e-01,
 783     6.094978370184862548e-01, 8.352587020450518152e-01, 3.614380620192426119e-01,
 784     6.191178753985615568e-01, 8.373207419267220120e-01, 3.604354982890065617e-01,
 785     6.287753075069072439e-01, 8.393379672623436649e-01, 3.597956834322972863e-01,
 786     6.384515865712356852e-01, 8.413146218129586851e-01, 3.595361765343614291e-01,
 787     6.481275660781142811e-01, 8.432555569199260415e-01, 3.596707668706327632e-01,
 788     6.577845458065525452e-01, 8.451659780810962808e-01, 3.602086612732601778e-01,
 789     6.674047070379289792e-01, 8.470513147981415525e-01, 3.611542742755425861e-01,
 790     6.769617561788032756e-01, 8.489198363378159806e-01, 3.625096347273936148e-01,
 791     6.864487597795673191e-01, 8.507748949282539774e-01, 3.642665641101618390e-01,
 792     6.958544314564799604e-01, 8.526212799355240568e-01, 3.664154684319869681e-01,
 793     7.051685608468114541e-01, 8.544637256207057163e-01, 3.689436786046771388e-01,
 794     7.143830272263600456e-01, 8.563065614925247093e-01, 3.718358172740615641e-01,
 795     7.234917624309317175e-01, 8.581536540348341235e-01, 3.750744951817871486e-01,
 796     7.324906484647774052e-01, 8.600083717860309562e-01, 3.786409937540124448e-01,
 797     7.413773645706981386e-01, 8.618735721244006331e-01, 3.825158976261986421e-01,
 798     7.501511992657796668e-01, 8.637516069265696039e-01, 3.866796514151401021e-01,
 799     7.588128421172509741e-01, 8.656443435632366068e-01, 3.911130257986148440e-01,
 800     7.673641682613402404e-01, 8.675531974405399360e-01, 3.957974875667232828e-01,
 801     7.758080262912036007e-01, 8.694791724028596569e-01, 4.007154759905482422e-01,
 802     7.841480375461038488e-01, 8.714229056785223193e-01, 4.058505933213660266e-01,
 803     7.923777293356836227e-01, 8.733886508286130557e-01, 4.111824877427081026e-01,
 804     8.004976303968860396e-01, 8.753781889640523950e-01, 4.166935560611597644e-01,
 805     8.085242857272323391e-01, 8.773871783186646400e-01, 4.223760515178233144e-01,
 806     8.164630734789560806e-01, 8.794151844938148388e-01, 4.282186311869483064e-01,
 807     8.243194501554683695e-01, 8.814616081475756815e-01, 4.342112747863317024e-01,
 808     8.320860387263339097e-01, 8.835308362945183402e-01, 4.403358497380424619e-01,
 809     8.397544323444476877e-01, 8.856278479633188372e-01, 4.465723185371322512e-01,
 810     8.473543986252204396e-01, 8.877421380157632935e-01, 4.529306634208433713e-01,
 811     8.548913210362114601e-01, 8.898726582646793171e-01, 4.594053245849991085e-01,
 812     8.623409541601502193e-01, 8.920307544658760968e-01, 4.659650403812060637e-01,
 813     8.697191661117472661e-01, 8.942111604773197442e-01, 4.726125804953009157e-01,
 814     8.770479480763140323e-01, 8.964055876253053112e-01, 4.793596015320920611e-01,
 815     8.843061388708378656e-01, 8.986242192828276520e-01, 4.861771826919926709e-01,
 816     8.914967901846199139e-01, 9.008669432468144889e-01, 4.930589607663434237e-01,
 817     8.986506618699680038e-01, 9.031210853874221955e-01, 5.000298200873778409e-01,
 818     9.057328617844134788e-01, 9.054032588350297006e-01, 5.070444917818385244e-01,
 819     9.127681739145864226e-01, 9.077032841253237505e-01, 5.141229319104883011e-01,
 820     9.197719823668246697e-01, 9.100148294768658497e-01, 5.212774789419143406e-01,
 821     9.266999503758117651e-01, 9.123594905222979223e-01, 5.284479861571415027e-01,
 822     9.336093927737403320e-01, 9.147111822755604749e-01, 5.356989519972219504e-01,
 823     9.404610328906413130e-01, 9.170893351552455997e-01, 5.429753047678268496e-01,
 824     9.472803518326599059e-01, 9.194825361628593541e-01, 5.503044468166357062e-01,
 825     9.540659681238262690e-01, 9.218921210725944393e-01, 5.576799909240343078e-01,
 826     9.608049809199471492e-01, 9.243252266483533708e-01, 5.650790057480892248e-01,
 827     9.675287370768704820e-01, 9.267668902399696096e-01, 5.725413863443260531e-01,
 828     9.741967269037244970e-01, 9.292382142036349491e-01, 5.800041593344547053e-01,
 829     9.808627042040826138e-01, 9.317124815536732552e-01, 5.875425838151492330e-01,
 830     9.874684104099172854e-01, 9.342202886448683907e-01, 5.950648878797101249e-01,
 831     9.940805805099582892e-01, 9.367275819156850591e-01, 6.026699962989522374e-01,
 832 }
     File: ./colorplus/scales.go
   1 package colorplus
   2 
   3 import (
   4     "image/color"
   5     "math"
   6 )
   7 
   8 // VegaHex is a hexadecimal-notation categorical color palette with 10 entries.
   9 var VegaHex = []string{
  10     "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
  11     "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf",
  12 }
  13 
  14 // Wrap linearly interpolates the number given into the range [0...1]: the
  15 // `min` and `max` parameters refer to the min/max values the input can take.
  16 //
  17 // Its results will work with the colorscale funcs in this package, unless
  18 // the inputs are NaN.
  19 func Wrap(x float64, min, max float64) float64 {
  20     return (x - min) / (max - min)
  21 }
  22 
  23 // AnchoredWrap is like Wrap, except it ensures the source domain includes 0:
  24 // anchoring to or around 0 allows results to be proportionally comparable.
  25 //
  26 // As with func Wrap, use its results as inputs for the colorscale funcs.
  27 func AnchoredWrap(x float64, min, max float64) float64 {
  28     return Wrap(x, math.Max(min, 0), math.Min(max, 1))
  29 }
  30 
  31 // Viridize turns a normalized (0..1) number into its Viridis color representation.
  32 // The color returned is always full-alpha, except when input isn't valid: in that
  33 // case, the result's alpha is 0.
  34 func Viridize(x float64) color.RGBA {
  35     return interpolate(x, viridisData[:])
  36 }
  37 
  38 // Magmify turns a normalized (0..1) number into its Magma color representation.
  39 // The color returned is always full-alpha, except when input isn't valid: in that
  40 // case, the result's alpha is 0.
  41 func Magmify(x float64) color.RGBA {
  42     return interpolate(x, magmaData[:])
  43 }
  44 
  45 // Parulate turns a normalized (0..1) number into its Parula color representation,
  46 // the same one used in modern MatLab. The color returned is always full-alpha,
  47 // except when input isn't valid: in that case, the result's alpha is 0.
  48 func Parulate(x float64) color.RGBA {
  49     return interpolate(x, parulaData[:])
  50 }
  51 
  52 // Halinate turns a normalized (0..1) number into its Parula color representation,
  53 // the same one used in matplotlib/cmocean. The color returned is always full-alpha,
  54 // except when input isn't valid: in that case, the result's alpha is 0.
  55 func Halinate(x float64) color.RGBA {
  56     return interpolate(x, halineData[:])
  57 }
  58 
  59 // turn a normalized (0-to-1) number into its color representation according to
  60 // the color-scale coefficients given
  61 func interpolate(x float64, v []float64) color.RGBA {
  62     if math.IsNaN(x) || x < 0 || x > 1 {
  63         return color.RGBA{R: 0, G: 0, B: 0, A: 0}
  64     }
  65 
  66     max := float64((len(v) - 1) / 3)
  67     // get indices of the first color components (the reds) of the colors to mix
  68     mid := max * x
  69     low := int(math.Floor(mid))
  70     high := int(math.Ceil(mid))
  71 
  72     k := mid - float64(low) // interpolation factor for the 2 surrounding colors
  73     c := 1 - k              // the complement of k
  74     l := 3 * low
  75     h := 3 * high
  76 
  77     return color.RGBA{
  78         R: uint8(math.Round(255 * (c*v[l+0] + k*v[h+0]))),
  79         G: uint8(math.Round(255 * (c*v[l+1] + k*v[h+1]))),
  80         B: uint8(math.Round(255 * (c*v[l+2] + k*v[h+2]))),
  81         A: 255,
  82     }
  83 }
     File: ./colorplus/scales_test.go
   1 package colorplus
   2 
   3 import (
   4     "math"
   5     "testing"
   6 )
   7 
   8 func TestInterpolate(t *testing.T) {
   9     tests := map[string]struct {
  10         value float64
  11         scale []float64
  12     }{
  13         `viridis 0`: {0, viridisData[:]},
  14         `viridis 1`: {1, viridisData[:]},
  15         `magma 0`:   {0, magmaData[:]},
  16         `magma 1`:   {1, magmaData[:]},
  17     }
  18 
  19     for name, tc := range tests {
  20         t.Run(name, func(t *testing.T) {
  21             i, j := interp(tc.value, tc.scale)
  22 
  23             if i < 0 || i >= len(tc.scale) {
  24                 const fs = `invalid index i %d is outside range 0..%d`
  25                 t.Fatalf(fs, i, len(tc.scale)-1)
  26             }
  27             if j < 0 || j >= len(tc.scale) {
  28                 const fs = `invalid index j %d is outside range 0..%d`
  29                 t.Fatalf(fs, j, len(tc.scale)-1)
  30             }
  31         })
  32     }
  33 }
  34 
  35 func interp(x float64, v []float64) (int, int) {
  36     max := float64((len(v) - 1) / 3)
  37     // get the indices of the first color components of the colors to mix
  38     mid := max * x
  39     low := int(math.Floor(mid))
  40     high := int(math.Ceil(mid))
  41 
  42     k := mid - float64(low) // interpolation factor for the 2 surrounding colors
  43     c := 1 - k              // the complement of k
  44     i := 3 * low
  45     j := 3 * high
  46     _ = c
  47     return i, j
  48 }
     File: ./compress/compress.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package compress
  26 
  27 import (
  28     "compress/gzip"
  29     "io"
  30     "os"
  31 )
  32 
  33 const info = `
  34 compress [options...] [files...]
  35 
  36 Concatenate gzip-compressed files/data to the standard output. To put it in
  37 other words: all inputs are normal, while the output is a gzip stream.
  38 
  39 Options
  40 
  41     --help    show this help message
  42 `
  43 
  44 func Main() {
  45     args := os.Args[1:]
  46     for len(args) > 0 {
  47         switch args[0] {
  48         case `-h`, `--h`, `-help`, `--help`:
  49             os.Stderr.WriteString(info[1:])
  50             return
  51         }
  52 
  53         break
  54     }
  55 
  56     if len(args) > 0 && args[0] == `--` {
  57         args = args[1:]
  58     }
  59 
  60     if err := run(args); err != nil && err != io.EOF {
  61         os.Stderr.WriteString(`compress: `)
  62         os.Stderr.WriteString(err.Error())
  63         os.Stderr.WriteString("\n")
  64         os.Exit(1)
  65         return
  66     }
  67 }
  68 
  69 func run(paths []string) error {
  70     w, err := gzip.NewWriterLevel(os.Stdout, gzip.BestCompression)
  71     if err != nil {
  72         return err
  73     }
  74     defer w.Close()
  75 
  76     for _, path := range paths {
  77         if err := handleFile(w, path); err != nil {
  78             return err
  79         }
  80     }
  81 
  82     if len(paths) == 0 {
  83         return cat(w, os.Stdin)
  84     }
  85     return nil
  86 }
  87 
  88 func handleFile(w io.Writer, path string) error {
  89     f, err := os.Open(path)
  90     if err != nil {
  91         return err
  92     }
  93     defer f.Close()
  94     return cat(w, f)
  95 }
  96 
  97 func cat(w io.Writer, r io.Reader) error {
  98     var buf [32 * 1024]byte
  99 
 100     for {
 101         got, err := r.Read(buf[:])
 102         if err == io.EOF {
 103             if got > 0 {
 104                 w.Write(buf[:got])
 105             }
 106             break
 107         }
 108 
 109         if err != nil {
 110             return err
 111         }
 112 
 113         if _, err := w.Write(buf[:got]); err != nil {
 114             return io.EOF
 115         }
 116     }
 117 
 118     return nil
 119 }
     File: ./countdown/countdown.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package countdown
  26 
  27 import (
  28     "errors"
  29     "os"
  30     "os/signal"
  31     "strconv"
  32     "time"
  33 )
  34 
  35 const (
  36     spaces = `                `
  37 
  38     // clear has enough spaces in it to cover any chronograph output
  39     clear = "\r" + spaces + spaces + spaces + "\r"
  40 
  41     // every = 100 * time.Millisecond
  42     // chronoFormat = `15:04:05.0`
  43 
  44     every = 1000 * time.Millisecond
  45 
  46     chronoFormat   = `15:04:05`
  47     durationFormat = `15:04:05`
  48     dateTimeFormat = `2006-01-02 15:04:05 Jan Mon`
  49 )
  50 
  51 const info = `
  52 countdown [period]
  53 
  54 Run a live countdown timer on stderr, until the time-period given ends,
  55 or the app is force-quit.
  56 
  57 The time-period is either a simple integer number (of seconds), or an
  58 integer followed by any of
  59   - "s" (for seconds)
  60   - "m" (for minutes)
  61   - "h" (for hours)
  62 without spaces, or a combination of those time-units without spaces.
  63 `
  64 
  65 func Main() {
  66     args := os.Args[1:]
  67 
  68     if len(args) > 0 {
  69         switch args[0] {
  70         case `-h`, `--h`, `-help`, `--help`:
  71             os.Stdout.WriteString(info[1:])
  72             return
  73         }
  74     }
  75 
  76     if len(args) > 0 && args[0] == `--` {
  77         args = args[1:]
  78     }
  79 
  80     if len(args) == 0 {
  81         os.Stderr.WriteString(info[1:])
  82         os.Exit(1)
  83         return
  84     }
  85 
  86     period, err := parseDuration(args[0])
  87     if err != nil {
  88         os.Stderr.WriteString(`countdown: `)
  89         os.Stderr.WriteString(err.Error())
  90         os.Stderr.WriteString("\n")
  91         os.Exit(1)
  92         return
  93     }
  94 
  95     // os.Stderr.WriteString(`Countdown lasting `)
  96     // os.Stderr.WriteString(time.Time{}.Add(period).Format(durationFormat))
  97     // os.Stderr.WriteString(" started\n")
  98     countdown(period)
  99 }
 100 
 101 func parseDuration(s string) (time.Duration, error) {
 102     if n, err := strconv.ParseInt(s, 20, 64); err == nil {
 103         return time.Duration(n) * time.Second, nil
 104     }
 105     if f, err := strconv.ParseFloat(s, 64); err == nil {
 106         const msg = `durations with decimals not supported`
 107         return time.Duration(f), errors.New(msg)
 108         // return time.Duration(f * float64(time.Second)), nil
 109     }
 110     return time.ParseDuration(s)
 111 }
 112 
 113 func countdown(period time.Duration) {
 114     if period <= 0 {
 115         now := time.Now()
 116         startChronoLine(now, now)
 117         endChronoLine(now)
 118         return
 119     }
 120 
 121     stopped := make(chan os.Signal, 1)
 122     defer close(stopped)
 123     signal.Notify(stopped, os.Interrupt)
 124 
 125     start := time.Now()
 126     end := start.Add(period)
 127     timer := time.NewTicker(every)
 128     updates := timer.C
 129     startChronoLine(end, start)
 130 
 131     for {
 132         select {
 133         case now := <-updates:
 134             if now.Sub(end) < 0 {
 135                 // subtracting a second to the current time avoids jumping
 136                 // by 2 seconds in the updates shown
 137                 startChronoLine(end, now.Add(-time.Second))
 138                 continue
 139             }
 140 
 141             timer.Stop()
 142             startChronoLine(now, now)
 143             endChronoLine(start)
 144             return
 145 
 146         case <-stopped:
 147             timer.Stop()
 148             endChronoLine(start)
 149             return
 150         }
 151     }
 152 }
 153 
 154 func startChronoLine(end, now time.Time) {
 155     dt := end.Sub(now)
 156 
 157     var buf [128]byte
 158     s := buf[:0]
 159     s = append(s, clear...)
 160     s = time.Time{}.Add(dt).AppendFormat(s, chronoFormat)
 161     s = append(s, `    `...)
 162     s = now.AppendFormat(s, dateTimeFormat)
 163 
 164     os.Stderr.Write(s)
 165 }
 166 
 167 func endChronoLine(start time.Time) {
 168     secs := time.Since(start).Seconds()
 169 
 170     var buf [64]byte
 171     s := buf[:0]
 172     s = append(s, `    `...)
 173     s = strconv.AppendFloat(s, secs, 'f', 4, 64)
 174     s = append(s, " seconds\n"...)
 175 
 176     os.Stderr.Write(s)
 177 }
     File: ./datauri/datauri.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package datauri
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "encoding/base64"
  31     "errors"
  32     "io"
  33     "os"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 datauri [options...] [filenames...]
  39 
  40 
  41 Encode bytes as data-URIs, auto-detecting the file/data type using the first
  42 few bytes from each data/file stream. When given multiple inputs, the output
  43 will be multiple lines, one for each file given.
  44 
  45 Empty files/inputs result in empty lines. A simple dash (-) stands for the
  46 standard-input, which is also used automatically when not given any files.
  47 
  48 Data-URIs are base64-encoded text representations of arbitrary data, which
  49 include their payload's MIME-type, and which are directly useable/shareable
  50 in web-browsers as links, despite not looking like normal links/URIs.
  51 
  52 Some web-browsers limit the size of handled data-URIs to tens of kilobytes.
  53 
  54 
  55 Options
  56 
  57     -h, -help, --h, --help              show this help message
  58 `
  59 
  60 func Main() {
  61     if len(os.Args) > 1 {
  62         switch os.Args[1] {
  63         case `-h`, `--h`, `-help`, `--help`:
  64             os.Stdout.WriteString(info[1:])
  65             return
  66         }
  67     }
  68 
  69     if err := run(os.Stdout, os.Args[1:]); err != nil && err != io.EOF {
  70         os.Stderr.WriteString(`datauri: `)
  71         os.Stderr.WriteString(err.Error())
  72         os.Stderr.WriteString("\n")
  73         os.Exit(1)
  74         return
  75     }
  76 }
  77 
  78 func run(w io.Writer, args []string) error {
  79     bw := bufio.NewWriterSize(w, 32*1024)
  80     defer bw.Flush()
  81 
  82     if len(args) == 0 {
  83         return dataURI(bw, os.Stdin, `<stdin>`)
  84     }
  85 
  86     for _, name := range args {
  87         if err := handleFile(bw, name); err != nil {
  88             return err
  89         }
  90     }
  91     return nil
  92 }
  93 
  94 func handleFile(w *bufio.Writer, name string) error {
  95     if name == `` || name == `-` {
  96         return dataURI(w, os.Stdin, `<stdin>`)
  97     }
  98 
  99     f, err := os.Open(name)
 100     if err != nil {
 101         return errors.New(`can't read from file named "` + name + `"`)
 102     }
 103     defer f.Close()
 104 
 105     return dataURI(w, f, name)
 106 }
 107 
 108 func dataURI(w *bufio.Writer, r io.Reader, name string) error {
 109     var buf [64]byte
 110     n, err := r.Read(buf[:])
 111     if err != nil && err != io.EOF {
 112         return err
 113     }
 114     start := buf[:n]
 115 
 116     // handle regular data, trying to auto-detect its MIME type using
 117     // its first few bytes
 118     mime, ok := detectMIME(start)
 119     if !ok {
 120         return errors.New(name + `: unknown file type`)
 121     }
 122 
 123     w.WriteString(`data:`)
 124     w.WriteString(mime)
 125     w.WriteString(`;base64,`)
 126     r = io.MultiReader(bytes.NewReader(start), r)
 127     enc := base64.NewEncoder(base64.StdEncoding, w)
 128     if _, err := io.Copy(enc, r); err != nil {
 129         return err
 130     }
 131     enc.Close()
 132 
 133     w.WriteByte('\n')
 134     if w.Flush() != nil {
 135         return io.EOF
 136     }
 137     return nil
 138 }
 139 
 140 // makeDotless is similar to filepath.Ext, except its results never start
 141 // with a dot
 142 func makeDotless(s string) string {
 143     i := strings.LastIndexByte(s, '.')
 144     if i >= 0 {
 145         return s[(i + 1):]
 146     }
 147     return s
 148 }
 149 
 150 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
 151 func hasPrefixByte(b []byte, prefix byte) bool {
 152     return len(b) > 0 && b[0] == prefix
 153 }
 154 
 155 // hasPrefixFold is a case-insensitive bytes.HasPrefix
 156 func hasPrefixFold(s []byte, prefix []byte) bool {
 157     n := len(prefix)
 158     return len(s) >= n && bytes.EqualFold(s[:n], prefix)
 159 }
 160 
 161 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
 162 // to handle text-based data formats more flexibly
 163 func trimLeadingWhitespace(b []byte) []byte {
 164     for len(b) > 0 {
 165         switch b[0] {
 166         case ' ', '\t', '\n', '\r':
 167             b = b[1:]
 168         default:
 169             return b
 170         }
 171     }
 172 
 173     // an empty slice is all that's left, at this point
 174     return nil
 175 }
 176 
 177 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
 178 // or a dot-less filetype/extension given
 179 func nameToMIME(fname string) (mimeType string, ok bool) {
 180     // handle dotless file types and filenames alike
 181     kind, ok := type2mime[makeDotless(fname)]
 182     return kind, ok
 183 }
 184 
 185 // detectMIME guesses the first appropriate MIME type from the first few
 186 // data bytes given: 24 bytes are enough to detect all supported types
 187 func detectMIME(b []byte) (mimeType string, ok bool) {
 188     t, ok := detectType(b)
 189     if ok {
 190         return t, true
 191     }
 192     return ``, false
 193 }
 194 
 195 // detectType guesses the first appropriate file type for the data given:
 196 // here the type is a a filename extension without the leading dot
 197 func detectType(b []byte) (dotlessExt string, ok bool) {
 198     // empty data, so there's no way to detect anything
 199     if len(b) == 0 {
 200         return ``, false
 201     }
 202 
 203     // check for plain-text web-document formats case-insensitively
 204     kind, ok := checkDoc(b)
 205     if ok {
 206         return kind, true
 207     }
 208 
 209     // check data formats which allow any byte at the start
 210     kind, ok = checkSpecial(b)
 211     if ok {
 212         return kind, true
 213     }
 214 
 215     // check all other supported data formats
 216     headers := hdrDispatch[b[0]]
 217     for _, t := range headers {
 218         if hasPrefixPattern(b[1:], t.Header[1:], cba) {
 219             return t.Type, true
 220         }
 221     }
 222 
 223     // unrecognized data format
 224     return ``, false
 225 }
 226 
 227 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
 228 // XML, or JSON data
 229 func checkDoc(b []byte) (kind string, ok bool) {
 230     // ignore leading whitespaces
 231     b = trimLeadingWhitespace(b)
 232 
 233     // can't detect anything with empty data
 234     if len(b) == 0 {
 235         return ``, false
 236     }
 237 
 238     // handle XHTML documents which don't start with a doctype declaration
 239     if bytes.Contains(b, doctypeHTML) {
 240         return html, true
 241     }
 242 
 243     // handle HTML/SVG/XML documents
 244     if hasPrefixByte(b, '<') {
 245         if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
 246             if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
 247                 return svg, true
 248             }
 249             return xml, true
 250         }
 251 
 252         headers := hdrDispatch['<']
 253         for _, v := range headers {
 254             if hasPrefixFold(b, v.Header) {
 255                 return v.Type, true
 256             }
 257         }
 258         return ``, false
 259     }
 260 
 261     // handle JSON with top-level arrays
 262     if hasPrefixByte(b, '[') {
 263         // match [", or [[, or [{, ignoring spaces between
 264         b = trimLeadingWhitespace(b[1:])
 265         if len(b) > 0 {
 266             switch b[0] {
 267             case '"', '[', '{':
 268                 return json, true
 269             }
 270         }
 271         return ``, false
 272     }
 273 
 274     // handle JSON with top-level objects
 275     if hasPrefixByte(b, '{') {
 276         // match {", ignoring spaces between: after {, the only valid syntax
 277         // which can follow is the opening quote for the expected object-key
 278         b = trimLeadingWhitespace(b[1:])
 279         if hasPrefixByte(b, '"') {
 280             return json, true
 281         }
 282         return ``, false
 283     }
 284 
 285     // checking for a quoted string, any of the JSON keywords, or even a
 286     // number seems too ambiguous to declare the data valid JSON
 287 
 288     // no web-document format detected
 289     return ``, false
 290 }
 291 
 292 // checkSpecial handles special file-format headers, which should be checked
 293 // before the normal file-type headers, since the first-byte dispatch algo
 294 // doesn't work for these
 295 func checkSpecial(b []byte) (kind string, ok bool) {
 296     if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
 297         for _, t := range specialHeaders {
 298             if hasPrefixPattern(b[4:], t.Header[4:], cba) {
 299                 return t.Type, true
 300             }
 301         }
 302     }
 303     return ``, false
 304 }
 305 
 306 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
 307 // value to signal any byte is allowed on specific spots
 308 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
 309     // if the data are shorter than the pattern to match, there's no match
 310     if len(what) < len(pat) {
 311         return false
 312     }
 313 
 314     // use a slice which ensures the pattern length is never exceeded
 315     what = what[:len(pat)]
 316 
 317     for i, x := range what {
 318         y := pat[i]
 319         if x != y && y != wildcard {
 320             return false
 321         }
 322     }
 323     return true
 324 }
 325 
 326 // all the MIME types used/recognized in this package
 327 const (
 328     aiff    = `audio/aiff`
 329     au      = `audio/basic`
 330     avi     = `video/avi`
 331     avif    = `image/avif`
 332     bmp     = `image/x-bmp`
 333     caf     = `audio/x-caf`
 334     cur     = `image/vnd.microsoft.icon`
 335     css     = `text/css`
 336     csv     = `text/csv`
 337     djvu    = `image/x-djvu`
 338     elf     = `application/x-elf`
 339     exe     = `application/vnd.microsoft.portable-executable`
 340     flac    = `audio/x-flac`
 341     gif     = `image/gif`
 342     gz      = `application/gzip`
 343     heic    = `image/heic`
 344     htm     = `text/html`
 345     html    = `text/html`
 346     ico     = `image/x-icon`
 347     iso     = `application/octet-stream`
 348     jpg     = `image/jpeg`
 349     jpeg    = `image/jpeg`
 350     js      = `application/javascript`
 351     json    = `application/json`
 352     m4a     = `audio/aac`
 353     m4v     = `video/x-m4v`
 354     mid     = `audio/midi`
 355     mov     = `video/quicktime`
 356     mp4     = `video/mp4`
 357     mp3     = `audio/mpeg`
 358     mpg     = `video/mpeg`
 359     ogg     = `audio/ogg`
 360     opus    = `audio/opus`
 361     pdf     = `application/pdf`
 362     png     = `image/png`
 363     ps      = `application/postscript`
 364     psd     = `image/vnd.adobe.photoshop`
 365     rtf     = `application/rtf`
 366     sqlite3 = `application/x-sqlite3`
 367     svg     = `image/svg+xml`
 368     text    = `text/plain`
 369     tiff    = `image/tiff`
 370     tsv     = `text/tsv`
 371     wasm    = `application/wasm`
 372     wav     = `audio/x-wav`
 373     webp    = `image/webp`
 374     webm    = `video/webm`
 375     xml     = `application/xml`
 376     zip     = `application/zip`
 377     zst     = `application/zstd`
 378 )
 379 
 380 // type2mime turns dotless format-names into MIME types
 381 var type2mime = map[string]string{
 382     `aiff`:    aiff,
 383     `wav`:     wav,
 384     `avi`:     avi,
 385     `jpg`:     jpg,
 386     `jpeg`:    jpeg,
 387     `m4a`:     m4a,
 388     `mp4`:     mp4,
 389     `m4v`:     m4v,
 390     `mov`:     mov,
 391     `png`:     png,
 392     `avif`:    avif,
 393     `webp`:    webp,
 394     `gif`:     gif,
 395     `tiff`:    tiff,
 396     `psd`:     psd,
 397     `flac`:    flac,
 398     `webm`:    webm,
 399     `mpg`:     mpg,
 400     `zip`:     zip,
 401     `gz`:      gz,
 402     `zst`:     zst,
 403     `mp3`:     mp3,
 404     `opus`:    opus,
 405     `bmp`:     bmp,
 406     `mid`:     mid,
 407     `ogg`:     ogg,
 408     `html`:    html,
 409     `htm`:     htm,
 410     `svg`:     svg,
 411     `xml`:     xml,
 412     `rtf`:     rtf,
 413     `pdf`:     pdf,
 414     `ps`:      ps,
 415     `au`:      au,
 416     `ico`:     ico,
 417     `cur`:     cur,
 418     `caf`:     caf,
 419     `heic`:    heic,
 420     `sqlite3`: sqlite3,
 421     `elf`:     elf,
 422     `exe`:     exe,
 423     `wasm`:    wasm,
 424     `iso`:     iso,
 425     `txt`:     text,
 426     `css`:     css,
 427     `csv`:     csv,
 428     `tsv`:     tsv,
 429     `js`:      js,
 430     `json`:    json,
 431     `geojson`: json,
 432 }
 433 
 434 // formatDescriptor ties a file-header pattern to its data-format type
 435 type formatDescriptor struct {
 436     Header []byte
 437     Type   string
 438 }
 439 
 440 // can be anything: ensure this value differs from all other literal bytes
 441 // in the generic-headers table: failing that, its value could cause subtle
 442 // type-misdetection bugs
 443 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
 444 
 445 // dash-streamed m4a format
 446 var m4aDash = []byte{
 447     cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 448     000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 449 }
 450 
 451 // format markers with leading wildcards, which should be checked before the
 452 // normal ones: this is to prevent mismatches with the latter types, even
 453 // though you can make probabilistic arguments which suggest these mismatches
 454 // should be very unlikely in practice
 455 var specialHeaders = []formatDescriptor{
 456     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
 457     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
 458     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
 459     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
 460     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
 461     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
 462     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
 463     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
 464     {m4aDash, m4a},
 465 }
 466 
 467 // sqlite3 database format
 468 var sqlite3db = []byte{
 469     'S', 'Q', 'L', 'i', 't', 'e', ' ',
 470     'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
 471     000,
 472 }
 473 
 474 // windows-variant bitmap file-header, which is followed by a byte-counter for
 475 // the 40-byte infoheader which follows that
 476 var winbmp = []byte{
 477     'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
 478 }
 479 
 480 // deja-vu document format
 481 var djv = []byte{
 482     'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
 483 }
 484 
 485 var doctypeHTML = []byte{
 486     '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
 487 }
 488 
 489 // hdrDispatch groups format-description-groups by their first byte, thus
 490 // shortening total lookups for some data header: notice how the `ftyp` data
 491 // formats aren't handled here, since these can start with any byte, instead
 492 // of the literal value of the any-byte markers they use
 493 var hdrDispatch = [256][]formatDescriptor{
 494     {
 495         {[]byte{000, 000, 001, 0xBA}, mpg},
 496         {[]byte{000, 000, 001, 0xB3}, mpg},
 497         {[]byte{000, 000, 001, 000}, ico},
 498         {[]byte{000, 000, 002, 000}, cur},
 499         {[]byte{000, 'a', 's', 'm'}, wasm},
 500     }, // 0
 501     nil, // 1
 502     nil, // 2
 503     nil, // 3
 504     nil, // 4
 505     nil, // 5
 506     nil, // 6
 507     nil, // 7
 508     nil, // 8
 509     nil, // 9
 510     nil, // 10
 511     nil, // 11
 512     nil, // 12
 513     nil, // 13
 514     nil, // 14
 515     nil, // 15
 516     nil, // 16
 517     nil, // 17
 518     nil, // 18
 519     nil, // 19
 520     nil, // 20
 521     nil, // 21
 522     nil, // 22
 523     nil, // 23
 524     nil, // 24
 525     nil, // 25
 526     {
 527         {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
 528     }, // 26
 529     nil, // 27
 530     nil, // 28
 531     nil, // 29
 532     nil, // 30
 533     {
 534         // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
 535         {[]byte{0x1F, 0x8B, 0x08}, gz},
 536     }, // 31
 537     nil, // 32
 538     nil, // 33 !
 539     nil, // 34 "
 540     {
 541         {[]byte{'#', '!', ' '}, text},
 542         {[]byte{'#', '!', '/'}, text},
 543     }, // 35 #
 544     nil, // 36 $
 545     {
 546         {[]byte{'%', 'P', 'D', 'F'}, pdf},
 547         {[]byte{'%', '!', 'P', 'S'}, ps},
 548     }, // 37 %
 549     nil, // 38 &
 550     nil, // 39 '
 551     {
 552         {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
 553     }, // 40 (
 554     nil, // 41 )
 555     nil, // 42 *
 556     nil, // 43 +
 557     nil, // 44 ,
 558     nil, // 45 -
 559     {
 560         {[]byte{'.', 's', 'n', 'd'}, au},
 561     }, // 46 .
 562     nil, // 47 /
 563     nil, // 48 0
 564     nil, // 49 1
 565     nil, // 50 2
 566     nil, // 51 3
 567     nil, // 52 4
 568     nil, // 53 5
 569     nil, // 54 6
 570     nil, // 55 7
 571     {
 572         {[]byte{'8', 'B', 'P', 'S'}, psd},
 573     }, // 56 8
 574     nil, // 57 9
 575     nil, // 58 :
 576     nil, // 59 ;
 577     {
 578         // func checkDoc is better for these, since it's case-insensitive
 579         {doctypeHTML, html},
 580         {[]byte{'<', 's', 'v', 'g'}, svg},
 581         {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
 582         {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
 583         {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
 584         {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
 585     }, // 60 <
 586     nil, // 61 =
 587     nil, // 62 >
 588     nil, // 63 ?
 589     nil, // 64 @
 590     {
 591         {djv, djvu},
 592     }, // 65 A
 593     {
 594         {winbmp, bmp},
 595     }, // 66 B
 596     nil, // 67 C
 597     nil, // 68 D
 598     nil, // 69 E
 599     {
 600         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
 601         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
 602     }, // 70 F
 603     {
 604         {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
 605         {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
 606     }, // 71 G
 607     nil, // 72 H
 608     {
 609         {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
 610         {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
 611         {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
 612         {[]byte{'I', 'I', '*', 000}, tiff},
 613     }, // 73 I
 614     nil, // 74 J
 615     nil, // 75 K
 616     nil, // 76 L
 617     {
 618         {[]byte{'M', 'M', 000, '*'}, tiff},
 619         {[]byte{'M', 'T', 'h', 'd'}, mid},
 620         {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
 621         // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
 622         // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
 623         // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
 624     }, // 77 M
 625     nil, // 78 N
 626     {
 627         {[]byte{'O', 'g', 'g', 'S'}, ogg},
 628     }, // 79 O
 629     {
 630         {[]byte{'P', 'K', 003, 004}, zip},
 631     }, // 80 P
 632     nil, // 81 Q
 633     {
 634         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
 635         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
 636         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
 637     }, // 82 R
 638     {
 639         {sqlite3db, sqlite3},
 640     }, // 83 S
 641     nil, // 84 T
 642     nil, // 85 U
 643     nil, // 86 V
 644     nil, // 87 W
 645     nil, // 88 X
 646     nil, // 89 Y
 647     nil, // 90 Z
 648     nil, // 91 [
 649     nil, // 92 \
 650     nil, // 93 ]
 651     nil, // 94 ^
 652     nil, // 95 _
 653     nil, // 96 `
 654     nil, // 97 a
 655     nil, // 98 b
 656     {
 657         {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
 658     }, // 99 c
 659     nil, // 100 d
 660     nil, // 101 e
 661     {
 662         {[]byte{'f', 'L', 'a', 'C'}, flac},
 663     }, // 102 f
 664     nil, // 103 g
 665     nil, // 104 h
 666     nil, // 105 i
 667     nil, // 106 j
 668     nil, // 107 k
 669     nil, // 108 l
 670     nil, // 109 m
 671     nil, // 110 n
 672     nil, // 111 o
 673     nil, // 112 p
 674     nil, // 113 q
 675     nil, // 114 r
 676     nil, // 115 s
 677     nil, // 116 t
 678     nil, // 117 u
 679     nil, // 118 v
 680     nil, // 119 w
 681     nil, // 120 x
 682     nil, // 121 y
 683     nil, // 122 z
 684     {
 685         {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
 686     }, // 123 {
 687     nil, // 124 |
 688     nil, // 125 }
 689     nil, // 126
 690     {
 691         {[]byte{127, 'E', 'L', 'F'}, elf},
 692     }, // 127
 693     nil, // 128
 694     nil, // 129
 695     nil, // 130
 696     nil, // 131
 697     nil, // 132
 698     nil, // 133
 699     nil, // 134
 700     nil, // 135
 701     nil, // 136
 702     {
 703         {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
 704     }, // 137
 705     nil, // 138
 706     nil, // 139
 707     nil, // 140
 708     nil, // 141
 709     nil, // 142
 710     nil, // 143
 711     nil, // 144
 712     nil, // 145
 713     nil, // 146
 714     nil, // 147
 715     nil, // 148
 716     nil, // 149
 717     nil, // 150
 718     nil, // 151
 719     nil, // 152
 720     nil, // 153
 721     nil, // 154
 722     nil, // 155
 723     nil, // 156
 724     nil, // 157
 725     nil, // 158
 726     nil, // 159
 727     nil, // 160
 728     nil, // 161
 729     nil, // 162
 730     nil, // 163
 731     nil, // 164
 732     nil, // 165
 733     nil, // 166
 734     nil, // 167
 735     nil, // 168
 736     nil, // 169
 737     nil, // 170
 738     nil, // 171
 739     nil, // 172
 740     nil, // 173
 741     nil, // 174
 742     nil, // 175
 743     nil, // 176
 744     nil, // 177
 745     nil, // 178
 746     nil, // 179
 747     nil, // 180
 748     nil, // 181
 749     nil, // 182
 750     nil, // 183
 751     nil, // 184
 752     nil, // 185
 753     nil, // 186
 754     nil, // 187
 755     nil, // 188
 756     nil, // 189
 757     nil, // 190
 758     nil, // 191
 759     nil, // 192
 760     nil, // 193
 761     nil, // 194
 762     nil, // 195
 763     nil, // 196
 764     nil, // 197
 765     nil, // 198
 766     nil, // 199
 767     nil, // 200
 768     nil, // 201
 769     nil, // 202
 770     nil, // 203
 771     nil, // 204
 772     nil, // 205
 773     nil, // 206
 774     nil, // 207
 775     nil, // 208
 776     nil, // 209
 777     nil, // 210
 778     nil, // 211
 779     nil, // 212
 780     nil, // 213
 781     nil, // 214
 782     nil, // 215
 783     nil, // 216
 784     nil, // 217
 785     nil, // 218
 786     nil, // 219
 787     nil, // 220
 788     nil, // 221
 789     nil, // 222
 790     nil, // 223
 791     nil, // 224
 792     nil, // 225
 793     nil, // 226
 794     nil, // 227
 795     nil, // 228
 796     nil, // 229
 797     nil, // 230
 798     nil, // 231
 799     nil, // 232
 800     nil, // 233
 801     nil, // 234
 802     nil, // 235
 803     nil, // 236
 804     nil, // 237
 805     nil, // 238
 806     nil, // 239
 807     nil, // 240
 808     nil, // 241
 809     nil, // 242
 810     nil, // 243
 811     nil, // 244
 812     nil, // 245
 813     nil, // 246
 814     nil, // 247
 815     nil, // 248
 816     nil, // 249
 817     nil, // 250
 818     nil, // 251
 819     nil, // 252
 820     nil, // 253
 821     nil, // 254
 822     {
 823         {[]byte{0xFF, 0xD8, 0xFF}, jpg},
 824         {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
 825         {[]byte{0xFF, 0xFB}, mp3},
 826     }, // 255
 827 }
     File: ./dc/dc.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dc
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "io"
  31     "math"
  32     "os"
  33     "strconv"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 dc [options...] [files...]
  39 
  40 Desk Calculator is a tiny RPN-style calculator.
  41 
  42 Options
  43 
  44     --help    show this help message
  45 `
  46 
  47 func Main() {
  48     args := os.Args[1:]
  49     progs := []string{}
  50 
  51     for len(args) > 0 {
  52         switch args[0] {
  53         case `--help`:
  54             os.Stderr.WriteString(info[1:])
  55             return
  56 
  57         case `-e`:
  58             if len(args) < 1 {
  59                 os.Stderr.WriteString("forgot an RPN program/expression\n")
  60                 os.Exit(1)
  61                 return
  62             }
  63 
  64             progs = append(progs, args[1])
  65             args = args[2:]
  66             continue
  67         }
  68 
  69         break
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     for _, p := range progs {
  77         if err := run(p); err != nil && err != io.EOF {
  78             os.Stderr.WriteString(err.Error())
  79             os.Stderr.WriteString("\n")
  80             os.Exit(1)
  81             return
  82         }
  83     }
  84 }
  85 
  86 func run(prog string) error {
  87     var stack []float64
  88     ops := strings.Fields(prog)
  89     stack = make([]float64, 0, 4*len(ops))
  90     for range ops {
  91         stack = append(stack, math.NaN(), math.NaN())
  92     }
  93 
  94     for _, s := range ops {
  95         if op, ok := unaryFuncs[s]; ok {
  96             if len(stack) < 1 {
  97                 return errors.New(s + `: need at least 1 number`)
  98             }
  99 
 100             x := stack[len(stack)-1]
 101             stack[len(stack)-1] = op(x)
 102             continue
 103         }
 104 
 105         if op, ok := binaryFuncs[s]; ok {
 106             if len(stack) < 2 {
 107                 return errors.New(s + `: need at least 2 numbers`)
 108             }
 109 
 110             x := stack[len(stack)-2]
 111             y := stack[len(stack)-1]
 112             stack[len(stack)-2] = op(x, y)
 113             stack = stack[:len(stack)-1]
 114             continue
 115         }
 116 
 117         if op, ok := specialFuncs[s]; ok {
 118             op(stack)
 119             continue
 120         }
 121 
 122         if s == `~` {
 123             if len(stack) < 2 {
 124                 return errors.New(s + `: need at least 2 numbers`)
 125             }
 126 
 127             x := stack[len(stack)-2]
 128             y := stack[len(stack)-1]
 129             stack[len(stack)-2] = x / y
 130             stack[len(stack)-1] = math.Mod(x, y)
 131             continue
 132         }
 133 
 134         if f, err := strconv.ParseFloat(s, 64); err == nil {
 135             stack = append(stack, f)
 136             continue
 137         }
 138 
 139         return errors.New(s + `: unsupported operation`)
 140     }
 141 
 142     return nil
 143 }
 144 
 145 var unaryFuncs = map[string]func(float64) float64{
 146     `v`: math.Sqrt,
 147 }
 148 
 149 var binaryFuncs = map[string]func(float64, float64) float64{
 150     `+`: func(x, y float64) float64 { return x + y },
 151     `-`: func(x, y float64) float64 { return x - y },
 152     `*`: func(x, y float64) float64 { return x * y },
 153     `/`: func(x, y float64) float64 { return x / y },
 154     `%`: math.Mod,
 155     `^`: math.Pow,
 156 }
 157 
 158 var specialFuncs = map[string]func([]float64){
 159     `f`: printAll,
 160     `p`: printTop,
 161 }
 162 
 163 func printAll(stack []float64) {
 164     for _, f := range stack {
 165         fmt.Println(f)
 166     }
 167 }
 168 
 169 func printTop(stack []float64) {
 170     if len(stack) > 0 {
 171         fmt.Println(stack[len(stack)-1])
 172     }
 173 }
     File: ./dcol/dcol.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dcol
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "sort"
  32     "strconv"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 dcol [column names...]
  38 
  39 
  40 Drop COLumns lets you ignore a subset of a table's columns, matching the
  41 column names given using the first line from the standard input. Input lines
  42 can be either space-separated or tab-separated; output lines are always TSV
  43 (Tab-Separated Values) ones, where trailing tabs are added if any values are
  44 missing.
  45 
  46 When a column name isn't matched exactly, a case-insensitive match is tried:
  47 if the latter also fails, number-matching is finally tried, before giving up
  48 on that column name. Column numbers start at 1, and can be negative to count
  49 backward from the last column.
  50 
  51 Running this with no arguments is also useful, since no columns are dropped,
  52 and you get TSV output with always the same number of fields per line.
  53 
  54 All (optional) leading options start with either single or double-dash:
  55 
  56     -h, -help    show this help message
  57 `
  58 
  59 func Main() {
  60     buffered := false
  61     args := os.Args[1:]
  62 
  63     if len(args) > 0 {
  64         switch args[0] {
  65         case `-b`, `--b`, `-buffered`, `--buffered`:
  66             buffered = true
  67             args = args[1:]
  68 
  69         case `-h`, `--h`, `-help`, `--help`:
  70             os.Stdout.WriteString(info[1:])
  71             return
  72         }
  73     }
  74 
  75     if len(args) > 0 && args[0] == `--` {
  76         args = args[1:]
  77     }
  78 
  79     liveLines := !buffered
  80     if !buffered {
  81         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  82             liveLines = false
  83         }
  84     }
  85 
  86     if err := run(args, liveLines); err != nil && err != io.EOF {
  87         os.Stderr.WriteString(`dcol: `)
  88         os.Stderr.WriteString(err.Error())
  89         os.Stderr.WriteString("\n")
  90         os.Exit(1)
  91         return
  92     }
  93 }
  94 
  95 type itemFunc func(i int, s string) bool
  96 type handler func(s string, f itemFunc)
  97 
  98 func run(args []string, live bool) error {
  99     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 100     defer w.Flush()
 101 
 102     const gb = 1024 * 1024 * 1024
 103     sc := bufio.NewScanner(os.Stdin)
 104     sc.Buffer(nil, 8*gb)
 105 
 106     var count int
 107     var which []int
 108     var handle handler
 109 
 110     for i := 0; sc.Scan(); i++ {
 111         s := sc.Text()
 112         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 113             s = s[3:]
 114         }
 115 
 116         if i == 0 {
 117             picks, n, h, ok := match(s, args)
 118             if !ok {
 119                 return nil
 120             }
 121             handle = h
 122             which = picks
 123             count = n
 124 
 125             sort.Ints(which)
 126             for len(which) > 0 && which[0] < 0 {
 127                 which = which[1:]
 128             }
 129         }
 130 
 131         got := 0
 132         avoid := which
 133 
 134         handle(s, func(i int, s string) bool {
 135             for len(avoid) > 0 && avoid[0] < i {
 136                 avoid = avoid[1:]
 137             }
 138             if len(avoid) > 0 && avoid[0] == i {
 139                 return true
 140             }
 141 
 142             if got > 0 {
 143                 w.WriteByte('\t')
 144             }
 145             w.WriteString(s)
 146             got++
 147             return true
 148         })
 149 
 150         if got > 0 {
 151             for got < count {
 152                 w.WriteByte('\t')
 153                 got++
 154             }
 155         }
 156 
 157         if w.WriteByte('\n') != nil {
 158             return io.EOF
 159         }
 160 
 161         if live && w.Flush() != nil {
 162             return io.EOF
 163         }
 164     }
 165 
 166     return sc.Err()
 167 }
 168 
 169 func match(s string, args []string) (which []int, count int, handle handler, ok bool) {
 170     if strings.IndexByte(s, '\t') >= 0 {
 171         handle = loopItemsTSV
 172     } else {
 173         handle = loopItemsSSV
 174     }
 175 
 176     // count columns, so negative indices can be fixed with it
 177     count = 0
 178     handle(s, func(i int, s string) bool {
 179         count++
 180         return true
 181     })
 182 
 183     for _, arg := range args {
 184         ok := false
 185 
 186         // try exact matches
 187         handle(s, func(i int, s string) bool {
 188             if s == arg {
 189                 ok = true
 190                 which = append(which, i)
 191                 return false
 192             }
 193             return true
 194         })
 195 
 196         if ok {
 197             continue
 198         }
 199 
 200         // try case-insensitive matches
 201         handle(s, func(i int, s string) bool {
 202             if strings.EqualFold(s, arg) {
 203                 ok = true
 204                 which = append(which, i)
 205                 return false
 206             }
 207             return true
 208         })
 209 
 210         if ok {
 211             continue
 212         }
 213 
 214         // try 1-based indices, even negative ones
 215         if n, err := strconv.Atoi(arg); err == nil {
 216             if n < 0 {
 217                 n += count
 218             } else if n > 0 {
 219                 n--
 220             }
 221 
 222             if 0 <= n && n < count {
 223                 which = append(which, n)
 224             }
 225         }
 226     }
 227 
 228     return which, count, handle, true
 229 }
 230 
 231 // loopItemsSSV loops over a line's items, allocation-free style; when given
 232 // empty strings, the callback func is never called
 233 func loopItemsSSV(s string, f itemFunc) {
 234     s = trimTrailingSpaces(s)
 235 
 236     for i := 0; true; i++ {
 237         s = trimLeadingSpaces(s)
 238         if len(s) == 0 {
 239             return
 240         }
 241 
 242         j := strings.IndexByte(s, ' ')
 243         if j < 0 {
 244             if !f(i, s) {
 245                 return
 246             }
 247             return
 248         }
 249 
 250         if !f(i, s[:j]) {
 251             return
 252         }
 253         s = s[j+1:]
 254     }
 255 }
 256 
 257 func trimLeadingSpaces(s string) string {
 258     for len(s) > 0 && s[0] == ' ' {
 259         s = s[1:]
 260     }
 261     return s
 262 }
 263 
 264 func trimTrailingSpaces(s string) string {
 265     for len(s) > 0 && s[len(s)-1] == ' ' {
 266         s = s[:len(s)-1]
 267     }
 268     return s
 269 }
 270 
 271 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 272 // when given empty strings, the callback func is never called
 273 func loopItemsTSV(s string, f itemFunc) {
 274     if len(s) == 0 {
 275         return
 276     }
 277 
 278     for i := 0; true; i++ {
 279         j := strings.IndexByte(s, '\t')
 280         if j < 0 {
 281             if !f(i, s) {
 282                 return
 283             }
 284             return
 285         }
 286 
 287         if !f(i, s[:j]) {
 288             return
 289         }
 290         s = s[j+1:]
 291     }
 292 }
     File: ./debase64/debase64.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package debase64
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "encoding/base64"
  31     "errors"
  32     "io"
  33     "os"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 debase64 [file/data-URI...]
  39 
  40 Decode base64-encoded files and/or data-URIs.
  41 `
  42 
  43 func Main() {
  44     args := os.Args[1:]
  45 
  46     if len(args) > 0 {
  47         switch args[0] {
  48         case `-h`, `--h`, `-help`, `--help`:
  49             os.Stdout.WriteString(info[1:])
  50             return
  51 
  52         case `--`:
  53             args = args[1:]
  54         }
  55     }
  56 
  57     if len(args) > 1 {
  58         os.Stderr.WriteString(info[1:])
  59         os.Exit(1)
  60         return
  61     }
  62 
  63     name := `-`
  64     if len(args) == 1 {
  65         name = args[0]
  66     }
  67 
  68     if err := run(name); err != nil {
  69         os.Stderr.WriteString(`debase64: `)
  70         os.Stderr.WriteString(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73         return
  74     }
  75 }
  76 
  77 func run(s string) error {
  78     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  79     defer bw.Flush()
  80     w := bw
  81 
  82     if s == `-` {
  83         return debase64(w, os.Stdin)
  84     }
  85 
  86     if seemsDataURI(s) {
  87         return debase64(w, strings.NewReader(s))
  88     }
  89 
  90     f, err := os.Open(s)
  91     if err != nil {
  92         return err
  93     }
  94     defer f.Close()
  95 
  96     return debase64(w, f)
  97 }
  98 
  99 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
 100 // apart from output-writing ones
 101 func debase64(w io.Writer, r io.Reader) error {
 102     br := bufio.NewReaderSize(r, 32*1024)
 103     start, err := br.Peek(64)
 104     if err != nil && err != io.EOF {
 105         return err
 106     }
 107 
 108     skip, err := skipIntroDataURI(start)
 109     if err != nil {
 110         return err
 111     }
 112 
 113     if skip > 0 {
 114         br.Discard(skip)
 115     }
 116 
 117     dec := base64.NewDecoder(base64.StdEncoding, br)
 118     _, err = io.Copy(w, dec)
 119     return err
 120 }
 121 
 122 func skipIntroDataURI(chunk []byte) (skip int, err error) {
 123     if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 124         chunk = chunk[3:]
 125         skip += 3
 126     }
 127 
 128     if !bytes.HasPrefix(chunk, []byte(`data:`)) {
 129         return skip, nil
 130     }
 131 
 132     const l = len(`data:,`)
 133     if len(chunk) == l && chunk[l-1] == ',' {
 134         return l, nil
 135     }
 136 
 137     start := chunk
 138     if len(start) > 64 {
 139         start = start[:64]
 140     }
 141 
 142     i := bytes.Index(start, []byte(`;base64,`))
 143     if i < 0 {
 144         return skip, errors.New(`invalid data URI`)
 145     }
 146 
 147     skip += i + len(`;base64,`)
 148     return skip, nil
 149 }
 150 
 151 func seemsDataURI(s string) bool {
 152     start := s
 153     if len(s) > 64 {
 154         start = s[:64]
 155     }
 156     return strings.HasPrefix(s, `data:`) && strings.Contains(start, `;base64,`)
 157 }
     File: ./decap/decap.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package decap
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "os/exec"
  33     "strconv"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 decap [options...] [max lines...] [command...] [command args...]
  39 
  40 DECAPitate emits the first n lines from the standard input, handing the rest
  41 (if any) to the command given. If a leading number isn't given, the default
  42 is to 'decapitate' just the first line. If no command is given, this tool
  43 just emits up to the first n lines, or just the first line by default.
  44 
  45 All (optional) leading options start with either single or double-dash:
  46 
  47     -h, -help    show this help message
  48 `
  49 
  50 func Main() {
  51     buffered := false
  52     args := os.Args[1:]
  53     headerLines := 1
  54 
  55     for len(args) > 0 {
  56         switch args[0] {
  57         case `-b`, `--b`, `-buffered`, `--buffered`:
  58             buffered = true
  59             args = args[1:]
  60             continue
  61 
  62         case `-h`, `--h`, `-help`, `--help`:
  63             os.Stdout.WriteString(info[1:])
  64             return
  65 
  66         case `-n`, `--n`:
  67             if len(args) < 2 {
  68                 os.Stderr.WriteString("forgot the number\n")
  69                 os.Exit(1)
  70                 return
  71             }
  72             s := strings.Replace(args[1], `_`, ``, -1)
  73             if n, err := strconv.ParseInt(s, 10, 64); err == nil {
  74                 headerLines = int(n)
  75                 args = args[2:]
  76                 continue
  77             }
  78             os.Stderr.WriteString(args[1] + " isn't a valid number\n")
  79             os.Exit(1)
  80             return
  81         }
  82 
  83         break
  84     }
  85 
  86     if len(args) > 0 {
  87         s := strings.Replace(args[0], `_`, ``, -1)
  88         n, err := strconv.ParseInt(s, 10, 64)
  89         if err == nil {
  90             headerLines = int(n)
  91             args = args[1:]
  92         }
  93     }
  94 
  95     if len(args) > 0 && args[0] == `--` {
  96         args = args[1:]
  97     }
  98 
  99     if headerLines < 1 && len(args) == 0 {
 100         return
 101     }
 102 
 103     liveLines := !buffered
 104     if !buffered {
 105         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 106             liveLines = false
 107         }
 108     }
 109 
 110     err := run(os.Stdout, args, headerLines, liveLines)
 111     if err != nil && err != io.EOF {
 112         os.Stderr.WriteString(`decap: `)
 113         os.Stderr.WriteString(err.Error())
 114         os.Stderr.WriteString("\n")
 115         os.Exit(1)
 116         return
 117     }
 118 }
 119 
 120 func run(w io.Writer, args []string, headerLines int, liveLines bool) error {
 121     bw := bufio.NewWriterSize(w, 32*1024)
 122     defer bw.Flush()
 123 
 124     br := bufio.NewReaderSize(os.Stdin, 32*1024)
 125 
 126     if headerLines > 0 {
 127         if err := first(bw, br, &headerLines, liveLines); err != nil {
 128             return err
 129         }
 130     }
 131 
 132     if len(args) == 0 {
 133         return nil
 134     }
 135 
 136     cmd := exec.Command(args[0], args[1:]...)
 137     cmd.Stdin = br
 138     if liveLines {
 139         cmd.Stdout = lineFlusher{bw}
 140     } else {
 141         cmd.Stdout = bw
 142     }
 143     cmd.Stderr = os.Stderr
 144 
 145     bw.Flush()
 146 
 147     if err := cmd.Start(); err != nil {
 148         os.Stderr.WriteString(`decap: `)
 149         os.Stderr.WriteString(err.Error())
 150         os.Stderr.WriteString("\n")
 151         os.Exit(1)
 152         return nil
 153     }
 154 
 155     err := cmd.Wait()
 156     if _, ok := err.(*exec.ExitError); ok {
 157         err = nil
 158     }
 159 
 160     if err != nil {
 161         os.Stderr.WriteString(`decap: `)
 162         os.Stderr.WriteString(err.Error())
 163         os.Stderr.WriteString("\n")
 164     }
 165 
 166     bw.Flush()
 167 
 168     os.Exit(cmd.ProcessState.ExitCode())
 169     return nil
 170 }
 171 
 172 func first(w *bufio.Writer, r *bufio.Reader, left *int, live bool) error {
 173     if *left < 1 {
 174         return io.EOF
 175     }
 176 
 177     due := false
 178 
 179     for i := 0; true; i++ {
 180         s, incomplete, err := r.ReadLine()
 181         if err != nil {
 182             return err
 183         }
 184 
 185         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 186             s = s[3:]
 187         }
 188 
 189         w.Write(s)
 190         if incomplete {
 191             due = true
 192             continue
 193         }
 194         due = false
 195 
 196         if w.WriteByte('\n') != nil {
 197             return io.EOF
 198         }
 199 
 200         if live && w.Flush() != nil {
 201             return io.EOF
 202         }
 203 
 204         *left--
 205         if *left < 1 {
 206             break
 207         }
 208     }
 209 
 210     if due {
 211         if w.WriteByte('\n') != nil {
 212             return io.EOF
 213         }
 214 
 215         if live && w.Flush() != nil {
 216             return io.EOF
 217         }
 218 
 219         *left--
 220     }
 221 
 222     return nil
 223 }
 224 
 225 type lineFlusher struct {
 226     bw *bufio.Writer
 227 }
 228 
 229 func (lf lineFlusher) Write(p []byte) (n int, err error) {
 230     n, err = lf.bw.Write(p)
 231     if err != nil {
 232         return n, err
 233     }
 234 
 235     if bytes.IndexByte(p, '\n') >= 0 {
 236         err = lf.bw.Flush()
 237     }
 238 
 239     return n, err
 240 }
     File: ./decsv/decsv.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package decsv
  26 
  27 import (
  28     "bufio"
  29     "encoding/csv"
  30     "encoding/json"
  31     "errors"
  32     "io"
  33     "os"
  34     "strings"
  35     "unicode"
  36 )
  37 
  38 const info = `
  39 decsv [options...] [filepath...]
  40 
  41 
  42 This cmd-line app turns CSV (comma-separated values) data into either TSV
  43 (tab-separated values), JSONS (JSON Strings), or general JSON (JavaScript
  44 Object Notation).
  45 
  46 When not given a filepath, the input is read from the standard input.
  47 
  48 Options, when given, can either start with a single or a double-dash:
  49 
  50   -h, -help    show this help message
  51   -json        emit JSON, where numbers are auto-detected
  52   -jsonl       emit JSON Lines, where numbers are auto-detected
  53   -jsons       emit JSON Strings, where object values are strings or null
  54   -tsv         emit TSV (tab-separated values) lines
  55 `
  56 
  57 // handler is the type all CSV-converter funcs adhere to
  58 type handler func(*bufio.Writer, *csv.Reader) error
  59 
  60 var handlers = map[string]handler{
  61     `-json`:   emitJSON,
  62     `--json`:  emitJSON,
  63     `-jsonl`:  emitJSONL,
  64     `--jsonl`: emitJSONL,
  65     `-jsons`:  emitJSONS,
  66     `--jsons`: emitJSONS,
  67     `-tsv`:    emitTSV,
  68     `--tsv`:   emitTSV,
  69 }
  70 
  71 func Main() {
  72     emit := emitTSV
  73     buffered := false
  74     args := os.Args[1:]
  75 
  76     for len(args) > 0 {
  77         switch args[0] {
  78         case `-b`, `--b`, `-buffered`, `--buffered`:
  79             buffered = true
  80             args = args[1:]
  81             continue
  82 
  83         case `-h`, `--h`, `-help`, `--help`:
  84             os.Stdout.WriteString(info[1:])
  85             return
  86         }
  87 
  88         if v, ok := handlers[args[0]]; ok {
  89             emit = v
  90             args = args[1:]
  91             continue
  92         }
  93 
  94         break
  95     }
  96 
  97     if len(args) > 0 && args[0] == `--` {
  98         args = args[1:]
  99     }
 100 
 101     if len(args) > 1 {
 102         os.Stdout.WriteString(info[1:])
 103         os.Exit(1)
 104         return
 105     }
 106 
 107     liveLines := !buffered
 108     if !buffered {
 109         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 110             liveLines = false
 111         }
 112     }
 113 
 114     path := `-`
 115     if len(args) > 0 {
 116         path = args[0]
 117     }
 118 
 119     err := run(os.Stdout, path, emit, liveLines)
 120     if err != nil && err != io.EOF {
 121         os.Stderr.WriteString(`decsv: `)
 122         os.Stderr.WriteString(err.Error())
 123         os.Stderr.WriteString("\n")
 124         os.Exit(1)
 125         return
 126     }
 127 }
 128 
 129 func run(w io.Writer, path string, handle handler, live bool) error {
 130     bw := bufio.NewWriterSize(w, 32*1024)
 131     defer bw.Flush()
 132 
 133     if path == `-` {
 134         return handle(bw, makeRowReader(os.Stdin))
 135     }
 136 
 137     f, err := os.Open(path)
 138     if err != nil {
 139         // on windows, file-not-found error messages may mention `CreateFile`,
 140         // even when trying to open files in read-only mode
 141         return errors.New(`can't open file named ` + path)
 142     }
 143     defer f.Close()
 144 
 145     return handle(bw, makeRowReader(f))
 146 }
 147 
 148 func emitJSON(w *bufio.Writer, rr *csv.Reader) error {
 149     got := 0
 150     var keys []string
 151 
 152     err := loopCSV(rr, func(i int, row []string) error {
 153         got++
 154 
 155         if i == 0 {
 156             keys = make([]string, 0, len(row))
 157             for _, s := range row {
 158                 keys = append(keys, strings.Clone(s))
 159             }
 160             return nil
 161         }
 162 
 163         if i == 1 {
 164             w.WriteByte('[')
 165         } else {
 166             err := w.WriteByte(',')
 167             if err != nil {
 168                 return io.EOF
 169             }
 170         }
 171 
 172         w.WriteByte('{')
 173         for i, s := range row {
 174             if i > 0 {
 175                 w.WriteByte(',')
 176             }
 177 
 178             if numberLike(s) {
 179                 w.WriteByte('"')
 180                 writeInnerStringJSON(w, keys[i])
 181                 w.WriteString(`":`)
 182                 w.WriteString(s)
 183                 continue
 184             }
 185 
 186             writeKV(w, keys[i], s)
 187         }
 188 
 189         for i := len(row); i < len(keys); i++ {
 190             if i > 0 {
 191                 w.WriteByte(',')
 192             }
 193             w.WriteByte('"')
 194             writeInnerStringJSON(w, keys[i])
 195             w.WriteString(`":null`)
 196         }
 197         w.WriteByte('}')
 198 
 199         return nil
 200     })
 201 
 202     if err != nil {
 203         return err
 204     }
 205 
 206     if got > 1 {
 207         w.WriteString("]\n")
 208     }
 209     return nil
 210 }
 211 
 212 func emitJSONL(w *bufio.Writer, rr *csv.Reader) error {
 213     var keys []string
 214 
 215     return loopCSV(rr, func(i int, row []string) error {
 216         if i == 0 {
 217             keys = make([]string, 0, len(row))
 218             for _, s := range row {
 219                 c := string(append([]byte{}, s...))
 220                 keys = append(keys, c)
 221             }
 222             return nil
 223         }
 224 
 225         w.WriteByte('{')
 226         for i, s := range row {
 227             if i > 0 {
 228                 w.WriteByte(',')
 229                 w.WriteByte(' ')
 230             }
 231 
 232             if numberLike(s) {
 233                 w.WriteByte('"')
 234                 writeInnerStringJSON(w, keys[i])
 235                 w.WriteString(`": `)
 236                 w.WriteString(s)
 237                 continue
 238             }
 239 
 240             writeKV(w, keys[i], s)
 241         }
 242 
 243         for i := len(row); i < len(keys); i++ {
 244             if i > 0 {
 245                 w.WriteByte(',')
 246                 w.WriteByte(' ')
 247             }
 248             w.WriteByte('"')
 249             writeInnerStringJSON(w, keys[i])
 250             w.WriteString(`": null`)
 251         }
 252         w.WriteByte('}')
 253 
 254         w.WriteByte('\n')
 255         if w.Flush() != nil {
 256             return io.EOF
 257         }
 258         return nil
 259     })
 260 }
 261 
 262 func emitJSONS(w *bufio.Writer, rr *csv.Reader) error {
 263     got := 0
 264     var keys []string
 265 
 266     err := loopCSV(rr, func(i int, row []string) error {
 267         got++
 268 
 269         if i == 0 {
 270             keys = make([]string, 0, len(row))
 271             for _, s := range row {
 272                 c := string(append([]byte{}, s...))
 273                 keys = append(keys, c)
 274             }
 275             return nil
 276         }
 277 
 278         if i == 1 {
 279             w.WriteByte('[')
 280         } else {
 281             err := w.WriteByte(',')
 282             if err != nil {
 283                 return io.EOF
 284             }
 285         }
 286 
 287         w.WriteByte('{')
 288         for i, s := range row {
 289             if i > 0 {
 290                 w.WriteByte(',')
 291             }
 292             writeKV(w, keys[i], s)
 293         }
 294 
 295         for i := len(row); i < len(keys); i++ {
 296             if i > 0 {
 297                 w.WriteByte(',')
 298             }
 299             w.WriteByte('"')
 300             writeInnerStringJSON(w, keys[i])
 301             w.WriteString(`":null`)
 302         }
 303         w.WriteByte('}')
 304 
 305         return nil
 306     })
 307 
 308     if err != nil {
 309         return err
 310     }
 311 
 312     if got > 1 {
 313         w.WriteString("]\n")
 314     }
 315     return nil
 316 }
 317 
 318 func emitTSV(w *bufio.Writer, rr *csv.Reader) error {
 319     width := -1
 320 
 321     return loopCSV(rr, func(i int, row []string) error {
 322         if width < 0 {
 323             width = len(row)
 324         }
 325 
 326         for i, s := range row {
 327             if strings.IndexByte(s, '\t') >= 0 {
 328                 const msg = `can't convert CSV whose items have tabs to TSV`
 329                 return errors.New(msg)
 330             }
 331             if i > 0 {
 332                 w.WriteByte('\t')
 333             }
 334             w.WriteString(s)
 335         }
 336 
 337         for i := len(row); i < width; i++ {
 338             w.WriteByte('\t')
 339         }
 340 
 341         w.WriteByte('\n')
 342         if w.Flush() != nil {
 343             // a write error may be the consequence of stdout being closed,
 344             // perhaps by another app along a pipe
 345             return io.EOF
 346         }
 347         return nil
 348     })
 349 }
 350 
 351 // writeInnerStringJSON helps JSON-encode strings more quickly
 352 func writeInnerStringJSON(w *bufio.Writer, s string) {
 353     needsEscaping := false
 354     for _, r := range s {
 355         if '#' <= r && r <= '~' && r != '\\' {
 356             continue
 357         }
 358         if r == ' ' || r == '!' || unicode.IsLetter(r) {
 359             continue
 360         }
 361 
 362         needsEscaping = true
 363         break
 364     }
 365 
 366     if !needsEscaping {
 367         w.WriteString(s)
 368         return
 369     }
 370 
 371     outer, err := json.Marshal(s)
 372     if err != nil {
 373         return
 374     }
 375     inner := outer[1 : len(outer)-1]
 376     w.Write(inner)
 377 }
 378 
 379 func writeKV(w *bufio.Writer, k string, s string) {
 380     w.WriteByte('"')
 381     writeInnerStringJSON(w, k)
 382     w.WriteString(`": "`)
 383     writeInnerStringJSON(w, s)
 384     w.WriteByte('"')
 385 }
 386 
 387 func numberLike(s string) bool {
 388     if len(s) == 0 {
 389         return false
 390     }
 391 
 392     if s[0] == '-' {
 393         s = s[1:]
 394     }
 395 
 396     if len(s) == 0 || s[0] < '0' || s[0] > '9' {
 397         return false
 398     }
 399 
 400     for len(s) > 0 {
 401         lead := s[0]
 402         s = s[1:]
 403 
 404         if lead == '.' {
 405             return allDigits(s)
 406         }
 407         if lead < '0' || lead > '9' {
 408             return false
 409         }
 410     }
 411 
 412     return true
 413 }
 414 
 415 func allDigits(s string) bool {
 416     if len(s) == 0 {
 417         return false
 418     }
 419 
 420     for _, r := range s {
 421         if r < '0' || r > '9' {
 422             return false
 423         }
 424     }
 425     return true
 426 }
 427 
 428 func makeRowReader(r io.Reader) *csv.Reader {
 429     rr := csv.NewReader(r)
 430     rr.LazyQuotes = true
 431     rr.ReuseRecord = true
 432     rr.FieldsPerRecord = -1
 433     return rr
 434 }
 435 
 436 func loopCSV(rr *csv.Reader, handle func(i int, row []string) error) error {
 437     width := 0
 438 
 439     for i := 0; true; i++ {
 440         row, err := rr.Read()
 441         if err == io.EOF {
 442             return nil
 443         }
 444 
 445         if err != nil {
 446             return err
 447         }
 448 
 449         if i == 0 {
 450             width = len(row)
 451         }
 452 
 453         if len(row) > width {
 454             return errors.New(`data-row has more items than the header`)
 455         }
 456 
 457         if err := handle(i, row); err != nil {
 458             return err
 459         }
 460     }
 461 
 462     return nil
 463 }
     File: ./dedent/dedent.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dedent
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 dedent [options...] [files...]
  37 
  38 Ignore the common leading-space indentation from the input(s).
  39 
  40 All (optional) leading options start with either single or double-dash:
  41 
  42     -h, -help    show this help message
  43 `
  44 
  45 func Main() {
  46     buffered := false
  47     args := os.Args[1:]
  48 
  49     if len(args) > 0 {
  50         switch args[0] {
  51         case `-b`, `--b`, `-buffered`, `--buffered`:
  52             buffered = true
  53             args = args[1:]
  54 
  55         case `-h`, `--h`, `-help`, `--help`:
  56             os.Stdout.WriteString(info[1:])
  57             return
  58         }
  59     }
  60 
  61     if len(args) > 0 && args[0] == `--` {
  62         args = args[1:]
  63     }
  64 
  65     liveLines := !buffered
  66     if !buffered {
  67         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  68             liveLines = false
  69         }
  70     }
  71 
  72     err := run(os.Stdout, args, liveLines)
  73     if err != nil && err != io.EOF {
  74         os.Stderr.WriteString(`dedent: `)
  75         os.Stderr.WriteString(err.Error())
  76         os.Stderr.WriteString("\n")
  77         os.Exit(1)
  78         return
  79     }
  80 }
  81 
  82 type config struct {
  83     indent int
  84     lines  [][]byte
  85     live   bool
  86 }
  87 
  88 func run(w io.Writer, args []string, live bool) error {
  89     bw := bufio.NewWriterSize(w, 32*1024)
  90     defer bw.Flush()
  91 
  92     var cfg config
  93     cfg.indent = -1
  94     cfg.live = live
  95 
  96     if len(args) == 0 {
  97         if err := handleReader(bw, os.Stdin, &cfg); err != nil {
  98             return err
  99         }
 100     }
 101 
 102     for _, name := range args {
 103         if err := handleFile(bw, name, &cfg); err != nil {
 104             return err
 105         }
 106     }
 107 
 108     if dump(bw, cfg.lines, cfg.indent) != nil {
 109         return io.EOF
 110     }
 111     cfg.lines = nil
 112     return nil
 113 }
 114 
 115 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 116     if name == `` || name == `-` {
 117         return handleReader(w, os.Stdin, cfg)
 118     }
 119 
 120     f, err := os.Open(name)
 121     if err != nil {
 122         return errors.New(`can't read from file named "` + name + `"`)
 123     }
 124     defer f.Close()
 125 
 126     return handleReader(w, f, cfg)
 127 }
 128 
 129 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 130     const gb = 1024 * 1024 * 1024
 131     sc := bufio.NewScanner(r)
 132     sc.Buffer(nil, 8*gb)
 133 
 134     for i := 0; sc.Scan(); i++ {
 135         s := sc.Bytes()
 136         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 137             s = s[3:]
 138         }
 139 
 140         if cfg.indent != 0 {
 141             n := countIndent(s)
 142             if cfg.indent > n || cfg.indent < 0 {
 143                 cfg.indent = n
 144             }
 145         }
 146 
 147         if cfg.indent > 0 {
 148             cfg.lines = append(cfg.lines, s)
 149             continue
 150         }
 151 
 152         if dump(w, cfg.lines, cfg.indent) != nil {
 153             return io.EOF
 154         }
 155         cfg.lines = nil
 156         w.Write(dedent(s, cfg.indent))
 157 
 158         if w.WriteByte('\n') != nil {
 159             return io.EOF
 160         }
 161 
 162         if cfg.live && w.Flush() != nil {
 163             return io.EOF
 164         }
 165     }
 166 
 167     return sc.Err()
 168 }
 169 
 170 func countIndent(s []byte) int {
 171     indent := 0
 172     for len(s) > 0 && s[0] == ' ' {
 173         indent++
 174         s = s[1:]
 175     }
 176     return indent
 177 }
 178 
 179 func dedent(s []byte, max int) []byte {
 180     for i := 0; i < max && len(s) > 0 && s[0] == ' '; i++ {
 181         s = s[1:]
 182     }
 183     return s
 184 }
 185 
 186 func dump(w *bufio.Writer, lines [][]byte, indent int) error {
 187     for _, l := range lines {
 188         w.Write(dedent(l, indent))
 189         if w.WriteByte('\n') != nil {
 190             return io.EOF
 191         }
 192     }
 193 
 194     if w.Flush() != nil {
 195         return io.EOF
 196     }
 197     return nil
 198 }
     File: ./dedup/dedup.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dedup
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 dedup [options...] [file...]
  37 
  38 
  39 DEDUPlicate lines prevents the same line from appearing again in the output,
  40 after the first time. Unique lines are remembered across inputs.
  41 
  42 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  43 feeds by default.
  44 
  45 All (optional) leading options start with either single or double-dash:
  46 
  47     -h, -help    show this help message
  48     -i, -ins     match lines case-insensitively
  49 `
  50 
  51 type stringSet map[string]struct{}
  52 
  53 type config struct {
  54     live      bool
  55     sensitive bool
  56 }
  57 
  58 func Main() {
  59     buffered := false
  60     sensitive := true
  61     args := os.Args[1:]
  62 
  63     if len(args) > 0 {
  64         switch args[0] {
  65         case `-b`, `--b`, `-buffered`, `--buffered`:
  66             buffered = true
  67             args = args[1:]
  68 
  69         case `-h`, `--h`, `-help`, `--help`:
  70             os.Stdout.WriteString(info[1:])
  71             return
  72 
  73         case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
  74             sensitive = false
  75             args = args[1:]
  76         }
  77     }
  78 
  79     if len(args) > 0 && args[0] == `--` {
  80         args = args[1:]
  81     }
  82 
  83     liveLines := !buffered
  84     if !buffered {
  85         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  86             liveLines = false
  87         }
  88     }
  89 
  90     var cfg config
  91     cfg.live = liveLines
  92     cfg.sensitive = sensitive
  93 
  94     err := run(os.Stdout, args, cfg)
  95     if err != nil && err != io.EOF {
  96         os.Stderr.WriteString(`dedup: `)
  97         os.Stderr.WriteString(err.Error())
  98         os.Stderr.WriteString("\n")
  99         os.Exit(1)
 100         return
 101     }
 102 }
 103 
 104 func run(w io.Writer, args []string, cfg config) error {
 105     files := make(stringSet)
 106     lines := make(stringSet)
 107     bw := bufio.NewWriterSize(w, 32*1024)
 108     defer bw.Flush()
 109 
 110     for _, name := range args {
 111         if _, ok := files[name]; ok {
 112             continue
 113         }
 114         files[name] = struct{}{}
 115 
 116         if err := handleFile(bw, name, lines, cfg); err != nil {
 117             return err
 118         }
 119     }
 120 
 121     if len(args) == 0 {
 122         return dedup(bw, os.Stdin, lines, cfg)
 123     }
 124     return nil
 125 }
 126 
 127 func handleFile(w *bufio.Writer, name string, got stringSet, cfg config) error {
 128     if name == `` || name == `-` {
 129         return dedup(w, os.Stdin, got, cfg)
 130     }
 131 
 132     f, err := os.Open(name)
 133     if err != nil {
 134         return errors.New(`can't read from file named "` + name + `"`)
 135     }
 136     defer f.Close()
 137 
 138     return dedup(w, f, got, cfg)
 139 }
 140 
 141 func dedup(w *bufio.Writer, r io.Reader, got stringSet, cfg config) error {
 142     const gb = 1024 * 1024 * 1024
 143     sc := bufio.NewScanner(r)
 144     sc.Buffer(nil, 8*gb)
 145 
 146     for sc.Scan() {
 147         line := sc.Text()
 148         key := line
 149         if !cfg.sensitive {
 150             key = strings.ToLower(key)
 151         }
 152 
 153         if _, ok := got[key]; ok {
 154             continue
 155         }
 156         got[key] = struct{}{}
 157 
 158         w.Write(sc.Bytes())
 159         if w.WriteByte('\n') != nil {
 160             return io.EOF
 161         }
 162 
 163         if cfg.live && w.Flush() != nil {
 164             return io.EOF
 165         }
 166     }
 167 
 168     return sc.Err()
 169 }
     File: ./dejsonl/dejsonl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dejsonl
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 dejsonl [filepath...]
  38 
  39 Turn JSON Lines (JSONL) into proper-JSON arrays. The JSON Lines format is
  40 simply plain-text lines, where each line is valid JSON on its own.
  41 `
  42 
  43 const indent = `  `
  44 
  45 func Main() {
  46     buffered := false
  47     args := os.Args[1:]
  48 
  49     if len(args) > 0 {
  50         switch args[0] {
  51         case `-b`, `--b`, `-buffered`, `--buffered`:
  52             buffered = true
  53             args = args[1:]
  54 
  55         case `-h`, `--h`, `-help`, `--help`:
  56             os.Stdout.WriteString(info[1:])
  57             return
  58         }
  59     }
  60 
  61     if len(args) > 0 && args[0] == `--` {
  62         args = args[1:]
  63     }
  64 
  65     liveLines := !buffered
  66     if !buffered {
  67         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  68             liveLines = false
  69         }
  70     }
  71 
  72     err := run(os.Stdout, os.Args[1:], liveLines)
  73     if err != nil && err != io.EOF {
  74         os.Stderr.WriteString(`dejsonl: `)
  75         os.Stderr.WriteString(err.Error())
  76         os.Stderr.WriteString("\n")
  77         os.Exit(1)
  78         return
  79     }
  80 }
  81 
  82 func run(w io.Writer, args []string, live bool) error {
  83     dashes := 0
  84     for _, path := range args {
  85         if path == `-` {
  86             dashes++
  87         }
  88         if dashes > 1 {
  89             return errors.New(`can't read stdin (dash) more than once`)
  90         }
  91     }
  92 
  93     bw := bufio.NewWriterSize(w, 32*1024)
  94     defer bw.Flush()
  95 
  96     if len(args) == 0 {
  97         return dejsonl(bw, os.Stdin, live)
  98     }
  99 
 100     for _, path := range args {
 101         if err := handleInput(bw, path, live); err != nil {
 102             return err
 103         }
 104     }
 105 
 106     return nil
 107 }
 108 
 109 // handleInput simplifies control-flow for func main
 110 func handleInput(w *bufio.Writer, path string, live bool) error {
 111     if path == `-` {
 112         return dejsonl(w, os.Stdin, live)
 113     }
 114 
 115     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
 116     //  resp, err := http.Get(path)
 117     //  if err != nil {
 118     //      return err
 119     //  }
 120     //  defer resp.Body.Close()
 121     //  return dejsonl(w, resp.Body, live)
 122     // }
 123 
 124     f, err := os.Open(path)
 125     if err != nil {
 126         // on windows, file-not-found error messages may mention `CreateFile`,
 127         // even when trying to open files in read-only mode
 128         return errors.New(`can't open file named ` + path)
 129     }
 130     defer f.Close()
 131     return dejsonl(w, f, live)
 132 }
 133 
 134 // dejsonl simplifies control-flow for func handleInput
 135 func dejsonl(w *bufio.Writer, r io.Reader, live bool) error {
 136     const gb = 1024 * 1024 * 1024
 137     sc := bufio.NewScanner(r)
 138     sc.Buffer(nil, 8*gb)
 139     got := 0
 140 
 141     for i := 0; sc.Scan(); i++ {
 142         s := sc.Text()
 143         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 144             s = s[3:]
 145         }
 146 
 147         // trim spaces at both ends of the current line
 148         for len(s) > 0 && s[0] == ' ' {
 149             s = s[1:]
 150         }
 151         for len(s) > 0 && s[len(s)-1] == ' ' {
 152             s = s[:len(s)-1]
 153         }
 154 
 155         // ignore empty(ish) lines
 156         if len(s) == 0 {
 157             continue
 158         }
 159 
 160         // ignore lines starting with unix-style comments
 161         if len(s) > 0 && s[0] == '#' {
 162             continue
 163         }
 164 
 165         if err := checkJSONL(strings.NewReader(s)); err != nil {
 166             return err
 167         }
 168 
 169         if got == 0 {
 170             w.WriteByte('[')
 171         } else {
 172             w.WriteByte(',')
 173         }
 174         if w.WriteByte('\n') != nil {
 175             return io.EOF
 176         }
 177         w.WriteString(indent)
 178         w.WriteString(s)
 179         got++
 180 
 181         if live && w.Flush() != nil {
 182             return io.EOF
 183         }
 184     }
 185 
 186     if got == 0 {
 187         w.WriteString("[\n]\n")
 188     } else {
 189         w.WriteString("\n]\n")
 190     }
 191     return sc.Err()
 192 }
 193 
 194 func checkJSONL(r io.Reader) error {
 195     dec := json.NewDecoder(r)
 196     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 197     // even if JSON parsers aren't required to guarantee such input-fidelity
 198     // for numbers
 199     dec.UseNumber()
 200 
 201     t, err := dec.Token()
 202     if err == io.EOF {
 203         return errors.New(`input has no JSON values`)
 204     }
 205 
 206     if err := checkToken(dec, t); err != nil {
 207         return err
 208     }
 209 
 210     _, err = dec.Token()
 211     if err == io.EOF {
 212         // input is over, so it's a success
 213         return nil
 214     }
 215 
 216     if err == nil {
 217         // a successful `read` is a failure, as it means there are
 218         // trailing JSON tokens
 219         return errors.New(`unexpected trailing data`)
 220     }
 221 
 222     // any other error, perhaps some invalid-JSON-syntax-type error
 223     return err
 224 }
 225 
 226 // checkToken handles recursion for func checkJSONL
 227 func checkToken(dec *json.Decoder, t json.Token) error {
 228     switch t := t.(type) {
 229     case json.Delim:
 230         switch t {
 231         case json.Delim('['):
 232             return checkArray(dec)
 233         case json.Delim('{'):
 234             return checkObject(dec)
 235         default:
 236             return errors.New(`unsupported JSON syntax ` + string(t))
 237         }
 238 
 239     case nil, bool, float64, json.Number, string:
 240         return nil
 241 
 242     default:
 243         // return fmt.Errorf(`unsupported token type %T`, t)
 244         return errors.New(`invalid JSON token`)
 245     }
 246 }
 247 
 248 // handleArray handles arrays for func checkToken
 249 func checkArray(dec *json.Decoder) error {
 250     for {
 251         t, err := dec.Token()
 252         if err != nil {
 253             return err
 254         }
 255 
 256         if t == json.Delim(']') {
 257             return nil
 258         }
 259 
 260         if err := checkToken(dec, t); err != nil {
 261             return err
 262         }
 263     }
 264 }
 265 
 266 // handleObject handles objects for func checkToken
 267 func checkObject(dec *json.Decoder) error {
 268     for {
 269         t, err := dec.Token()
 270         if err != nil {
 271             return err
 272         }
 273 
 274         if t == json.Delim('}') {
 275             return nil
 276         }
 277 
 278         if _, ok := t.(string); !ok {
 279             return errors.New(`expected a string for a key-value pair`)
 280         }
 281 
 282         t, err = dec.Token()
 283         if err == io.EOF || t == json.Delim('}') {
 284             return errors.New(`expected a value for a key-value pair`)
 285         }
 286 
 287         if err := checkToken(dec, t); err != nil {
 288             return err
 289         }
 290     }
 291 }
     File: ./delay/delay.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package delay
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "math"
  33     "os"
  34     "strconv"
  35     "time"
  36 )
  37 
  38 const info = `
  39 delay [options...] [seconds delay...] [files...]
  40 
  41 Wait the number of seconds given before emitting each input line, or wait 1
  42 second before emitting each line by default.
  43 
  44 All (optional) leading options start with either single or double-dash:
  45 
  46     -h, -help       show this help message
  47 `
  48 
  49 type config struct {
  50     delay     time.Duration
  51     liveLines bool
  52 }
  53 
  54 func Main() {
  55     var cfg config
  56     cfg.delay = time.Second
  57     cfg.liveLines = true
  58     args := os.Args[1:]
  59 
  60     for len(args) > 0 {
  61         switch args[0] {
  62         case `-b`, `--b`, `-buffered`, `--buffered`:
  63             cfg.liveLines = false
  64             args = args[1:]
  65             continue
  66 
  67         case `-h`, `--h`, `-help`, `--help`:
  68             os.Stdout.WriteString(info[1:])
  69             return
  70         }
  71 
  72         break
  73     }
  74 
  75     if len(args) > 0 {
  76         if d, ok := parseDelay(args[0]); ok {
  77             total := d
  78             args = args[1:]
  79 
  80             for len(args) > 0 {
  81                 if d, ok := parseDelay(args[0]); ok {
  82                     total += d
  83                     args = args[1:]
  84                     continue
  85                 }
  86                 break
  87             }
  88 
  89             cfg.delay = total
  90         }
  91     }
  92 
  93     if len(args) > 0 && args[0] == `--` {
  94         args = args[1:]
  95     }
  96 
  97     if cfg.liveLines {
  98         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  99             cfg.liveLines = false
 100         }
 101     }
 102 
 103     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 104         os.Stderr.WriteString(`delay: `)
 105         os.Stderr.WriteString(err.Error())
 106         os.Stderr.WriteString("\n")
 107         os.Exit(1)
 108         return
 109     }
 110 }
 111 
 112 func parseDelay(s string) (delay time.Duration, ok bool) {
 113     f, err := strconv.ParseFloat(s, 64)
 114     if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 115         if f < 0 {
 116             f = 0
 117         }
 118         return time.Duration(f * float64(time.Second)), true
 119     }
 120 
 121     if d, err := time.ParseDuration(s); err == nil {
 122         return d, true
 123     }
 124 
 125     return 0, false
 126 }
 127 
 128 func run(w io.Writer, args []string, cfg config) error {
 129     bw := bufio.NewWriterSize(w, 32*1024)
 130     defer bw.Flush()
 131 
 132     dashes := 0
 133     for _, name := range args {
 134         if name == `-` {
 135             dashes++
 136         }
 137         if dashes > 1 {
 138             return errors.New(`can't read stdin (dash) more than once`)
 139         }
 140     }
 141 
 142     if len(args) == 0 {
 143         return delay(bw, os.Stdin, cfg)
 144     }
 145 
 146     for _, name := range args {
 147         if name == `-` {
 148             if err := delay(bw, os.Stdin, cfg); err != nil {
 149                 return err
 150             }
 151             continue
 152         }
 153 
 154         if err := handleFile(bw, name, cfg); err != nil {
 155             return err
 156         }
 157     }
 158     return nil
 159 }
 160 
 161 func handleFile(w *bufio.Writer, name string, cfg config) error {
 162     if name == `` || name == `-` {
 163         return delay(w, os.Stdin, cfg)
 164     }
 165 
 166     f, err := os.Open(name)
 167     if err != nil {
 168         return errors.New(`can't read from file named "` + name + `"`)
 169     }
 170     defer f.Close()
 171 
 172     return delay(w, f, cfg)
 173 }
 174 
 175 func delay(w *bufio.Writer, r io.Reader, cfg config) error {
 176     const gb = 1024 * 1024 * 1024
 177     sc := bufio.NewScanner(r)
 178     sc.Buffer(nil, 8*gb)
 179 
 180     for i := 0; sc.Scan(); i++ {
 181         s := sc.Bytes()
 182         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 183             s = s[3:]
 184         }
 185 
 186         time.Sleep(cfg.delay)
 187 
 188         w.Write(s)
 189         if w.WriteByte('\n') != nil {
 190             return io.EOF
 191         }
 192 
 193         if cfg.liveLines && w.Flush() != nil {
 194             return io.EOF
 195         }
 196     }
 197 
 198     return sc.Err()
 199 }
     File: ./design.txt
   1 Design of `easybox`
   2 
   3 Easybox is a multi-tool command-line app along the lines of `busybox`, where
   4 either the leading command-line argument or the alias-name being used is the
   5 name of the specific tool to run. Having one app act as several others can
   6 save quite some file-space, and can be very filesystem-cache friendly.
   7 
   8 All tools are non-interactive command-line-oriented and platform-agnostic.
   9 
  10 Most tools try to auto-detect whether the standard output is being piped, and
  11 by default use line-oriented buffering, flushing each output line.
  12 
  13 Flushing each output line can massively slow down when emitting much output,
  14 but it avoids the subtle problem of information possibly being stuck in a pipe
  15 for a long time, at the expense of efficiency.
  16 
  17 Most tools defaulting to this slow-but-safe behavior also have a hidden option
  18 to fully-buffer without constant line-flushing, via hidden options `-b`, `--b`,
  19 `-buffer`, or even `--buffer`, which may massively speed things up.
  20 
  21 The main (main.go) program is a simple dispatch to call the `Main` functions
  22 exported by the various sub-packages. Again the app's own filename, in case
  23 it's being called from a file-alias deliberately named, or its leading argument
  24 is used to lookup the tool. Each `Main` function handles running its respective
  25 tool until quitting: some such `Main` funcs even call `os.Exit` directly.
  26 
  27 This deliberate structure makes each sub-package/sub-folder easy to extract
  28 and adapt, by simply renaming its package name into `main` and its exported
  29 `Main` function into `main`: once done, the copy should be its own compilable
  30 go/tinygo standalone tool.
  31 
  32 The subset of `go` being used is compatible both with the official `go`
  33 compiler, as well as with the alternative `tinygo` compiler; test-related code
  34 isn't following such restrictions, only being meant for the `go test` command.
  35 
  36 Compiling with `tinygo` is arguably preferable, with a few relatively-minor
  37 trade-offs, a notable gain in much lower memory use, either no change in speed
  38 or a slight gain in speed, and a much smaller final app size.
  39 
  40 One trade-off of compiling with `tinygo` is losing the seamless multi-core
  41 speed-up in a few tools like `coby`: while its non-trivial use of channels to
  42 manage concurrent behavior is wasted when compiled with `tinygo`, its behavior
  43 is needed when compiled with `go`, allowing full-core use, emitting results as
  44 soon as they're available, while also keeping the original order of the files
  45 given to it.
     File: ./dessv/dessv.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dessv
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 dessv [filenames...]
  37 
  38 Turn Space(s)-Separated Values (SSV) into Tab-Separated Values (TSV), where
  39 both leading and trailing spaces from input lines are ignored.
  40 `
  41 
  42 func Main() {
  43     buffered := false
  44     args := os.Args[1:]
  45 
  46     if len(args) > 0 {
  47         switch args[0] {
  48         case `-b`, `--b`, `-buffered`, `--buffered`:
  49             buffered = true
  50             args = args[1:]
  51 
  52         case `-h`, `--h`, `-help`, `--help`:
  53             os.Stdout.WriteString(info[1:])
  54             return
  55         }
  56     }
  57 
  58     if len(args) > 0 && args[0] == `--` {
  59         args = args[1:]
  60     }
  61 
  62     liveLines := !buffered
  63     if !buffered {
  64         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  65             liveLines = false
  66         }
  67     }
  68 
  69     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  70         os.Stderr.WriteString(`dessv: `)
  71         os.Stderr.WriteString(err.Error())
  72         os.Stderr.WriteString("\n")
  73         os.Exit(1)
  74         return
  75     }
  76 }
  77 
  78 func run(w io.Writer, args []string, live bool) error {
  79     bw := bufio.NewWriterSize(w, 32*1024)
  80     defer bw.Flush()
  81 
  82     if len(args) == 0 {
  83         return dessv(bw, os.Stdin, live)
  84     }
  85 
  86     for _, name := range args {
  87         if err := handleFile(bw, name, live); err != nil {
  88             return err
  89         }
  90     }
  91     return nil
  92 }
  93 
  94 func handleFile(w *bufio.Writer, name string, live bool) error {
  95     if name == `` || name == `-` {
  96         return dessv(w, os.Stdin, live)
  97     }
  98 
  99     f, err := os.Open(name)
 100     if err != nil {
 101         return errors.New(`can't read from file named "` + name + `"`)
 102     }
 103     defer f.Close()
 104 
 105     return dessv(w, f, live)
 106 }
 107 
 108 func dessv(w *bufio.Writer, r io.Reader, live bool) error {
 109     const gb = 1024 * 1024 * 1024
 110     sc := bufio.NewScanner(r)
 111     sc.Buffer(nil, 8*gb)
 112     handleRow := handleRowSSV
 113     numTabs := ^0
 114 
 115     for i := 0; sc.Scan(); i++ {
 116         s := sc.Bytes()
 117         if i == 0 {
 118             if bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 119                 s = s[3:]
 120             }
 121 
 122             for _, b := range s {
 123                 if b == '\t' {
 124                     handleRow = handleRowTSV
 125                     break
 126                 }
 127             }
 128             numTabs = handleRow(w, s, numTabs)
 129         } else {
 130             handleRow(w, s, numTabs)
 131         }
 132 
 133         if w.WriteByte('\n') != nil {
 134             return io.EOF
 135         }
 136 
 137         if live && w.Flush() != nil {
 138             return io.EOF
 139         }
 140     }
 141 
 142     return sc.Err()
 143 }
 144 
 145 func handleRowSSV(w *bufio.Writer, s []byte, n int) int {
 146     for len(s) > 0 && s[0] == ' ' {
 147         s = s[1:]
 148     }
 149     for len(s) > 0 && s[len(s)-1] == ' ' {
 150         s = s[:len(s)-1]
 151     }
 152 
 153     got := 0
 154 
 155     for got = 0; len(s) > 0; got++ {
 156         if got > 0 {
 157             w.WriteByte('\t')
 158         }
 159 
 160         i := bytes.IndexByte(s, ' ')
 161         if i < 0 {
 162             w.Write(s)
 163             s = nil
 164             n--
 165             break
 166         }
 167 
 168         w.Write(s[:i])
 169         s = s[i+1:]
 170         for len(s) > 0 && s[0] == ' ' {
 171             s = s[1:]
 172         }
 173         n--
 174     }
 175 
 176     w.Write(s)
 177     writeTabs(w, n)
 178     return got
 179 }
 180 
 181 func handleRowTSV(w *bufio.Writer, s []byte, n int) int {
 182     got := 0
 183     for _, b := range s {
 184         if b == '\t' {
 185             got++
 186         }
 187     }
 188 
 189     w.Write(s)
 190     writeTabs(w, n-got)
 191     return got
 192 }
 193 
 194 func writeTabs(w *bufio.Writer, n int) {
 195     for n > 0 {
 196         w.WriteByte('\t')
 197         n--
 198     }
 199 }
     File: ./detab/detab.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package detab
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34     "strings"
  35     "unicode/utf8"
  36 )
  37 
  38 const info = `
  39 detab [options...] [tab-stop...] [files...]
  40 
  41 
  42 Expand each tab into up to the number of spaces given, or up to 4 spaces by
  43 default. Alternatively, you can join TSVs (tab-separated values) using the
  44 separator string given, instead of expanding each tab into a run of spaces.
  45 
  46 Also, you can ignore all tabs either by using 0 as the tab-stop, or using
  47 an empty separator string via the join option.
  48 
  49 All (optional) leading options start with either single or double-dash:
  50 
  51     -h, -help            show this help message
  52     -i, -initial         don't convert tabs after non-blanks
  53     -n, -t               change tab-stop number using the next argument
  54     -j=SEP, -join=SEP    replace tabs with the string given instead
  55 `
  56 
  57 type config struct {
  58     tabStop   uint
  59     join      string
  60     initial   bool
  61     liveLines bool
  62 }
  63 
  64 func Main() {
  65     var cfg config
  66     cfg.tabStop = 4
  67     cfg.liveLines = true
  68     args := os.Args[1:]
  69 
  70     for len(args) > 0 {
  71         switch args[0] {
  72         case `-b`, `--b`, `-buffered`, `--buffered`:
  73             cfg.liveLines = false
  74             args = args[1:]
  75             continue
  76 
  77         case `-h`, `--h`, `-help`, `--help`:
  78             os.Stdout.WriteString(info[1:])
  79             return
  80 
  81         case `-i`, `--i`, `-initial`, `--initial`:
  82             cfg.initial = true
  83             args = args[1:]
  84             continue
  85 
  86         case `-j`, `--j`, `-join`, `--join`:
  87             if len(args) < 2 {
  88                 os.Stderr.WriteString("forgot the join/separator string\n")
  89                 os.Exit(1)
  90                 return
  91             }
  92             cfg.join = args[1]
  93             cfg.tabStop = 0
  94             args = args[2:]
  95             continue
  96 
  97         case `-n`, `--n`, `-t`, `--t`:
  98             if len(args) < 2 {
  99                 os.Stderr.WriteString("forgot the tab-stop number\n")
 100                 os.Exit(1)
 101                 return
 102             }
 103             if n, err := strconv.ParseUint(args[1], 10, 64); err == nil {
 104                 cfg.tabStop = uint(n)
 105                 args = args[2:]
 106                 continue
 107             }
 108             os.Stderr.WriteString(args[1] + " isn't a valid number\n")
 109             os.Exit(1)
 110             return
 111         }
 112 
 113         if hasAnyPrefix(args[0], `-j=`, `--j=`, `-join=`, `--join=`) {
 114             cfg.join = args[0][strings.IndexByte(args[0], '=')+1:]
 115             cfg.tabStop = 0
 116             args = args[1:]
 117             continue
 118         }
 119 
 120         break
 121     }
 122 
 123     if len(args) > 0 {
 124         if n, err := strconv.ParseUint(args[0], 10, 64); err == nil {
 125             cfg.tabStop = uint(n)
 126             args = args[1:]
 127         }
 128     }
 129 
 130     if len(args) > 0 && args[0] == `--` {
 131         args = args[1:]
 132     }
 133 
 134     if cfg.liveLines {
 135         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 136             cfg.liveLines = false
 137         }
 138     }
 139 
 140     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 141         os.Stderr.WriteString(`detab: `)
 142         os.Stderr.WriteString(err.Error())
 143         os.Stderr.WriteString("\n")
 144         os.Exit(1)
 145         return
 146     }
 147 }
 148 
 149 func hasAnyPrefix(s string, prefixes ...string) bool {
 150     for _, pre := range prefixes {
 151         if strings.HasPrefix(s, pre) {
 152             return true
 153         }
 154     }
 155     return false
 156 }
 157 
 158 func run(w io.Writer, args []string, cfg config) error {
 159     bw := bufio.NewWriterSize(w, 32*1024)
 160     defer bw.Flush()
 161 
 162     dashes := 0
 163     for _, name := range args {
 164         if name == `-` {
 165             dashes++
 166         }
 167         if dashes > 1 {
 168             return errors.New(`can't read stdin (dash) more than once`)
 169         }
 170     }
 171 
 172     if len(args) == 0 {
 173         return detab(bw, os.Stdin, cfg)
 174     }
 175 
 176     for _, name := range args {
 177         if name == `-` {
 178             if err := detab(bw, os.Stdin, cfg); err != nil {
 179                 return err
 180             }
 181             continue
 182         }
 183 
 184         if err := handleFile(bw, name, cfg); err != nil {
 185             return err
 186         }
 187     }
 188     return nil
 189 }
 190 
 191 func handleFile(w *bufio.Writer, name string, cfg config) error {
 192     if name == `` || name == `-` {
 193         return detab(w, os.Stdin, cfg)
 194     }
 195 
 196     f, err := os.Open(name)
 197     if err != nil {
 198         return errors.New(`can't read from file named "` + name + `"`)
 199     }
 200     defer f.Close()
 201 
 202     return detab(w, f, cfg)
 203 }
 204 
 205 func detab(w *bufio.Writer, r io.Reader, cfg config) error {
 206     if cfg.join != `` {
 207         return replace(w, r, cfg)
 208     }
 209     if cfg.tabStop == 0 {
 210         return ignore(w, r, cfg)
 211     }
 212     return expand(w, r, cfg)
 213 }
 214 
 215 func expand(w *bufio.Writer, r io.Reader, cfg config) error {
 216     const gb = 1024 * 1024 * 1024
 217     sc := bufio.NewScanner(r)
 218     sc.Buffer(nil, 8*gb)
 219 
 220     var buf []byte
 221     maxSpaces := int(cfg.tabStop)
 222 
 223     for i := 0; sc.Scan(); i++ {
 224         s := sc.Bytes()
 225         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 226             s = s[3:]
 227         }
 228 
 229         if maxSpaces < 1 {
 230             w.Write(s)
 231         } else if cfg.initial {
 232             tabs := 0
 233             for len(s) > 0 && s[0] == '\t' {
 234                 tabs++
 235                 s = s[1:]
 236             }
 237             writeSpaces(w, maxSpaces*tabs)
 238             w.Write(s)
 239         } else if bytes.IndexByte(s, '\t') < 0 {
 240             w.Write(s)
 241         } else {
 242             buf = expandTabs(buf[:0], s, maxSpaces)
 243             w.Write(buf)
 244         }
 245 
 246         if w.WriteByte('\n') != nil {
 247             return io.EOF
 248         }
 249 
 250         if cfg.liveLines && w.Flush() != nil {
 251             return io.EOF
 252         }
 253     }
 254 
 255     return sc.Err()
 256 }
 257 
 258 func ignore(w *bufio.Writer, r io.Reader, cfg config) error {
 259     const gb = 1024 * 1024 * 1024
 260     sc := bufio.NewScanner(r)
 261     sc.Buffer(nil, 8*gb)
 262 
 263     for i := 0; sc.Scan(); i++ {
 264         s := sc.Bytes()
 265         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 266             s = s[3:]
 267         }
 268 
 269         if bytes.IndexByte(s, '\t') < 0 {
 270             w.Write(s)
 271         } else {
 272             for len(s) > 0 {
 273                 j := bytes.IndexByte(s, '\t')
 274                 if j < 0 {
 275                     w.Write(s)
 276                     break
 277                 }
 278                 w.Write(s[:j])
 279                 s = s[j+1:]
 280             }
 281         }
 282 
 283         if w.WriteByte('\n') != nil {
 284             return io.EOF
 285         }
 286 
 287         if cfg.liveLines && w.Flush() != nil {
 288             return io.EOF
 289         }
 290     }
 291 
 292     return sc.Err()
 293 }
 294 
 295 func replace(w *bufio.Writer, r io.Reader, cfg config) error {
 296     const gb = 1024 * 1024 * 1024
 297     sc := bufio.NewScanner(r)
 298     sc.Buffer(nil, 8*gb)
 299 
 300     var buf []byte
 301 
 302     for i := 0; sc.Scan(); i++ {
 303         s := sc.Bytes()
 304         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 305             s = s[3:]
 306         }
 307 
 308         if bytes.IndexByte(s, '\t') < 0 {
 309             w.Write(s)
 310         } else {
 311             buf = replaceTabs(buf[:0], s, cfg.join)
 312             w.Write(buf)
 313         }
 314 
 315         if w.WriteByte('\n') != nil {
 316             return io.EOF
 317         }
 318 
 319         if cfg.liveLines && w.Flush() != nil {
 320             return io.EOF
 321         }
 322     }
 323 
 324     return sc.Err()
 325 }
 326 
 327 func expandTabs(dst []byte, src []byte, tabStop int) []byte {
 328     n := 0
 329 
 330     if tabStop < 1 {
 331         return append(dst, src...)
 332     }
 333 
 334     for len(src) > 0 {
 335         r, size := utf8.DecodeRune(src)
 336 
 337         if r != '\t' {
 338             dst = append(dst, src[:size]...)
 339             n++
 340         } else {
 341             spaces := tabStop - n%tabStop
 342             dst = appendSpaces(dst, spaces)
 343             n += spaces
 344         }
 345 
 346         src = src[size:]
 347     }
 348 
 349     return dst
 350 }
 351 
 352 func appendSpaces(dst []byte, n int) []byte {
 353     const (
 354         half   = `                                `
 355         spaces = half + half
 356     )
 357 
 358     for n >= len(spaces) {
 359         dst = append(dst, spaces...)
 360         n -= len(spaces)
 361     }
 362     if n > 0 {
 363         dst = append(dst, spaces[:n]...)
 364     }
 365     return dst
 366 }
 367 
 368 func replaceTabs(dst []byte, src []byte, with string) []byte {
 369     for len(src) > 0 {
 370         i := bytes.IndexByte(src, '\t')
 371         if i < 0 {
 372             dst = append(dst, src...)
 373             break
 374         }
 375         r, size := utf8.DecodeRune(src)
 376 
 377         if r != '\t' {
 378             dst = append(dst, src[:size]...)
 379         } else {
 380             dst = append(dst, with...)
 381         }
 382 
 383         src = src[size:]
 384     }
 385 
 386     return dst
 387 }
 388 
 389 func writeSpaces(w *bufio.Writer, n int) {
 390     const (
 391         half   = `                                `
 392         spaces = half + half
 393     )
 394 
 395     for n >= len(spaces) {
 396         w.WriteString(spaces)
 397         n -= len(spaces)
 398     }
 399     if n > 0 {
 400         w.WriteString(spaces[:n])
 401     }
 402 }
     File: ./dtsv/dtsv.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dtsv
  26 
  27 import (
  28     "bufio"
  29     "os"
  30     "sort"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 dtsv [options...] [columns/indices...]
  37 
  38 Drop Tab-Separated Values avoids the columns using the arguments given, no
  39 matter the order given. Arguments are matched to columns, first exactly,
  40 failing that case-insensitively, and failing even that, as 1-based indices,
  41 with negative numbers counting backward from the last column.
  42 
  43 Input is TSV (tab-separated values) lines from the standard input, where the
  44 first/header line has all the column names.
  45 
  46 The options are, available both in single and double-dash versions
  47 
  48     -h, -help    show this help message
  49 `
  50 
  51 func Main() {
  52     args := os.Args[1:]
  53 
  54     if len(args) > 0 {
  55         switch args[0] {
  56         case `-h`, `--h`, `-help`, `--help`:
  57             os.Stdout.WriteString(info[1:])
  58             return
  59         }
  60     }
  61 
  62     if len(args) > 0 && args[0] == `--` {
  63         args = args[1:]
  64     }
  65 
  66     // if len(args) == 0 {
  67     //  os.Stderr.WriteString(info[1:])
  68     //  os.Exit(1)
  69     //  return
  70     // }
  71 
  72     var items []string
  73     indices := make([]int, 0, len(args))
  74 
  75     sc := bufio.NewScanner(os.Stdin)
  76     sc.Buffer(nil, 8*1024*1024*1024)
  77 
  78     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  79     defer bw.Flush()
  80 
  81     // the number of columns, which is determined by the first/header line
  82     cols := 0
  83 
  84     for i := 0; sc.Scan(); i++ {
  85         line := sc.Text()
  86 
  87         if i == 0 {
  88             if strings.HasPrefix(line, "\xef\xbb\xbf") {
  89                 line = line[3:]
  90             }
  91 
  92             items = appendTSV(items[:0], line)
  93 
  94             for _, s := range args {
  95                 if j := findColumn(items, s); j >= 0 {
  96                     indices = append(indices, j)
  97                 }
  98             }
  99             sort.Ints(indices)
 100 
 101             cols = len(items) - len(indices)
 102         } else {
 103             items = appendTSV(items[:0], line)
 104         }
 105 
 106         if emit(bw, items, indices, cols) != nil {
 107             return
 108         }
 109     }
 110 }
 111 
 112 func appendTSV(to []string, line string) []string {
 113     for len(line) > 0 {
 114         i := strings.IndexByte(line, '\t')
 115         if i < 0 {
 116             return append(to, line)
 117         }
 118 
 119         to = append(to, line[:i])
 120         line = line[i+1:]
 121     }
 122 
 123     return to
 124 }
 125 
 126 // emit writes the current items which aren't meant to be dropped; indices
 127 // given are assumed to be sorted in increasing order; the max number of
 128 // columns is also needed to pad rows with too few items in them
 129 func emit(w *bufio.Writer, items []string, indices []int, cols int) error {
 130     n := 0
 131 
 132     for i, s := range items {
 133         if len(indices) > 0 && i == indices[0] {
 134             indices = indices[1:]
 135             continue
 136         }
 137 
 138         if n > 0 {
 139             w.WriteByte('\t')
 140         }
 141         n++
 142         w.WriteString(s)
 143     }
 144 
 145     // pad missing columns with empty values
 146     for cols > n {
 147         w.WriteByte('\t')
 148         cols--
 149     }
 150 
 151     return w.WriteByte('\n')
 152 }
 153 
 154 // findColumn tries to find the 0-based column-index, given the first/header
 155 // row, and a name/integer-index
 156 func findColumn(items []string, name string) int {
 157     // try exact match
 158     for i, s := range items {
 159         if s == name {
 160             return i
 161         }
 162     }
 163 
 164     // try case-insensitive match
 165     for i, s := range items {
 166         if strings.EqualFold(s, name) {
 167             return i
 168         }
 169     }
 170 
 171     // try a non-zero numeric index
 172     i, err := strconv.Atoi(name)
 173     if err != nil || i == 0 {
 174         return -1
 175     }
 176 
 177     // make indices 0-based, and correct negative indices
 178     if i > 0 {
 179         i--
 180     } else if i < 0 {
 181         i += len(items)
 182     }
 183 
 184     // see if numeric index is in range
 185     if 0 <= i && i < len(items) {
 186         return i
 187     }
 188 
 189     return -1
 190 }
     File: ./ecoli/ecoli.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ecoli
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33 )
  34 
  35 const info = `
  36 ecoli [options...] [regex/style pairs...]
  37 
  38 
  39 Expressions COloring LInes tries to match each line read from the standard
  40 input to the regexes given, coloring/styling with the named-style paired
  41 to the first matching regex, if any. Lines not matching any regex stay the
  42 same.
  43 
  44 The options are, available both in single and double-dash versions
  45 
  46     -h, -help                 show this help message
  47     -i, -ins, -insensitive    match the regexes given case-insensitively
  48 
  49 Some of the colors/styles available are:
  50 
  51     blue         blueback
  52     bold
  53     gray         grayback
  54     green        greenback
  55     inverse
  56     magenta      magentaback
  57     orange       orangeback
  58     purple       purpleback
  59     red          redback
  60     underline
  61 
  62 Some style aliases are:
  63 
  64     b       blue                  bb       blueback
  65     g       green                 gb       greenback
  66     m       magenta               mb       magentaback
  67     o       orange                ob       orangeback
  68     p       purple                pb       purpleback
  69     r       red                   rb       redback
  70     i       inverse (highlight)
  71     u       underline
  72 `
  73 
  74 var styleAliases = map[string]string{
  75     `b`: `blue`,
  76     `g`: `green`,
  77     `m`: `magenta`,
  78     `o`: `orange`,
  79     `p`: `purple`,
  80     `r`: `red`,
  81     `u`: `underline`,
  82 
  83     `bb`: `blueback`,
  84     `bg`: `greenback`,
  85     `bm`: `magentaback`,
  86     `bo`: `orangeback`,
  87     `bp`: `purpleback`,
  88     `br`: `redback`,
  89 
  90     `gb`: `greenback`,
  91     `mb`: `magentaback`,
  92     `ob`: `orangeback`,
  93     `pb`: `purpleback`,
  94     `rb`: `redback`,
  95 
  96     `hi`:  `inverse`,
  97     `inv`: `inverse`,
  98     `mag`: `magenta`,
  99 
 100     `du`: `doubleunderline`,
 101 
 102     `flip`: `inverse`,
 103     `swap`: `inverse`,
 104 
 105     `reset`:     `plain`,
 106     `highlight`: `inverse`,
 107     `hilite`:    `inverse`,
 108     `invert`:    `inverse`,
 109     `inverted`:  `inverse`,
 110     `swapped`:   `inverse`,
 111 
 112     `dunderline`:  `doubleunderline`,
 113     `dunderlined`: `doubleunderline`,
 114 
 115     `strikethrough`: `strike`,
 116     `strikethru`:    `strike`,
 117     `struck`:        `strike`,
 118 
 119     `underlined`: `underline`,
 120 
 121     `bblue`:    `blueback`,
 122     `bgray`:    `grayback`,
 123     `bgreen`:   `greenback`,
 124     `bmagenta`: `magentaback`,
 125     `borange`:  `orangeback`,
 126     `bpurple`:  `purpleback`,
 127     `bred`:     `redback`,
 128 
 129     `bgblue`:    `blueback`,
 130     `bggray`:    `grayback`,
 131     `bggreen`:   `greenback`,
 132     `bgmag`:     `magentaback`,
 133     `bgmagenta`: `magentaback`,
 134     `bgorange`:  `orangeback`,
 135     `bgpurple`:  `purpleback`,
 136     `bgred`:     `redback`,
 137 
 138     `bluebg`:    `blueback`,
 139     `graybg`:    `grayback`,
 140     `greenbg`:   `greenback`,
 141     `magbg`:     `magentaback`,
 142     `magentabg`: `magentaback`,
 143     `orangebg`:  `orangeback`,
 144     `purplebg`:  `purpleback`,
 145     `redbg`:     `redback`,
 146 
 147     `backblue`:    `blueback`,
 148     `backgray`:    `grayback`,
 149     `backgreen`:   `greenback`,
 150     `backmag`:     `magentaback`,
 151     `backmagenta`: `magentaback`,
 152     `backorange`:  `orangeback`,
 153     `backpurple`:  `purpleback`,
 154     `backred`:     `redback`,
 155 }
 156 
 157 var styles = map[string]string{
 158     `blue`:            "\x1b[38;2;0;95;215m",
 159     `bold`:            "\x1b[1m",
 160     `doubleunderline`: "\x1b[21m",
 161     `gray`:            "\x1b[38;2;168;168;168m",
 162     `green`:           "\x1b[38;2;0;135;95m",
 163     `inverse`:         "\x1b[7m",
 164     `magenta`:         "\x1b[38;2;215;0;255m",
 165     `orange`:          "\x1b[38;2;215;95;0m",
 166     `plain`:           "\x1b[0m",
 167     `purple`:          "\x1b[38;2;135;95;255m",
 168     `red`:             "\x1b[38;2;204;0;0m",
 169     `strike`:          "\x1b[9m",
 170     `underline`:       "\x1b[4m",
 171 
 172     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 173     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 174     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 175     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 176     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 177     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 178     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 179 }
 180 
 181 // pair has a regular-expression and its associated ANSI-code style together
 182 type pair struct {
 183     expr  *regexp.Regexp
 184     style string
 185 }
 186 
 187 func Main() {
 188     buffered := false
 189     insensitive := false
 190     args := os.Args[1:]
 191 
 192     for len(args) > 0 {
 193         switch args[0] {
 194         case `-b`, `--b`, `-buffered`, `--buffered`:
 195             buffered = true
 196             args = args[1:]
 197             continue
 198 
 199         case `-h`, `--h`, `-help`, `--help`:
 200             os.Stdout.WriteString(info[1:])
 201             return
 202 
 203         case `-i`, `--i`, `-ins`, `--ins`:
 204             insensitive = true
 205             args = args[1:]
 206             continue
 207         }
 208 
 209         break
 210     }
 211 
 212     if len(args) > 0 && args[0] == `--` {
 213         args = args[1:]
 214     }
 215 
 216     if len(args)%2 != 0 {
 217         const msg = "you forgot the style-name for/after the last regex\n"
 218         os.Stderr.WriteString(msg)
 219         os.Exit(1)
 220         return
 221     }
 222 
 223     nerr := 0
 224     pairs := make([]pair, 0, len(args)/2)
 225 
 226     for len(args) >= 2 {
 227         src := args[0]
 228         sname := args[1]
 229 
 230         var err error
 231         var exp *regexp.Regexp
 232         if insensitive {
 233             exp, err = regexp.Compile(`(?i)` + src)
 234         } else {
 235             exp, err = regexp.Compile(src)
 236         }
 237         if err != nil {
 238             os.Stderr.WriteString(`ecoli: `)
 239             os.Stderr.WriteString(err.Error())
 240             os.Stderr.WriteString("\n")
 241             nerr++
 242         }
 243 
 244         if alias, ok := styleAliases[sname]; ok {
 245             sname = alias
 246         }
 247 
 248         style, ok := styles[sname]
 249         if !ok {
 250             os.Stderr.WriteString("no style named `")
 251             os.Stderr.WriteString(args[1])
 252             os.Stderr.WriteString("`\n")
 253             nerr++
 254         }
 255 
 256         pairs = append(pairs, pair{expr: exp, style: style})
 257         args = args[2:]
 258     }
 259 
 260     if nerr > 0 {
 261         os.Exit(1)
 262         return
 263     }
 264 
 265     liveLines := !buffered
 266     if !buffered {
 267         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 268             liveLines = false
 269         }
 270     }
 271 
 272     sc := bufio.NewScanner(os.Stdin)
 273     sc.Buffer(nil, 8*1024*1024*1024)
 274     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 275     var plain []byte
 276 
 277     for i := 0; sc.Scan(); i++ {
 278         s := sc.Bytes()
 279         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 280             s = s[3:]
 281         }
 282         plain = appendPlain(plain[:0], s)
 283 
 284         if err := handleLine(bw, s, noANSI(plain), pairs); err != nil {
 285             return
 286         }
 287 
 288         if !liveLines {
 289             continue
 290         }
 291 
 292         if err := bw.Flush(); err != nil {
 293             return
 294         }
 295     }
 296 }
 297 
 298 // appendPlain extends the slice given using the non-ANSI parts of a string
 299 func appendPlain(dst []byte, src []byte) []byte {
 300     for len(src) > 0 {
 301         i, j := indexEscapeSequence(src)
 302         if i < 0 {
 303             dst = append(dst, src...)
 304             break
 305         }
 306         if j < 0 {
 307             j = len(src)
 308         }
 309 
 310         if i > 0 {
 311             dst = append(dst, src[:i]...)
 312         }
 313 
 314         src = src[j:]
 315     }
 316 
 317     return dst
 318 }
 319 
 320 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 321 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 322 // indices which can be independently negative when either the start/end of
 323 // a sequence isn't found; given their fairly-common use, even the hyperlink
 324 // ESC]8 sequences are supported
 325 func indexEscapeSequence(s []byte) (int, int) {
 326     var prev byte
 327 
 328     for i, b := range s {
 329         if prev == '\x1b' && b == '[' {
 330             j := indexLetter(s[i+1:])
 331             if j < 0 {
 332                 return i, -1
 333             }
 334             return i - 1, i + 1 + j + 1
 335         }
 336 
 337         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 338             j := indexPair(s[i+1:], '\x1b', '\\')
 339             if j < 0 {
 340                 return i, -1
 341             }
 342             return i - 1, i + 1 + j + 2
 343         }
 344 
 345         prev = b
 346     }
 347 
 348     return -1, -1
 349 }
 350 
 351 func indexLetter(s []byte) int {
 352     for i, b := range s {
 353         upper := b &^ 32
 354         if 'A' <= upper && upper <= 'Z' {
 355             return i
 356         }
 357     }
 358 
 359     return -1
 360 }
 361 
 362 func indexPair(s []byte, x byte, y byte) int {
 363     var prev byte
 364 
 365     for i, b := range s {
 366         if prev == x && b == y && i > 0 {
 367             return i
 368         }
 369         prev = b
 370     }
 371 
 372     return -1
 373 }
 374 
 375 // noANSI ensures arguments to func handleLine are given in the right order
 376 type noANSI []byte
 377 
 378 // handleLine styles the current line given to it using the first matching
 379 // regex, keeping it as given if none of the regexes match; it's given 2
 380 // strings: the first is the original line, the latter is its plain-text
 381 // version (with no ANSI codes) and is used for the regex-matching, since
 382 // ANSI codes use a mix of numbers and letters, which can themselves match
 383 func handleLine(w *bufio.Writer, s []byte, plain noANSI, pairs []pair) error {
 384     for _, p := range pairs {
 385         if p.expr.Match(plain) {
 386             w.WriteString(p.style)
 387             w.Write(s)
 388             w.WriteString("\x1b[0m")
 389             return w.WriteByte('\n')
 390         }
 391     }
 392 
 393     w.Write(s)
 394     return w.WriteByte('\n')
 395 }
     File: ./erase/erase.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package erase
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33 )
  34 
  35 const info = `
  36 erase [options...] [regexes...]
  37 
  38 
  39 Ignore/remove all occurrences of all regex matches along lines read from the
  40 standard input. The regular-expression mode used is "re2", which is a superset
  41 of the commonly-used "extended-mode".
  42 
  43 All ANSI-style sequences are removed before trying to match-remove things, to
  44 avoid messing those up. Each regex erases all its occurrences on the current
  45 line in the order given among the arguments, so regex-order matters.
  46 
  47 The options are, available both in single and double-dash versions
  48 
  49     -h, -help    show this help message
  50     -i, -ins     match regexes case-insensitively
  51 `
  52 
  53 func Main() {
  54     args := os.Args[1:]
  55     buffered := false
  56     insensitive := false
  57 
  58     for len(args) > 0 {
  59         switch args[0] {
  60         case `-b`, `--b`, `-buffered`, `--buffered`:
  61             buffered = true
  62             args = args[1:]
  63             continue
  64 
  65         case `-h`, `--h`, `-help`, `--help`:
  66             os.Stdout.WriteString(info[1:])
  67             return
  68 
  69         case `-i`, `--i`, `-ins`, `--ins`:
  70             insensitive = true
  71             args = args[1:]
  72             continue
  73         }
  74 
  75         break
  76     }
  77 
  78     if len(args) > 0 && args[0] == `--` {
  79         args = args[1:]
  80     }
  81 
  82     exprs := make([]*regexp.Regexp, 0, len(args))
  83 
  84     for _, s := range args {
  85         var err error
  86         var exp *regexp.Regexp
  87 
  88         if insensitive {
  89             exp, err = regexp.Compile(`(?i)` + s)
  90         } else {
  91             exp, err = regexp.Compile(s)
  92         }
  93 
  94         if err != nil {
  95             os.Stderr.WriteString(`erase: `)
  96             os.Stderr.WriteString(err.Error())
  97             os.Stderr.WriteString("\n")
  98             continue
  99         }
 100 
 101         exprs = append(exprs, exp)
 102     }
 103 
 104     // quit right away when given invalid regexes
 105     if len(exprs) < len(args) {
 106         os.Exit(1)
 107         return
 108     }
 109 
 110     liveLines := !buffered
 111     if !buffered {
 112         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 113             liveLines = false
 114         }
 115     }
 116 
 117     err := run(os.Stdout, os.Stdin, exprs, liveLines)
 118     if err != nil && err != io.EOF {
 119         os.Stderr.WriteString(`erase: `)
 120         os.Stderr.WriteString(err.Error())
 121         os.Stderr.WriteString("\n")
 122         os.Exit(1)
 123         return
 124     }
 125 }
 126 
 127 func run(w io.Writer, r io.Reader, exprs []*regexp.Regexp, live bool) error {
 128     var buf []byte
 129     sc := bufio.NewScanner(r)
 130     sc.Buffer(nil, 8*1024*1024*1024)
 131     bw := bufio.NewWriterSize(w, 32*1024)
 132     defer bw.Flush()
 133 
 134     src := make([]byte, 8*1024)
 135     dst := make([]byte, 8*1024)
 136 
 137     for i := 0; sc.Scan(); i++ {
 138         line := sc.Bytes()
 139         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 140             line = line[3:]
 141         }
 142 
 143         s := line
 144         if bytes.IndexByte(s, '\x1b') >= 0 {
 145             buf = plain(buf[:0], s)
 146             s = buf
 147         }
 148 
 149         if len(exprs) > 0 {
 150             src = append(src[:0], s...)
 151             for _, exp := range exprs {
 152                 dst = erase(dst[:0], src, exp)
 153                 src = append(src[:0], dst...)
 154             }
 155             bw.Write(dst)
 156         } else {
 157             bw.Write(s)
 158         }
 159 
 160         if bw.WriteByte('\n') != nil {
 161             return io.EOF
 162         }
 163 
 164         if live && bw.Flush() != nil {
 165             return io.EOF
 166         }
 167     }
 168 
 169     return sc.Err()
 170 }
 171 
 172 func erase(dst []byte, src []byte, with *regexp.Regexp) []byte {
 173     for len(src) > 0 {
 174         span := with.FindIndex(src)
 175         // also ignore empty regex matches to avoid infinite outer loops,
 176         // as skipping empty slices isn't advancing at all, leaving the
 177         // string stuck to being empty-matched forever by the same regex
 178         if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
 179             return append(dst, src...)
 180         }
 181 
 182         start, end := span[0], span[1]
 183         dst = append(dst, src[:start]...)
 184         // avoid infinite loops caused by empty regex matches
 185         if start == end && end < len(src) {
 186             dst = append(dst, src[end])
 187             end++
 188         }
 189         src = src[end:]
 190     }
 191 
 192     return dst
 193 }
 194 
 195 func plain(dst []byte, src []byte) []byte {
 196     for len(src) > 0 {
 197         i, j := indexEscapeSequence(src)
 198         if i < 0 {
 199             dst = append(dst, src...)
 200             break
 201         }
 202         if j < 0 {
 203             j = len(src)
 204         }
 205 
 206         if i > 0 {
 207             dst = append(dst, src[:i]...)
 208         }
 209 
 210         src = src[j:]
 211     }
 212 
 213     return dst
 214 }
 215 
 216 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 217 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 218 // indices which can be independently negative when either the start/end of
 219 // a sequence isn't found; given their fairly-common use, even the hyperlink
 220 // ESC]8 sequences are supported
 221 func indexEscapeSequence(s []byte) (int, int) {
 222     var prev byte
 223 
 224     for i, b := range s {
 225         if prev == '\x1b' && b == '[' {
 226             j := indexLetter(s[i+1:])
 227             if j < 0 {
 228                 return i, -1
 229             }
 230             return i - 1, i + 1 + j + 1
 231         }
 232 
 233         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 234             j := indexPair(s[i+1:], '\x1b', '\\')
 235             if j < 0 {
 236                 return i, -1
 237             }
 238             return i - 1, i + 1 + j + 2
 239         }
 240 
 241         prev = b
 242     }
 243 
 244     return -1, -1
 245 }
 246 
 247 func indexLetter(s []byte) int {
 248     for i, b := range s {
 249         upper := b &^ 32
 250         if 'A' <= upper && upper <= 'Z' {
 251             return i
 252         }
 253     }
 254 
 255     return -1
 256 }
 257 
 258 func indexPair(s []byte, x byte, y byte) int {
 259     var prev byte
 260 
 261     for i, b := range s {
 262         if prev == x && b == y && i > 0 {
 263             return i
 264         }
 265         prev = b
 266     }
 267 
 268     return -1
 269 }
     File: ./expand/expand.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package expand
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34     "strings"
  35     "unicode/utf8"
  36 )
  37 
  38 const info = `
  39 expand [options...] [files...]
  40 
  41 Expand each tab into up to the number of spaces given, or up to 4 spaces by
  42 default.
  43 
  44 All (optional) leading options start with either single or double-dash:
  45 
  46     -h, -help        show this help message
  47     -i, -initial     don't convert tabs after non-blanks
  48     -t [tab-stop]    change the tap-stop, or the max spaces for tab expansion
  49 `
  50 
  51 type config struct {
  52     tabStop   uint
  53     initial   bool
  54     liveLines bool
  55 }
  56 
  57 func Main() {
  58     var cfg config
  59     cfg.tabStop = 4
  60     cfg.liveLines = true
  61     args := os.Args[1:]
  62 
  63     for len(args) > 0 {
  64         switch args[0] {
  65         case `-b`, `--b`, `-buffered`, `--buffered`:
  66             cfg.liveLines = false
  67             args = args[1:]
  68             continue
  69 
  70         case `-h`, `--h`, `-help`, `--help`:
  71             os.Stdout.WriteString(info[1:])
  72             return
  73 
  74         case `-i`, `--i`, `-initial`, `--initial`:
  75             cfg.initial = true
  76             args = args[1:]
  77             continue
  78 
  79         case `-t`, `--t`:
  80             if len(args) < 2 {
  81                 os.Stderr.WriteString("forgot the tab-stop number\n")
  82                 os.Exit(1)
  83                 return
  84             }
  85 
  86             if n, err := strconv.ParseUint(args[1], 10, 64); err == nil {
  87                 cfg.tabStop = uint(n)
  88             }
  89             args = args[2:]
  90             continue
  91         }
  92 
  93         if strings.HasPrefix(args[0], `--tabs=`) {
  94             s := strings.TrimPrefix(args[0], `--tabs=`)
  95             if n, err := strconv.ParseUint(s, 10, 64); err == nil {
  96                 cfg.tabStop = uint(n)
  97             } else {
  98                 os.Stderr.WriteString("forgot the tab-stop number\n")
  99                 os.Exit(1)
 100                 return
 101             }
 102             args = args[1:]
 103             continue
 104         }
 105 
 106         break
 107     }
 108 
 109     if len(args) > 0 && args[0] == `--` {
 110         args = args[1:]
 111     }
 112 
 113     if cfg.liveLines {
 114         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 115             cfg.liveLines = false
 116         }
 117     }
 118 
 119     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 120         os.Stderr.WriteString(`expand: `)
 121         os.Stderr.WriteString(err.Error())
 122         os.Stderr.WriteString("\n")
 123         os.Exit(1)
 124         return
 125     }
 126 }
 127 
 128 func run(w io.Writer, args []string, cfg config) error {
 129     bw := bufio.NewWriterSize(w, 32*1024)
 130     defer bw.Flush()
 131 
 132     dashes := 0
 133     for _, name := range args {
 134         if name == `-` {
 135             dashes++
 136         }
 137         if dashes > 1 {
 138             return errors.New(`can't read stdin (dash) more than once`)
 139         }
 140     }
 141 
 142     if len(args) == 0 {
 143         return detab(bw, os.Stdin, cfg)
 144     }
 145 
 146     for _, name := range args {
 147         if name == `-` {
 148             if err := detab(bw, os.Stdin, cfg); err != nil {
 149                 return err
 150             }
 151             continue
 152         }
 153 
 154         if err := handleFile(bw, name, cfg); err != nil {
 155             return err
 156         }
 157     }
 158     return nil
 159 }
 160 
 161 func handleFile(w *bufio.Writer, name string, cfg config) error {
 162     if name == `` || name == `-` {
 163         return detab(w, os.Stdin, cfg)
 164     }
 165 
 166     f, err := os.Open(name)
 167     if err != nil {
 168         return errors.New(`can't read from file named "` + name + `"`)
 169     }
 170     defer f.Close()
 171 
 172     return detab(w, f, cfg)
 173 }
 174 
 175 func detab(w *bufio.Writer, r io.Reader, cfg config) error {
 176     const gb = 1024 * 1024 * 1024
 177     sc := bufio.NewScanner(r)
 178     sc.Buffer(nil, 8*gb)
 179 
 180     var buf []byte
 181     maxSpaces := int(cfg.tabStop)
 182 
 183     for i := 0; sc.Scan(); i++ {
 184         s := sc.Bytes()
 185         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 186             s = s[3:]
 187         }
 188 
 189         if maxSpaces < 1 {
 190             w.Write(s)
 191         } else if cfg.initial {
 192             tabs := 0
 193             for len(s) > 0 && s[0] == '\t' {
 194                 tabs++
 195                 s = s[1:]
 196             }
 197             writeSpaces(w, maxSpaces*tabs)
 198             w.Write(s)
 199         } else if bytes.IndexByte(s, '\t') < 0 {
 200             w.Write(s)
 201         } else {
 202             buf = expandTabs(buf[:0], s, maxSpaces)
 203             w.Write(buf)
 204         }
 205 
 206         if w.WriteByte('\n') != nil {
 207             return io.EOF
 208         }
 209 
 210         if cfg.liveLines && w.Flush() != nil {
 211             return io.EOF
 212         }
 213     }
 214 
 215     return sc.Err()
 216 }
 217 
 218 func expandTabs(dst []byte, src []byte, tabStop int) []byte {
 219     n := 0
 220 
 221     if tabStop < 1 {
 222         return append(dst, src...)
 223     }
 224 
 225     for len(src) > 0 {
 226         r, size := utf8.DecodeRune(src)
 227 
 228         if r != '\t' {
 229             dst = append(dst, src[:size]...)
 230             n++
 231         } else {
 232             spaces := tabStop - n%tabStop
 233             dst = appendSpaces(dst, spaces)
 234             n += spaces
 235         }
 236 
 237         src = src[size:]
 238     }
 239 
 240     return dst
 241 }
 242 
 243 func appendSpaces(dst []byte, n int) []byte {
 244     const (
 245         half   = `                                `
 246         spaces = half + half
 247     )
 248 
 249     for n >= len(spaces) {
 250         dst = append(dst, spaces...)
 251         n -= len(spaces)
 252     }
 253     if n > 0 {
 254         dst = append(dst, spaces[:n]...)
 255     }
 256     return dst
 257 }
 258 
 259 func writeSpaces(w *bufio.Writer, n int) {
 260     const (
 261         half   = `                                `
 262         spaces = half + half
 263     )
 264 
 265     for n >= len(spaces) {
 266         w.WriteString(spaces)
 267         n -= len(spaces)
 268     }
 269     if n > 0 {
 270         w.WriteString(spaces[:n])
 271     }
 272 }
     File: ./factor/factor.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package factor
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "math"
  32     "math/bits"
  33     "os"
  34     "strconv"
  35     "strings"
  36 )
  37 
  38 const info = `
  39 factor [options...] [numbers...]
  40 
  41 Find all prime factors for the numbers given. If no numbers are given, the
  42 numbers to factor are read as space-separated fields from each line from the
  43 standard input.
  44 
  45 Options
  46 
  47     --help    show this help message
  48 `
  49 
  50 func Main() {
  51     args := os.Args[1:]
  52 
  53     if len(args) > 0 {
  54         switch args[0] {
  55         case `-h`, `--h`, `-help`, `--help`:
  56             os.Stderr.WriteString(info[1:])
  57             return
  58         }
  59     }
  60 
  61     if len(args) > 0 && args[0] == `--` {
  62         args = args[1:]
  63     }
  64 
  65     if err := run(args); err != nil && err != io.EOF {
  66         os.Stderr.WriteString(`factor: `)
  67         os.Stderr.WriteString(err.Error())
  68         os.Stderr.WriteString("\n")
  69         os.Exit(1)
  70         return
  71     }
  72 }
  73 
  74 func run(args []string) error {
  75     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  76     defer w.Flush()
  77 
  78     if len(args) == 0 {
  79         return factorLines(w)
  80     }
  81 
  82     for _, s := range args {
  83         n, ok := parseInt64(s)
  84         if !ok {
  85             return errors.New(s + ` isn't a valid integer`)
  86         }
  87 
  88         if err := factor(w, int64(n)); err != nil {
  89             return err
  90         }
  91     }
  92     return nil
  93 }
  94 
  95 func factor(w *bufio.Writer, n int64) error {
  96     var buf [24]byte
  97 
  98     w.Write(strconv.AppendInt(buf[:0], n, 10))
  99     w.WriteByte(':')
 100 
 101     for n > 0 && n%2 == 0 {
 102         w.WriteByte(' ')
 103         w.WriteByte('2')
 104         n /= 2
 105     }
 106 
 107     // avoid O(n**2) time-complexity by only checking up to the square-root
 108     max := uint64(math.Ceil(math.Sqrt(float64(n))))
 109 
 110     for div := uint64(3); div <= max; div += 2 {
 111         quo, rem := bits.Div64(0, uint64(n), div)
 112         if rem == 0 {
 113             s := strconv.AppendInt(append(buf[:0], ' '), int64(div), 10)
 114 
 115             w.Write(s)
 116             n = int64(quo)
 117 
 118             for {
 119                 quo, rem := bits.Div64(0, uint64(n), div)
 120                 if rem != 0 {
 121                     break
 122                 }
 123 
 124                 w.Write(s)
 125                 n = int64(quo)
 126             }
 127         }
 128     }
 129 
 130     if n > 1 {
 131         w.WriteByte(' ')
 132         w.Write(strconv.AppendInt(buf[:0], int64(n), 10))
 133     }
 134 
 135     if w.WriteByte('\n') != nil {
 136         return io.EOF
 137     }
 138     return nil
 139 }
 140 
 141 func factorLines(w *bufio.Writer) error {
 142     const gb = 1024 * 1024 * 1024
 143     sc := bufio.NewScanner(os.Stdin)
 144     sc.Buffer(nil, 8*gb)
 145 
 146     for sc.Scan() {
 147         line := strings.TrimSpace(sc.Text())
 148 
 149         for len(line) > 0 {
 150             item, rest := advance(line)
 151             line = rest
 152 
 153             n, ok := parseInt64(item)
 154             if !ok {
 155                 return errors.New(item + ` isn't a valid integer`)
 156             }
 157 
 158             if err := factor(w, int64(n)); err != nil {
 159                 return err
 160             }
 161 
 162             if w.Flush() != nil {
 163                 return io.EOF
 164             }
 165         }
 166     }
 167 
 168     return sc.Err()
 169 }
 170 
 171 func advance(s string) (lead string, rest string) {
 172     for len(s) > 0 && (s[0] == ' ' || s[0] == '\t' || s[0] == '\r') {
 173         s = s[1:]
 174     }
 175 
 176     for i, r := range s {
 177         if r == ' ' || r == '\t' || r == '\r' {
 178             return s[:i], s[i:]
 179         }
 180     }
 181 
 182     return s, ``
 183 }
 184 
 185 func parseInt64(s string) (n int64, ok bool) {
 186     // n, err := strconv.ParseInt(s, 10, 64)
 187     // return n, err == nil
 188 
 189     // handle an optional leading sign
 190     sign := int64(1)
 191     if len(s) > 0 {
 192         if s[0] == '-' {
 193             sign = -1
 194             s = s[1:]
 195         } else if s[0] == '+' {
 196             s = s[1:]
 197         }
 198     }
 199 
 200     if len(s) == 0 {
 201         return 0, false
 202     }
 203 
 204     digits := 0
 205 
 206     for len(s) > 0 {
 207         // less-than-0 byte-wraps around into some bigger-than-9 value
 208         if d := s[0] - '0'; d <= 9 {
 209             digits++
 210             n *= 10
 211             n += int64(d)
 212             s = s[1:]
 213             continue
 214         }
 215 
 216         // ignore underscores, which make long numbers easier to type right
 217         if s[0] == '_' {
 218             s = s[1:]
 219             continue
 220         }
 221 
 222         return sign * n, false
 223     }
 224 
 225     return sign * n, digits > 0
 226 }
     File: ./factor/factor_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package factor
  26 
  27 import (
  28     "math/rand"
  29     "strconv"
  30     "strings"
  31     "testing"
  32     "time"
  33 )
  34 
  35 var tests = []string{
  36     ``,
  37     `0`,
  38     `0e`,
  39     `123456789012`,
  40     `+123456789012`,
  41     `-123456789012`,
  42     `00000123456789012`,
  43     `+00000123456789012`,
  44     `-00000123456789012`,
  45 
  46     `123_456_789_012`,
  47     `+123_456_789_012`,
  48     `-123_456_789_012`,
  49     `00000123_456_789_012`,
  50     `+00000123_456_789_012`,
  51     `-00000123_456_789_012`,
  52 }
  53 
  54 func testCustomParser(t *testing.T, s string) {
  55     got, ok := parseInt64(s)
  56     expected, err := strconv.ParseInt(strings.Replace(s, `_`, ``, -1), 10, 64)
  57 
  58     if ok != (err == nil) {
  59         if err == nil {
  60             t.Fatalf("unexpectedly successful parse")
  61         } else {
  62             t.Fatalf("unexpectedly failed to parse")
  63         }
  64         return
  65     }
  66 
  67     if err == nil && got != expected {
  68         t.Fatalf("expected %v, got %v instead", expected, got)
  69     }
  70 }
  71 
  72 func TestParseResults(t *testing.T) {
  73     for _, s := range tests {
  74         t.Run(s, func(t *testing.T) {
  75             testCustomParser(t, s)
  76         })
  77     }
  78 }
  79 
  80 func TestFuzzParseResults(t *testing.T) {
  81     const max = 1_000_000_000
  82     r := rand.New(rand.NewSource(time.Now().UnixNano()))
  83 
  84     for i := 0; i < 1_000_000; i++ {
  85         n := int64(r.Intn(max) - 2*max)
  86         testCustomParser(t, strconv.FormatInt(n, 10))
  87     }
  88 }
  89 
  90 func BenchmarkCustomParsing(b *testing.B) {
  91     for i := 0; i < b.N; i++ {
  92         for _, s := range tests {
  93             _, _ = parseInt64(s)
  94         }
  95     }
  96 }
  97 
  98 func BenchmarkParseInt(b *testing.B) {
  99     for i := 0; i < b.N; i++ {
 100         for _, s := range tests {
 101             _, _ = strconv.ParseInt(s, 10, 64)
 102         }
 103     }
 104 }
 105 
 106 func BenchmarkParseAtoi(b *testing.B) {
 107     for i := 0; i < b.N; i++ {
 108         for _, s := range tests {
 109             _, _ = strconv.Atoi(s)
 110         }
 111     }
 112 }
     File: ./fail/fail.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fail
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "os/exec"
  33     "strconv"
  34 )
  35 
  36 const info = `
  37 fail [options...] [status code...] [command...] [command args...]
  38 
  39 
  40 Just quit with the status code/number given as an argument; if no status
  41 code/number was given, quit using code 1 by default. If given 0 for the
  42 status code, it will paradoxically succeed, as 0 is the success code.
  43 
  44 If given a command after the (optional) error/exit code, that command is
  45 run before failing, ignoring the command's own success/failure.
  46 
  47 The options are, available both in single and double-dash versions
  48 
  49     -h, -help    show this help message
  50 `
  51 
  52 func Main() {
  53     buffered := false
  54     args := os.Args[1:]
  55     code := 1
  56 
  57     for len(args) > 0 {
  58         switch args[0] {
  59         case `-b`, `--b`, `-buffered`, `--buffered`:
  60             buffered = true
  61             args = args[1:]
  62             continue
  63 
  64         case `-h`, `--h`, `-help`, `--help`:
  65             os.Stdout.WriteString(info[1:])
  66             return
  67         }
  68 
  69         break
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     if len(args) > 0 {
  77         if n, err := strconv.ParseInt(args[0], 10, 64); err == nil {
  78             code = int(n)
  79             args = args[1:]
  80         }
  81     }
  82 
  83     liveLines := !buffered
  84     if len(args) > 0 && !buffered {
  85         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  86             liveLines = false
  87         }
  88     }
  89 
  90     if err := run(args, liveLines); err != nil {
  91         os.Stderr.WriteString(`fail: `)
  92         os.Stderr.WriteString(err.Error())
  93         os.Stderr.WriteString("\n")
  94     }
  95 
  96     os.Exit(code)
  97 }
  98 
  99 func run(args []string, liveLines bool) error {
 100     if len(args) == 0 {
 101         return nil
 102     }
 103 
 104     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 105     defer bw.Flush()
 106 
 107     cmd := exec.Command(args[0], args[1:]...)
 108     cmd.Stdin = os.Stdin
 109     if liveLines {
 110         cmd.Stdout = lineFlusher{bw}
 111     } else {
 112         cmd.Stdout = bw
 113     }
 114     cmd.Stderr = os.Stderr
 115 
 116     bw.Flush()
 117 
 118     if err := cmd.Start(); err != nil {
 119         os.Stderr.WriteString(`fail: `)
 120         os.Stderr.WriteString(err.Error())
 121         os.Stderr.WriteString("\n")
 122         os.Exit(1)
 123         return nil
 124     }
 125 
 126     err := cmd.Wait()
 127     if _, ok := err.(*exec.ExitError); ok {
 128         err = nil
 129     }
 130 
 131     if err != nil {
 132         os.Stderr.WriteString(`fail: `)
 133         os.Stderr.WriteString(err.Error())
 134         os.Stderr.WriteString("\n")
 135     }
 136 
 137     bw.Flush()
 138 
 139     return nil
 140 }
 141 
 142 type lineFlusher struct {
 143     bw *bufio.Writer
 144 }
 145 
 146 func (lf lineFlusher) Write(p []byte) (n int, err error) {
 147     n, err = lf.bw.Write(p)
 148     if err != nil {
 149         return n, err
 150     }
 151 
 152     if bytes.IndexByte(p, '\n') >= 0 {
 153         err = lf.bw.Flush()
 154     }
 155 
 156     return n, err
 157 }
     File: ./fh/config.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fh
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "image/color"
  31     "math"
  32     "os"
  33     "strconv"
  34     "strings"
  35 )
  36 
  37 const (
  38     // all output formats as constants, to prevent typos
  39     pngOutput             = `png`
  40     pngFastOutput         = `fast-png`
  41     pngSmallestOutput     = `smallest-png`
  42     pngUncompressedOutput = `uncompressed-png`
  43     bmpOutput             = `bmp`
  44     jpegOutput            = `jpeg`
  45 
  46     // all colorscales as constants, to prevent typos
  47     magmaScale   = `magma`
  48     parulaScale  = `parula`
  49     viridisScale = `viridis`
  50     grayScale    = `gray`
  51     binaryScale  = `binary`
  52     signScale    = `sign`
  53 )
  54 
  55 // fmtAliases normalizes values for the output-format option
  56 var fmtAliases = map[string]string{
  57     `b`:      bmpOutput,
  58     `bitmap`: bmpOutput,
  59     `bmp`:    bmpOutput,
  60     `j`:      jpegOutput,
  61     `jpeg`:   jpegOutput,
  62     `jpg`:    jpegOutput,
  63     `p`:      pngOutput,
  64     `ping`:   pngOutput,
  65     `png`:    pngOutput,
  66 
  67     `f`:                pngFastOutput,
  68     `fast`:             pngFastOutput,
  69     `fast-png`:         pngFastOutput,
  70     `fp`:               pngFastOutput,
  71     `fpng`:             pngFastOutput,
  72     `s`:                pngSmallestOutput,
  73     `small`:            pngSmallestOutput,
  74     `smallest-png`:     pngSmallestOutput,
  75     `small-png`:        pngSmallestOutput,
  76     `sp`:               pngSmallestOutput,
  77     `spng`:             pngSmallestOutput,
  78     `u`:                pngUncompressedOutput,
  79     `unc`:              pngUncompressedOutput,
  80     `uncompressed-png`: pngUncompressedOutput,
  81 }
  82 
  83 // paletteAliases normalizes values for the colorscale/palette option
  84 var paletteAliases = map[string]string{
  85     `b`:      binaryScale,
  86     `bin`:    binaryScale,
  87     `binary`: binaryScale,
  88 
  89     `g`:    grayScale,
  90     `gr`:   grayScale,
  91     `gray`: grayScale,
  92 
  93     `m`:     magmaScale,
  94     `mag`:   magmaScale,
  95     `magma`: magmaScale,
  96 
  97     `s`:    signScale,
  98     `sgn`:  signScale,
  99     `sign`: signScale,
 100 
 101     `py`:      viridisScale,
 102     `python`:  viridisScale,
 103     `numpy`:   viridisScale,
 104     `v`:       viridisScale,
 105     `vir`:     viridisScale,
 106     `viridis`: viridisScale,
 107 
 108     `matlab`: parulaScale,
 109     `p`:      parulaScale,
 110     `par`:    parulaScale,
 111     `parula`: parulaScale,
 112 }
 113 
 114 // outputSize is the value type for the resAliases lookup table
 115 type outputSize struct {
 116     Width  int
 117     Height int
 118 }
 119 
 120 // resAliases normalizes values for option -res
 121 var resAliases = map[string]outputSize{
 122     `sq`:      {2160, 2160},
 123     `sqr`:     {2160, 2160},
 124     `square`:  {2160, 2160},
 125     `squared`: {2160, 2160},
 126 
 127     `4k`:    {3840, 2160},
 128     `2160`:  {3840, 2160},
 129     `2160p`: {3840, 2160},
 130     `3840`:  {3840, 2160},
 131 
 132     `2.5k`:  {2560, 1440},
 133     `1440`:  {2560, 1440},
 134     `1440p`: {2560, 1440},
 135     `2560`:  {2560, 1440},
 136 
 137     `2k`:     {1920, 1080},
 138     `hd`:     {1920, 1080},
 139     `fhd`:    {1920, 1080},
 140     `fullhd`: {1920, 1080},
 141     `1080`:   {1920, 1080},
 142     `1080p`:  {1920, 1080},
 143     `1920`:   {1920, 1080},
 144 
 145     `720`:  {1280, 720},
 146     `720p`: {1280, 720},
 147 
 148     `480p`: {640, 480},
 149     `480`:  {640, 480},
 150 
 151     `2ks`:   {1080, 1080},
 152     `4ks`:   {2160, 2160},
 153     `2160s`: {2160, 2160},
 154     `1440s`: {1440, 1440},
 155     `1080s`: {1080, 1080},
 156     `720s`:  {720, 720},
 157     `480s`:  {480, 480},
 158 }
 159 
 160 // config has all parsed cmd-line arguments
 161 type config struct {
 162     Width  int
 163     Height int
 164 
 165     XMin float64
 166     XMax float64
 167     YMin float64
 168     YMax float64
 169 
 170     Formula string
 171     Output  string
 172 
 173     Palette func(float64) color.RGBA
 174     Bad     color.RGBA
 175 
 176     Integers bool
 177 }
 178 
 179 // parseFlags is the constructor for type config
 180 func parseFlags(usage string) (config, error) {
 181     cfg := config{
 182         Width:  3840,
 183         Height: 2160,
 184 
 185         XMin: 0,
 186         XMax: 1,
 187         YMin: 0,
 188         YMax: 1,
 189 
 190         Output: pngOutput,
 191     }
 192 
 193     cfg.Output = pngOutput
 194     pal := palettes[parulaScale]
 195     cfg.Palette = pal.Func
 196     cfg.Bad = pal.Bad
 197 
 198     args := os.Args[1:]
 199     if len(args) == 0 {
 200         fmt.Fprint(os.Stderr, usage)
 201         os.Exit(0)
 202         return cfg, nil
 203     }
 204 
 205     for _, s := range args {
 206         switch s {
 207         case `help`, `-h`, `--h`, `-help`, `--help`:
 208             fmt.Fprint(os.Stdout, usage)
 209             os.Exit(0)
 210             return cfg, nil
 211         }
 212 
 213         err := cfg.handleArg(s)
 214         if err != nil {
 215             return cfg, err
 216         }
 217     }
 218 
 219     if cfg.Integers {
 220         cfg.XMin = math.Ceil(float64(cfg.XMin))
 221         cfg.XMax = math.Floor(float64(cfg.XMax))
 222         cfg.YMin = math.Ceil(float64(cfg.YMin))
 223         cfg.YMax = math.Floor(float64(cfg.YMax))
 224     }
 225 
 226     if strings.TrimSpace(cfg.Formula) == `` {
 227         return cfg, errors.New(`no main formula given`)
 228     }
 229     return cfg, nil
 230 }
 231 
 232 // handleArg parses/uses the cmd-line argument given, except for the help
 233 // option and its aliases, which can only be detected separately
 234 func (c *config) handleArg(s string) error {
 235     switch s {
 236     case `int`, `ints`, `integers`:
 237         c.Integers = true
 238         return nil
 239     }
 240 
 241     lcDotless := strings.TrimPrefix(strings.ToLower(s), `.`)
 242     if alias, ok := fmtAliases[lcDotless]; ok {
 243         c.Output = alias
 244         return nil
 245     }
 246 
 247     if w, h, ok := parseResolution(s); ok {
 248         c.Width = w
 249         c.Height = h
 250         return nil
 251     }
 252 
 253     if colors, ok := paletteAliases[s]; ok {
 254         pal := palettes[colors]
 255         c.Palette = pal.Func
 256         c.Bad = pal.Bad
 257         return nil
 258     }
 259 
 260     varname, min, max, err := parseDomain(s)
 261     if err != nil {
 262         return err
 263     }
 264 
 265     switch varname {
 266     case ``:
 267         // no variable name means it's the main formula
 268         if c.Formula != `` {
 269             const fs = `%q: can't use more than 1 main formula`
 270             return fmt.Errorf(fs, s)
 271         }
 272         c.Formula = s
 273         return nil
 274 
 275     case `x`:
 276         c.XMin = min
 277         c.XMax = max
 278         return nil
 279 
 280     case `y`:
 281         c.YMin = min
 282         c.YMax = max
 283         return nil
 284 
 285     case `xy`:
 286         c.XMin = min
 287         c.XMax = max
 288         c.YMin = min
 289         c.YMax = max
 290         return nil
 291 
 292     default:
 293         const fs = "domain variable %q isn't any of `x`, `y`, or `xy`"
 294         return fmt.Errorf(fs, varname)
 295     }
 296 }
 297 
 298 // parseResolution tries to get a width/height resolution out of the
 299 // cmd-line argument given to it
 300 func parseResolution(s string) (width int, height int, ok bool) {
 301     if res, ok := resAliases[s]; ok {
 302         return res.Width, res.Height, true
 303     }
 304 
 305     i := strings.IndexByte(s, 'x')
 306     if i < 0 {
 307         return 0, 0, false
 308     }
 309 
 310     w, werr := strconv.ParseInt(s[:i], 10, 64)
 311     h, herr := strconv.ParseInt(s[i+1:], 10, 64)
 312     if werr == nil && herr == nil && w > 0 && h > 0 {
 313         return int(w), int(h), true
 314     }
 315     return 0, 0, false
 316 }
 317 
 318 func (c config) IntegerSize() (w, h int) {
 319     w = int(math.Abs(c.XMax - c.XMin + 1))
 320     h = int(math.Abs(c.YMax - c.YMin + 1))
 321     return w, h
 322 }
 323 
 324 // parseDomain tries to parse domain/variable-range formulas of the form(s)
 325 //
 326 // - x:=a..b
 327 // - y:=a..b
 328 // - xy:=a..b
 329 //
 330 // where a and b represent valid floating-point numbers; when an empty is
 331 // returned, it means the strings given wasn't recognized as a variable's
 332 // domain, suggesting it may be another option, or the main formula instead
 333 func parseDomain(s string) (string, float64, float64, error) {
 334     i := strings.Index(s, `:=`)
 335     if i < 0 {
 336         return ``, 0, 0, nil
 337     }
 338 
 339     v := strings.TrimSpace(s[:i])
 340     rng := strings.TrimSpace(s[i+2:])
 341     min, max, err := parseSpan(rng)
 342     return v, min, max, err
 343 }
 344 
 345 // parseSpan tries to parse a pair of numbers with `..` between them
 346 func parseSpan(s string) (float64, float64, error) {
 347     pair := strings.Split(s, `..`)
 348     if len(pair) != 2 {
 349         const fs = "missing `..` in domain-span %s"
 350         return 0, 1, fmt.Errorf(fs, s)
 351     }
 352 
 353     a, err := strconv.ParseFloat(pair[0], 64)
 354     if err != nil {
 355         const fs = `can't parse %q in domain-span %s`
 356         return 0, 1, fmt.Errorf(fs, pair[0], s)
 357     }
 358     b, err := strconv.ParseFloat(pair[1], 64)
 359     if err != nil {
 360         const fs = `can't parse %q in domain-span %s`
 361         return 0, 1, fmt.Errorf(fs, pair[1], s)
 362     }
 363     return a, b, nil
 364 }
     File: ./fh/config_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fh
  26 
  27 import "testing"
  28 
  29 func TestTables(t *testing.T) {
  30     for _, kind := range fmtAliases {
  31         // check all canonical format names are in the table
  32         if _, ok := fmtAliases[kind]; !ok {
  33             const fs = `format %q itself isn't in the format-table`
  34             t.Fatalf(fs, kind)
  35             return
  36         }
  37 
  38         if _, ok := encoders[kind]; !ok {
  39             const fs = `no encoder for %q`
  40             t.Fatalf(fs, kind)
  41             return
  42         }
  43     }
  44 
  45     for _, kind := range paletteAliases {
  46         // check all canonical colorscale names are in the table
  47         if _, ok := paletteAliases[kind]; !ok {
  48             const fs = `format %q itself isn't in the format-table`
  49             t.Fatalf(fs, kind)
  50             return
  51         }
  52 
  53         if _, ok := palettes[kind]; !ok {
  54             const fs = `no palette for %q`
  55             t.Fatalf(fs, kind)
  56             return
  57         }
  58     }
  59 }
     File: ./fh/examples.txt
   1 # ripples
   2 fh xy:=-3..3 'exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))' | si
   3 
   4 # floor lights
   5 fh x:=-5..5 y:=1..5 'x.sin.abs / y**1.4' | si
   6 
   7 # beta gradient
   8 fh x:=-5..5 y:=1..5 'lbeta(x + 5.1, y + 5.1)' | si
   9 
  10 # hot bars / horizontal bars
  11 fh xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' | si
  12 fh xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' | si
  13 
  14 # domain hole
  15 fh xy:=-5..5 'log1p(sin(x + y) + (x - y)**2 - 1.5*x + 2.5*y + 1)' | si
  16 
  17 # crazy grids
  18 fh xy:=-10..10 'sin(x.sin+y.cos) + cos(sin(x*y)+cos(y*y))' | si
  19 
  20 # panda / smiling ghost
  21 fh xy:=-5..5 'log1p(((x - 1)**2 + y*y - 4)*((x + 1)**2 + y*y - 4)*(x*x + (y - sqrt(3))**2 - 4) - 5)' | si
  22 
  23 # lcm 200
  24 fh xy:=0.01..199.99 'lcm(x.ceil, y.ceil)' | si
  25 
  26 # light tiles
  27 fh 'gauss(2*(sin(50.0*x)*cos(50.0*9/16*y) + 1.0)/2.0)' | si
  28 
  29 # shaky results... at least for me
  30 fh 'cos(160*tau*x) + sin(90*tau*y)' | si
  31 
  32 # 90-degree square tiles
  33 fh 'sign(cos(160*tau*x) + sin(90*tau*y))' | si
     File: ./fh/info.txt
   1 fh [options...] [x/y ranges...] formula
   2 
   3 
   4 Function Heatmapper emits a picture showing a heatmap view of the function
   5 f(x, y) implied by the math expression given. Plenty of math functions and
   6 constants are available, all their names being lowercase; the syntax is
   7 almost identical to Python/JavaScript's math notation, and has no keywords.
   8 
   9 For convenience, you can treat any 1-input func as a fake-property of its
  10 only input; you can also pretend all functions are fake-methods, where the
  11 1st input comes before the dot preceding the func name, followed by all the
  12 other args to it. All values and functions are global: without namespaces
  13 of any kind.
  14 
  15 Ranges for variables `x` and `y` are 0 to 1 by default, but you can change
  16 them via the special syntax shown on some of the examples below. Using the
  17 keyword `int`, `ints`, or `integers` enables integer-mode, where both `x`
  18 and `y` values are only sampled as integers: in that case, formula results
  19 will be used to fill whole tiles, instead of single pixels.
  20 
  21 By default, output is PNG-encoded using a good tradeoff between encoding
  22 speed and final payload size. Output resolutions can be as shown below, or
  23 consist of the width, followed by `x`, followed by the height wanted, such
  24 as `1024x768`, for example.
  25 
  26 Options have no flags/prefixes, and are accepted in any order.
  27 
  28 
  29 Options
  30 
  31     resolution                          resolution
  32 
  33     4k           3840x2160              4ks           2160x2160
  34     hd           1920x1080              hds           1080x1080
  35 
  36     2160p        3840x2160              2160s         2160x2160
  37     1440p        2560x1440              1440s         1440x1440
  38     1080p        1920x1080              1080s         1080x1080
  39     720p         1280x720               720s          720x720
  40 
  41 
  42     output     aliases                  colorscale    aliases
  43 
  44     png                                 magma         mag, m
  45     bmp        bitmap                   parula        par, p
  46     jpg        jpeg                     viridis       vir, v
  47 
  48 
  49 Concrete Examples
  50 
  51 
  52 fh 'x/(x+y)' > corner-fan-1.png
  53 
  54 fh 'y/(x+y)' > corner-fan-2.png
  55 
  56 fh 4k x:=-5..5 y:=1..5 'x.sin.abs / y**1.4' > floor-lights.png
  57 
  58 fh vir x:=-5..5 y:=1..5 'lbeta(x + 5.1, y + 5.1)' > beta-gradient.png
  59 
  60 fh mag 4k xy:=0.01..199.99 'lcm(x.ceil, y.ceil)' > lcm-200.png
  61 
  62 fh par 4k xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' > bars.png
  63 
  64 fh x:=-1.5..0.5 y:=-1..1 'mandel(16/9*x, y)' > mandelbrot.png
  65 
  66 fh x:=-1.5..0.5 y:=-1..1 'absmandel(16/9*x, y)' > wobbly-mandelbrot.png
  67 
  68 fh 4k 'sign(cos(160*tau*x) + sin(90*tau*y))' > 90-deg-square-tiles.png
  69 
  70 fh xy:=-10..10 'sin(x.sin+y.cos) + cos(sin(x*y)+cos(y*y))' > crazy-grids.png
  71 
  72 fh 'gauss(sin(50*x) * cos(50*9/16*y) + 1)' > light-tiles.png
  73 
  74 fh xy:=-2..3 'sgn(log((x*x-1)*(x-2-y)/(x*x+2+2*y)))' > abstract-shapes.png
  75 
  76 fh xy:=-10..10 square 'sinc(0.55 * hypot(x, y))' > central-ripple.png
     File: ./fh/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fh
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "image"
  31     "os"
  32 
  33     _ "embed"
  34 )
  35 
  36 //go:embed info.txt
  37 var usage string
  38 
  39 func Main() {
  40     cfg, err := parseFlags(usage)
  41     if err != nil {
  42         fmt.Fprintln(os.Stderr, err.Error())
  43         os.Exit(1)
  44         return
  45     }
  46 
  47     if _, ok := encoders[cfg.Output]; !ok {
  48         const fs = "unsupported output format %s\n"
  49         fmt.Fprintf(os.Stderr, fs, cfg.Output)
  50         os.Exit(1)
  51         return
  52     }
  53 
  54     addDetermFuncs()
  55 
  56     if err := run(cfg); err != nil {
  57         fmt.Fprintln(os.Stderr, err.Error())
  58         os.Exit(1)
  59         return
  60     }
  61 }
  62 
  63 func run(cfg config) error {
  64     // f, err := os.Create(`fh.prof`)
  65     // if err != nil {
  66     //  return err
  67     // }
  68     // defer f.Close()
  69 
  70     // pprof.StartCPUProfile(f)
  71     // defer pprof.StopCPUProfile()
  72 
  73     encode, ok := encoders[cfg.Output]
  74     if !ok {
  75         const fs = `unsupported output format %q`
  76         return fmt.Errorf(fs, cfg.Output)
  77     }
  78 
  79     if cfg.Integers {
  80         w, h := cfg.IntegerSize()
  81         cfg.Width = w
  82         cfg.Height = h
  83     }
  84 
  85     // allow runner to use up to 32 cores
  86     r, err := newRunner(cfg, 32)
  87     if err != nil {
  88         return err
  89     }
  90 
  91     res, err := r.Run(cfg)
  92     if err != nil {
  93         return err
  94     }
  95 
  96     img := image.NewRGBA(image.Rectangle{
  97         Min: image.Point{X: 0, Y: 0},
  98         Max: image.Point{X: cfg.Width, Y: cfg.Height},
  99     })
 100 
 101     w := bufio.NewWriterSize(os.Stdout, 64*1024)
 102     defer w.Flush()
 103 
 104     // give back a blank picture if results aren't usable
 105     if !res.isValid() {
 106         return encode(w, img, cfg)
 107     }
 108 
 109     // handle integers-only coordinate-inputs
 110     if cfg.Integers {
 111         width, height := cfg.IntegerSize()
 112         fillExpandedImage(img, res, cfg, width, height)
 113         return encode(w, img, cfg)
 114     }
 115 
 116     // handle domain-sampled images
 117     fillImage(img, res, cfg)
 118     return encode(w, img, cfg)
 119 }
     File: ./fh/output.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fh
  26 
  27 import (
  28     "bufio"
  29     "encoding/binary"
  30     "image"
  31     "image/color"
  32     "image/jpeg"
  33     "image/png"
  34     "math"
  35 
  36     "../colorplus"
  37 )
  38 
  39 var (
  40     // red is the invalid color for all palettes with dark/black colors
  41     red = color.RGBA{R: 255, G: 0, B: 0, A: 255}
  42 
  43     // black is the invalid color for the more colorful palettes
  44     black = color.RGBA{R: 0, G: 0, B: 0, A: 255}
  45 )
  46 
  47 // paletteSettings describes the full behavior of a palette
  48 type paletteSettings struct {
  49     Func func(float64) color.RGBA
  50     Bad  color.RGBA
  51 }
  52 
  53 // palettes completely describes the behavior of all supported palettes
  54 var palettes = map[string]paletteSettings{
  55     grayScale:    {gray, red},
  56     magmaScale:   {colorplus.Magmify, red},
  57     viridisScale: {colorplus.Viridize, black},
  58     parulaScale:  {colorplus.Parulate, black},
  59     binaryScale:  {colorBinary, black},
  60     signScale:    {colorSign, black},
  61 }
  62 
  63 // gray implements the grayscale coloring option, and is meant to be paired
  64 // with a red color for invalid inputs, such as NaNs
  65 func gray(x float64) color.RGBA {
  66     // restrict input to range 0..1
  67     if x < 0 {
  68         x = 0
  69     } else if x > 1 {
  70         x = 1
  71     }
  72 
  73     v := uint8(math.Round(255 * x))
  74     return color.RGBA{R: v, G: v, B: v, A: 255}
  75 }
  76 
  77 // colorBinary assigns 2 colors, thresholding the number given on 0.5
  78 func colorBinary(x float64) color.RGBA {
  79     if x < 0.5 {
  80         return color.RGBA{R: 234, G: 85, B: 58, A: 255}
  81     }
  82     return color.RGBA{R: 0, G: 95, B: 0, A: 255}
  83 }
  84 
  85 // colorSign assigns 3 colors, depending on the sign of the number given
  86 func colorSign(x float64) color.RGBA {
  87     if x > 0 {
  88         return color.RGBA{R: 0, G: 95, B: 0, A: 255}
  89     }
  90     if x < 0 {
  91         return color.RGBA{R: 234, G: 85, B: 58, A: 255}
  92     }
  93     return color.RGBA{R: 0, G: 135, B: 215, A: 255}
  94 }
  95 
  96 // encoders translates output-format settings into the right func to call
  97 var encoders = map[string]func(*bufio.Writer, *image.RGBA, config) error{
  98     pngOutput:  encodePNG,
  99     bmpOutput:  encodeBMP,
 100     jpegOutput: encodeJPEG,
 101 
 102     pngFastOutput:         encodeFastPNG,
 103     pngSmallestOutput:     encodeSmallestPNG,
 104     pngUncompressedOutput: encodeUncompressedPNG,
 105 }
 106 
 107 // fillImage fills/renders an image using previously calculated values
 108 func fillImage(img *image.RGBA, res result, cfg config) {
 109     k := 0
 110     f := cfg.Palette
 111 
 112     for i := 0; i < cfg.Height; i++ {
 113         for j := 0; j < cfg.Width; j++ {
 114             v := res.Values[k]
 115 
 116             var c color.RGBA
 117             if math.IsNaN(v) || math.IsInf(v, 0) {
 118                 c = cfg.Bad
 119             } else {
 120                 c = f(colorplus.Wrap(v, res.Min, res.Max))
 121             }
 122 
 123             img.SetRGBA(j, i, c)
 124             k++
 125         }
 126     }
 127 }
 128 
 129 // fillExpandedImage is like func fillImage, but rendering stretches what
 130 // would otherwise be single pixels into rectangles, representing regions
 131 // where the integer-parts of x/y inputs stay the same
 132 func fillExpandedImage(img *image.RGBA, res result, cfg config, w, h int) {
 133     width := img.Rect.Max.X
 134     xmax := float64(img.Rect.Max.X)
 135     ymax := float64(img.Rect.Max.Y)
 136 
 137     f := cfg.Palette
 138     dx := float64(w) / xmax
 139     dy := float64(h) / ymax
 140 
 141     for i := 0; i < cfg.Height; i++ {
 142         y := int(dy * float64(i))
 143         for j := 0; j < cfg.Width; j++ {
 144             x := int(dx * float64(j))
 145             k := y*width + x
 146             v := res.Values[k]
 147 
 148             var c color.RGBA
 149             if math.IsNaN(v) || math.IsInf(v, 0) {
 150                 c = cfg.Bad
 151             } else {
 152                 c = f(colorplus.Wrap(v, res.Min, res.Max))
 153             }
 154             img.SetRGBA(j, i, c)
 155         }
 156     }
 157 }
 158 
 159 // encodePNG seems a good default both for its main format (PNG), as well as
 160 // its reasonable default tradeoff between speed and output size, compared
 161 // to the PNG-encoding alternatives available
 162 func encodePNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 163     var enc png.Encoder
 164     return enc.Encode(w, img)
 165 }
 166 
 167 // encodeFastPNG may not always be much faster than the default PNG encoder
 168 func encodeFastPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 169     var enc png.Encoder
 170     enc.CompressionLevel = png.BestSpeed
 171     return enc.Encode(w, img)
 172 }
 173 
 174 // encodeSmallestPNG is substantially slower than the other PNG encoders
 175 func encodeSmallestPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 176     var enc png.Encoder
 177     enc.CompressionLevel = png.BestCompression
 178     return enc.Encode(w, img)
 179 }
 180 
 181 // encodeUncompressedPNG is mostly to compare it to BMP output: it turns out
 182 // BMP is slightly smaller than this
 183 func encodeUncompressedPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 184     var enc png.Encoder
 185     enc.CompressionLevel = png.NoCompression
 186     return enc.Encode(w, img)
 187 }
 188 
 189 // encodeJPEG encodes result at max JPEG setting: this usually results in
 190 // highly-detailed results for substantially-fewer bytes, compared to PNG
 191 // output
 192 func encodeJPEG(w *bufio.Writer, img *image.RGBA, cfg config) error {
 193     opt := jpeg.Options{Quality: 100}
 194     return jpeg.Encode(w, img, &opt)
 195 }
 196 
 197 // https://en.wikipedia.org/wiki/BMP_file_format
 198 
 199 // encodeBMP encodes as BMP/bitmap, a simple uncompressed format, which has
 200 // been widely supported for many decades
 201 func encodeBMP(w *bufio.Writer, img *image.RGBA, cfg config) error {
 202     const (
 203         dibsize = 40           // the DIB is the 2nd header
 204         hdrsize = 14 + dibsize // total size of all headers
 205     )
 206     imgsize := 3 * cfg.Width * cfg.Height
 207 
 208     w.WriteString(`BM`)
 209     binary.Write(w, binary.LittleEndian, uint32(hdrsize+imgsize))
 210     binary.Write(w, binary.LittleEndian, uint16(0))
 211     binary.Write(w, binary.LittleEndian, uint16(0))
 212     binary.Write(w, binary.LittleEndian, uint32(hdrsize))
 213     binary.Write(w, binary.LittleEndian, uint32(dibsize))
 214     binary.Write(w, binary.LittleEndian, int32(cfg.Width))
 215     binary.Write(w, binary.LittleEndian, int32(cfg.Height))
 216 
 217     // 1 color plane
 218     binary.Write(w, binary.LittleEndian, uint16(1))
 219     // 24 bits per pixel
 220     binary.Write(w, binary.LittleEndian, uint16(24))
 221     // no compression
 222     binary.Write(w, binary.LittleEndian, uint32(0))
 223     // number of bytes for the pixels
 224     binary.Write(w, binary.LittleEndian, uint32(imgsize))
 225     // horizontal & vertical pixels/m
 226     binary.Write(w, binary.LittleEndian, int32(0))
 227     binary.Write(w, binary.LittleEndian, int32(0))
 228     // 2**n palette colors
 229     binary.Write(w, binary.LittleEndian, uint32(0))
 230     // all colors are important
 231     binary.Write(w, binary.LittleEndian, uint32(0))
 232 
 233     stride := img.Stride
 234     // rows/lines are apparently stored bottom-to-top
 235     for y := cfg.Height - 1; y >= 0; y-- {
 236         start := y * stride
 237         buf := img.Pix[start : start+stride]
 238 
 239         for len(buf) >= 3 {
 240             // color-channel order seems to be BGR, instead of RGB
 241             w.WriteByte(buf[2])
 242             w.WriteByte(buf[1])
 243             err := w.WriteByte(buf[0])
 244             if err != nil {
 245                 // use errors to quit immediately: chances are
 246                 // the error is the result of a closed-pipe
 247                 return nil
 248             }
 249 
 250             // also skip the alpha channel
 251             buf = buf[4:]
 252         }
 253     }
 254     return nil
 255 }
     File: ./fh/scripts.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fh
  26 
  27 import (
  28     "math"
  29     "math/cmplx"
  30     "math/rand"
  31     "runtime"
  32     "sync"
  33     "time"
  34 
  35     "../fmscripts"
  36     "../mathplus"
  37 )
  38 
  39 // result has all results, including a summary of the range of values, so the
  40 // image renderer can normalize values accordingly
  41 type result struct {
  42     // Values has all results, which can be normalized into 0..1 using
  43     // fields Min and Max.
  44     Values []float64
  45 
  46     // Min is the lowest value in Values.
  47     Min float64
  48 
  49     // Max is the highest value in Values.
  50     Max float64
  51 }
  52 
  53 // isValid checks if the result should be a non-blank picture
  54 func (r result) isValid() bool {
  55     return r.Min <= r.Max && !math.IsInf(r.Min, 0) && !math.IsInf(r.Max, 0)
  56 }
  57 
  58 // runner has various twin script-runners, and automatically multicore-splits
  59 // the load among tasks along alternating groups of lines, in a striping
  60 // manner
  61 type runner struct {
  62     numTasks int
  63 
  64     // values can have its items updated concurrently, since each vertical
  65     // image line is changed by a single task.
  66     values []float64
  67 
  68     programs []fmscripts.Program
  69 }
  70 
  71 // newRunner is the constructor for type runner
  72 func newRunner(cfg config, maxtasks int) (runner, error) {
  73     numtasks := runtime.NumCPU()
  74     if maxtasks > 0 && numtasks > maxtasks {
  75         numtasks = maxtasks
  76     }
  77     progs := make([]fmscripts.Program, 0, numtasks)
  78 
  79     for i := 0; i < numtasks; i++ {
  80         // compiling the same formula multiple times seems wasteful, but
  81         // each compilation is very quick; this repetition is necessary
  82         // to isolate each task's input variables and pseudo-random state,
  83         // anyway
  84         p, err := compile(cfg.Formula, cfg, time.Now().UnixNano())
  85         if err != nil {
  86             return runner{}, err
  87         }
  88         progs = append(progs, p)
  89     }
  90 
  91     return runner{
  92         numTasks: numtasks,
  93         values:   make([]float64, cfg.Width*cfg.Height),
  94         programs: progs,
  95     }, nil
  96 }
  97 
  98 // Run is the entry-point func which handles everything from start to finish.
  99 func (r *runner) Run(cfg config) (res result, err error) {
 100     var wg sync.WaitGroup
 101     wg.Add(r.numTasks)
 102 
 103     // fully allocate min/max slices, as appending is concurrently unsafe
 104     lmin := make([]float64, r.numTasks)
 105     lmax := make([]float64, r.numTasks)
 106 
 107     // run parallel tasks: updating the shared value-slice works, as long
 108     // as each process sticks to its own index and output lines
 109     for i := 0; i < r.numTasks; i++ {
 110         go func(i int) {
 111             defer wg.Done()
 112             min, max := r.runSlice(i, cfg)
 113             lmin[i] = min
 114             lmax[i] = max
 115         }(i)
 116     }
 117     wg.Wait()
 118 
 119     // get overall min/max
 120     min := math.Inf(+1)
 121     max := math.Inf(-1)
 122     for i := range lmin {
 123         min = math.Min(min, lmin[i])
 124         max = math.Max(max, lmax[i])
 125     }
 126     return result{Values: r.values, Min: min, Max: max}, nil
 127 }
 128 
 129 // runSlice handles the task a specific core is supposed to handle: call
 130 // run instead of this func directly
 131 func (r *runner) runSlice(task int, cfg config) (min, max float64) {
 132     p := r.programs[task]
 133     x, _ := p.Get(`x`)
 134     y, _ := p.Get(`y`)
 135     zs := r.values
 136 
 137     w := cfg.Width
 138     h := cfg.Height
 139     n := r.numTasks
 140     xmin := math.Min(cfg.XMin, cfg.XMax)
 141     ymax := math.Max(cfg.YMax, cfg.YMin)
 142     wf := float64(w)
 143 
 144     zmin := math.Inf(+1)
 145     zmax := math.Inf(-1)
 146     dx := math.Abs(cfg.XMax-cfg.XMin) / float64(cfg.Width-1)
 147     dy := math.Abs(cfg.YMax-cfg.YMin) / float64(cfg.Height-1)
 148 
 149     for i := task; i < h; i += n {
 150         k := w * i
 151         *y = ymax - dy*float64(i)
 152 
 153         for j := 0.0; j < wf; j++ {
 154             *x = dx*j + xmin
 155             z := p.Run()
 156             zs[k] = z
 157             k++
 158 
 159             if !math.IsNaN(z) {
 160                 zmin = math.Min(zmin, z)
 161                 zmax = math.Max(zmax, z)
 162             }
 163         }
 164     }
 165     return zmin, zmax
 166 }
 167 
 168 // compile extends the built-in fast-math script functionality by adding
 169 // pseudo-random generators initialized with the seed number given
 170 func compile(src string, cfg config, seed int64) (fmscripts.Program, error) {
 171     r := rand.New(rand.NewSource(seed))
 172     rand01 := func() float64 {
 173         return fmscripts.Random(r)
 174     }
 175     rint := func(min, max float64) float64 {
 176         return fmscripts.RandomInt(r, min, max)
 177     }
 178     runif := func(min, max float64) float64 {
 179         return fmscripts.RandomUnif(r, min, max)
 180     }
 181     rexp := func(scale float64) float64 {
 182         return fmscripts.RandomExp(r, scale)
 183     }
 184     rnorm := func(mu, sigma float64) float64 {
 185         return fmscripts.RandomNorm(r, mu, sigma)
 186     }
 187     rgamma := func(scale float64) float64 {
 188         return fmscripts.RandomGamma(r, scale)
 189     }
 190     rbeta := func(a, b float64) float64 {
 191         return fmscripts.RandomBeta(r, a, b)
 192     }
 193 
 194     var c fmscripts.Compiler
 195     return c.Compile(src, map[string]any{
 196         `x`: 0.0,
 197         `y`: 0.0,
 198 
 199         `w`:        float64(cfg.Width),
 200         `h`:        float64(cfg.Height),
 201         `ar`:       float64(cfg.Width) / float64(cfg.Height),
 202         `aspratio`: float64(cfg.Width) / float64(cfg.Height),
 203 
 204         `rand`:   rand01,
 205         `rbeta`:  rbeta,
 206         `rexp`:   rexp,
 207         `rgamma`: rgamma,
 208         `rint`:   rint,
 209         `rnorm`:  rnorm,
 210         `runif`:  runif,
 211 
 212         `randbeta`:  rbeta,
 213         `randexp`:   rexp,
 214         `randexpo`:  rexp,
 215         `randgam`:   rgamma,
 216         `randgamma`: rgamma,
 217         `randint`:   rint,
 218         `randnorm`:  rnorm,
 219         `randunif`:  runif,
 220 
 221         `random`: rand01,
 222         `rbet`:   rbeta,
 223         `rgam`:   rgamma,
 224         `rnd`:    rand01,
 225     })
 226 }
 227 
 228 // addDetermFuncs does what it says, ensuring these funcs are optimizable when
 229 // they're given all-constant expressions as inputs
 230 func addDetermFuncs() {
 231     fmscripts.DefineDetFuncs(map[string]any{
 232         `ascale`:       mathplus.AnchoredScale,
 233         `awrap`:        mathplus.AnchoredWrap,
 234         `choose`:       comb,
 235         `clamp`:        mathplus.Clamp,
 236         `comb`:         comb,
 237         `dbinom`:       dbinom,
 238         `dnorm`:        mathplus.NormalDensity,
 239         `epa`:          mathplus.Epanechnikov,
 240         `epanechnikov`: mathplus.Epanechnikov,
 241         `etamag`:       etamag,
 242         `etamagcap`:    etamagcap,
 243         `fract`:        mathplus.Fract,
 244         `gauss`:        mathplus.Gauss,
 245         `gcd`:          gcd,
 246         `horner`:       mathplus.Polyval,
 247         `ieta`:         etaimag,
 248         `isprime`:      isPrime,
 249         `lcm`:          lcm,
 250         `logistic`:     mathplus.Logistic,
 251         `mageta`:       etamag,
 252         `magetacap`:    etamagcap,
 253         `magzeta`:      zetamag,
 254         `magzetacap`:   zetamagcap,
 255         `mix`:          mathplus.Mix,
 256         `perm`:         perm,
 257         `pbinom`:       pbinom,
 258         `pnorm`:        mathplus.CumulativeNormalDensity,
 259         `polyval`:      mathplus.Polyval,
 260         `reta`:         etare,
 261         `scale`:        mathplus.Scale,
 262         `sign`:         mathplus.Sign,
 263         `sinc`:         mathplus.Sinc,
 264         `smoothstep`:   mathplus.SmoothStep,
 265         `step`:         mathplus.Step,
 266         `tricube`:      mathplus.Tricube,
 267         `unwrap`:       mathplus.Unwrap,
 268         `wrap`:         mathplus.Wrap,
 269         `zetamag`:      zetamag,
 270         `zetamagcap`:   zetamagcap,
 271 
 272         `absmandel`:     absmandel,
 273         `absmandelcap`:  absmandelcap,
 274         `itermandel`:    itermandel,
 275         `itermandelcap`: itermandelcap,
 276         `mandel`:        itermandel,
 277     })
 278 }
 279 
 280 // absmandel returns the abs value of the complex number used in the mandelbrot
 281 // recurrence relation; recurrence is automatically truncated to a default
 282 // threshold and/or max number of loops
 283 func absmandel(x, y float64) float64 {
 284     return absmandelcap(x, y, 50)
 285 }
 286 
 287 // absmandelcap is like func absmandel, except the cap/threshold is an explicit
 288 // parameter
 289 func absmandelcap(x, y, threshold float64) float64 {
 290     z := 0 + 0i
 291     c := complex(x, y)
 292     const max = 1000
 293     // using the threshold's square to avoid using sqrt
 294     ts := threshold * threshold
 295 
 296     for n := 0.0; n < max; n++ {
 297         sqmag := real(z)*real(z) + imag(z)*imag(z)
 298         if sqmag > ts {
 299             return math.Sqrt(sqmag)
 300         }
 301         z = z*z + c
 302     }
 303     return cmplx.Abs(z)
 304 }
 305 
 306 // itermandel returns the number of iterations used in the mandelbrot
 307 // recurrence relation; recurrence is automatically truncated to a default
 308 // threshold and/or max number of loops
 309 func itermandel(x, y float64) float64 {
 310     return itermandelcap(x, y, 50)
 311 }
 312 
 313 // itermandelcap returns the number of mandelbrot recurrence iterations like
 314 // func itermandel, except the cap/threshold is an explicit parameter
 315 func itermandelcap(x, y, threshold float64) float64 {
 316     z := 0 + 0i
 317     c := complex(x, y)
 318     const max = 1000
 319     // using the threshold's square to avoid using sqrt
 320     ts := threshold * threshold
 321 
 322     for n := 0.0; n < max; n++ {
 323         sqmag := real(z)*real(z) + imag(z)*imag(z)
 324         if sqmag > ts {
 325             return n
 326         }
 327         z = z*z + c
 328     }
 329     return max
 330 }
 331 
 332 func comb(x, y float64) float64 {
 333     return float64(mathplus.Choose(int(x), int(y)))
 334 }
 335 
 336 func perm(x, y float64) float64 {
 337     return float64(mathplus.Perm(int(x), int(y)))
 338 }
 339 
 340 func gcd(x, y float64) float64 {
 341     return float64(mathplus.GCD(int64(x), int64(y)))
 342 }
 343 
 344 func lcm(x, y float64) float64 {
 345     return float64(mathplus.LCM(int64(x), int64(y)))
 346 }
 347 
 348 func dbinom(x, n, p float64) float64 {
 349     return mathplus.BinomialMass(int(x), int(n), p)
 350 }
 351 
 352 func pbinom(x, n, p float64) float64 {
 353     return mathplus.CumulativeBinomialDensity(int(x), int(n), p)
 354 }
 355 
 356 func isPrime(x float64) float64 {
 357     if mathplus.IsPrime(int64(x)) {
 358         return 1
 359     }
 360     return 0
 361 }
 362 
 363 const (
 364     // etaTrunc is when the summation for the eta funcs stops by default
 365     etaTrunc = 50
 366 
 367     // zetaTrunc is when the summation for the zeta funcs stops by default
 368     zetaTrunc = 50
 369 )
 370 
 371 // etamag call func etamagcap with a default truncation
 372 func etamag(x, y float64) float64 {
 373     return cmplx.Abs(eta(complex(x, y)))
 374 }
 375 
 376 // etamagcap is the real-valued magnitude of the truncated approx. of func eta
 377 func etamagcap(x, y float64, max float64) float64 {
 378     return cmplx.Abs(etacap(complex(x, y), int(max)))
 379 }
 380 
 381 // etare is the real part of the truncated approx. of func eta
 382 func etare(x, y float64) float64 { return real(eta(complex(x, y))) }
 383 
 384 // etaimag is the imaginary part of the truncated approx. of func eta
 385 func etaimag(x, y float64) float64 { return imag(eta(complex(x, y))) }
 386 
 387 // eta approximates the dirichlet eta function by truncation
 388 func eta(x complex128) complex128 { return etacap(x, etaTrunc) }
 389 
 390 // etacap accepts a cap/max iteration value for the eta func truncation
 391 func etacap(x complex128, max int) complex128 {
 392     y := 0 + 0i
 393     v := 1 + 0i
 394     sign := 1 + 0i
 395 
 396     for n := 1; n <= max; n++ {
 397         y += sign * v
 398         sign *= -1
 399         v /= x
 400     }
 401     return y
 402 }
 403 
 404 // zetamag call func zetamagcap with a default truncation
 405 func zetamag(x, y float64) float64 {
 406     return cmplx.Abs(zetacap(complex(x, y), zetaTrunc))
 407 }
 408 
 409 // etamagcap is the real-valued magnitude of the truncated approx. of func eta
 410 func zetamagcap(x, y float64, max float64) float64 {
 411     return cmplx.Abs(etacap(complex(x, y), int(max)))
 412 }
 413 
 414 // zetacap accepts a cap/max iteration value for the zeta func truncation
 415 func zetacap(x complex128, max int) complex128 {
 416     y := 0 + 0i
 417     v := 1 + 0i
 418 
 419     for n := 1; n <= max; n++ {
 420         y += v
 421         v /= x
 422     }
 423     return y
 424 }
     File: ./files/files.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package files
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "io/fs"
  31     "os"
  32     "path/filepath"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 files [options...] [files/folders...]
  38 
  39 Find/list all files in the folders given, without repetitions.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44     -t, -top     turn off recursive behavior; top-level entries only
  45 `
  46 
  47 func Main() {
  48     top := false
  49     buffered := false
  50     args := os.Args[1:]
  51 
  52     for len(args) > 0 {
  53         switch args[0] {
  54         case `-b`, `--b`, `-buffered`, `--buffered`:
  55             buffered = true
  56             args = args[1:]
  57             continue
  58 
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62 
  63         case `-t`, `--t`, `-top`, `--top`:
  64             top = true
  65             args = args[1:]
  66             continue
  67         }
  68 
  69         break
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     liveLines := !buffered
  77     if !buffered {
  78         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  79             liveLines = false
  80         }
  81     }
  82 
  83     var cfg config
  84     if top {
  85         cfg.skipSubfolder = fs.SkipDir
  86     }
  87     cfg.liveLines = liveLines
  88 
  89     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  90         os.Stderr.WriteString(`files: `)
  91         os.Stderr.WriteString(err.Error())
  92         os.Stderr.WriteString("\n")
  93         os.Exit(1)
  94         return
  95     }
  96 }
  97 
  98 type config struct {
  99     skipSubfolder error
 100     liveLines     bool
 101 }
 102 
 103 func run(w io.Writer, paths []string, cfg config) error {
 104     bw := bufio.NewWriterSize(w, 32*1024)
 105     defer bw.Flush()
 106 
 107     got := make(map[string]struct{})
 108 
 109     if len(paths) == 0 {
 110         paths = []string{`.`}
 111     }
 112 
 113     // handle is the callback for func filepath.WalkDir
 114     handle := func(path string, e fs.DirEntry, err error) error {
 115         if err != nil {
 116             return err
 117         }
 118 
 119         if _, ok := got[path]; ok {
 120             return nil
 121         }
 122         got[path] = struct{}{}
 123 
 124         if e.IsDir() {
 125             return cfg.skipSubfolder
 126         }
 127 
 128         return handleEntry(bw, path, cfg.liveLines)
 129     }
 130 
 131     for _, path := range paths {
 132         if _, ok := got[path]; ok {
 133             continue
 134         }
 135         got[path] = struct{}{}
 136 
 137         st, err := os.Stat(path)
 138         if err != nil {
 139             return err
 140         }
 141 
 142         if st.IsDir() {
 143             if !strings.HasSuffix(path, `/`) {
 144                 path = path + `/`
 145             }
 146             got[path] = struct{}{}
 147 
 148             if err := filepath.WalkDir(path, handle); err != nil {
 149                 return err
 150             }
 151             continue
 152         }
 153 
 154         if err := handleEntry(bw, path, cfg.liveLines); err != nil {
 155             return err
 156         }
 157     }
 158 
 159     return nil
 160 }
 161 
 162 func handleEntry(w *bufio.Writer, path string, live bool) error {
 163     abs, err := filepath.Abs(path)
 164     if err != nil {
 165         return err
 166     }
 167 
 168     w.WriteString(abs)
 169     w.WriteByte('\n')
 170 
 171     if live && w.Flush() != nil {
 172         return io.EOF
 173     }
 174     return nil
 175 }
     File: ./filesizes/filesizes.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package filesizes
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "io/fs"
  31     "os"
  32     "path/filepath"
  33     "sort"
  34     "strconv"
  35     "strings"
  36 )
  37 
  38 const info = `
  39 filesizes [options...] [files/folders...]
  40 
  41 Find/list all files in the folders given, without repetitions, also showing
  42 their size in bytes. Output is lines of tab-separated values, starting with
  43 a header line with the column names.
  44 
  45 All (optional) leading options start with either single or double-dash:
  46 
  47     -h, -help             show this help message
  48     -s, -sort, -sorted    backward-sort entries largest to smallest
  49     -t, -top              turn off recursive behavior; top-level entries only
  50 `
  51 
  52 func Main() {
  53     var cfg config
  54     cfg.recursive = true
  55     cfg.sorted = false
  56     cfg.blockSize = 4
  57     buffered := false
  58     args := os.Args[1:]
  59 
  60     for len(args) > 0 {
  61         if size, ok := parseBlockSizeOption(args[0]); ok {
  62             cfg.blockSize = size
  63             args = args[1:]
  64             continue
  65         }
  66 
  67         switch args[0] {
  68         case `-b`, `--b`, `-buffered`, `--buffered`:
  69             buffered = true
  70             args = args[1:]
  71             continue
  72 
  73         case `-h`, `--h`, `-help`, `--help`:
  74             os.Stdout.WriteString(info[1:])
  75             return
  76 
  77         case `-s`, `--s`, `-sort`, `--sort`, `-sorted`, `--sorted`:
  78             cfg.sorted = true
  79             args = args[1:]
  80             continue
  81 
  82         case `-t`, `--t`, `-top`, `--top`:
  83             cfg.recursive = false
  84             args = args[1:]
  85             continue
  86         }
  87 
  88         break
  89     }
  90 
  91     if len(args) > 0 && args[0] == `--` {
  92         args = args[1:]
  93     }
  94 
  95     cfg.liveLines = !buffered && !cfg.sorted
  96     if !buffered && !cfg.sorted {
  97         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  98             cfg.liveLines = false
  99         }
 100     }
 101 
 102     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 103         os.Stderr.WriteString(`filesizes: `)
 104         os.Stderr.WriteString(err.Error())
 105         os.Stderr.WriteString("\n")
 106         os.Exit(1)
 107         return
 108     }
 109 }
 110 
 111 func parseBlockSizeOption(s string) (size int, ok bool) {
 112     if !strings.HasPrefix(s, `-`) {
 113         return 0, false
 114     }
 115     if len(s) > 0 && s[0] == '-' {
 116         s = s[1:]
 117     }
 118     if len(s) > 0 && s[0] == '-' {
 119         s = s[1:]
 120     }
 121 
 122     if !strings.HasSuffix(s, `k`) && !strings.HasSuffix(s, `K`) {
 123         return 0, false
 124     }
 125     s = s[:len(s)-1]
 126 
 127     if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 {
 128         return int(n), true
 129     }
 130     return 0, false
 131 }
 132 
 133 type config struct {
 134     blockSize int
 135     sorted    bool
 136     recursive bool
 137     liveLines bool
 138 }
 139 
 140 type entry struct {
 141     path string
 142     size int64
 143 }
 144 
 145 type handlers struct {
 146     w             *bufio.Writer
 147     entries       []entry
 148     blockSize     int
 149     skipSubfolder error
 150 
 151     file func(h *handlers, path string, size int64) error
 152 
 153     liveLines bool
 154 }
 155 
 156 func (h handlers) countBlocks(size int64) int64 {
 157     bs := int64(h.blockSize)
 158     n := size / (1024 * bs)
 159     if size%(1024*bs) != 0 {
 160         n++
 161     }
 162     return n * bs
 163 }
 164 
 165 func run(w io.Writer, paths []string, cfg config) error {
 166     bw := bufio.NewWriterSize(w, 32*1024)
 167     defer bw.Flush()
 168 
 169     bw.WriteString("name\tbytes\tblocks\n")
 170 
 171     var h handlers
 172     h.w = bw
 173     h.blockSize = cfg.blockSize
 174     h.skipSubfolder = nil
 175     if !cfg.recursive {
 176         h.skipSubfolder = fs.SkipDir
 177     }
 178     h.file = emitEntry
 179     if cfg.sorted {
 180         h.file = keepEntry
 181     }
 182 
 183     got := make(map[string]struct{})
 184 
 185     if len(paths) == 0 {
 186         paths = []string{`.`}
 187     }
 188 
 189     // handle is the callback for func filepath.WalkDir
 190     handle := func(path string, e fs.DirEntry, err error) error {
 191         if err != nil {
 192             return err
 193         }
 194 
 195         if _, ok := got[path]; ok {
 196             return nil
 197         }
 198         got[path] = struct{}{}
 199 
 200         if e.IsDir() {
 201             return h.skipSubfolder
 202         }
 203 
 204         info, err := e.Info()
 205         if err != nil {
 206             return err
 207         }
 208 
 209         return h.file(&h, path, info.Size())
 210     }
 211 
 212     for _, path := range paths {
 213         if _, ok := got[path]; ok {
 214             continue
 215         }
 216         got[path] = struct{}{}
 217 
 218         st, err := os.Stat(path)
 219         if err != nil {
 220             return err
 221         }
 222 
 223         if st.IsDir() {
 224             if !strings.HasSuffix(path, `/`) {
 225                 path = path + `/`
 226             }
 227             got[path] = struct{}{}
 228 
 229             if err := filepath.WalkDir(path, handle); err != nil {
 230                 return err
 231             }
 232             continue
 233         }
 234 
 235         var size int64
 236         if path != `/dev/stdin` {
 237             size = st.Size()
 238         } else {
 239             size = countBytes(os.Stdin)
 240         }
 241 
 242         if err := h.file(&h, path, size); err != nil {
 243             return err
 244         }
 245     }
 246 
 247     if !cfg.sorted {
 248         return nil
 249     }
 250 
 251     sort.Slice(h.entries, func(i, j int) bool {
 252         return h.entries[i].size > h.entries[j].size
 253     })
 254 
 255     for _, e := range h.entries {
 256         if err := writeEntry(bw, e, h); err != nil {
 257             return err
 258         }
 259     }
 260 
 261     return nil
 262 }
 263 
 264 func countBytes(r io.Reader) (count int64) {
 265     var buf [32 * 1024]byte
 266 
 267     for {
 268         got, err := r.Read(buf[:])
 269         if got > 0 {
 270             count += int64(got)
 271         }
 272         if err != nil {
 273             break
 274         }
 275     }
 276 
 277     return count
 278 }
 279 
 280 func emitEntry(h *handlers, path string, size int64) error {
 281     return writeEntry(h.w, entry{path, size}, *h)
 282 }
 283 
 284 func keepEntry(h *handlers, path string, size int64) error {
 285     h.entries = append(h.entries, entry{path, size})
 286     return nil
 287 }
 288 
 289 func writeEntry(w *bufio.Writer, e entry, h handlers) error {
 290     abs, err := filepath.Abs(e.path)
 291     if err != nil {
 292         return err
 293     }
 294 
 295     var buf [24]byte
 296     w.WriteString(abs)
 297     w.WriteByte('\t')
 298     w.Write(strconv.AppendInt(buf[:0], e.size, 10))
 299     w.WriteByte('\t')
 300     w.Write(strconv.AppendInt(buf[:0], h.countBlocks(e.size), 10))
 301     w.WriteByte('\n')
 302 
 303     if h.liveLines && w.Flush() != nil {
 304         return io.EOF
 305     }
 306     return nil
 307 }
     File: ./filetypes/filetypes.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package filetypes
  26 
  27 import "bytes"
  28 
  29 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
  30 // or a dot-less filetype/extension given
  31 func nameToMIME(fname string) (mimeType string, ok bool) {
  32     // handle dotless file types and filenames alike
  33     kind, ok := type2mime[makeDotless(fname)]
  34     return kind, ok
  35 }
  36 
  37 // DetectMIME guesses the first appropriate MIME type from the first few
  38 // data bytes given: 24 bytes are enough to detect all supported types
  39 func DetectMIME(b []byte) (mimeType string, ok bool) {
  40     if t, ok := detectType(b); ok {
  41         return t, true
  42     }
  43     return ``, false
  44 }
  45 
  46 // detectType guesses the first appropriate file type for the data given:
  47 // here the type is a a filename extension without the leading dot
  48 func detectType(b []byte) (dotlessExt string, ok bool) {
  49     // empty data, so there's no way to detect anything
  50     if len(b) == 0 {
  51         return ``, false
  52     }
  53 
  54     // check for plain-text web-document formats case-insensitively
  55     kind, ok := checkDoc(b)
  56     if ok {
  57         return kind, true
  58     }
  59 
  60     // check data formats which allow any byte at the start
  61     kind, ok = checkSpecial(b)
  62     if ok {
  63         return kind, true
  64     }
  65 
  66     // check all other supported data formats
  67     headers := hdrDispatch[b[0]]
  68     for _, t := range headers {
  69         if hasPrefixPattern(b[1:], t.Header[1:], cba) {
  70             return t.Type, true
  71         }
  72     }
  73 
  74     // unrecognized data format
  75     return ``, false
  76 }
  77 
  78 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
  79 // XML, or JSON data
  80 func checkDoc(b []byte) (kind string, ok bool) {
  81     // ignore leading whitespaces
  82     b = trimLeadingWhitespace(b)
  83 
  84     // can't detect anything with empty data
  85     if len(b) == 0 {
  86         return ``, false
  87     }
  88 
  89     // handle XHTML documents which don't start with a doctype declaration
  90     if bytes.Contains(b, doctypeHTML) {
  91         return html, true
  92     }
  93 
  94     // handle HTML/SVG/XML documents
  95     if hasPrefixByte(b, '<') {
  96         if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
  97             if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
  98                 return svg, true
  99             }
 100             return xml, true
 101         }
 102 
 103         headers := hdrDispatch['<']
 104         for _, v := range headers {
 105             if hasPrefixFold(b, v.Header) {
 106                 return v.Type, true
 107             }
 108         }
 109         return ``, false
 110     }
 111 
 112     // handle JSON with top-level arrays
 113     if hasPrefixByte(b, '[') {
 114         // match [", or [[, or [{, ignoring spaces between
 115         b = trimLeadingWhitespace(b[1:])
 116         if len(b) > 0 {
 117             switch b[0] {
 118             case '"', '[', '{':
 119                 return json, true
 120             }
 121         }
 122         return ``, false
 123     }
 124 
 125     // handle JSON with top-level objects
 126     if hasPrefixByte(b, '{') {
 127         // match {", ignoring spaces between: after {, the only valid syntax
 128         // which can follow is the opening quote for the expected object-key
 129         b = trimLeadingWhitespace(b[1:])
 130         if hasPrefixByte(b, '"') {
 131             return json, true
 132         }
 133         return ``, false
 134     }
 135 
 136     // checking for a quoted string, any of the JSON keywords, or even a
 137     // number seems too ambiguous to declare the data valid JSON
 138 
 139     // no web-document format detected
 140     return ``, false
 141 }
 142 
 143 // checkSpecial handles special file-format headers, which should be checked
 144 // before the normal file-type headers, since the first-byte dispatch algo
 145 // doesn't work for these
 146 func checkSpecial(b []byte) (kind string, ok bool) {
 147     if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
 148         for _, t := range specialHeaders {
 149             if hasPrefixPattern(b[4:], t.Header[4:], cba) {
 150                 return t.Type, true
 151             }
 152         }
 153     }
 154     return ``, false
 155 }
 156 
 157 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
 158 // value to signal any byte is allowed on specific spots
 159 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
 160     // if the data are shorter than the pattern to match, there's no match
 161     if len(what) < len(pat) {
 162         return false
 163     }
 164 
 165     // use a slice which ensures the pattern length is never exceeded
 166     what = what[:len(pat)]
 167 
 168     for i, x := range what {
 169         y := pat[i]
 170         if x != y && y != wildcard {
 171             return false
 172         }
 173     }
 174     return true
 175 }
     File: ./filetypes/filetypes_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package filetypes
  26 
  27 import (
  28     "bytes"
  29     "testing"
  30 )
  31 
  32 func TestCheckDoc(t *testing.T) {
  33     const (
  34         lf       = "\n"
  35         crlf     = "\r\n"
  36         tab      = "\t"
  37         xmlIntro = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`
  38     )
  39 
  40     tests := []struct {
  41         Input    string
  42         Expected string
  43     }{
  44         {``, ``},
  45         {`{"abc":123}`, json},
  46         {`[` + lf + ` {"abc":123}`, json},
  47         {`[` + lf + `  {"abc":123}`, json},
  48         {`[` + crlf + tab + `{"abc":123}`, json},
  49 
  50         {``, ``},
  51         {`<?xml?>`, xml},
  52         {`<?xml?><records>`, xml},
  53         {`<?xml?>` + lf + `<records>`, xml},
  54         {`<?xml?><svg>`, svg},
  55         {`<?xml?>` + crlf + `<svg>`, svg},
  56         {xmlIntro + lf + `<svg`, svg},
  57         {xmlIntro + crlf + `<svg`, svg},
  58     }
  59 
  60     for _, tc := range tests {
  61         t.Run(tc.Input, func(t *testing.T) {
  62             res, _ := checkDoc([]byte(tc.Input))
  63             if res != tc.Expected {
  64                 t.Fatalf(`got %v, expected %v instead`, res, tc.Expected)
  65             }
  66         })
  67     }
  68 }
  69 
  70 func TestHasPrefixPattern(t *testing.T) {
  71     var (
  72         data = []byte{
  73             'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
  74         }
  75         pat = []byte{
  76             'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
  77         }
  78     )
  79 
  80     if !hasPrefixPattern(data, pat, cba) {
  81         t.Fatal(`wildcard pattern not working`)
  82     }
  83 }
  84 
  85 func BenchmarkHasPrefixMatch(b *testing.B) {
  86     var (
  87         data = []byte{
  88             'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
  89         }
  90         pat = []byte{
  91             'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
  92         }
  93     )
  94 
  95     b.ReportAllocs()
  96     b.ResetTimer()
  97 
  98     for i := 0; i < b.N; i++ {
  99         if !bytes.HasPrefix(data, pat) {
 100             b.Fatal(`pattern was specifically chosen to match, but didn't`)
 101         }
 102     }
 103 }
 104 
 105 func BenchmarkHasPrefixPatternMatch(b *testing.B) {
 106     var (
 107         data = []byte{
 108             'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
 109         }
 110         pat = []byte{
 111             'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
 112         }
 113     )
 114 
 115     b.ReportAllocs()
 116     b.ResetTimer()
 117 
 118     for i := 0; i < b.N; i++ {
 119         if !hasPrefixPattern(data, pat, cba) {
 120             b.Fatal(`pattern was specifically chosen to match, but didn't`)
 121         }
 122     }
 123 }
     File: ./filetypes/mimedata.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package filetypes
  26 
  27 // all the MIME types used/recognized in this package
  28 const (
  29     aiff    = `audio/aiff`
  30     au      = `audio/basic`
  31     avi     = `video/avi`
  32     avif    = `image/avif`
  33     bmp     = `image/x-bmp`
  34     caf     = `audio/x-caf`
  35     cur     = `image/vnd.microsoft.icon`
  36     css     = `text/css`
  37     csv     = `text/csv`
  38     djvu    = `image/x-djvu`
  39     elf     = `application/x-elf`
  40     exe     = `application/vnd.microsoft.portable-executable`
  41     flac    = `audio/x-flac`
  42     gif     = `image/gif`
  43     gz      = `application/gzip`
  44     heic    = `image/heic`
  45     htm     = `text/html`
  46     html    = `text/html`
  47     ico     = `image/x-icon`
  48     iso     = `application/octet-stream`
  49     jpg     = `image/jpeg`
  50     jpeg    = `image/jpeg`
  51     js      = `application/javascript`
  52     json    = `application/json`
  53     m4a     = `audio/aac`
  54     m4v     = `video/x-m4v`
  55     mid     = `audio/midi`
  56     mov     = `video/quicktime`
  57     mp4     = `video/mp4`
  58     mp3     = `audio/mpeg`
  59     mpg     = `video/mpeg`
  60     ogg     = `audio/ogg`
  61     opus    = `audio/opus`
  62     pdf     = `application/pdf`
  63     png     = `image/png`
  64     ps      = `application/postscript`
  65     psd     = `image/vnd.adobe.photoshop`
  66     rtf     = `application/rtf`
  67     sqlite3 = `application/x-sqlite3`
  68     svg     = `image/svg+xml`
  69     text    = `text/plain`
  70     tiff    = `image/tiff`
  71     tsv     = `text/tsv`
  72     wasm    = `application/wasm`
  73     wav     = `audio/x-wav`
  74     webp    = `image/webp`
  75     webm    = `video/webm`
  76     xml     = `application/xml`
  77     zip     = `application/zip`
  78     zst     = `application/zstd`
  79 )
  80 
  81 // type2mime turns dotless format-names into MIME types
  82 var type2mime = map[string]string{
  83     `aiff`:    aiff,
  84     `wav`:     wav,
  85     `avi`:     avi,
  86     `jpg`:     jpg,
  87     `jpeg`:    jpeg,
  88     `m4a`:     m4a,
  89     `mp4`:     mp4,
  90     `m4v`:     m4v,
  91     `mov`:     mov,
  92     `png`:     png,
  93     `avif`:    avif,
  94     `webp`:    webp,
  95     `gif`:     gif,
  96     `tiff`:    tiff,
  97     `psd`:     psd,
  98     `flac`:    flac,
  99     `webm`:    webm,
 100     `mpg`:     mpg,
 101     `zip`:     zip,
 102     `gz`:      gz,
 103     `zst`:     zst,
 104     `mp3`:     mp3,
 105     `opus`:    opus,
 106     `bmp`:     bmp,
 107     `mid`:     mid,
 108     `ogg`:     ogg,
 109     `html`:    html,
 110     `htm`:     htm,
 111     `svg`:     svg,
 112     `xml`:     xml,
 113     `rtf`:     rtf,
 114     `pdf`:     pdf,
 115     `ps`:      ps,
 116     `au`:      au,
 117     `ico`:     ico,
 118     `cur`:     cur,
 119     `caf`:     caf,
 120     `heic`:    heic,
 121     `sqlite3`: sqlite3,
 122     `elf`:     elf,
 123     `exe`:     exe,
 124     `wasm`:    wasm,
 125     `iso`:     iso,
 126     `txt`:     text,
 127     `css`:     css,
 128     `csv`:     csv,
 129     `tsv`:     tsv,
 130     `js`:      js,
 131     `json`:    json,
 132     `geojson`: json,
 133 }
 134 
 135 // formatDescriptor ties a file-header pattern to its data-format type
 136 type formatDescriptor struct {
 137     Header []byte
 138     Type   string
 139 }
 140 
 141 // can be anything: ensure this value differs from all other literal bytes
 142 // in the generic-headers table: failing that, its value could cause subtle
 143 // type-misdetection bugs
 144 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
 145 
 146 // dash-streamed m4a format
 147 var m4aDash = []byte{
 148     cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 149     000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 150 }
 151 
 152 // format markers with leading wildcards, which should be checked before the
 153 // normal ones: this is to prevent mismatches with the latter types, even
 154 // though you can make probabilistic arguments which suggest these mismatches
 155 // should be very unlikely in practice
 156 var specialHeaders = []formatDescriptor{
 157     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
 158     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
 159     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
 160     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
 161     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
 162     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
 163     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
 164     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
 165     {m4aDash, m4a},
 166 }
 167 
 168 // sqlite3 database format
 169 var sqlite3db = []byte{
 170     'S', 'Q', 'L', 'i', 't', 'e', ' ',
 171     'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
 172     000,
 173 }
 174 
 175 // windows-variant bitmap file-header, which is followed by a byte-counter for
 176 // the 40-byte infoheader which follows that
 177 var winbmp = []byte{
 178     'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
 179 }
 180 
 181 // deja-vu document format
 182 var djv = []byte{
 183     'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
 184 }
 185 
 186 var doctypeHTML = []byte{
 187     '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
 188 }
 189 
 190 // hdrDispatch groups format-description-groups by their first byte, thus
 191 // shortening total lookups for some data header: notice how the `ftyp` data
 192 // formats aren't handled here, since these can start with any byte, instead
 193 // of the literal value of the any-byte markers they use
 194 var hdrDispatch = [256][]formatDescriptor{
 195     {
 196         {[]byte{000, 000, 001, 0xBA}, mpg},
 197         {[]byte{000, 000, 001, 0xB3}, mpg},
 198         {[]byte{000, 000, 001, 000}, ico},
 199         {[]byte{000, 000, 002, 000}, cur},
 200         {[]byte{000, 'a', 's', 'm'}, wasm},
 201     }, // 0
 202     nil, // 1
 203     nil, // 2
 204     nil, // 3
 205     nil, // 4
 206     nil, // 5
 207     nil, // 6
 208     nil, // 7
 209     nil, // 8
 210     nil, // 9
 211     nil, // 10
 212     nil, // 11
 213     nil, // 12
 214     nil, // 13
 215     nil, // 14
 216     nil, // 15
 217     nil, // 16
 218     nil, // 17
 219     nil, // 18
 220     nil, // 19
 221     nil, // 20
 222     nil, // 21
 223     nil, // 22
 224     nil, // 23
 225     nil, // 24
 226     nil, // 25
 227     {
 228         {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
 229     }, // 26
 230     nil, // 27
 231     nil, // 28
 232     nil, // 29
 233     nil, // 30
 234     {
 235         // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
 236         {[]byte{0x1F, 0x8B, 0x08}, gz},
 237     }, // 31
 238     nil, // 32
 239     nil, // 33 !
 240     nil, // 34 "
 241     {
 242         {[]byte{'#', '!', ' '}, text},
 243         {[]byte{'#', '!', '/'}, text},
 244     }, // 35 #
 245     nil, // 36 $
 246     {
 247         {[]byte{'%', 'P', 'D', 'F'}, pdf},
 248         {[]byte{'%', '!', 'P', 'S'}, ps},
 249     }, // 37 %
 250     nil, // 38 &
 251     nil, // 39 '
 252     {
 253         {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
 254     }, // 40 (
 255     nil, // 41 )
 256     nil, // 42 *
 257     nil, // 43 +
 258     nil, // 44 ,
 259     nil, // 45 -
 260     {
 261         {[]byte{'.', 's', 'n', 'd'}, au},
 262     }, // 46 .
 263     nil, // 47 /
 264     nil, // 48 0
 265     nil, // 49 1
 266     nil, // 50 2
 267     nil, // 51 3
 268     nil, // 52 4
 269     nil, // 53 5
 270     nil, // 54 6
 271     nil, // 55 7
 272     {
 273         {[]byte{'8', 'B', 'P', 'S'}, psd},
 274     }, // 56 8
 275     nil, // 57 9
 276     nil, // 58 :
 277     nil, // 59 ;
 278     {
 279         // func checkDoc is better for these, since it's case-insensitive
 280         {doctypeHTML, html},
 281         {[]byte{'<', 's', 'v', 'g'}, svg},
 282         {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
 283         {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
 284         {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
 285         {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
 286     }, // 60 <
 287     nil, // 61 =
 288     nil, // 62 >
 289     nil, // 63 ?
 290     nil, // 64 @
 291     {
 292         {djv, djvu},
 293     }, // 65 A
 294     {
 295         {winbmp, bmp},
 296     }, // 66 B
 297     nil, // 67 C
 298     nil, // 68 D
 299     nil, // 69 E
 300     {
 301         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
 302         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
 303     }, // 70 F
 304     {
 305         {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
 306         {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
 307     }, // 71 G
 308     nil, // 72 H
 309     {
 310         {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
 311         {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
 312         {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
 313         {[]byte{'I', 'I', '*', 000}, tiff},
 314     }, // 73 I
 315     nil, // 74 J
 316     nil, // 75 K
 317     nil, // 76 L
 318     {
 319         {[]byte{'M', 'M', 000, '*'}, tiff},
 320         {[]byte{'M', 'T', 'h', 'd'}, mid},
 321         {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
 322         // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
 323         // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
 324         // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
 325     }, // 77 M
 326     nil, // 78 N
 327     {
 328         {[]byte{'O', 'g', 'g', 'S'}, ogg},
 329     }, // 79 O
 330     {
 331         {[]byte{'P', 'K', 003, 004}, zip},
 332     }, // 80 P
 333     nil, // 81 Q
 334     {
 335         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
 336         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
 337         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
 338     }, // 82 R
 339     {
 340         {sqlite3db, sqlite3},
 341     }, // 83 S
 342     nil, // 84 T
 343     nil, // 85 U
 344     nil, // 86 V
 345     nil, // 87 W
 346     nil, // 88 X
 347     nil, // 89 Y
 348     nil, // 90 Z
 349     nil, // 91 [
 350     nil, // 92 \
 351     nil, // 93 ]
 352     nil, // 94 ^
 353     nil, // 95 _
 354     nil, // 96 `
 355     nil, // 97 a
 356     nil, // 98 b
 357     {
 358         {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
 359     }, // 99 c
 360     nil, // 100 d
 361     nil, // 101 e
 362     {
 363         {[]byte{'f', 'L', 'a', 'C'}, flac},
 364     }, // 102 f
 365     nil, // 103 g
 366     nil, // 104 h
 367     nil, // 105 i
 368     nil, // 106 j
 369     nil, // 107 k
 370     nil, // 108 l
 371     nil, // 109 m
 372     nil, // 110 n
 373     nil, // 111 o
 374     nil, // 112 p
 375     nil, // 113 q
 376     nil, // 114 r
 377     nil, // 115 s
 378     nil, // 116 t
 379     nil, // 117 u
 380     nil, // 118 v
 381     nil, // 119 w
 382     nil, // 120 x
 383     nil, // 121 y
 384     nil, // 122 z
 385     {
 386         {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
 387     }, // 123 {
 388     nil, // 124 |
 389     nil, // 125 }
 390     nil, // 126
 391     {
 392         {[]byte{127, 'E', 'L', 'F'}, elf},
 393     }, // 127
 394     nil, // 128
 395     nil, // 129
 396     nil, // 130
 397     nil, // 131
 398     nil, // 132
 399     nil, // 133
 400     nil, // 134
 401     nil, // 135
 402     nil, // 136
 403     {
 404         {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
 405     }, // 137
 406     nil, // 138
 407     nil, // 139
 408     nil, // 140
 409     nil, // 141
 410     nil, // 142
 411     nil, // 143
 412     nil, // 144
 413     nil, // 145
 414     nil, // 146
 415     nil, // 147
 416     nil, // 148
 417     nil, // 149
 418     nil, // 150
 419     nil, // 151
 420     nil, // 152
 421     nil, // 153
 422     nil, // 154
 423     nil, // 155
 424     nil, // 156
 425     nil, // 157
 426     nil, // 158
 427     nil, // 159
 428     nil, // 160
 429     nil, // 161
 430     nil, // 162
 431     nil, // 163
 432     nil, // 164
 433     nil, // 165
 434     nil, // 166
 435     nil, // 167
 436     nil, // 168
 437     nil, // 169
 438     nil, // 170
 439     nil, // 171
 440     nil, // 172
 441     nil, // 173
 442     nil, // 174
 443     nil, // 175
 444     nil, // 176
 445     nil, // 177
 446     nil, // 178
 447     nil, // 179
 448     nil, // 180
 449     nil, // 181
 450     nil, // 182
 451     nil, // 183
 452     nil, // 184
 453     nil, // 185
 454     nil, // 186
 455     nil, // 187
 456     nil, // 188
 457     nil, // 189
 458     nil, // 190
 459     nil, // 191
 460     nil, // 192
 461     nil, // 193
 462     nil, // 194
 463     nil, // 195
 464     nil, // 196
 465     nil, // 197
 466     nil, // 198
 467     nil, // 199
 468     nil, // 200
 469     nil, // 201
 470     nil, // 202
 471     nil, // 203
 472     nil, // 204
 473     nil, // 205
 474     nil, // 206
 475     nil, // 207
 476     nil, // 208
 477     nil, // 209
 478     nil, // 210
 479     nil, // 211
 480     nil, // 212
 481     nil, // 213
 482     nil, // 214
 483     nil, // 215
 484     nil, // 216
 485     nil, // 217
 486     nil, // 218
 487     nil, // 219
 488     nil, // 220
 489     nil, // 221
 490     nil, // 222
 491     nil, // 223
 492     nil, // 224
 493     nil, // 225
 494     nil, // 226
 495     nil, // 227
 496     nil, // 228
 497     nil, // 229
 498     nil, // 230
 499     nil, // 231
 500     nil, // 232
 501     nil, // 233
 502     nil, // 234
 503     nil, // 235
 504     nil, // 236
 505     nil, // 237
 506     nil, // 238
 507     nil, // 239
 508     nil, // 240
 509     nil, // 241
 510     nil, // 242
 511     nil, // 243
 512     nil, // 244
 513     nil, // 245
 514     nil, // 246
 515     nil, // 247
 516     nil, // 248
 517     nil, // 249
 518     nil, // 250
 519     nil, // 251
 520     nil, // 252
 521     nil, // 253
 522     nil, // 254
 523     {
 524         {[]byte{0xFF, 0xD8, 0xFF}, jpg},
 525         {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
 526         {[]byte{0xFF, 0xFB}, mp3},
 527     }, // 255
 528 }
     File: ./filetypes/mimedata_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package filetypes
  26 
  27 import (
  28     "strconv"
  29     "testing"
  30 )
  31 
  32 func TestData(t *testing.T) {
  33     t.Run(`could-be-anything constant`, func(t *testing.T) {
  34         if len(hdrDispatch[cba]) != 0 {
  35             const fs = `chosen constant %d collides with header entries`
  36             t.Fatalf(fs, cba)
  37         }
  38     })
  39 
  40     for i, v := range hdrDispatch {
  41         t.Run(`dispatch @ `+strconv.Itoa(i), func(t *testing.T) {
  42             const fs = `expected leading byte to be %d, but got %d instead`
  43             for _, e := range v {
  44                 if e.Header[0] != byte(i) {
  45                     t.Fatalf(fs, i, e.Header[0])
  46                     return
  47                 }
  48             }
  49         })
  50     }
  51 }
     File: ./filetypes/strings.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package filetypes
  26 
  27 import (
  28     "bytes"
  29     "strings"
  30 )
  31 
  32 // makeDotless is similar to filepath.Ext, except its results never start
  33 // with a dot
  34 func makeDotless(s string) string {
  35     if i := strings.LastIndexByte(s, '.'); i >= 0 {
  36         return s[(i + 1):]
  37     }
  38     return s
  39 }
  40 
  41 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
  42 func hasPrefixByte(b []byte, prefix byte) bool {
  43     return len(b) > 0 && b[0] == prefix
  44 }
  45 
  46 // hasPrefixFold is a case-insensitive bytes.HasPrefix
  47 func hasPrefixFold(s []byte, prefix []byte) bool {
  48     n := len(prefix)
  49     return len(s) >= n && bytes.EqualFold(s[:n], prefix)
  50 }
  51 
  52 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
  53 // to handle text-based data formats more flexibly
  54 func trimLeadingWhitespace(b []byte) []byte {
  55     for len(b) > 0 {
  56         switch b[0] {
  57         case ' ', '\t', '\n', '\r':
  58             b = b[1:]
  59         default:
  60             return b
  61         }
  62     }
  63 
  64     // an empty slice is all that's left, at this point
  65     return nil
  66 }
     File: ./filetypes/strings_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package filetypes
  26 
  27 import (
  28     "bytes"
  29     "testing"
  30 )
  31 
  32 func TestHasPrefixByte(t *testing.T) {
  33     tests := []struct {
  34         Data     []byte
  35         Prefix   byte
  36         Expected bool
  37     }{
  38         {nil, 'x', false},
  39         {[]byte(`x`), 'x', true},
  40         {[]byte(` x`), 'x', false},
  41         {[]byte(`xyz`), 'a', false},
  42         {[]byte(`abcxyz`), 'a', true},
  43     }
  44 
  45     for _, tc := range tests {
  46         t.Run(string(tc.Data), func(t *testing.T) {
  47             got := hasPrefixByte(tc.Data, tc.Prefix)
  48             if got != tc.Expected {
  49                 const fs = `expected %v, but got %v instead`
  50                 t.Fatalf(fs, tc.Expected, got)
  51             }
  52         })
  53     }
  54 }
  55 
  56 func TestHasPrefixFold(t *testing.T) {
  57     tests := []struct {
  58         Data     []byte
  59         Prefix   []byte
  60         Expected bool
  61     }{
  62         {[]byte("<!docTYPE html>\n<html>"), []byte(`<!doctype HTML`), true},
  63     }
  64 
  65     for _, tc := range tests {
  66         t.Run("", func(t *testing.T) {
  67             got := hasPrefixFold(tc.Data, tc.Prefix)
  68             if got != tc.Expected {
  69                 const fs = `expected %v, but got %v instead`
  70                 t.Fatalf(fs, tc.Expected, got)
  71             }
  72         })
  73     }
  74 }
  75 
  76 func TestTrimLeadingWhitespaces(t *testing.T) {
  77     tests := []struct {
  78         Data     []byte
  79         Expected []byte
  80     }{
  81         {[]byte(`abc`), []byte(`abc`)},
  82         {[]byte(" \t"), nil},
  83         {[]byte("  \tabc"), []byte(`abc`)},
  84         {[]byte("\r\nabc"), []byte(`abc`)},
  85     }
  86 
  87     for _, tc := range tests {
  88         t.Run("", func(t *testing.T) {
  89             got := trimLeadingWhitespace(tc.Data)
  90             if !bytes.Equal(got, tc.Expected) {
  91                 const fs = `expected %#v, but got %#v instead`
  92                 t.Fatalf(fs, tc.Expected, got)
  93             }
  94         })
  95     }
  96 }
     File: ./finfo/config.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "flag"
  29     "fmt"
  30     "strings"
  31 )
  32 
  33 type config struct {
  34     To     string // output format: any of TSV, JSON, or HTML
  35     SortBy string // how to sort results
  36     Title  string // title to use when emitting HTML
  37 
  38     Bytes bool // show file sizes in bytes
  39     KiB   bool // show file sizes in kib
  40     MiB   bool // show file sizes in mib
  41     GiB   bool // show file sizes in gib
  42 
  43     Lines    bool // calculate and show lines (treating all files as plain-text)
  44     Text     bool // calculate and show plain-text-related info (treating all files as such)
  45     Duration bool // calculate and show playing duration (for supported media files)
  46     HMS      bool // also show playing duration in hour-minute-seconds format
  47     Picture  bool // find and show picture widths and heights (for supported files)
  48     Type     bool // show file types (its extension without the starting dot)
  49     MIMEType bool // find and show MIME file types
  50     Ext      bool // show the filename extension
  51     Folder   bool // show the folders files are in
  52 }
  53 
  54 const (
  55     picResUsage   = "show width, height, and bits per pixel for supported picture files"
  56     linesUsage    = "show the number of lines by treating files as plain-text ones"
  57     textUsage     = "show plain-text-related info, treating all files as plain-text"
  58     durationUsage = "show the playing duration for supported media files"
  59     hmsUsage      = "also show the playing duration in hour-minute-seconds format"
  60     typeUsage     = "show file types using dotless filename extensions"
  61 )
  62 
  63 func parseFlags(usage string) config {
  64     flag.Usage = func() {
  65         fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
  66         flag.PrintDefaults()
  67     }
  68 
  69     cfg := config{
  70         To:     "tsv",
  71         SortBy: "bytes",
  72         Title:  "File Info",
  73 
  74         Type:  true,
  75         Bytes: true,
  76     }
  77 
  78     flag.StringVar(&cfg.To, "to", cfg.To, "output format: one of tsv, json, or html")
  79     flag.StringVar(&cfg.SortBy, "sort", cfg.SortBy, "what to (reverse-)sort results by")
  80     flag.StringVar(&cfg.Title, "title", cfg.Title, "title to use when emitting HTML")
  81     flag.BoolVar(&cfg.Bytes, "bytes", cfg.Bytes, "show file sizes in bytes")
  82     flag.BoolVar(&cfg.KiB, "kib", cfg.KiB, "show file sizes in KiBs")
  83     flag.BoolVar(&cfg.MiB, "mib", cfg.MiB, "show file sizes in MiBs")
  84     flag.BoolVar(&cfg.GiB, "gib", cfg.GiB, "show file sizes in GiBs")
  85     flag.BoolVar(&cfg.Lines, "l", cfg.Lines, "alias for option -lines")
  86     flag.BoolVar(&cfg.Duration, "d", cfg.Duration, "alias for option -duration")
  87     flag.BoolVar(&cfg.Picture, "res", cfg.Picture, "alias for option -resolution")
  88     flag.BoolVar(&cfg.Picture, "resolution", cfg.Picture, picResUsage)
  89     flag.BoolVar(&cfg.Lines, "lines", cfg.Lines, linesUsage)
  90     flag.BoolVar(&cfg.Text, "text", cfg.Text, textUsage)
  91     flag.BoolVar(&cfg.Duration, "duration", cfg.Duration, durationUsage)
  92     flag.BoolVar(&cfg.HMS, "hms", cfg.HMS, hmsUsage)
  93     flag.BoolVar(&cfg.Type, "type", cfg.Type, typeUsage)
  94     flag.BoolVar(&cfg.MIMEType, "mime", cfg.MIMEType, "show the file's MIME type")
  95     flag.BoolVar(&cfg.Folder, "folder", cfg.Folder, "show folder names")
  96     flag.Parse()
  97 
  98     // normalize values for option `-to`
  99     cfg.To = strings.ToLower(strings.TrimPrefix(cfg.To, "."))
 100 
 101     // normalize value aliases for option -sort and auto-enable any settings these imply
 102     switch strings.ToLower(cfg.SortBy) {
 103     case "", "byte", "bytes", "size", "kb", "mb", "b", "s":
 104         cfg.SortBy = "bytes"
 105     case "line", "lines", "ln", "l":
 106         cfg.SortBy = "lines"
 107         // auto-enable the line-counter if sorting by lines
 108         cfg.Lines = true
 109     case "duration", "time", "dur", "d":
 110         cfg.SortBy = "duration"
 111         // auto-enable the time-duration-counter if sorting by duration
 112         cfg.Duration = true
 113     case "column", "columns", "c":
 114         cfg.SortBy = "columns"
 115         // auto-enable text-specific info
 116         cfg.Text = true
 117     case "cr":
 118         cfg.SortBy = "cr"
 119         // auto-enable text-specific info
 120         cfg.Text = true
 121     case "bom":
 122         cfg.SortBy = "bom"
 123         // auto-enable text-specific info
 124         cfg.Text = true
 125     case "name", "n":
 126         cfg.SortBy = "name"
 127     }
 128 
 129     // auto-enable duration when asked to also show it in HH:MM:SS foramt
 130     if cfg.HMS {
 131         cfg.Duration = true
 132     }
 133     return cfg
 134 }
 135 
 136 func (c config) NeedsExtraInfo() bool {
 137     return c.Lines || c.Text || c.Duration || c.Picture || c.MIMEType
 138 }
     File: ./finfo/fileinfo.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "io"
  29     "os"
  30     "path/filepath"
  31     "strings"
  32 
  33     "../filetypes"
  34     "../mediainfo"
  35 )
  36 
  37 // FileInfo has all sorts of summary stats about files, including its auto-detected
  38 // type and multimedia properties when these are available.
  39 type FileInfo struct {
  40     // general info
  41     Name   string `json:"name"`
  42     Folder string `json:"folder"`
  43     Size   int    `json:"size"`
  44 
  45     // plain-text stats
  46     Lines           int  `json:"lines"`
  47     Columns         int  `json:"columns"`
  48     Separator       rune `json:"separator"`
  49     CarriageReturns int  `json:"cr"`
  50     ByteOrderMarks  int  `json:"bom"`
  51 
  52     // sound/image stats
  53     Duration     float64 `json:"duration"`
  54     Width        int     `json:"width"`
  55     Height       int     `json:"height"`
  56     BitsPerPixel int     `json:"bpp"`
  57 
  58     MIMEType string `json:"mime"`
  59     Problem  error  `json:"-"`
  60 }
  61 
  62 func newFileInfo(fname string, size int) FileInfo {
  63     return FileInfo{
  64         Name:   fname,
  65         Folder: filepath.Dir(fname),
  66         Size:   size,
  67 
  68         Lines:           -1,
  69         Columns:         -1,
  70         CarriageReturns: -1,
  71         ByteOrderMarks:  -1,
  72 
  73         Duration:     -1,
  74         Width:        -1,
  75         Height:       -1,
  76         BitsPerPixel: -1,
  77 
  78         MIMEType: "",
  79         Problem:  nil,
  80     }
  81 }
  82 
  83 func (fi *FileInfo) hasLines() bool {
  84     switch fi.MIMEType {
  85     case "", "application/xml", "application/json":
  86         return true
  87     }
  88     return strings.HasPrefix(fi.MIMEType, "text/") || strings.HasPrefix(fi.MIMEType, "image/svg")
  89 }
  90 
  91 func (r *FileInfo) calculateStats(cfg config) {
  92     // empty files have no lines and last no time: no need to open a file in this case
  93     if r.Size == 0 {
  94         return
  95     }
  96 
  97     f, err := os.Open(r.Name)
  98     if err != nil {
  99         r.Problem = err
 100         return
 101     }
 102     defer f.Close()
 103 
 104     if cfg.Duration {
 105         d, err := mediainfo.FileDuration(f)
 106         if err != nil {
 107             r.Problem = err
 108         } else {
 109             r.Duration = d
 110         }
 111 
 112         // later readers must start from the beginning of the file
 113         f.Seek(0, io.SeekStart)
 114     }
 115 
 116     if cfg.Picture {
 117         w, h, bd, err := mediainfo.Resolution(f, r.Name)
 118         if err == nil {
 119             r.Width = w
 120             r.Height = h
 121             r.BitsPerPixel = bd
 122         } else {
 123             r.Problem = err
 124         }
 125 
 126         // later readers must start from the beginning of the file
 127         f.Seek(0, io.SeekStart)
 128     }
 129 
 130     if cfg.MIMEType || cfg.Text {
 131         var b [128]byte
 132         n, err := f.Read(b[:])
 133         mime, ok := filetypes.DetectMIME(b[:n])
 134         if !ok || (err != nil && err != io.EOF) {
 135             mime = ""
 136         }
 137         r.MIMEType = mime
 138 
 139         // later readers must start from the beginning of the file
 140         f.Seek(0, io.SeekStart)
 141     }
 142 
 143     // don't count lines for most mime types
 144     if (cfg.Lines || cfg.Text) && r.hasLines() {
 145         st, err := summarizePlainText(f)
 146         // csv files need special handling to count their # of columns and autodetect their separator
 147         if strings.HasSuffix(r.Name, ".csv") || strings.HasSuffix(r.Name, ".CSV") {
 148             f.Seek(0, io.SeekStart)
 149             adjustForCSV(f, &st)
 150         }
 151 
 152         if err == nil {
 153             r.Lines = st.Lines
 154             // non-empty text files without newlines count as having 1 line
 155             if r.Lines == 0 && r.Size > 0 {
 156                 r.Lines = 1
 157             }
 158             // non-empty text files with a detected field-separator count as having at least 1 column
 159             r.Columns = st.Columns
 160             if r.Columns == 0 && r.Size > 0 && st.EmptyLines < r.Lines {
 161                 r.Columns = 1
 162             }
 163             r.Separator = st.Separator
 164             r.CarriageReturns = st.CarriageReturns
 165             r.ByteOrderMarks = st.ByteOrderMarks
 166         } else {
 167             r.Problem = err
 168         }
 169 
 170         // xml and json data have no separators nor columns to count
 171         if strings.HasPrefix(r.MIMEType, "application/") {
 172             r.Columns = -1
 173             r.Separator = rune(0)
 174         }
 175     }
 176 }
     File: ./finfo/grouping.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "path/filepath"
  29     "strings"
  30 )
  31 
  32 type FolderInfo struct {
  33     Name    string       `json:"name"`
  34     Path    string       `json:"path"`
  35     Folders []FolderInfo `json:"folders"`
  36     Files   []FileInfo   `json:"files"`
  37 
  38     NumItems        int     `json:"items"`
  39     Size            int     `json:"size"`
  40     Lines           int     `json:"lines"`
  41     CarriageReturns int     `json:"cr"`
  42     ByteOrderMarks  int     `json:"bom"`
  43     Duration        float64 `json:"duration"`
  44 }
  45 
  46 func (fi *FolderInfo) Update() {
  47     fi.NumItems = len(fi.Files)
  48     fi.Size = 0
  49     fi.Lines = 0
  50     fi.CarriageReturns = 0
  51     fi.ByteOrderMarks = 0
  52     fi.Duration = 0
  53 
  54     for i := range fi.Folders {
  55         fi.Folders[i].Update()
  56     }
  57 
  58     for _, v := range fi.Folders {
  59         fi.NumItems += v.NumItems
  60         fi.Size += v.Size
  61         fi.Lines += v.Lines
  62         fi.CarriageReturns += v.CarriageReturns
  63         fi.ByteOrderMarks += v.ByteOrderMarks
  64         fi.Duration += v.Duration
  65     }
  66 
  67     for _, v := range fi.Files {
  68         fi.Size += v.Size
  69         fi.Lines += v.Lines
  70         fi.CarriageReturns += v.CarriageReturns
  71         fi.ByteOrderMarks += v.ByteOrderMarks
  72         fi.Duration += v.Duration
  73     }
  74 
  75     if fi.Size < 0 {
  76         fi.Size = 0
  77     }
  78     if fi.Lines < 0 {
  79         fi.Lines = 0
  80     }
  81     if fi.CarriageReturns < 0 {
  82         fi.CarriageReturns = 0
  83     }
  84     if fi.ByteOrderMarks < 0 {
  85         fi.ByteOrderMarks = 0
  86     }
  87     if fi.Duration < 0 {
  88         fi.Duration = 0
  89     }
  90 }
  91 
  92 func (fi *FolderInfo) FindFolder(path string) *FolderInfo {
  93     parent := fi
  94     parts := strings.Split(path, string(filepath.Separator))
  95     for _, s := range parts {
  96         parent = parent.findFolder(path, s)
  97     }
  98     return parent
  99 }
 100 
 101 func (fi *FolderInfo) findFolder(path, s string) *FolderInfo {
 102     for i, v := range fi.Folders {
 103         if v.Name == s {
 104             return &fi.Folders[i]
 105         }
 106     }
 107 
 108     fi.Folders = append(fi.Folders, FolderInfo{Name: s, Path: path})
 109     return &fi.Folders[len(fi.Folders)-1]
 110 }
 111 
 112 func (fi *FolderInfo) Inspect(f func(fi *FolderInfo)) {
 113     f(fi)
 114     for i := range fi.Folders {
 115         f(&fi.Folders[i])
 116     }
 117 }
 118 
 119 func group(files []FileInfo) FolderInfo {
 120     var res FolderInfo
 121     cache := make(map[string]*FolderInfo)
 122 
 123     for _, v := range files {
 124         parent, _ := filepath.Split(v.Name)
 125         parent = strings.TrimSuffix(parent, "\\")
 126         parent = strings.TrimSuffix(parent, "/")
 127 
 128         ptr, ok := cache[parent]
 129         if !ok {
 130             ptr = res.FindFolder(parent)
 131             cache[parent] = ptr
 132         }
 133         ptr.Files = append(ptr.Files, v)
 134     }
 135 
 136     res.Update()
 137     return res
 138 }
     File: ./finfo/info.txt
   1 finfo [options...] [files/folders...]
   2 
   3 Show various file info, mainly filesizes in decreasing order (biggest to
   4 smallest): all folders given are explored recursively to find all files in
   5 them.
   6 
   7 When exploring files in the current folder, use a dot as the folder name;
   8 when not given any file/folder names, it reads those from standard input one
   9 name per line, so file paths with spaces don't cause any problem.
  10 
  11 Besides file size and name, it can show other info
  12 
  13     - line counts, except for recognized media files
  14     - column counts, in the context of delimiter-separated tabular text data
  15     - carriage-return and byte-order-mark counts, except for known media types
  16     - width, height, and bits per pixel for pictures and video files
  17     - duration/play-length in seconds for all common audio/video files
  18     - path of containing folder
  19     - extension
  20     - MIME type
  21 
  22 Results can also be reverse-sorted by line count, by duration, or any other
  23 numeric option/column: there's no way to increase-sort for any of the numeric
  24 ranking options.
     File: ./finfo/json.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "fmt"
  29     "io"
  30     "strings"
  31 )
  32 
  33 func folders2JSON(w io.Writer, x []FolderInfo) error {
  34     fmt.Fprint(w, "[")
  35     for i, v := range x {
  36         if i > 0 {
  37             fmt.Fprint(w, ", ")
  38         }
  39         if err := folder2JSON(w, v); err != nil {
  40             return err
  41         }
  42     }
  43     _, err := fmt.Fprint(w, "]")
  44     return err
  45 }
  46 
  47 func folder2JSON(w io.Writer, fi FolderInfo) error {
  48     // return json.NewEncoder(w).Encode(fi)
  49 
  50     fmt.Fprint(w, "{")
  51     writeStringJSON(w, "name", unixPath(fi.Name))
  52     writeStringJSON(w, "path", unixPath(fi.Path))
  53     fmt.Fprintf(w, `"folders": [`)
  54     for i, v := range fi.Folders {
  55         if i > 0 {
  56             fmt.Fprint(w, ", ")
  57         }
  58         if err := folder2JSON(w, v); err != nil {
  59             return err
  60         }
  61     }
  62     fmt.Fprint(w, "], ")
  63     fmt.Fprintf(w, `"files": [`)
  64     for i, v := range fi.Files {
  65         if i > 0 {
  66             fmt.Fprint(w, ", ")
  67         }
  68         if err := file2JSON(w, v); err != nil {
  69             return err
  70         }
  71     }
  72     fmt.Fprint(w, "]")
  73     writeIntJSON(w, "items", fi.NumItems)
  74     writeIntJSON(w, "size", fi.Size)
  75     writeIntJSON(w, "lines", fi.Lines)
  76     writeIntJSON(w, "cr", fi.CarriageReturns)
  77     writeIntJSON(w, "bom", fi.ByteOrderMarks)
  78     if fi.Duration >= 0 {
  79         fmt.Fprintf(w, `, "duration": %.2f`, fi.Duration)
  80     }
  81     _, err := fmt.Fprint(w, "}")
  82     return err
  83 }
  84 
  85 func file2JSON(w io.Writer, fi FileInfo) error {
  86     // return json.NewEncoder(w).Encode(fi)
  87     fmt.Fprint(w, "{")
  88     writeStringJSON(w, "name", unixPath(fi.Name))
  89     writeStringJSON(w, "folder", unixPath(fi.Folder))
  90     writeStringJSON(w, "mime", fi.MIMEType)
  91     fmt.Fprintf(w, `"size": %d`, fi.Size)
  92     writeIntJSON(w, "lines", fi.Lines)
  93     writeIntJSON(w, "columns", fi.Columns)
  94     // fi.Separator
  95     writeIntJSON(w, "cr", fi.CarriageReturns)
  96     writeIntJSON(w, "bom", fi.ByteOrderMarks)
  97     if fi.Duration >= 0 {
  98         fmt.Fprintf(w, `, "duration": %.2f`, fi.Duration)
  99     }
 100     writeIntJSON(w, "width", fi.Width)
 101     writeIntJSON(w, "height", fi.Height)
 102     writeIntJSON(w, "bpp", fi.BitsPerPixel)
 103     _, err := fmt.Fprint(w, "}")
 104     return err
 105 }
 106 
 107 func writeStringJSON(w io.Writer, k, s string) {
 108     s = strings.TrimSpace(s)
 109     if strings.Contains(s, `"`) {
 110         s = strings.ReplaceAll(s, `"`, `\"`)
 111     }
 112     if strings.Contains(s, `\`) {
 113         s = strings.ReplaceAll(s, `\`, `\\`)
 114     }
 115     fmt.Fprintf(w, `"%s": "%s", `, k, s)
 116 }
 117 
 118 func writeIntJSON(w io.Writer, k string, n int) {
 119     if n >= 0 {
 120         fmt.Fprintf(w, `, "%s": %d`, k, n)
 121     }
 122 }
 123 
 124 func unixPath(s string) string {
 125     if strings.Contains(s, `\`) {
 126         return strings.ReplaceAll(s, `\`, `/`)
 127     }
 128     return s
 129 }
     File: ./finfo/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "bufio"
  29     "flag"
  30     "fmt"
  31     "os"
  32     "sort"
  33 
  34     _ "embed"
  35 )
  36 
  37 //go:embed info.txt
  38 var usage string
  39 
  40 // //go:embed style.css
  41 // var css string
  42 
  43 const maxbufsize = 8 * 1024 * 1024 * 1024
  44 
  45 func Main() {
  46     cfg := parseFlags(usage)
  47     if err := run(cfg); err != nil {
  48         fmt.Fprintln(os.Stderr, err.Error())
  49         os.Exit(1)
  50         return
  51     }
  52 }
  53 
  54 func run(cfg config) error {
  55     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  56     defer w.Flush()
  57 
  58     switch cfg.To {
  59     case "tsv":
  60         td := newTableDisplay(cfg)
  61         // show header immediately to reassure user in case scanning/sorting is taking a while
  62         td.FprintlnHeader(os.Stdout)
  63 
  64         data := scan(flag.Args(), cfg)
  65         sortItems(data, cfg.SortBy)
  66         for _, e := range data {
  67             if err := td.Fprintln(w, e); err != nil {
  68                 return nil // probably a pipe was closed
  69             }
  70         }
  71         return nil
  72 
  73     case "json":
  74         data := scan(flag.Args(), cfg)
  75         grouped := group(data)
  76         grouped.Inspect(func(fi *FolderInfo) {
  77             sortItems(fi.Files, cfg.SortBy)
  78 
  79             // avoid null values anywhere
  80             if fi.Folders == nil {
  81                 fi.Folders = []FolderInfo{}
  82             }
  83             if fi.Files == nil {
  84                 fi.Files = []FileInfo{}
  85             }
  86         })
  87 
  88         folders2JSON(w, grouped.Folders)
  89         w.Write([]byte("\n"))
  90         return nil
  91 
  92     default:
  93         return fmt.Errorf("unknown output-type %q", cfg.To)
  94     }
  95 }
  96 
  97 func scan(args []string, cfg config) []FileInfo {
  98     // get all file/folder names to check, then check them all: if no arguments
  99     // were given, use stdin to read them line by line
 100     if len(args) == 0 {
 101         sc := bufio.NewScanner(os.Stdin)
 102         sc.Buffer(nil, maxbufsize)
 103         for sc.Scan() {
 104             args = append(args, sc.Text())
 105         }
 106     }
 107 
 108     // get file sizes, count lines, count media duration, etc.
 109     agg := newAggregator(cfg)
 110     agg.Scan(args)
 111     data := agg.Results()
 112     return data
 113 }
 114 
 115 func sortItems(data []FileInfo, by string) {
 116     // show the list sorted by decreasing size, text lines, time duration, # of columns,
 117     // # of carriage-returns, # of byte-order marks, or forward-sorted by filename
 118     switch by {
 119     case "lines":
 120         sort.Sort(sortableLineCountInfo(data))
 121     case "duration":
 122         sort.Sort(sortableDurationInfo(data))
 123     case "columns":
 124         sort.Sort(sortableColumnCountInfo(data))
 125     case "cr":
 126         sort.Sort(sortableCarriageReturnInfo(data))
 127     case "bom":
 128         sort.Sort(sortableByteOrderMarkInfo(data))
 129     case "name":
 130         sort.Sort(sortableNameInfo(data))
 131     default:
 132         sort.Sort(sortableSizeInfo(data))
 133     }
 134 }
     File: ./finfo/plaintext.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "encoding/csv"
  31     "io"
  32     "strings"
  33 )
  34 
  35 // 0xefbbbf
  36 var bom = []byte{0xef, 0xbb, 0xbf}
  37 
  38 type plainTextStats struct {
  39     Lines           int
  40     Columns         int
  41     CarriageReturns int
  42     ByteOrderMarks  int
  43     EmptyLines      int
  44     AlphanumASCII   int
  45     Separator       rune
  46 }
  47 
  48 func summarizePlainText(f io.Reader) (plainTextStats, error) {
  49     st := plainTextStats{}
  50     sc := bufio.NewScanner(f)
  51     sc.Buffer(nil, maxbufsize)
  52     sc.Split(splitUnixLines)
  53 
  54     nonempty := 0
  55     for ; sc.Scan(); st.Lines++ {
  56         err := sc.Err()
  57         if err != nil {
  58             return st, err
  59         }
  60 
  61         if st.Lines == 0 && bytes.HasPrefix(sc.Bytes(), bom) {
  62             st.ByteOrderMarks = 1
  63         }
  64 
  65         line := sc.Text()
  66         // ignore empty lines
  67         if line == "" {
  68             st.EmptyLines++
  69             continue
  70         }
  71         nonempty++
  72 
  73         // first line: count tabs, pipes, and colons to update # columns
  74         if nonempty == 1 {
  75             summarizePlainTextHeader(&st, line)
  76             continue
  77         }
  78 
  79         for _, r := range line {
  80             if r == '\r' {
  81                 st.CarriageReturns++
  82             } else if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
  83                 st.AlphanumASCII++
  84             }
  85         }
  86     }
  87 
  88     return st, nil
  89 }
  90 
  91 // used only in function summarizePlainText
  92 func summarizePlainTextHeader(st *plainTextStats, line string) {
  93     tabs := 0
  94     pipes := 0
  95     colons := 0
  96     for _, r := range line {
  97         switch r {
  98         case '\t':
  99             tabs++
 100         case '|':
 101             pipes++
 102         case ':':
 103             colons++
 104         case '\r':
 105             st.CarriageReturns++
 106         }
 107     }
 108 
 109     // # of columns is # of tabs + 1
 110     if tabs > 0 && tabs+1 > st.Columns {
 111         st.Columns = tabs + 1
 112         st.Separator = '\t'
 113         return
 114     }
 115 
 116     if pipes > 0 && pipes+1 > st.Columns {
 117         st.Columns = pipes + 1
 118         st.Separator = '|'
 119         return
 120     }
 121 
 122     // 2 as the minimum avoids file patterns (text or not) where there are no colon separators:
 123     // in practice they're only used in unix-like config files where there are many fields anyway
 124 
 125     // note: still use 1 as the minimum count for now
 126     if colons > 0 && colons+1 > st.Columns {
 127         st.Columns = colons + 1
 128         st.Separator = ':'
 129     }
 130 
 131     if st.Columns == 0 && st.Separator != 0 {
 132         st.Columns = 1
 133     }
 134 }
 135 
 136 // used only in function summarizePlainText
 137 func splitUnixLines(b []byte, eof bool) (int, []byte, error) {
 138     if eof && len(b) == 0 {
 139         return 0, nil, nil
 140     }
 141     i := bytes.IndexByte(b, '\n')
 142     if i >= 0 {
 143         return i + 1, b[0:i], nil
 144     }
 145     // last line
 146     if eof {
 147         return len(b), b, nil
 148     }
 149     return 0, nil, nil
 150 }
 151 
 152 func adjustForCSV(f io.Reader, st *plainTextStats) {
 153     // get the first nonempty line
 154     line := ""
 155     sc := bufio.NewScanner(f)
 156     sc.Buffer(nil, maxbufsize)
 157     for sc.Scan() {
 158         if sc.Err() != nil {
 159             break
 160         }
 161         line = sc.Text()
 162         if line != "" {
 163             break
 164         }
 165     }
 166 
 167     w := 0
 168     if st.Separator != rune(0) {
 169         w = csvCountHeader(strings.NewReader(line), st.Separator)
 170     }
 171     wc := csvCountHeader(strings.NewReader(line), ',')
 172     ws := csvCountHeader(strings.NewReader(line), ';')
 173     if w > wc && w > ws {
 174         st.Columns = w
 175     } else if wc > w && wc > ws {
 176         st.Columns = wc
 177         st.Separator = ','
 178     } else if ws > w && ws > wc {
 179         st.Columns = ws
 180         st.Separator = ';'
 181     }
 182 }
 183 
 184 func csvCountHeader(f io.Reader, sep rune) int {
 185     sc := csv.NewReader(f)
 186     sc.LazyQuotes = true
 187     sc.ReuseRecord = true
 188     if sep == rune(0) {
 189         sep = ','
 190     }
 191     sc.Comma = sep
 192     for {
 193         row, err := sc.Read()
 194         if err != nil {
 195             return 0
 196         }
 197         // just use the first non-empty line to estimate the # of columns
 198         if len(row) > 0 {
 199             return len(row)
 200         }
 201     }
 202 }
     File: ./finfo/sorting.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "strings"
  29 )
  30 
  31 // allow reverse-sorting by size in bytes
  32 type sortableSizeInfo []FileInfo
  33 
  34 func (r sortableSizeInfo) Len() int      { return len(r) }
  35 func (r sortableSizeInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
  36 func (r sortableSizeInfo) Less(i, j int) bool {
  37     if diff := r[i].Size - r[j].Size; diff != 0 {
  38         return diff > 0
  39     }
  40     return strings.Compare(r[i].Name, r[j].Name) < 0
  41 }
  42 
  43 // allow reverse-sorting by lines of text counted
  44 type sortableLineCountInfo []FileInfo
  45 
  46 func (r sortableLineCountInfo) Len() int      { return len(r) }
  47 func (r sortableLineCountInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
  48 func (r sortableLineCountInfo) Less(i, j int) bool {
  49     if diff := r[i].Lines - r[j].Lines; diff != 0 {
  50         return diff > 0
  51     }
  52     return strings.Compare(r[i].Name, r[j].Name) < 0
  53 }
  54 
  55 // allow reverse-sorting by time duration
  56 type sortableDurationInfo []FileInfo
  57 
  58 func (r sortableDurationInfo) Len() int      { return len(r) }
  59 func (r sortableDurationInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
  60 func (r sortableDurationInfo) Less(i, j int) bool {
  61     v1 := r[i].Duration >= 0 //!math.IsNaN(r[i].Duration)
  62     v2 := r[j].Duration >= 0 //!math.IsNaN(r[j].Duration)
  63     if v1 && v2 {
  64         if diff := r[i].Duration - r[j].Duration; diff != 0 {
  65             return diff > 0
  66         }
  67         return strings.Compare(r[i].Name, r[j].Name) < 0
  68     }
  69 
  70     // treat nan < valid
  71     if !v1 && v2 {
  72         return false
  73     }
  74     if v1 && !v2 {
  75         return true
  76     }
  77 
  78     // if neither has a time duration, sort by size
  79     if diff := r[i].Size - r[j].Size; diff != 0 {
  80         return diff > 0
  81     }
  82     return strings.Compare(r[i].Name, r[j].Name) < 0
  83 }
  84 
  85 // allow reverse-sorting by data columns counted
  86 type sortableColumnCountInfo []FileInfo
  87 
  88 func (r sortableColumnCountInfo) Len() int      { return len(r) }
  89 func (r sortableColumnCountInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
  90 func (r sortableColumnCountInfo) Less(i, j int) bool {
  91     if diff := r[i].Columns - r[j].Columns; diff != 0 {
  92         return diff > 0
  93     }
  94     return strings.Compare(r[i].Name, r[j].Name) < 0
  95 }
  96 
  97 // allow reverse-sorting by carriage-returns counted
  98 type sortableCarriageReturnInfo []FileInfo
  99 
 100 func (r sortableCarriageReturnInfo) Len() int      { return len(r) }
 101 func (r sortableCarriageReturnInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
 102 func (r sortableCarriageReturnInfo) Less(i, j int) bool {
 103     if diff := r[i].CarriageReturns - r[j].CarriageReturns; diff != 0 {
 104         return diff > 0
 105     }
 106     return strings.Compare(r[i].Name, r[j].Name) < 0
 107 }
 108 
 109 // allow reverse-sorting by byte-order marks counted
 110 type sortableByteOrderMarkInfo []FileInfo
 111 
 112 func (r sortableByteOrderMarkInfo) Len() int      { return len(r) }
 113 func (r sortableByteOrderMarkInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
 114 func (r sortableByteOrderMarkInfo) Less(i, j int) bool {
 115     if diff := r[i].ByteOrderMarks - r[j].ByteOrderMarks; diff != 0 {
 116         return diff > 0
 117     }
 118     return strings.Compare(r[i].Name, r[j].Name) < 0
 119 }
 120 
 121 // allow forward-sorting by filename
 122 type sortableNameInfo []FileInfo
 123 
 124 func (r sortableNameInfo) Len() int      { return len(r) }
 125 func (r sortableNameInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
 126 func (r sortableNameInfo) Less(i, j int) bool {
 127     return strings.Compare(r[i].Name, r[j].Name) < 0
 128 }
     File: ./finfo/tables.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "fmt"
  29     "io"
  30     "math"
  31     "path/filepath"
  32     "strings"
  33 )
  34 
  35 // tableDisplay handles the final display of all the info gathered
  36 type tableDisplay struct {
  37     Headers    []string
  38     Formats    []string
  39     Conditions []bool
  40 
  41     values []any // to minimize memory allocations
  42 }
  43 
  44 func newTableDisplay(c config) tableDisplay {
  45     return tableDisplay{
  46         Headers: []string{
  47             "bytes", "KiB", "MiB", "GiB", // file size
  48             "duration", "hh:mm:ss", "width", "height", "bpp", // sound/picture info
  49             "lines", "columns", "cells", "CR", "BOM", "sep", // plain-text-related
  50             "name", "folder", "ext", "MIME", // name and type
  51         },
  52         Formats: []string{
  53             "%d", "%.2f", "%.2f", "%.2f", // file size
  54             "%.2f", "%v", "%d", "%d", "%d", // sound/picture info
  55             "%d", "%d", "%d", "%d", "%d", "%s", // plain-text-related
  56             "%s", "%s", "%s", "%s", // name and type
  57         },
  58         Conditions: []bool{
  59             c.Bytes, c.KiB, c.MiB, c.GiB, // file size
  60             c.Duration, c.HMS, c.Picture, c.Picture, c.Picture, // sound/picture info
  61             c.Lines || c.Text, c.Text, c.Text, c.Text, c.Text, c.Text, // plain-text-related
  62             true, c.Folder, c.Type, c.MIMEType, // name and type
  63         },
  64         values: make([]any, 0, 20),
  65     }
  66 }
  67 
  68 func (c tableDisplay) FprintlnHeader(w io.Writer) {
  69     first := true
  70     for i, cond := range c.Conditions {
  71         if !cond {
  72             continue
  73         }
  74         if !first {
  75             fmt.Fprint(w, "\t")
  76         }
  77         first = false
  78         fmt.Fprint(w, c.Headers[i])
  79     }
  80     fmt.Fprintln(w)
  81 }
  82 
  83 func (c tableDisplay) Fprintln(w io.Writer, e FileInfo) error {
  84     kib := float64(e.Size) / 1024
  85     mib := kib / 1024
  86     gib := mib / 1024
  87     name := strings.ReplaceAll(e.Name, "\\", "/")      // show unix-style folder separators
  88     ext := strings.TrimLeft(filepath.Ext(e.Name), ".") // file extension with no leading dot
  89     ext = strings.ToLower(ext)                         // handle uppercase file extensions
  90     folder := strings.ReplaceAll(e.Folder, "\\", "/")
  91     sep := ""
  92     switch e.Separator {
  93     case '\t':
  94         sep = "tab"
  95     case rune(0):
  96         sep = ""
  97     case ',':
  98         sep = "comma"
  99     case ';':
 100         sep = "semicolon"
 101     case '|':
 102         sep = "pipe"
 103     case ':':
 104         sep = "colon"
 105     default:
 106         sep = string(e.Separator)
 107     }
 108 
 109     hms := ""
 110     if c.Conditions[5] {
 111         hms = s2hms(e.Duration)
 112     }
 113     ncells := e.Lines * e.Columns
 114     if e.Lines < 2 || e.Columns < 2 {
 115         ncells = -1
 116     }
 117 
 118     c.values = c.values[:0]
 119     c.values = append(c.values,
 120         e.Size, kib, mib, gib, // file size
 121         e.Duration, hms, e.Width, e.Height, e.BitsPerPixel, // sound/picture info
 122         e.Lines, e.Columns, ncells, e.CarriageReturns, e.ByteOrderMarks, sep, // plain-text-related
 123         name, folder, ext, e.MIMEType, // name and type
 124     )
 125 
 126     first := true
 127     for i, cond := range c.Conditions {
 128         if !cond {
 129             continue
 130         }
 131         if !first {
 132             fmt.Fprint(w, "\t")
 133         }
 134         first = false
 135 
 136         v := c.values[i]
 137         // avoid emitting nan values as "NaN"
 138         f, ok := v.(float64)
 139         if ok && (math.IsNaN(f) || f < 0) {
 140             continue
 141         }
 142         // negative integer counters result from errors
 143         n, ok := v.(int)
 144         if ok && n < 0 {
 145             continue
 146         }
 147         fmt.Fprintf(w, c.Formats[i], v)
 148     }
 149 
 150     _, err := fmt.Fprintln(w)
 151     return err
 152 }
 153 
 154 func s2hms(t float64) string {
 155     if t < 0 || math.IsNaN(t) {
 156         return ""
 157     }
 158     h := math.Floor(t / 3600)
 159     m := math.Floor(math.Mod(t, 3600) / 60)
 160     s := math.Mod(t, 60)
 161     return fmt.Sprintf("%02d:%02d:%05.2f", int(h), int(m), s)
 162 }
     File: ./finfo/tasks.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package finfo
  26 
  27 import (
  28     "fmt"
  29     "io/fs"
  30     "os"
  31     "path/filepath"
  32     "runtime"
  33     "sync"
  34 )
  35 
  36 // a parallel results-collector
  37 type aggregator struct {
  38     cfg  config
  39     seen map[string]struct{} // avoid duplicate results for files
  40     res  []FileInfo
  41 }
  42 
  43 func newAggregator(cfg config) aggregator {
  44     return aggregator{
  45         cfg:  cfg,
  46         seen: make(map[string]struct{}),
  47         res:  make([]FileInfo, 0),
  48     }
  49 }
  50 
  51 func (a *aggregator) Results() []FileInfo {
  52     return a.res
  53 }
  54 
  55 func (a *aggregator) Scan(filenames []string) {
  56     for _, fname := range filenames {
  57         info, err := os.Stat(fname)
  58         if err != nil {
  59             fmt.Fprintln(os.Stderr, err.Error())
  60             continue
  61         }
  62 
  63         if info.IsDir() {
  64             err = a.handleFolder(fname)
  65             if err != nil {
  66                 fmt.Fprintln(os.Stderr, err.Error())
  67                 continue
  68             }
  69             continue
  70         }
  71         a.addFileEntry(fname, info)
  72     }
  73 
  74     // no need to open files when only name and file size are going to be shown
  75     if !a.cfg.NeedsExtraInfo() {
  76         return
  77     }
  78 
  79     var wg sync.WaitGroup
  80     wg.Add(len(a.res))
  81 
  82     // calculate stats/results asynchronously when told to; use a channel to limit how many
  83     // file-stats calculations are running at the same time: the limit is the number of cores
  84     exitTickets := make(chan struct{}, runtime.NumCPU())
  85     defer close(exitTickets)
  86     for i := range a.res {
  87         exitTickets <- struct{}{}
  88         go func(i int) {
  89             defer func() {
  90                 <-exitTickets
  91                 wg.Done()
  92             }()
  93             a.res[i].calculateStats(a.cfg)
  94         }(i)
  95     }
  96 
  97     // ensure all jobs are finished before returning
  98     wg.Wait()
  99 }
 100 
 101 func (a *aggregator) addFileEntry(fname string, info os.FileInfo) {
 102     // avoid going over the same places more than once
 103     _, ok := a.seen[fname]
 104     if ok {
 105         return
 106     }
 107     a.seen[fname] = struct{}{}
 108     a.res = append(a.res, newFileInfo(fname, int(info.Size())))
 109 }
 110 
 111 func (a *aggregator) handleFolder(fpath string) error {
 112     return filepath.WalkDir(fpath, func(path string, d fs.DirEntry, err error) error {
 113         // nothing to do when there's either an error or it's a folder
 114         if err != nil {
 115             return err
 116         }
 117         if d.IsDir() {
 118             return nil
 119         }
 120 
 121         info, err := d.Info()
 122         if err != nil {
 123             return err
 124         }
 125         a.addFileEntry(path, info)
 126         return nil
 127     })
 128 }
     File: ./first/first.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package first
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 first [options...] [max lines...] [files...]
  39 
  40 Keep only up to the first n lines from the input. If not given an explicit
  41 number, the default is to only keep the first line.
  42 
  43 All (optional) leading options start with either single or double-dash:
  44 
  45     -h, -help    show this help message
  46 `
  47 
  48 func Main() {
  49     buffered := false
  50     args := os.Args[1:]
  51 
  52     for len(args) > 0 {
  53         switch args[0] {
  54         case `-b`, `--b`, `-buffered`, `--buffered`:
  55             buffered = true
  56             args = args[1:]
  57             continue
  58 
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62         }
  63 
  64         break
  65     }
  66 
  67     linesLeft := 1
  68     if len(args) > 0 {
  69         s := strings.Replace(args[0], `_`, ``, -1)
  70         n, err := strconv.ParseInt(s, 10, 64)
  71         if err == nil {
  72             linesLeft = int(n)
  73             args = args[1:]
  74         }
  75     }
  76 
  77     if len(args) > 0 && args[0] == `--` {
  78         args = args[1:]
  79     }
  80 
  81     if linesLeft < 1 {
  82         return
  83     }
  84 
  85     liveLines := !buffered
  86     if !buffered {
  87         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  88             liveLines = false
  89         }
  90     }
  91 
  92     err := run(os.Stdout, args, linesLeft, liveLines)
  93     if err != nil && err != io.EOF {
  94         os.Stderr.WriteString(`first: `)
  95         os.Stderr.WriteString(err.Error())
  96         os.Stderr.WriteString("\n")
  97         os.Exit(1)
  98         return
  99     }
 100 }
 101 
 102 func run(w io.Writer, paths []string, linesLeft int, liveLines bool) error {
 103     bw := bufio.NewWriterSize(w, 32*1024)
 104     defer bw.Flush()
 105 
 106     for _, path := range paths {
 107         if err := handleFile(bw, path, &linesLeft, liveLines); err != nil {
 108             return err
 109         }
 110     }
 111 
 112     if len(paths) == 0 {
 113         return first(bw, os.Stdin, &linesLeft, liveLines)
 114     }
 115     return nil
 116 }
 117 
 118 func handleFile(w *bufio.Writer, name string, left *int, live bool) error {
 119     if name == `` || name == `-` {
 120         return first(w, os.Stdin, left, live)
 121     }
 122 
 123     f, err := os.Open(name)
 124     if err != nil {
 125         return errors.New(`can't read from file named "` + name + `"`)
 126     }
 127     defer f.Close()
 128 
 129     return first(w, f, left, live)
 130 }
 131 
 132 func first(w *bufio.Writer, r io.Reader, left *int, live bool) error {
 133     if *left < 1 {
 134         return io.EOF
 135     }
 136 
 137     const gb = 1024 * 1024 * 1024
 138     sc := bufio.NewScanner(r)
 139     sc.Buffer(nil, 8*gb)
 140 
 141     for i := 0; sc.Scan(); i++ {
 142         s := sc.Bytes()
 143         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 144             s = s[3:]
 145         }
 146 
 147         w.Write(s)
 148         if w.WriteByte('\n') != nil {
 149             return io.EOF
 150         }
 151 
 152         if live && w.Flush() != nil {
 153             return io.EOF
 154         }
 155 
 156         *left--
 157         if *left < 1 {
 158             break
 159         }
 160     }
 161 
 162     return sc.Err()
 163 }
     File: ./fixlines/fixlines.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fixlines
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 fixlines [options...] [filepaths...]
  37 
  38 
  39 This tool fixes lines in UTF-8 text, ignoring leading UTF-8 BOMs, trailing
  40 carriage-returns on all lines, and ensures no lines across inputs are
  41 accidentally joined, since all lines it outputs end with line-feeds,
  42 even when the original files don't.
  43 
  44 The options are, available both in single and double-dash versions
  45 
  46     -h, -help    show this help message
  47 
  48     -detrail, -trimtrail, -trim-trail, -trimtrails, -trim-trails
  49                  ignore trailing spaces on lines
  50 
  51     -squeeze     aggressively trim lines, ignoring both leading and
  52                  trailing spaces, spaces around tabs, and turn runs
  53                  of multiple spaces into single ones
  54 `
  55 
  56 type config struct {
  57     fix       func(w *bufio.Writer, r io.Reader, live bool) error
  58     liveLines bool
  59 }
  60 
  61 func Main() {
  62     buffered := false
  63     args := os.Args[1:]
  64     var cfg config
  65     cfg.fix = detrail
  66 
  67     if len(args) > 0 {
  68         switch args[0] {
  69         case `-b`, `--b`, `-buffered`, `--buffered`:
  70             buffered = true
  71             args = args[1:]
  72 
  73         case
  74             `-detrail`, `--detrail`, `-trimtrail`, `--trimtrail`,
  75             `-trim-trail`, `--trim-trail`, `-trimtrails`, `--trimtrails`,
  76             `-trim-trails`, `--trim-trails`:
  77             cfg.fix = detrail
  78             args = args[1:]
  79 
  80         case `-h`, `--h`, `-help`, `--help`:
  81             os.Stdout.WriteString(info[1:])
  82             return
  83 
  84         case
  85             `-keeptrail`, `--keeptrail`, `-keept-rail`, `--keep-trail`,
  86             `-keeptrails`, `--keeptrails`, `-keept-rails`, `--keep-trails`:
  87             cfg.fix = catl
  88             args = args[1:]
  89 
  90         case `-s`, `--s`, `-squeeze`, `--squeeze`:
  91             buffered = true
  92             args = args[1:]
  93         }
  94     }
  95 
  96     if len(args) > 0 && args[0] == `--` {
  97         args = args[1:]
  98     }
  99 
 100     cfg.liveLines = !buffered
 101     if !buffered {
 102         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 103             cfg.liveLines = false
 104         }
 105     }
 106 
 107     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 108         os.Stderr.WriteString(`fixlines: `)
 109         os.Stderr.WriteString(err.Error())
 110         os.Stderr.WriteString("\n")
 111         os.Exit(1)
 112         return
 113     }
 114 }
 115 
 116 func run(w io.Writer, args []string, cfg config) error {
 117     dashes := 0
 118     for _, name := range args {
 119         if name == `-` {
 120             dashes++
 121         }
 122         if dashes > 1 {
 123             return errors.New(`can't use stdin (dash) more than once`)
 124         }
 125     }
 126 
 127     bw := bufio.NewWriterSize(w, 32*1024)
 128     defer bw.Flush()
 129 
 130     if len(args) == 0 {
 131         return cfg.fix(bw, os.Stdin, cfg.liveLines)
 132     }
 133 
 134     for _, name := range args {
 135         if err := handleFile(bw, name, cfg); err != nil {
 136             return err
 137         }
 138     }
 139     return nil
 140 }
 141 
 142 func handleFile(w *bufio.Writer, name string, cfg config) error {
 143     if name == `` || name == `-` {
 144         return cfg.fix(w, os.Stdin, cfg.liveLines)
 145     }
 146 
 147     f, err := os.Open(name)
 148     if err != nil {
 149         return errors.New(`can't read from file named "` + name + `"`)
 150     }
 151     defer f.Close()
 152 
 153     return cfg.fix(w, f, cfg.liveLines)
 154 }
 155 
 156 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 157     const gb = 1024 * 1024 * 1024
 158     sc := bufio.NewScanner(r)
 159     sc.Buffer(nil, 8*gb)
 160 
 161     for i := 0; sc.Scan(); i++ {
 162         s := sc.Bytes()
 163         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 164             s = s[3:]
 165         }
 166 
 167         w.Write(s)
 168         if w.WriteByte('\n') != nil {
 169             return io.EOF
 170         }
 171 
 172         if live && w.Flush() != nil {
 173             return io.EOF
 174         }
 175     }
 176 
 177     return sc.Err()
 178 }
 179 
 180 func detrail(w *bufio.Writer, r io.Reader, live bool) error {
 181     const gb = 1024 * 1024 * 1024
 182     sc := bufio.NewScanner(r)
 183     sc.Buffer(nil, 8*gb)
 184 
 185     for i := 0; sc.Scan(); i++ {
 186         s := sc.Bytes()
 187 
 188         // ignore leading UTF-8 BOM on the first line
 189         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 190             s = s[3:]
 191         }
 192 
 193         // trim trailing spaces on the current line
 194         for len(s) > 0 && s[len(s)-1] == ' ' {
 195             s = s[:len(s)-1]
 196         }
 197 
 198         w.Write(s)
 199         if w.WriteByte('\n') != nil {
 200             return io.EOF
 201         }
 202 
 203         if live && w.Flush() != nil {
 204             return io.EOF
 205         }
 206     }
 207 
 208     return sc.Err()
 209 }
 210 
 211 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
 212     const gb = 1024 * 1024 * 1024
 213     sc := bufio.NewScanner(r)
 214     sc.Buffer(nil, 8*gb)
 215 
 216     for i := 0; sc.Scan(); i++ {
 217         s := sc.Bytes()
 218         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 219             s = s[3:]
 220         }
 221 
 222         writeSqueezed(w, s)
 223         if w.WriteByte('\n') != nil {
 224             return io.EOF
 225         }
 226 
 227         if live && w.Flush() != nil {
 228             return io.EOF
 229         }
 230     }
 231 
 232     return sc.Err()
 233 }
 234 
 235 func writeSqueezed(w *bufio.Writer, s []byte) {
 236     // ignore leading spaces
 237     for len(s) > 0 && s[0] == ' ' {
 238         s = s[1:]
 239     }
 240 
 241     // ignore trailing spaces
 242     for len(s) > 0 && s[len(s)-1] == ' ' {
 243         s = s[:len(s)-1]
 244     }
 245 
 246     space := false
 247 
 248     for len(s) > 0 {
 249         switch s[0] {
 250         case ' ':
 251             s = s[1:]
 252             space = true
 253 
 254         case '\t':
 255             s = s[1:]
 256             space = false
 257             for len(s) > 0 && s[0] == ' ' {
 258                 s = s[1:]
 259             }
 260             w.WriteByte('\t')
 261 
 262         default:
 263             if space {
 264                 w.WriteByte(' ')
 265                 space = false
 266             }
 267             w.WriteByte(s[0])
 268             s = s[1:]
 269         }
 270     }
 271 }
     File: ./fmscripts/benchmarks_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29     "testing"
  30 )
  31 
  32 const (
  33     interferingRipples = "" +
  34         "exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + " +
  35         "exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))"
  36 
  37     smilingGhost = "" +
  38         "log1p(((x - 1)**2 + y*y - 4)*((x + 1)**2 + y*y - 4)*" +
  39         "(x*x + (y - sqrt(3))**2 - 4) - 5)"
  40 )
  41 
  42 var benchmarks = []struct {
  43     Name   string
  44     Script string
  45     Native func(float64, float64) float64
  46 }{
  47     {"Load", "x", func(x, y float64) float64 { return x }},
  48     {"Constant", "4.25", func(x, y float64) float64 { return 4.25 }},
  49     {"Constant Addition", "1+2", func(x, y float64) float64 { return 1 + 2 }},
  50     {"0 Additions", "x", func(x, y float64) float64 { return x }},
  51     {"1 Additions", "x+x", func(x, y float64) float64 { return x + x }},
  52     {"2 Additions", "x+x+x", func(x, y float64) float64 { return x + x + x }},
  53     {"0 Multiplications", "x", func(x, y float64) float64 { return x }},
  54     {"1 Multiplications", "x*x", func(x, y float64) float64 { return x * x }},
  55     {"2 Multiplications", "x*x*x", func(x, y float64) float64 { return x * x * x }},
  56     {"0 Divisions", "x", func(x, y float64) float64 { return x }},
  57     {"1 Divisions", "x/x", func(x, y float64) float64 { return x / x }},
  58     {"2 Divisions", "x/x/x", func(x, y float64) float64 { return x / x / x }},
  59     {"e+pi", "e+pi", func(x, y float64) float64 { return math.E + math.Pi }},
  60     {"1-2", "1-2", func(x, y float64) float64 { return 1 - 2 }},
  61     {"e-pi", "e-pi", func(x, y float64) float64 { return math.E - math.Pi }},
  62     {"Add 0", "x+0", func(x, y float64) float64 { return x + 0 }},
  63     {"x*1", "x*1", func(x, y float64) float64 { return x * 1 }},
  64     {"Abs Func", "abs(-x)", func(x, y float64) float64 { return math.Abs(-x) }},
  65     {"Abs Syntax", "&(-x)", func(x, y float64) float64 { return math.Abs(-x) }},
  66     {
  67         "Square (pow)",
  68         "pow(x, 2)",
  69         func(x, y float64) float64 { return math.Pow(x, 2) },
  70     },
  71     {"Square", "square(x)", func(x, y float64) float64 { return x * x }},
  72     {"Square Syntax", "*x", func(x, y float64) float64 { return x * x }},
  73     {
  74         "Linear",
  75         "3.4 * x - 0.2",
  76         func(x, y float64) float64 { return 3.4*x - 0.2 },
  77     },
  78     {"Direct Square", "x*x", func(x, y float64) float64 { return x * x }},
  79     {
  80         "Cube (pow)",
  81         "pow(x, 3)",
  82         func(x, y float64) float64 { return math.Pow(x, 3) },
  83     },
  84     {"Cube", "cube(x)", func(x, y float64) float64 { return x * x * x }},
  85     {"Direct Cube", "x*x*x", func(x, y float64) float64 { return x * x * x }},
  86     {
  87         "Reciprocal (pow)",
  88         "pow(x, -1)",
  89         func(x, y float64) float64 { return math.Pow(x, -1) },
  90     },
  91     {
  92         "Reciprocal Func",
  93         "reciprocal(x)",
  94         func(x, y float64) float64 { return 1 / x },
  95     },
  96     {"Direct Reciprocal", "1/x", func(x, y float64) float64 { return 1 / x }},
  97     {"Variable Negation", "-x", func(x, y float64) float64 { return -x }},
  98     {"Constant Multip.", "1*2", func(x, y float64) float64 { return 1 * 2 }},
  99     {"e*pi", "e*pi", func(x, y float64) float64 { return math.E * math.Pi }},
 100     {
 101         "Variable y = mx + k",
 102         "2.1*x + 3.4",
 103         func(x, y float64) float64 { return 2.1*x + 3.4 },
 104     },
 105     {"sin(x)", "sin(x)", func(x, y float64) float64 { return math.Sin(x) }},
 106     {"cos(x)", "cos(x)", func(x, y float64) float64 { return math.Cos(x) }},
 107     {"exp(x)", "exp(x)", func(x, y float64) float64 { return math.Exp(x) }},
 108     {"expm1(x)", "expm1(x)", func(x, y float64) float64 { return math.Expm1(x) }},
 109     {"ln(x)", "ln(x)", func(x, y float64) float64 { return math.Log(x) }},
 110     {"log2(x)", "log2(x)", func(x, y float64) float64 { return math.Log2(x) }},
 111     {"log10(x)", "log10(x)", func(x, y float64) float64 { return math.Log10(x) }},
 112     {"1/2", "1/2", func(x, y float64) float64 { return 1.0 / 2.0 }},
 113     {"e/pi", "e/pi", func(x, y float64) float64 { return math.E / math.Pi }},
 114     {"exp(2)", "exp(2)", func(x, y float64) float64 { return math.Exp(2) }},
 115     {
 116         "Club Beat Pulse",
 117         "sin(10*tau * exp(-20*x)) * exp(-2*x)",
 118         func(x, y float64) float64 {
 119             return math.Sin(10*2*math.Pi*math.Exp(-20*x)) * math.Exp(-2*x)
 120         },
 121     },
 122     {
 123         "Interfering Ripples",
 124         interferingRipples,
 125         func(x, y float64) float64 {
 126             return math.Exp(-0.5*math.Sin(2*math.Hypot(x-2, y+1))) +
 127                 math.Exp(-0.5*math.Sin(10*math.Hypot(x+2, y-3.4)))
 128         },
 129     },
 130     {
 131         "Floor Lights",
 132         "abs(sin(x)) / y**1.4",
 133         func(x, y float64) float64 {
 134             return math.Abs(math.Sin(x)) / math.Pow(y, 1.4)
 135         },
 136     },
 137     {
 138         "Domain Hole",
 139         "log1p(sin(x + y) + (x - y)**2 - 1.5*x + 2.5*y + 1)",
 140         func(x, y float64) float64 {
 141             v := x - y
 142             return math.Log1p(math.Sin(x+y) + v*v - 1.5*x + 2.5*y + 1)
 143         },
 144     },
 145     {
 146         "Beta Gradient",
 147         "lbeta(x + 5.1, y + 5.1)",
 148         func(x, y float64) float64 { return lnBeta(x+5.1, y+5.1) },
 149     },
 150     {
 151         "Hot Bars",
 152         "abs(x) + sqrt(abs(sin(2*y)))",
 153         func(x, y float64) float64 {
 154             return math.Abs(x) + math.Sqrt(math.Abs(math.Sin(2*y)))
 155         },
 156     },
 157     {
 158         "Grid Pattern",
 159         "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(y**2))",
 160         func(x, y float64) float64 {
 161             return math.Sin(math.Sin(x)+math.Cos(y)) +
 162                 math.Cos(math.Sin(x*y)+math.Cos(y*y))
 163         },
 164     },
 165     {
 166         "Smiling Ghost",
 167         smilingGhost,
 168         func(x, y float64) float64 {
 169             xm1 := x - 1
 170             xp1 := x + 1
 171             v := y - math.Sqrt(3)
 172             w := (xm1*xm1 + y*y - 4) * (xp1*xp1 + y*y - 4) * (x*x + v*v - 4)
 173             return math.Log1p(w - 5)
 174         },
 175     },
 176     {
 177         "Forgot its Name",
 178         "1 / (1 + x.abs/y*y)",
 179         func(x, y float64) float64 {
 180             return 1 / (1 + math.Abs(x)/y*y)
 181         },
 182     },
 183 }
 184 
 185 func BenchmarkEmpty(b *testing.B) {
 186     b.Run("empty program", func(b *testing.B) {
 187         var p Program
 188         b.ReportAllocs()
 189         b.ResetTimer()
 190 
 191         for i := 0; i < b.N; i++ {
 192             _ = p.Run()
 193         }
 194     })
 195 
 196     f := func(float64, float64) float64 {
 197         return math.NaN()
 198     }
 199 
 200     b.Run("empty function", func(b *testing.B) {
 201         b.ReportAllocs()
 202         b.ResetTimer()
 203 
 204         for i := 0; i < b.N; i++ {
 205             _ = f(0, 0)
 206         }
 207     })
 208 }
 209 
 210 func BenchmarkBasicProgram(b *testing.B) {
 211     for _, tc := range benchmarks {
 212         var c Compiler
 213         defs := map[string]any{
 214             "x": 0.05,
 215             "y": 0,
 216             "z": 0,
 217         }
 218 
 219         b.Run(tc.Name, func(b *testing.B) {
 220             p, err := c.Compile(tc.Script, defs)
 221             if err != nil {
 222                 const fs = "while compiling %q, got error %q"
 223                 b.Fatalf(fs, tc.Script, err.Error())
 224                 return
 225             }
 226 
 227             x, _ := p.Get("x")
 228             _, _ = p.Get("y")
 229             _, _ = p.Get("z")
 230 
 231             b.ResetTimer()
 232             for i := 0; i < b.N; i++ {
 233                 _ = p.Run()
 234                 *x++
 235             }
 236         })
 237     }
 238 }
 239 
 240 func BenchmarkBasicFunc(b *testing.B) {
 241     for _, tc := range benchmarks {
 242         b.Run(tc.Name, func(b *testing.B) {
 243             x := 0.05
 244             y := 0.0
 245             fn := tc.Native
 246             b.ResetTimer()
 247 
 248             for i := 0; i < b.N; i++ {
 249                 fn(x, y)
 250                 x++
 251                 y++
 252             }
 253         })
 254     }
 255 }
 256 
 257 func BenchmarkSoundProgram(b *testing.B) {
 258     var soundBenchmarkTests = []struct {
 259         Name   string
 260         Script string
 261     }{
 262         {
 263             "Sine Wave",
 264             "sin(1000 * tau * x)",
 265         },
 266         {
 267             "Laser Pulse",
 268             "sin(100 * tau * exp(-40 * u))",
 269         },
 270         {
 271             "Club Beat Pulse",
 272             "sin(10 * tau * exp(-20 * x)) * exp(-2 * x)",
 273         },
 274     }
 275 
 276     for _, tc := range soundBenchmarkTests {
 277         var c Compiler
 278         defs := map[string]any{
 279             "t": 0.05,
 280             "u": 0,
 281         }
 282 
 283         const seconds = 2
 284         const sampleRate = 48_000
 285         dt := 1.0 / float64(sampleRate)
 286         // buf is the destination buffer for all calculated samples
 287         buf := make([]float64, 0, seconds*sampleRate)
 288 
 289         b.Run(tc.Name, func(b *testing.B) {
 290             p, err := c.Compile(tc.Script, defs)
 291             if err != nil {
 292                 const fs = "while compiling %q, got error %q"
 293                 b.Fatalf(fs, tc.Script, err.Error())
 294                 return
 295             }
 296 
 297             // input parameters for the program
 298             t, _ := p.Get("t")
 299             u, _ := p.Get("u")
 300 
 301             b.ResetTimer()
 302             for i := 0; i < b.N; i++ {
 303                 // avoid buffer expansions after the first run
 304                 buf = buf[:0]
 305 
 306                 // benchmark 1 second of generated sound
 307                 for j := 0; j < seconds*sampleRate; j++ {
 308                     // calculate time in seconds from current sample index
 309                     v := float64(j) * dt
 310                     *t = v
 311                     _, *u = math.Modf(v)
 312 
 313                     // calculate a mono sample
 314                     buf = append(buf, p.Run())
 315                 }
 316             }
 317         })
 318     }
 319 }
 320 
 321 func BenchmarkNativeSoundProgram(b *testing.B) {
 322     const tau = 2 * math.Pi
 323 
 324     var soundBenchmarkTests = []struct {
 325         Name string
 326         Fun  func(float64, float64) float64
 327     }{
 328         {
 329             "Sine Wave",
 330             func(t, u float64) float64 {
 331                 return math.Sin(1000 * tau * t)
 332             },
 333         },
 334         {
 335             "Laser Pulse",
 336             func(t, u float64) float64 {
 337                 return math.Sin(100 * tau * math.Exp(-40*u))
 338             },
 339         },
 340         {
 341             "Club Beat Pulse",
 342             func(t, u float64) float64 {
 343                 return math.Sin(10*tau*math.Exp(-20*t)) * math.Exp(-2*t)
 344             },
 345         },
 346     }
 347 
 348     for _, tc := range soundBenchmarkTests {
 349         const seconds = 2
 350         const sampleRate = 48_000
 351         dt := 1.0 / float64(sampleRate)
 352         // buf is the destination buffer for all calculated samples
 353         buf := make([]float64, 0, seconds*sampleRate)
 354 
 355         b.Run(tc.Name, func(b *testing.B) {
 356             // input parameters for the program
 357             t := 0.05
 358             u := 0.00
 359             fn := tc.Fun
 360             b.ResetTimer()
 361 
 362             for i := 0; i < b.N; i++ {
 363                 // avoid buffer expansions after the first run
 364                 buf = buf[:0]
 365 
 366                 // benchmark 1 second of generated sound
 367                 for j := 0; j < seconds*sampleRate; j++ {
 368                     // calculate time in seconds from current sample index
 369                     v := float64(j) * dt
 370                     t = v
 371                     _, u = math.Modf(v)
 372 
 373                     // calculate a mono sample
 374                     buf = append(buf, fn(t, u))
 375                 }
 376             }
 377         })
 378     }
 379 }
 380 
 381 func BenchmarkImageProgram(b *testing.B) {
 382     const (
 383         // use part of a full HD pic to give more runs for each test, giving
 384         // greater statistical-stability/comparability across benchmark runs
 385         w = 1920 / 4
 386         h = 1080 / 4
 387 
 388         intRipples = "" +
 389             "exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + " +
 390             "exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))"
 391         domainHole = "" +
 392             "log1p(sin(x + y) + pow(x - y, 2) - 1.5*x + 2.5*y + 1)"
 393         gridPatPow = "" +
 394             "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(pow(y, 2)))"
 395         gridPatSquare = "" +
 396             "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(square(y)))"
 397         smilingGhostFunc = "" +
 398             "log1p((pow(x - 1, 2) + y*y - 4)*" +
 399             "(pow(x + 1, 2) + y*y - 4)*" +
 400             "(x*x + pow(y - sqrt(3), 2) - 4) - 5)"
 401         smilingGhostSyntax = "" +
 402             "log1p((*(x - 1) + *y - 4)*" +
 403             "(*(x + 1) + *y - 4)*" +
 404             "(*x + *(y - sqrt(3)) - 4) - 5)"
 405     )
 406 
 407     var imageBenchmarkTests = []struct {
 408         Name   string
 409         Width  int
 410         Height int
 411         Script string
 412     }{
 413         {"Horizontal Linear", w, h, "x"},
 414         {"Multiplication", w, h, "x*y"},
 415         {"Star", w, h, "exp(-0.5 * hypot(x, y))"},
 416         {"Interfering Ripples", w, h, intRipples},
 417         {"Floor Lights", w, h, "abs(sin(x)) / pow(y, 1.4)"},
 418         {"Domain Hole", w, h, domainHole},
 419         {"Beta Gradient", w, h, "lbeta(x + 5.1, y + 5.1)"},
 420         {"Hot Bars", w, h, "abs(x) + sqrt(abs(sin(2*y)))"},
 421         {"Grid Pattern (pow)", w, h, gridPatPow},
 422         {"Grid Pattern (square)", w, h, gridPatSquare},
 423         {"Smiling Ghost (pow)", w, h, smilingGhostFunc},
 424         {"Smiling Ghost (square syntax)", w, h, smilingGhostSyntax},
 425     }
 426 
 427     for _, tc := range imageBenchmarkTests {
 428         var c Compiler
 429         defs := map[string]any{
 430             "x": 0,
 431             "y": 0,
 432         }
 433 
 434         // 4k-resolution results buffer
 435         buf := make([]float64, 1920*1080*4)
 436 
 437         b.Run(tc.Name, func(b *testing.B) {
 438             p, err := c.Compile(tc.Script, defs)
 439             if err != nil {
 440                 const fs = "while compiling %q, got error %q"
 441                 b.Fatalf(fs, tc.Script, err.Error())
 442                 return
 443             }
 444 
 445             // input parameters for the program
 446             x, _ := p.Get("x")
 447             y, _ := p.Get("y")
 448 
 449             w := tc.Width
 450             h := tc.Height
 451             dx := 1.0 / float64(w)
 452             dy := 1.0 / float64(h)
 453             xmin := -5.2
 454             ymax := 23.91
 455             b.ResetTimer()
 456             for i := 0; i < b.N; i++ {
 457                 // avoid buffer expansions after the first run
 458                 buf = buf[:0]
 459 
 460                 for j := 0; j < h; j++ {
 461                     *y = ymax - float64(j)*dy
 462                     for i := 0; i < w; i++ {
 463                         *x = float64(i)*dx + xmin
 464                         buf = append(buf, p.Run())
 465                     }
 466                 }
 467             }
 468         })
 469     }
 470 }
 471 
 472 func BenchmarkCompiler(b *testing.B) {
 473     f := func(name string, src string) {
 474         b.Run(name, func(b *testing.B) {
 475             for i := 0; i < b.N; i++ {
 476                 var c Compiler
 477                 _, err := c.Compile(src, map[string]any{
 478                     "x": 0.05,
 479                     "y": 0,
 480                     "z": 0,
 481                 })
 482 
 483                 if err != nil {
 484                     const fs = "while compiling %q, got error %q"
 485                     b.Fatalf(fs, src, err.Error())
 486                 }
 487             }
 488         })
 489     }
 490 
 491     for _, tc := range mathCorrectnessTests {
 492         f(tc.Name, tc.Script)
 493     }
 494 
 495     for _, tc := range benchmarks {
 496         f(tc.Name, tc.Script)
 497     }
 498 }
     File: ./fmscripts/compilers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "fmt"
  29     "math"
  30     "strconv"
  31 )
  32 
  33 // unary2op turns unary operators into their corresponding basic operations;
  34 // some entries are only for the optimizer, and aren't accessible directly
  35 // from valid source code
  36 var unary2op = map[string]opcode{
  37     "-": neg,
  38     "!": not,
  39     "&": abs,
  40     "*": square,
  41     "^": square,
  42     "/": rec,
  43     "%": mod1,
  44 }
  45 
  46 // binary2op turns binary operators into their corresponding basic operations
  47 var binary2op = map[string]opcode{
  48     "+":  add,
  49     "-":  sub,
  50     "*":  mul,
  51     "/":  div,
  52     "%":  mod,
  53     "&&": and,
  54     "&":  and,
  55     "||": or,
  56     "|":  or,
  57     "==": equal,
  58     "!=": notequal,
  59     "<>": notequal,
  60     "<":  less,
  61     "<=": lessoreq,
  62     ">":  more,
  63     ">=": moreoreq,
  64     "**": pow,
  65     "^":  pow,
  66 }
  67 
  68 // Compiler lets you create Program objects, which you can then run. The whole
  69 // point of this is to create quicker-to-run numeric scripts. You can even add
  70 // variables with initial values, as well as functions for the script to use.
  71 //
  72 // Common math funcs and constants are automatically detected, and constant
  73 // results are optimized away, unless builtins are redefined. In other words,
  74 // the optimizer is effectively disabled for all (sub)expressions containing
  75 // redefined built-ins, as there's no way to be sure those values won't change
  76 // from one run to the next.
  77 //
  78 // See the comment for type Program for more details.
  79 //
  80 // # Example
  81 //
  82 // var c fmscripts.Compiler
  83 //
  84 //  defs := map[string]any{
  85 //      "x": 0,    // define `x`, and initialize it to 0
  86 //      "k": 4.25, // define `k`, and initialize it to 4.25
  87 //      "b": true, // define `b`, and initialize it to 1.0
  88 //      "n": -23,  // define `n`, and initialize it to -23.0
  89 //      "pi": 3,   // define `pi`, overriding the default constant named `pi`
  90 //
  91 //      "f": numericKernel // type is func ([]float64) float64
  92 //      "g": otherFunc     // type is func (float64) float64
  93 //  }
  94 //
  95 // prog, err := c.Compile("log10(k) + f(sqrt(k) * exp(-x), 45, -0.23)", defs)
  96 // // ...
  97 //
  98 // x, _ := prog.Get("x") // Get returns (*float64, bool)
  99 // y, _ := prog.Get("y") // a useless pointer, since program doesn't use `y`
 100 // // ...
 101 //
 102 //  for i := 0; i < n; i++ {
 103 //      *x = float64(i)*dx + minx // you update inputs in place using pointers
 104 //      f := prog.Run()           // method Run gives you a float64 back
 105 //      // ...
 106 //  }
 107 type Compiler struct {
 108     maxStack int     // the exact stack size the resulting program needs
 109     ops      []numOp // the program's operations
 110 
 111     vaddr map[string]int // address lookup for values
 112     faddr map[string]int // address lookup for functions
 113     ftype map[string]any // keeps track of func types during compilation
 114 
 115     values []float64 // variables and constants available to programs
 116     funcs  []any     // funcs available to programs
 117 }
 118 
 119 // Compile parses the script given and generates a fast float64-only Program
 120 // made only of sequential steps: any custom funcs you provide it can use
 121 // their own internal looping and/or conditional logic, of course.
 122 func (c *Compiler) Compile(src string, defs map[string]any) (Program, error) {
 123     // turn source code into an abstract syntax-tree
 124     root, err := parse(src)
 125     if err != nil {
 126         return Program{}, err
 127     }
 128 
 129     // generate operations
 130     if err := c.reset(defs); err != nil {
 131         return Program{}, err
 132     }
 133     if err = c.compile(root, 0); err != nil {
 134         return Program{}, err
 135     }
 136 
 137     // create the resulting program
 138     var p Program
 139     p.stack = make([]float64, c.maxStack)
 140     p.values = make([]float64, len(c.values))
 141     copy(p.values, c.values)
 142     p.ops = make([]numOp, len(c.ops))
 143     copy(p.ops, c.ops)
 144     p.funcs = make([]any, len(c.ftype))
 145     copy(p.funcs, c.funcs)
 146     p.names = make(map[string]int, len(c.vaddr))
 147 
 148     // give the program's Get method access only to all allocated variables
 149     for k, v := range c.vaddr {
 150         // avoid exposing numeric constants on the program's name-lookup
 151         // table, which would allow users to change literals across runs
 152         if _, err := strconv.ParseFloat(k, 64); err == nil {
 153             continue
 154         }
 155         // expose only actual variable names
 156         p.names[k] = v
 157     }
 158 
 159     return p, nil
 160 }
 161 
 162 // reset prepares a compiler by satisfying internal preconditions func
 163 // compileExpr relies on, when given an abstract syntax-tree
 164 func (c *Compiler) reset(defs map[string]any) error {
 165     // reset the compiler's internal state
 166     c.maxStack = 0
 167     c.ops = c.ops[:0]
 168     c.vaddr = make(map[string]int)
 169     c.faddr = make(map[string]int)
 170     c.ftype = make(map[string]any)
 171     c.values = c.values[:0]
 172     c.funcs = c.funcs[:0]
 173 
 174     // allocate vars and funcs
 175     for k, v := range defs {
 176         if err := c.allocEntry(k, v); err != nil {
 177             return err
 178         }
 179     }
 180     return nil
 181 }
 182 
 183 // allocEntry simplifies the control-flow of func reset
 184 func (c *Compiler) allocEntry(k string, v any) error {
 185     const (
 186         maxExactRound = 2 << 52
 187         rangeErrFmt   = "%d is outside range of exact float64 integers"
 188         typeErrFmt    = "got value of unsupported type %T"
 189     )
 190 
 191     switch v := v.(type) {
 192     case float64:
 193         _, err := c.allocValue(k, v)
 194         return err
 195 
 196     case int:
 197         if math.Abs(float64(v)) > maxExactRound {
 198             return fmt.Errorf(rangeErrFmt, v)
 199         }
 200         _, err := c.allocValue(k, float64(v))
 201         return err
 202 
 203     case bool:
 204         _, err := c.allocValue(k, debool(v))
 205         return err
 206 
 207     default:
 208         if isSupportedFunc(v) {
 209             c.ftype[k] = v
 210             _, err := c.allocFunc(k, v)
 211             return err
 212         }
 213         return fmt.Errorf(typeErrFmt, v)
 214     }
 215 }
 216 
 217 // allocValue ensures there's a place to match the name given, returning its
 218 // index when successful; only programs using unreasonably many values will
 219 // cause this func to fail
 220 func (c *Compiler) allocValue(s string, f float64) (int, error) {
 221     if len(c.values) >= maxOpIndex {
 222         const fs = "programs can only use up to %d distinct float64 values"
 223         return -1, fmt.Errorf(fs, maxOpIndex+1)
 224     }
 225 
 226     i := len(c.values)
 227     c.vaddr[s] = i
 228     c.values = append(c.values, f)
 229     return i, nil
 230 }
 231 
 232 // valueIndex returns the index reserved for an allocated value/variable:
 233 // all unallocated values/variables are allocated here on first access
 234 func (c *Compiler) valueIndex(s string, f float64) (int, error) {
 235     // name is found: return the index of the already-allocated var
 236     if i, ok := c.vaddr[s]; ok {
 237         return i, nil
 238     }
 239 
 240     // name not found, but it's a known math constant
 241     if f, ok := mathConst[s]; ok {
 242         return c.allocValue(s, f)
 243     }
 244     // name not found, and it's not of a known math constant
 245     return c.allocValue(s, f)
 246 }
 247 
 248 // constIndex allocates a constant as a variable named as its own string
 249 // representation, which avoids multiple entries for repeated uses of the
 250 // same constant value
 251 func (c *Compiler) constIndex(f float64) (int, error) {
 252     // constants have no name, so use a canonical string representation
 253     return c.valueIndex(strconv.FormatFloat(f, 'f', 16, 64), f)
 254 }
 255 
 256 // funcIndex returns the index reserved for an allocated function: all
 257 // unallocated functions are allocated here on first access
 258 func (c *Compiler) funcIndex(name string) (int, error) {
 259     // check if func was already allocated
 260     if i, ok := c.faddr[name]; ok {
 261         return i, nil
 262     }
 263 
 264     // if name is reserved allocate an index for its matching func
 265     if fn, ok := c.ftype[name]; ok {
 266         return c.allocFunc(name, fn)
 267     }
 268 
 269     // if name wasn't reserved, see if it's a standard math func name
 270     if fn := c.autoFuncLookup(name); fn != nil {
 271         return c.allocFunc(name, fn)
 272     }
 273 
 274     // name isn't even a standard math func's
 275     return -1, fmt.Errorf("function not found")
 276 }
 277 
 278 // allocFunc ensures there's a place for the function name given, returning its
 279 // index when successful; only funcs of unsupported types will cause failure
 280 func (c *Compiler) allocFunc(name string, fn any) (int, error) {
 281     if isSupportedFunc(fn) {
 282         i := len(c.funcs)
 283         c.faddr[name] = i
 284         c.ftype[name] = fn
 285         c.funcs = append(c.funcs, fn)
 286         return i, nil
 287     }
 288     return -1, fmt.Errorf("can't use a %T as a number-crunching function", fn)
 289 }
 290 
 291 // autoFuncLookup checks built-in deterministic funcs for the name given: its
 292 // result is nil only if there's no match
 293 func (c *Compiler) autoFuncLookup(name string) any {
 294     if fn, ok := determFuncs[name]; ok {
 295         return fn
 296     }
 297     return nil
 298 }
 299 
 300 // genOp generates/adds a basic operation to the program, while keeping track
 301 // of the maximum depth the stack can reach
 302 func (c *Compiler) genOp(op numOp, depth int) {
 303     // add 2 defensively to ensure stack space for the inputs of binary ops
 304     n := depth + 2
 305     if c.maxStack < n {
 306         c.maxStack = n
 307     }
 308     c.ops = append(c.ops, op)
 309 }
 310 
 311 // compile is a recursive expression evaluator which does the actual compiling
 312 // as it goes along
 313 func (c *Compiler) compile(expr any, depth int) error {
 314     expr = c.optimize(expr)
 315 
 316     switch expr := expr.(type) {
 317     case float64:
 318         return c.compileLiteral(expr, depth)
 319     case string:
 320         return c.compileVariable(expr, depth)
 321     case []any:
 322         return c.compileCombo(expr, depth)
 323     case unaryExpr:
 324         return c.compileUnary(expr.op, expr.x, depth)
 325     case binaryExpr:
 326         return c.compileBinary(expr.op, expr.x, expr.y, depth)
 327     case callExpr:
 328         return c.compileCall(expr, depth)
 329     case assignExpr:
 330         return c.compileAssign(expr, depth)
 331     default:
 332         return fmt.Errorf("unsupported expression type %T", expr)
 333     }
 334 }
 335 
 336 // compileLiteral generates a load operation for the constant value given
 337 func (c *Compiler) compileLiteral(f float64, depth int) error {
 338     i, err := c.constIndex(f)
 339     if err != nil {
 340         return err
 341     }
 342     c.genOp(numOp{What: load, Index: opindex(i)}, depth)
 343     return nil
 344 }
 345 
 346 // compileVariable generates a load operation for the variable name given
 347 func (c *Compiler) compileVariable(name string, depth int) error {
 348     // handle names which aren't defined, but are known math constants
 349     if _, ok := c.vaddr[name]; !ok {
 350         if f, ok := mathConst[name]; ok {
 351             return c.compileLiteral(f, depth)
 352         }
 353     }
 354 
 355     // handle actual variables
 356     i, err := c.valueIndex(name, 0)
 357     if err != nil {
 358         return err
 359     }
 360     c.genOp(numOp{What: load, Index: opindex(i)}, depth)
 361     return nil
 362 }
 363 
 364 // compileCombo handles a sequence of expressions
 365 func (c *Compiler) compileCombo(exprs []any, depth int) error {
 366     for _, v := range exprs {
 367         err := c.compile(v, depth)
 368         if err != nil {
 369             return err
 370         }
 371     }
 372     return nil
 373 }
 374 
 375 // compileUnary handles unary expressions
 376 func (c *Compiler) compileUnary(op string, x any, depth int) error {
 377     err := c.compile(x, depth+1)
 378     if err != nil {
 379         return err
 380     }
 381 
 382     if op, ok := unary2op[op]; ok {
 383         c.genOp(numOp{What: op}, depth)
 384         return nil
 385     }
 386     return fmt.Errorf("unary operation %q is unsupported", op)
 387 }
 388 
 389 // compileBinary handles binary expressions
 390 func (c *Compiler) compileBinary(op string, x, y any, depth int) error {
 391     switch op {
 392     case "===", "!==":
 393         // handle binary expressions with no matching basic operation, by
 394         // treating them as aliases for function calls
 395         return c.compileCall(callExpr{name: op, args: []any{x, y}}, depth)
 396     }
 397 
 398     err := c.compile(x, depth+1)
 399     if err != nil {
 400         return err
 401     }
 402     err = c.compile(y, depth+2)
 403     if err != nil {
 404         return err
 405     }
 406 
 407     if op, ok := binary2op[op]; ok {
 408         c.genOp(numOp{What: op}, depth)
 409         return nil
 410     }
 411     return fmt.Errorf("binary operation %q is unsupported", op)
 412 }
 413 
 414 // compileCall handles function-call expressions
 415 func (c *Compiler) compileCall(expr callExpr, depth int) error {
 416     // lookup function name
 417     index, err := c.funcIndex(expr.name)
 418     if err != nil {
 419         return fmt.Errorf("%s: %s", expr.name, err.Error())
 420     }
 421     // get the func value, as its type determines the calling op to emit
 422     v, ok := c.ftype[expr.name]
 423     if !ok {
 424         return fmt.Errorf("%s: function is undefined", expr.name)
 425     }
 426 
 427     // figure which type of function operation to use
 428     op, ok := func2op(v)
 429     if !ok {
 430         return fmt.Errorf("%s: unsupported function type %T", expr.name, v)
 431     }
 432 
 433     // ensure number of args given to the func makes sense for the func type
 434     err = checkArgCount(func2info[op], expr.name, len(expr.args))
 435     if err != nil {
 436         return err
 437     }
 438 
 439     // generate operations to evaluate all args
 440     for i, v := range expr.args {
 441         err := c.compile(v, depth+i+1)
 442         if err != nil {
 443             return err
 444         }
 445     }
 446 
 447     // generate func-call operation
 448     given := len(expr.args)
 449     next := numOp{What: op, NumArgs: opargs(given), Index: opindex(index)}
 450     c.genOp(next, depth)
 451     return nil
 452 }
 453 
 454 // checkArgCount does what it says, returning informative errors when arg
 455 // counts are wrong
 456 func checkArgCount(info funcTypeInfo, name string, nargs int) error {
 457     if info.AtLeast < 0 && info.AtMost < 0 {
 458         return nil
 459     }
 460 
 461     if info.AtLeast == info.AtMost && nargs != info.AtMost {
 462         const fs = "%s: expected %d args, got %d instead"
 463         return fmt.Errorf(fs, name, info.AtMost, nargs)
 464     }
 465 
 466     if info.AtLeast >= 0 && info.AtMost >= 0 {
 467         const fs = "%s: expected between %d and %d args, got %d instead"
 468         if nargs < info.AtLeast || nargs > info.AtMost {
 469             return fmt.Errorf(fs, name, info.AtLeast, info.AtMost, nargs)
 470         }
 471     }
 472 
 473     if info.AtLeast >= 0 && nargs < info.AtLeast {
 474         const fs = "%s: expected at least %d args, got %d instead"
 475         return fmt.Errorf(fs, name, info.AtLeast, nargs)
 476     }
 477 
 478     if info.AtMost >= 0 && nargs > info.AtMost {
 479         const fs = "%s: expected at most %d args, got %d instead"
 480         return fmt.Errorf(fs, name, info.AtMost, nargs)
 481     }
 482 
 483     // all is good
 484     return nil
 485 }
 486 
 487 // compileAssign generates a store operation for the expression given
 488 func (c *Compiler) compileAssign(expr assignExpr, depth int) error {
 489     err := c.compile(expr.expr, depth)
 490     if err != nil {
 491         return err
 492     }
 493 
 494     i, err := c.allocValue(expr.name, 0)
 495     if err != nil {
 496         return err
 497     }
 498 
 499     c.genOp(numOp{What: store, Index: opindex(i)}, depth)
 500     return nil
 501 }
 502 
 503 // func sameFunc(x, y any) bool {
 504 //  if x == nil || y == nil {
 505 //      return false
 506 //  }
 507 //  return reflect.ValueOf(x).Pointer() == reflect.ValueOf(y).Pointer()
 508 // }
 509 
 510 // isSupportedFunc checks if a value's type is a supported func type
 511 func isSupportedFunc(fn any) bool {
 512     _, ok := func2op(fn)
 513     return ok
 514 }
 515 
 516 // funcTypeInfo is an entry in the func2info lookup table
 517 type funcTypeInfo struct {
 518     // AtLeast is the minimum number of inputs the func requires: negative
 519     // values are meant to be ignored
 520     AtLeast int
 521 
 522     // AtMost is the maximum number of inputs the func requires: negative
 523     // values are meant to be ignored
 524     AtMost int
 525 }
 526 
 527 // func2info is a lookup table to check the number of inputs funcs are given
 528 var func2info = map[opcode]funcTypeInfo{
 529     call0:  {AtLeast: +0, AtMost: +0},
 530     call1:  {AtLeast: +1, AtMost: +1},
 531     call2:  {AtLeast: +2, AtMost: +2},
 532     call3:  {AtLeast: +3, AtMost: +3},
 533     call4:  {AtLeast: +4, AtMost: +4},
 534     call5:  {AtLeast: +5, AtMost: +5},
 535     callv:  {AtLeast: -1, AtMost: -1},
 536     call1v: {AtLeast: +1, AtMost: -1},
 537 }
 538 
 539 // func2op tries to match a func type into a corresponding opcode
 540 func func2op(v any) (op opcode, ok bool) {
 541     switch v.(type) {
 542     case func() float64:
 543         return call0, true
 544     case func(float64) float64:
 545         return call1, true
 546     case func(float64, float64) float64:
 547         return call2, true
 548     case func(float64, float64, float64) float64:
 549         return call3, true
 550     case func(float64, float64, float64, float64) float64:
 551         return call4, true
 552     case func(float64, float64, float64, float64, float64) float64:
 553         return call5, true
 554     case func(...float64) float64:
 555         return callv, true
 556     case func(float64, ...float64) float64:
 557         return call1v, true
 558     default:
 559         return 0, false
 560     }
 561 }
     File: ./fmscripts/compilers_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "testing"
  29 )
  30 
  31 func TestBuiltinFuncs(t *testing.T) {
  32     list := []map[string]any{
  33         determFuncs,
  34     }
  35 
  36     for _, kv := range list {
  37         for k, v := range kv {
  38             t.Run(k, func(t *testing.T) {
  39                 if !isSupportedFunc(v) {
  40                     t.Fatalf("%s: invalid function type %T", k, v)
  41                 }
  42             })
  43         }
  44     }
  45 }
  46 
  47 var mathCorrectnessTests = []struct {
  48     Name     string
  49     Script   string
  50     Expected float64
  51     Error    string
  52 }{
  53     {
  54         Name:     "value",
  55         Script:   "-45.2",
  56         Expected: -45.2,
  57         Error:    "",
  58     },
  59     {
  60         Name:     "simple",
  61         Script:   "1 + 3",
  62         Expected: 4,
  63         Error:    "",
  64     },
  65     {
  66         Name:     "fancier",
  67         Script:   "1*8 + 3*2**3",
  68         Expected: 32,
  69         Error:    "",
  70     },
  71     {
  72         Name:     "function call",
  73         Script:   "log2(1024)",
  74         Expected: 10,
  75         Error:    "",
  76     },
  77     {
  78         Name:     "function call (2 args)",
  79         Script:   "pow(3, 3)",
  80         Expected: 27,
  81         Error:    "",
  82     },
  83     {
  84         Name:     "function call (3 args)",
  85         Script:   "fma(3.5, 56, -1.52)",
  86         Expected: 194.48,
  87         Error:    "",
  88     },
  89     {
  90         Name:     "vararg-function call",
  91         Script:   "min(log10(10_000), log10(1_000_000), log2(4_096), 100 - 130)",
  92         Expected: -30,
  93         Error:    "",
  94     },
  95     {
  96         Name:     "square shortcut",
  97         Script:   "*-3",
  98         Expected: 9,
  99         Error:    "",
 100     },
 101     {
 102         Name:     "abs shortcut",
 103         Script:   "&-3",
 104         Expected: 3,
 105         Error:    "",
 106     },
 107     {
 108         Name:     "negative constant",
 109         Script:   "(-3)",
 110         Expected: -3,
 111         Error:    "",
 112     },
 113     {
 114         Name:     "power syntax",
 115         Script:   "2**4",
 116         Expected: 16,
 117         Error:    "",
 118     },
 119     {
 120         Name:     "power syntax order",
 121         Script:   "3*2**4",
 122         Expected: 48,
 123         Error:    "",
 124     },
 125     {
 126         Script:   "2*3**4",
 127         Expected: 162,
 128         Error:    "",
 129     },
 130     {
 131         Script:   "2**3*4",
 132         Expected: 32,
 133         Error:    "",
 134     },
 135     {
 136         Script:   "(2*3)**4",
 137         Expected: 1_296,
 138         Error:    "",
 139     },
 140     {
 141         Script:   "*2*2",
 142         Expected: 8,
 143         Error:    "",
 144     },
 145     {
 146         Script:   "2*2*2",
 147         Expected: 8,
 148         Error:    "",
 149     },
 150     {
 151         Script:   "3 == 3 ? 10 : -1",
 152         Expected: 10,
 153         Error:    "",
 154     },
 155     {
 156         Script:   "3 == 4 ? 10 : -1",
 157         Expected: -1,
 158         Error:    "",
 159     },
 160     {
 161         Script:   "log10(-1) ?? 4",
 162         Expected: 4,
 163         Error:    "",
 164     },
 165     {
 166         Script:   "log10(10) ?? 4",
 167         Expected: 1,
 168         Error:    "",
 169     },
 170     {
 171         Script:   "abc = 123; abc",
 172         Expected: 123,
 173         Error:    "",
 174     },
 175     {
 176         Name:     "calling func(float64, ...float64) float64",
 177         Script:   "horner(2.5, 1, 2, 3)",
 178         Expected: 14.25,
 179         Error:    "",
 180     },
 181 }
 182 
 183 func TestCompiler(t *testing.T) {
 184     for _, tc := range mathCorrectnessTests {
 185         name := tc.Name
 186         if len(name) == 0 {
 187             name = tc.Script
 188         }
 189 
 190         t.Run(name, func(t *testing.T) {
 191             var c Compiler
 192             p, err := c.Compile(tc.Script, map[string]any{"x": 1})
 193             if err != nil {
 194                 t.Fatalf("got compiler error %q", err.Error())
 195             }
 196 
 197             f := p.Run()
 198             if (err != nil || tc.Error != "") && err.Error() != tc.Error {
 199                 const fs = "expected error %q, got error %q instead"
 200                 t.Fatalf(fs, err.Error(), tc.Error)
 201             }
 202 
 203             if f != tc.Expected {
 204                 const fs = "expected result to be %f, got %f instead"
 205                 t.Fatalf(fs, tc.Expected, f)
 206             }
 207         })
 208     }
 209 }
     File: ./fmscripts/doc.go
   1 /*
   2 # Floating-point Math Scripts
   3 
   4 This self-contained package gives you a compiler/interpreter combo to run
   5 number-crunching scripts. These are limited to float64 values, but they run
   6 surprisingly quickly: various simple benchmarks suggest it's between 1/2 and
   7 1/4 the speed of native funcs.
   8 
   9 There are several built-in numeric functions, and the compiler makes it easy
  10 to add your custom Go functions to further speed-up any operations specific to
  11 your app/domain.
  12 
  13 Finally, notice how float64 data don't really limit you as much as you might
  14 think at first, since they can act as booleans (treating 0 as false, and non-0
  15 as true), or as exact integers in the extremely-wide range [-2**53, +2**53].
  16 */
  17 
  18 package fmscripts
     File: ./fmscripts/math.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 const (
  32     // the maximum integer a float64 can represent exactly; -maxflint is the
  33     // minimum such integer, since floating-point values allow such symmetries
  34     maxflint = 2 << 52
  35 
  36     // base-2 versions of size multipliers
  37     kilobyte = 1024 * 1.0
  38     megabyte = 1024 * kilobyte
  39     gigabyte = 1024 * megabyte
  40     terabyte = 1024 * gigabyte
  41     petabyte = 1024 * terabyte
  42 
  43     // unit-conversion multipliers
  44     mi2kmMult   = 1 / 0.6213712
  45     nm2kmMult   = 1.852
  46     nmi2kmMult  = 1.852
  47     yd2mtMult   = 1 / 1.093613
  48     ft2mtMult   = 1 / 3.28084
  49     in2cmMult   = 2.54
  50     lb2kgMult   = 0.4535924
  51     ga2ltMult   = 1 / 0.2199692
  52     gal2lMult   = 1 / 0.2199692
  53     oz2mlMult   = 29.5735295625
  54     cup2lMult   = 0.2365882365
  55     mpg2kplMult = 0.2199692 / 0.6213712
  56     ton2kgMult  = 1 / 907.18474
  57     psi2paMult  = 1 / 0.00014503773800722
  58     deg2radMult = math.Pi / 180
  59     rad2degMult = 180 / math.Pi
  60 )
  61 
  62 // default math constants
  63 var mathConst = map[string]float64{
  64     "e":   math.E,
  65     "pi":  math.Pi,
  66     "tau": 2 * math.Pi,
  67     "phi": math.Phi,
  68     "nan": math.NaN(),
  69     "inf": math.Inf(+1),
  70 
  71     "minint":     -float64(maxflint - 1),
  72     "maxint":     +float64(maxflint - 1),
  73     "minsafeint": -float64(maxflint - 1),
  74     "maxsafeint": +float64(maxflint - 1),
  75 
  76     "false": 0.0,
  77     "true":  1.0,
  78     "f":     0.0,
  79     "t":     1.0,
  80 
  81     // conveniently-named multipliers
  82     "femto": 1e-15,
  83     "pico":  1e-12,
  84     "nano":  1e-09,
  85     "micro": 1e-06,
  86     "milli": 1e-03,
  87     "kilo":  1e+03,
  88     "mega":  1e+06,
  89     "giga":  1e+09,
  90     "tera":  1e+12,
  91     "peta":  1e+15,
  92 
  93     // unit-conversion multipliers
  94     "mi2km":   mi2kmMult,
  95     "nm2km":   nm2kmMult,
  96     "nmi2km":  nmi2kmMult,
  97     "yd2mt":   yd2mtMult,
  98     "ft2mt":   ft2mtMult,
  99     "in2cm":   in2cmMult,
 100     "lb2kg":   lb2kgMult,
 101     "ga2lt":   ga2ltMult,
 102     "gal2l":   gal2lMult,
 103     "oz2ml":   oz2mlMult,
 104     "cup2l":   cup2lMult,
 105     "mpg2kpl": mpg2kplMult,
 106     "ton2kg":  ton2kgMult,
 107     "psi2pa":  psi2paMult,
 108     "deg2rad": deg2radMult,
 109     "rad2deg": rad2degMult,
 110 
 111     // base-2 versions of size multipliers
 112     "kb":      kilobyte,
 113     "mb":      megabyte,
 114     "gb":      gigabyte,
 115     "tb":      terabyte,
 116     "pb":      petabyte,
 117     "binkilo": kilobyte,
 118     "binmega": megabyte,
 119     "bingiga": gigabyte,
 120     "bintera": terabyte,
 121     "binpeta": petabyte,
 122 
 123     // physical constants
 124     "c":   299_792_458,       // speed of light in m/s
 125     "g":   6.67430e-11,       // gravitational constant in N m2/kg2
 126     "h":   6.62607015e-34,    // planck constant in J s
 127     "ec":  1.602176634e-19,   // elementary charge in C
 128     "e0":  8.8541878128e-12,  // vacuum permittivity in C2/(Nm2)
 129     "mu0": 1.25663706212e-6,  // vacuum permeability in T m/A
 130     "k":   1.380649e-23,      // boltzmann constant in J/K
 131     "mu":  1.66053906660e-27, // atomic mass constant in kg
 132     "me":  9.1093837015e-31,  // electron mass in kg
 133     "mp":  1.67262192369e-27, // proton mass in kg
 134     "mn":  1.67492749804e-27, // neutron mass in kg
 135 
 136     // float64s can only vaguely approx. avogadro's mole (6.02214076e23)
 137 }
 138 
 139 // deterministic math functions lookup-table generated using the command
 140 //
 141 // go doc math | awk '/func/ { gsub(/func |\(.*/, ""); printf("\"%s\": math.%s,\n", tolower($0), $0) }'
 142 //
 143 // then hand-edited to remove funcs, or to use adapter funcs when needed: removed
 144 // funcs either had multiple returns (like SinCos) or dealt with float32 values
 145 var determFuncs = map[string]any{
 146     "abs":         math.Abs,
 147     "acos":        math.Acos,
 148     "acosh":       math.Acosh,
 149     "asin":        math.Asin,
 150     "asinh":       math.Asinh,
 151     "atan":        math.Atan,
 152     "atan2":       math.Atan2,
 153     "atanh":       math.Atanh,
 154     "cbrt":        math.Cbrt,
 155     "ceil":        math.Ceil,
 156     "copysign":    math.Copysign,
 157     "cos":         math.Cos,
 158     "cosh":        math.Cosh,
 159     "dim":         math.Dim,
 160     "erf":         math.Erf,
 161     "erfc":        math.Erfc,
 162     "erfcinv":     math.Erfcinv,
 163     "erfinv":      math.Erfinv,
 164     "exp":         math.Exp,
 165     "exp2":        math.Exp2,
 166     "expm1":       math.Expm1,
 167     "fma":         math.FMA,
 168     "floor":       math.Floor,
 169     "gamma":       math.Gamma,
 170     "inf":         inf,
 171     "isinf":       isInf,
 172     "isnan":       isNaN,
 173     "j0":          math.J0,
 174     "j1":          math.J1,
 175     "jn":          jn,
 176     "ldexp":       ldexp,
 177     "lgamma":      lgamma,
 178     "log10":       math.Log10,
 179     "log1p":       math.Log1p,
 180     "log2":        math.Log2,
 181     "logb":        math.Logb,
 182     "mod":         math.Mod,
 183     "nan":         math.NaN,
 184     "nextafter":   math.Nextafter,
 185     "pow":         math.Pow,
 186     "pow10":       pow10,
 187     "remainder":   math.Remainder,
 188     "round":       math.Round,
 189     "roundtoeven": math.RoundToEven,
 190     "signbit":     signbit,
 191     "sin":         math.Sin,
 192     "sinh":        math.Sinh,
 193     "sqrt":        math.Sqrt,
 194     "tan":         math.Tan,
 195     "tanh":        math.Tanh,
 196     "trunc":       math.Trunc,
 197     "y0":          math.Y0,
 198     "y1":          math.Y1,
 199     "yn":          yn,
 200 
 201     // a few aliases for the standard math funcs: some of the single-letter
 202     // aliases are named after the ones in `bc`, the basic calculator tool
 203     "a":          math.Abs,
 204     "c":          math.Cos,
 205     "ceiling":    math.Ceil,
 206     "cosine":     math.Cos,
 207     "e":          math.Exp,
 208     "isinf0":     isAnyInf,
 209     "isinfinite": isAnyInf,
 210     "l":          math.Log,
 211     "ln":         math.Log,
 212     "lg":         math.Log2,
 213     "modulus":    math.Mod,
 214     "power":      math.Pow,
 215     "rem":        math.Remainder,
 216     "s":          math.Sin,
 217     "sine":       math.Sin,
 218     "t":          math.Tan,
 219     "tangent":    math.Tan,
 220     "truncate":   math.Trunc,
 221     "truncated":  math.Trunc,
 222 
 223     // not from standard math: these custom funcs were added manually
 224     "beta":       beta,
 225     "bool":       num2bool,
 226     "clamp":      clamp,
 227     "cond":       cond, // vector-arg if-else chain
 228     "cube":       cube,
 229     "cubed":      cube,
 230     "degrees":    degrees,
 231     "deinf":      deInf,
 232     "denan":      deNaN,
 233     "factorial":  factorial,
 234     "fract":      fract,
 235     "horner":     polyval,
 236     "hypot":      hypot,
 237     "if":         ifElse,
 238     "ifelse":     ifElse,
 239     "inv":        reciprocal,
 240     "isanyinf":   isAnyInf,
 241     "isbad":      isBad,
 242     "isfin":      isFinite,
 243     "isfinite":   isFinite,
 244     "isgood":     isGood,
 245     "isinteger":  isInteger,
 246     "lbeta":      lnBeta,
 247     "len":        length,
 248     "length":     length,
 249     "lnbeta":     lnBeta,
 250     "log":        math.Log,
 251     "logistic":   logistic,
 252     "mag":        length,
 253     "max":        max,
 254     "min":        min,
 255     "neg":        negate,
 256     "negate":     negate,
 257     "not":        notBool,
 258     "polyval":    polyval,
 259     "radians":    radians,
 260     "range":      rangef, // vector-arg max - min
 261     "reciprocal": reciprocal,
 262     "rev":        revalue,
 263     "revalue":    revalue,
 264     "scale":      scale,
 265     "sgn":        sign,
 266     "sign":       sign,
 267     "sinc":       sinc,
 268     "sq":         sqr,
 269     "sqmin":      solveQuadMin,
 270     "sqmax":      solveQuadMax,
 271     "square":     sqr,
 272     "squared":    sqr,
 273     "unwrap":     unwrap,
 274     "wrap":       wrap,
 275 
 276     // a few aliases for the custom funcs
 277     "deg":   degrees,
 278     "isint": isInteger,
 279     "rad":   radians,
 280 
 281     // a few quicker 2-value versions of vararg funcs: the optimizer depends
 282     // on these to rewrite 2-input uses of their vararg counterparts
 283     "hypot2": math.Hypot,
 284     "max2":   math.Max,
 285     "min2":   math.Min,
 286 
 287     // a few entries to enable custom syntax: the parser depends on these to
 288     // rewrite binary expressions into func calls
 289     "??":  deNaN,
 290     "?:":  ifElse,
 291     "===": same,
 292     "!==": notSame,
 293 }
 294 
 295 // DefineDetFuncs adds more deterministic funcs to the default set. Such funcs
 296 // are considered optimizable, since calling them with the same constant inputs
 297 // is supposed to return the same constant outputs, as the name `deterministic`
 298 // suggests.
 299 //
 300 // Only call this before compiling any scripts, and ensure all funcs given are
 301 // supported and are deterministic. Random-output funcs certainly won't fit
 302 // the bill here.
 303 func DefineDetFuncs(funcs map[string]any) {
 304     for k, v := range funcs {
 305         determFuncs[k] = v
 306     }
 307 }
 308 
 309 func sqr(x float64) float64 {
 310     return x * x
 311 }
 312 
 313 func cube(x float64) float64 {
 314     return x * x * x
 315 }
 316 
 317 func num2bool(x float64) float64 {
 318     if x == 0 {
 319         return 0
 320     }
 321     return 1
 322 }
 323 
 324 func logistic(x float64) float64 {
 325     return 1 / (1 + math.Exp(-x))
 326 }
 327 
 328 func sign(x float64) float64 {
 329     if math.IsNaN(x) {
 330         return x
 331     }
 332     if x > 0 {
 333         return +1
 334     }
 335     if x < 0 {
 336         return -1
 337     }
 338     return 0
 339 }
 340 
 341 func sinc(x float64) float64 {
 342     if x == 0 {
 343         return 1
 344     }
 345     return math.Sin(x) / x
 346 }
 347 
 348 func isInteger(x float64) float64 {
 349     _, frac := math.Modf(x)
 350     if frac == 0 {
 351         return 1
 352     }
 353     return 0
 354 }
 355 
 356 func inf(sign float64) float64 {
 357     return math.Inf(int(sign))
 358 }
 359 
 360 func isInf(x float64, sign float64) float64 {
 361     if math.IsInf(x, int(sign)) {
 362         return 1
 363     }
 364     return 0
 365 }
 366 
 367 func isAnyInf(x float64) float64 {
 368     if math.IsInf(x, 0) {
 369         return 1
 370     }
 371     return 0
 372 }
 373 
 374 func isFinite(x float64) float64 {
 375     if math.IsInf(x, 0) {
 376         return 0
 377     }
 378     return 1
 379 }
 380 
 381 func isNaN(x float64) float64 {
 382     if math.IsNaN(x) {
 383         return 1
 384     }
 385     return 0
 386 }
 387 
 388 func isGood(x float64) float64 {
 389     if math.IsNaN(x) || math.IsInf(x, 0) {
 390         return 0
 391     }
 392     return 1
 393 }
 394 
 395 func isBad(x float64) float64 {
 396     if math.IsNaN(x) || math.IsInf(x, 0) {
 397         return 1
 398     }
 399     return 0
 400 }
 401 
 402 func same(x, y float64) float64 {
 403     if math.IsNaN(x) && math.IsNaN(y) {
 404         return 1
 405     }
 406     return debool(x == y)
 407 }
 408 
 409 func notSame(x, y float64) float64 {
 410     if math.IsNaN(x) && math.IsNaN(y) {
 411         return 0
 412     }
 413     return debool(x != y)
 414 }
 415 
 416 func deNaN(x float64, instead float64) float64 {
 417     if !math.IsNaN(x) {
 418         return x
 419     }
 420     return instead
 421 }
 422 
 423 func deInf(x float64, instead float64) float64 {
 424     if !math.IsInf(x, 0) {
 425         return x
 426     }
 427     return instead
 428 }
 429 
 430 func revalue(x float64, instead float64) float64 {
 431     if !math.IsNaN(x) && !math.IsInf(x, 0) {
 432         return x
 433     }
 434     return instead
 435 }
 436 
 437 func pow10(n float64) float64 {
 438     return math.Pow10(int(n))
 439 }
 440 
 441 func jn(n, x float64) float64 {
 442     return math.Jn(int(n), x)
 443 }
 444 
 445 func ldexp(frac, exp float64) float64 {
 446     return math.Ldexp(frac, int(exp))
 447 }
 448 
 449 func lgamma(x float64) float64 {
 450     y, s := math.Lgamma(x)
 451     if s < 0 {
 452         return math.NaN()
 453     }
 454     return y
 455 }
 456 
 457 func signbit(x float64) float64 {
 458     if math.Signbit(x) {
 459         return 1
 460     }
 461     return 0
 462 }
 463 
 464 func yn(n, x float64) float64 {
 465     return math.Yn(int(n), x)
 466 }
 467 
 468 func negate(x float64) float64 {
 469     return -x
 470 }
 471 
 472 func reciprocal(x float64) float64 {
 473     return 1 / x
 474 }
 475 
 476 func rangef(v ...float64) float64 {
 477     min := math.Inf(+1)
 478     max := math.Inf(-1)
 479     for _, f := range v {
 480         min = math.Min(min, f)
 481         max = math.Max(max, f)
 482     }
 483     return max - min
 484 }
 485 
 486 func cond(v ...float64) float64 {
 487     for {
 488         switch len(v) {
 489         case 0:
 490             // either no values are left, or no values were given at all
 491             return math.NaN()
 492 
 493         case 1:
 494             // either all previous conditions failed, and this last value is
 495             // automatically chosen, or only 1 value was given to begin with
 496             return v[0]
 497 
 498         default:
 499             // check condition: if true (non-0), return the value after it
 500             if v[0] != 0 {
 501                 return v[1]
 502             }
 503             // condition was false, so skip the leading pair of values
 504             v = v[2:]
 505         }
 506     }
 507 }
 508 
 509 func notBool(x float64) float64 {
 510     if x == 0 {
 511         return 1
 512     }
 513     return 0
 514 }
 515 
 516 func ifElse(cond float64, yes, no float64) float64 {
 517     if cond != 0 {
 518         return yes
 519     }
 520     return no
 521 }
 522 
 523 func lnGamma(x float64) float64 {
 524     y, s := math.Lgamma(x)
 525     if s < 0 {
 526         return math.NaN()
 527     }
 528     return y
 529 }
 530 
 531 func lnBeta(x float64, y float64) float64 {
 532     return lnGamma(x) + lnGamma(y) - lnGamma(x+y)
 533 }
 534 
 535 func beta(x float64, y float64) float64 {
 536     return math.Exp(lnBeta(x, y))
 537 }
 538 
 539 func factorial(n float64) float64 {
 540     return math.Round(math.Gamma(n + 1))
 541 }
 542 
 543 func degrees(rad float64) float64 {
 544     return rad2degMult * rad
 545 }
 546 
 547 func radians(deg float64) float64 {
 548     return deg2radMult * deg
 549 }
 550 
 551 func fract(x float64) float64 {
 552     return x - math.Floor(x)
 553 }
 554 
 555 func min(v ...float64) float64 {
 556     min := +math.Inf(+1)
 557     for _, f := range v {
 558         min = math.Min(min, f)
 559     }
 560     return min
 561 }
 562 
 563 func max(v ...float64) float64 {
 564     max := +math.Inf(-1)
 565     for _, f := range v {
 566         max = math.Max(max, f)
 567     }
 568     return max
 569 }
 570 
 571 func hypot(v ...float64) float64 {
 572     sumsq := 0.0
 573     for _, f := range v {
 574         sumsq += f * f
 575     }
 576     return math.Sqrt(sumsq)
 577 }
 578 
 579 func length(v ...float64) float64 {
 580     ss := 0.0
 581     for _, f := range v {
 582         ss += f * f
 583     }
 584     return math.Sqrt(ss)
 585 }
 586 
 587 // solveQuadMin finds the lowest solution of a 2nd-degree polynomial, using
 588 // a formula which is more accurate than the textbook one
 589 func solveQuadMin(a, b, c float64) float64 {
 590     disc := math.Sqrt(b*b - 4*a*c)
 591     r1 := 2 * c / (-b - disc)
 592     return r1
 593 }
 594 
 595 // solveQuadMax finds the highest solution of a 2nd-degree polynomial, using
 596 // a formula which is more accurate than the textbook one
 597 func solveQuadMax(a, b, c float64) float64 {
 598     disc := math.Sqrt(b*b - 4*a*c)
 599     r2 := 2 * c / (-b + disc)
 600     return r2
 601 }
 602 
 603 func wrap(x float64, min, max float64) float64 {
 604     return (x - min) / (max - min)
 605 }
 606 
 607 func unwrap(x float64, min, max float64) float64 {
 608     return (max-min)*x + min
 609 }
 610 
 611 func clamp(x float64, min, max float64) float64 {
 612     return math.Min(math.Max(x, min), max)
 613 }
 614 
 615 func scale(x float64, xmin, xmax, ymin, ymax float64) float64 {
 616     k := (x - xmin) / (xmax - xmin)
 617     return (ymax-ymin)*k + ymin
 618 }
 619 
 620 // polyval runs horner's algorithm on a value, with the polynomial coefficients
 621 // given after it, higher-order first
 622 func polyval(x float64, v ...float64) float64 {
 623     if len(v) == 0 {
 624         return 0
 625     }
 626 
 627     x0 := x
 628     x = 1.0
 629     y := 0.0
 630     for i := len(v) - 1; i >= 0; i-- {
 631         y += v[i] * x
 632         x *= x0
 633     }
 634     return y
 635 }
     File: ./fmscripts/math_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import "testing"
  28 
  29 func TestTables(t *testing.T) {
  30     for k, v := range determFuncs {
  31         t.Run(k, func(t *testing.T) {
  32             if !isSupportedFunc(v) {
  33                 t.Fatalf("unsupported func of type %T", v)
  34             }
  35         })
  36     }
  37 }
     File: ./fmscripts/optimizers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 // unary2func matches unary operators to funcs the optimizer can use to eval
  32 // constant-input unary expressions into their results
  33 var unary2func = map[string]func(x float64) float64{
  34     // avoid unary +, since it's a no-op and thus there's no instruction for
  35     // it, which in turn makes unit-tests for these optimization tables fail
  36     // unnecessarily
  37     // "+": func(x float64) float64 { return +x },
  38 
  39     "-": func(x float64) float64 { return -x },
  40     "!": func(x float64) float64 { return debool(x == 0) },
  41     "&": func(x float64) float64 { return math.Abs(x) },
  42     "*": func(x float64) float64 { return x * x },
  43     "^": func(x float64) float64 { return x * x },
  44     "/": func(x float64) float64 { return 1 / x },
  45 }
  46 
  47 // binary2func matches binary operators to funcs the optimizer can use to eval
  48 // constant-input binary expressions into their results
  49 var binary2func = map[string]func(x, y float64) float64{
  50     "+":  func(x, y float64) float64 { return x + y },
  51     "-":  func(x, y float64) float64 { return x - y },
  52     "*":  func(x, y float64) float64 { return x * y },
  53     "/":  func(x, y float64) float64 { return x / y },
  54     "%":  func(x, y float64) float64 { return math.Mod(x, y) },
  55     "&":  func(x, y float64) float64 { return debool(x != 0 && y != 0) },
  56     "&&": func(x, y float64) float64 { return debool(x != 0 && y != 0) },
  57     "|":  func(x, y float64) float64 { return debool(x != 0 || y != 0) },
  58     "||": func(x, y float64) float64 { return debool(x != 0 || y != 0) },
  59     "==": func(x, y float64) float64 { return debool(x == y) },
  60     "!=": func(x, y float64) float64 { return debool(x != y) },
  61     "<>": func(x, y float64) float64 { return debool(x != y) },
  62     "<":  func(x, y float64) float64 { return debool(x < y) },
  63     "<=": func(x, y float64) float64 { return debool(x <= y) },
  64     ">":  func(x, y float64) float64 { return debool(x > y) },
  65     ">=": func(x, y float64) float64 { return debool(x >= y) },
  66     "**": func(x, y float64) float64 { return math.Pow(x, y) },
  67     "^":  func(x, y float64) float64 { return math.Pow(x, y) },
  68 }
  69 
  70 // func2unary turns built-in func names into built-in unary operators
  71 var func2unary = map[string]string{
  72     "a":          "&",
  73     "abs":        "&",
  74     "inv":        "/",
  75     "neg":        "-",
  76     "negate":     "-",
  77     "reciprocal": "/",
  78     "sq":         "*",
  79     "square":     "*",
  80     "squared":    "*",
  81 }
  82 
  83 // func2binary turns built-in func names into built-in binary operators
  84 var func2binary = map[string]string{
  85     "mod":     "%",
  86     "modulus": "%",
  87     "pow":     "**",
  88     "power":   "**",
  89 }
  90 
  91 // vararg2func2 matches variable-argument funcs to their 2-argument versions
  92 var vararg2func2 = map[string]string{
  93     "hypot": "hypot2",
  94     "max":   "max2",
  95     "min":   "min2",
  96 }
  97 
  98 // optimize tries to simplify the expression given as much as possible, by
  99 // simplifying constants whenever possible, and exploiting known built-in
 100 // funcs which are known to behave deterministically
 101 func (c *Compiler) optimize(expr any) any {
 102     switch expr := expr.(type) {
 103     case []any:
 104         return c.optimizeCombo(expr)
 105 
 106     case unaryExpr:
 107         return c.optimizeUnaryExpr(expr)
 108 
 109     case binaryExpr:
 110         return c.optimizeBinaryExpr(expr)
 111 
 112     case callExpr:
 113         return c.optimizeCallExpr(expr)
 114 
 115     case assignExpr:
 116         expr.expr = c.optimize(expr.expr)
 117         return expr
 118 
 119     default:
 120         f, ok := c.tryConstant(expr)
 121         if ok {
 122             return f
 123         }
 124         return expr
 125     }
 126 }
 127 
 128 // optimizeCombo handles a sequence of expressions for the optimizer
 129 func (c *Compiler) optimizeCombo(exprs []any) any {
 130     if len(exprs) == 1 {
 131         return c.optimize(exprs[0])
 132     }
 133 
 134     // count how many expressions are considered useful: these are
 135     // assignments as well as the last expression, since that's what
 136     // determines the script's final result
 137     useful := 0
 138     for i, v := range exprs {
 139         _, ok := v.(assignExpr)
 140         if ok || i == len(exprs)-1 {
 141             useful++
 142         }
 143     }
 144 
 145     // ignore all expressions which are a waste of time, and optimize
 146     // all other expressions
 147     res := make([]any, 0, useful)
 148     for i, v := range exprs {
 149         _, ok := v.(assignExpr)
 150         if ok || i == len(exprs)-1 {
 151             res = append(res, c.optimize(v))
 152         }
 153     }
 154     return res
 155 }
 156 
 157 // optimizeUnaryExpr handles unary expressions for the optimizer
 158 func (c *Compiler) optimizeUnaryExpr(expr unaryExpr) any {
 159     // recursively optimize input
 160     expr.x = c.optimize(expr.x)
 161 
 162     // optimize unary ops on a constant into concrete values
 163     if x, ok := expr.x.(float64); ok {
 164         if fn, ok := unary2func[expr.op]; ok {
 165             return fn(x)
 166         }
 167     }
 168 
 169     switch expr.op {
 170     case "+":
 171         // unary plus is an identity operation
 172         return expr.x
 173 
 174     default:
 175         return expr
 176     }
 177 }
 178 
 179 // optimizeBinaryExpr handles binary expressions for the optimizer
 180 func (c *Compiler) optimizeBinaryExpr(expr binaryExpr) any {
 181     // recursively optimize inputs
 182     expr.x = c.optimize(expr.x)
 183     expr.y = c.optimize(expr.y)
 184 
 185     // optimize binary ops on 2 constants into concrete values
 186     if x, ok := expr.x.(float64); ok {
 187         if y, ok := expr.y.(float64); ok {
 188             if fn, ok := binary2func[expr.op]; ok {
 189                 return fn(x, y)
 190             }
 191         }
 192     }
 193 
 194     switch expr.op {
 195     case "+":
 196         if expr.x == 0.0 {
 197             // 0+y -> y
 198             return expr.y
 199         }
 200         if expr.y == 0.0 {
 201             // x+0 -> x
 202             return expr.x
 203         }
 204 
 205     case "-":
 206         if expr.x == 0.0 {
 207             // 0-y -> -y
 208             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
 209         }
 210         if expr.y == 0.0 {
 211             // x-0 -> x
 212             return expr.x
 213         }
 214 
 215     case "*":
 216         if expr.x == 1.0 {
 217             // 1*y -> y
 218             return expr.y
 219         }
 220         if expr.y == 1.0 {
 221             // x*1 -> x
 222             return expr.x
 223         }
 224         if expr.x == -1.0 {
 225             // -1*y -> -y
 226             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
 227         }
 228         if expr.y == -1.0 {
 229             // x*-1 -> -x
 230             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
 231         }
 232 
 233     case "/":
 234         if expr.x == 1.0 {
 235             // 1/y -> reciprocal of y
 236             return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.y})
 237         }
 238         if expr.y == 1.0 {
 239             // x/1 -> x
 240             return expr.x
 241         }
 242         if expr.y == -1.0 {
 243             // x/-1 -> -x
 244             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
 245         }
 246 
 247     case "**":
 248         switch expr.y {
 249         case -1.0:
 250             // x**-1 -> 1/x, reciprocal of x
 251             return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.x})
 252         case 1.0:
 253             // x**1 -> x
 254             return expr.x
 255         case 2.0:
 256             // x**2 -> *x
 257             return c.optimizeUnaryExpr(unaryExpr{op: "*", x: expr.x})
 258         case 3.0:
 259             // x**3 -> *x*x
 260             sq := unaryExpr{op: "*", x: expr.x}
 261             return c.optimizeBinaryExpr(binaryExpr{op: "*", x: sq, y: expr.x})
 262         }
 263     }
 264 
 265     // no simplifiable patterns were detected
 266     return expr
 267 }
 268 
 269 // optimizeCallExpr optimizes special cases of built-in func calls
 270 func (c *Compiler) optimizeCallExpr(call callExpr) any {
 271     // recursively optimize all inputs, and keep track if they're all
 272     // constants in the end
 273     numlit := 0
 274     for i, v := range call.args {
 275         v = c.optimize(v)
 276         call.args[i] = v
 277         if _, ok := v.(float64); ok {
 278             numlit++
 279         }
 280     }
 281 
 282     // if func is overridden, there's no guarantee the new func works the same
 283     if _, ok := determFuncs[call.name]; c.ftype[call.name] != nil || !ok {
 284         return call
 285     }
 286 
 287     // from this point on, func is guaranteed to be built-in and deterministic
 288 
 289     // handle all-const inputs, by calling func and using its result
 290     if numlit == len(call.args) {
 291         in := make([]float64, 0, len(call.args))
 292         for _, v := range call.args {
 293             f, _ := v.(float64)
 294             in = append(in, f)
 295         }
 296 
 297         if f, ok := tryCall(determFuncs[call.name], in); ok {
 298             return f
 299         }
 300     }
 301 
 302     switch len(call.args) {
 303     case 1:
 304         if op, ok := func2unary[call.name]; ok {
 305             expr := unaryExpr{op: op, x: call.args[0]}
 306             return c.optimizeUnaryExpr(expr)
 307         }
 308         return call
 309 
 310     case 2:
 311         if op, ok := func2binary[call.name]; ok {
 312             expr := binaryExpr{op: op, x: call.args[0], y: call.args[1]}
 313             return c.optimizeBinaryExpr(expr)
 314         }
 315         if name, ok := vararg2func2[call.name]; ok {
 316             call.name = name
 317             return call
 318         }
 319         return call
 320 
 321     default:
 322         return call
 323     }
 324 }
 325 
 326 // tryConstant tries to optimize the expression given into a constant
 327 func (c *Compiler) tryConstant(expr any) (value float64, ok bool) {
 328     switch expr := expr.(type) {
 329     case float64:
 330         return expr, true
 331 
 332     case string:
 333         if _, ok := c.vaddr[expr]; !ok {
 334             // name isn't explicitly defined
 335             if f, ok := mathConst[expr]; ok {
 336                 // and is a known math constant
 337                 return f, true
 338             }
 339         }
 340         return 0, false
 341 
 342     default:
 343         return 0, false
 344     }
 345 }
 346 
 347 // tryCall tries to simplify the function expression given into a constant
 348 func tryCall(fn any, in []float64) (value float64, ok bool) {
 349     switch fn := fn.(type) {
 350     case func(float64) float64:
 351         if len(in) == 1 {
 352             return fn(in[0]), true
 353         }
 354         return 0, false
 355 
 356     case func(float64, float64) float64:
 357         if len(in) == 2 {
 358             return fn(in[0], in[1]), true
 359         }
 360         return 0, false
 361 
 362     case func(float64, float64, float64) float64:
 363         if len(in) == 3 {
 364             return fn(in[0], in[1], in[2]), true
 365         }
 366         return 0, false
 367 
 368     case func(float64, float64, float64, float64) float64:
 369         if len(in) == 4 {
 370             return fn(in[0], in[1], in[2], in[3]), true
 371         }
 372         return 0, false
 373 
 374     case func(float64, float64, float64, float64, float64) float64:
 375         if len(in) == 5 {
 376             return fn(in[0], in[1], in[2], in[3], in[4]), true
 377         }
 378         return 0, false
 379 
 380     case func(...float64) float64:
 381         return fn(in...), true
 382 
 383     case func(float64, ...float64) float64:
 384         if len(in) >= 1 {
 385             return fn(in[0], in[1:]...), true
 386         }
 387         return 0, false
 388 
 389     default:
 390         // type isn't a supported func
 391         return 0, false
 392     }
 393 }
     File: ./fmscripts/optimizers_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29     "reflect"
  30     "testing"
  31 )
  32 
  33 func TestOptimizerTables(t *testing.T) {
  34     for k := range unary2func {
  35         t.Run(k, func(t *testing.T) {
  36             if _, ok := unary2op[k]; !ok {
  37                 t.Fatalf("missing unary constant optimizer for %q", k)
  38             }
  39         })
  40     }
  41 
  42     for k := range binary2func {
  43         t.Run(k, func(t *testing.T) {
  44             if _, ok := binary2op[k]; !ok {
  45                 t.Fatalf("missing binary constant optimizer for %q", k)
  46             }
  47         })
  48     }
  49 
  50     for k, name := range func2unary {
  51         t.Run(k, func(t *testing.T) {
  52             const fs = "func(x) optimizer %q has no matching built-in func"
  53             if _, ok := determFuncs[k]; !ok {
  54                 t.Fatalf(fs, k)
  55             }
  56 
  57             if _, ok := unary2op[name]; !ok {
  58                 t.Fatalf("missing unary func optimizer for %q", k)
  59             }
  60         })
  61     }
  62 
  63     for k, name := range func2binary {
  64         t.Run(k, func(t *testing.T) {
  65             const fs = "func(x, y) optimizer %q has no matching built-in func"
  66             if _, ok := determFuncs[k]; !ok {
  67                 t.Fatalf(fs, k)
  68             }
  69 
  70             if _, ok := binary2op[name]; !ok {
  71                 t.Fatalf("missing binary func optimizer for %q", k)
  72             }
  73         })
  74     }
  75 
  76     for k := range vararg2func2 {
  77         t.Run(k, func(t *testing.T) {
  78             if _, ok := determFuncs[k]; !ok {
  79                 const fs = "vararg optimizer %q has no matching built-in func"
  80                 t.Fatalf(fs, k)
  81             }
  82         })
  83     }
  84 }
  85 
  86 func TestOptimizer(t *testing.T) {
  87     tests := map[string]any{
  88         `1`:                         1.0,
  89         `3+4*5`:                     23.0,
  90         `e`:                         math.E,
  91         `pi`:                        math.Pi,
  92         `phi`:                       math.Phi,
  93         `2*pi`:                      2 * math.Pi,
  94         `4.51*phi-14.23564`:         4.51*math.Phi - 14.23564,
  95         `-e`:                        -math.E,
  96         `exp(2*pi)`:                 math.Exp(2 * math.Pi),
  97         `log(2342.55) / log(43.21)`: math.Log(2342.55) / math.Log(43.21),
  98         `f(3)`:                      callExpr{name: `f`, args: []any{3.0}},
  99         `min(3, 2, -1.5)`:           -1.5,
 100 
 101         `hypot(x, 4)`: callExpr{name: `hypot2`, args: []any{`x`, 4.0}},
 102         `max(x, 4)`:   callExpr{name: `max2`, args: []any{`x`, 4.0}},
 103         `min(x, 4)`:   callExpr{name: `min2`, args: []any{`x`, 4.0}},
 104         `rand()`:      callExpr{name: `rand`},
 105 
 106         `sin(2_000 * x * tau * x)`: callExpr{
 107             name: `sin`,
 108             args: []any{
 109                 binaryExpr{
 110                     `*`,
 111                     binaryExpr{
 112                         `*`,
 113                         binaryExpr{`*`, 2_000.0, `x`},
 114                         2 * math.Pi,
 115                     },
 116                     `x`,
 117                 },
 118             },
 119         },
 120 
 121         `sin(10 * tau * exp(-20 * x)) * exp(-2 * x)`: binaryExpr{
 122             `*`,
 123             // sin(...)
 124             callExpr{
 125                 name: `sin`,
 126                 args: []any{
 127                     // 10 * tau * exp(...)
 128                     binaryExpr{
 129                         `*`,
 130                         10 * 2 * math.Pi,
 131                         // exp(-20 * x)
 132                         callExpr{
 133                             name: `exp`,
 134                             args: []any{
 135                                 binaryExpr{`*`, -20.0, `x`},
 136                             },
 137                         },
 138                     },
 139                 },
 140             },
 141             // exp(-2 * x)
 142             callExpr{
 143                 name: `exp`,
 144                 args: []any{binaryExpr{`*`, -2.0, `x`}},
 145             },
 146         },
 147     }
 148 
 149     defs := map[string]any{
 150         `x`: 3.5,
 151         `f`: math.Exp,
 152     }
 153 
 154     for source, expected := range tests {
 155         t.Run(source, func(t *testing.T) {
 156             var c Compiler
 157             root, err := parse(source)
 158             if err != nil {
 159                 t.Fatal(err)
 160                 return
 161             }
 162 
 163             if err := c.reset(defs); err != nil {
 164                 t.Fatal(err)
 165                 return
 166             }
 167 
 168             got := c.optimize(root)
 169             if !reflect.DeepEqual(got, expected) {
 170                 const fs = "expected result to be\n%#v\ninstead of\n%#v"
 171                 t.Fatalf(fs, expected, got)
 172                 return
 173             }
 174         })
 175     }
 176 }
     File: ./fmscripts/parsing.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "strconv"
  31     "strings"
  32 )
  33 
  34 // parse turns source code into an expression type interpreters can use.
  35 func parse(src string) (any, error) {
  36     tok := newTokenizer(src)
  37     par, err := newParser(&tok)
  38     if err != nil {
  39         return nil, err
  40     }
  41 
  42     v, err := par.parse()
  43     err = par.improveError(err, src)
  44     return v, err
  45 }
  46 
  47 // pickLine slices a string, picking one of its lines via a 1-based index:
  48 // func improveError uses it to isolate the line of code an error came from
  49 func pickLine(src string, linenum int) string {
  50     // skip the lines before the target one
  51     for i := 0; i < linenum && len(src) > 0; i++ {
  52         j := strings.IndexByte(src, '\n')
  53         if j < 0 {
  54             break
  55         }
  56         src = src[j+1:]
  57     }
  58 
  59     // limit leftover to a single line
  60     i := strings.IndexByte(src, '\n')
  61     if i >= 0 {
  62         return src[:i]
  63     }
  64     return src
  65 }
  66 
  67 // unaryExpr is a unary expression
  68 type unaryExpr struct {
  69     op string
  70     x  any
  71 }
  72 
  73 // binaryExpr is a binary expression
  74 type binaryExpr struct {
  75     op string
  76     x  any
  77     y  any
  78 }
  79 
  80 // callExpr is a function call and its arguments
  81 type callExpr struct {
  82     name string
  83     args []any
  84 }
  85 
  86 // assignExpr is a value/variable assignment
  87 type assignExpr struct {
  88     name string
  89     expr any
  90 }
  91 
  92 // parser is a parser for JavaScript-like syntax, limited to operations on
  93 // 64-bit floating-point numbers
  94 type parser struct {
  95     tokens []token
  96     line   int
  97     pos    int
  98     toklen int
  99 }
 100 
 101 // newParser is the constructor for type parser
 102 func newParser(t *tokenizer) (parser, error) {
 103     var p parser
 104 
 105     // get all tokens from the source code
 106     for {
 107         v, err := t.next()
 108         if v.kind != unknownToken {
 109             p.tokens = append(p.tokens, v)
 110         }
 111 
 112         if err == errEOS {
 113             // done scanning/tokenizing
 114             return p, nil
 115         }
 116 
 117         if err != nil {
 118             // handle actual errors
 119             return p, err
 120         }
 121     }
 122 }
 123 
 124 // improveError adds more info about where exactly in the source code an error
 125 // came from, thus making error messages much more useful
 126 func (p *parser) improveError(err error, src string) error {
 127     if err == nil {
 128         return nil
 129     }
 130 
 131     line := pickLine(src, p.line)
 132     if len(line) == 0 || p.pos < 1 {
 133         const fs = "(line %d: pos %d): %w"
 134         return fmt.Errorf(fs, p.line, p.pos, err)
 135     }
 136 
 137     ptr := strings.Repeat(" ", p.pos) + "^"
 138     const fs = "(line %d: pos %d): %w\n%s\n%s"
 139     return fmt.Errorf(fs, p.line, p.pos, err, line, ptr)
 140 }
 141 
 142 // parse tries to turn tokens into a compilable abstract syntax tree, and is
 143 // the parser's entry point
 144 func (p *parser) parse() (any, error) {
 145     // source codes with no tokens are always errors
 146     if len(p.tokens) == 0 {
 147         const msg = "source code is empty, or has no useful expressions"
 148         return nil, errors.New(msg)
 149     }
 150 
 151     // handle optional excel-like leading equal sign
 152     p.acceptSyntax(`=`)
 153 
 154     // ignore trailing semicolons
 155     for len(p.tokens) > 0 {
 156         t := p.tokens[len(p.tokens)-1]
 157         if t.kind != syntaxToken || t.value != `;` {
 158             break
 159         }
 160         p.tokens = p.tokens[:len(p.tokens)-1]
 161     }
 162 
 163     // handle single expressions as well as multiple semicolon-separated
 164     // expressions: the latter allow value assignments/updates in scripts
 165     var res []any
 166     for keepGoing := true; keepGoing && len(p.tokens) > 0; {
 167         v, err := p.parseExpression()
 168         if err != nil && err != errEOS {
 169             return v, err
 170         }
 171 
 172         res = append(res, v)
 173         // handle optional separator/continuation semicolons
 174         _, keepGoing = p.acceptSyntax(`;`)
 175     }
 176 
 177     // unexpected unparsed trailing tokens are always errors; any trailing
 178     // semicolons in the original script are already trimmed
 179     if len(p.tokens) > 0 {
 180         const fs = "unexpected %s"
 181         return res, fmt.Errorf(fs, p.tokens[0].value)
 182     }
 183 
 184     // make scripts ending in an assignment also load that value, so they're
 185     // useful, as assignments result in no useful value by themselves
 186     assign, ok := res[len(res)-1].(assignExpr)
 187     if ok {
 188         res = append(res, assign.name)
 189     }
 190 
 191     // turn 1-item combo expressions into their only expression: some
 192     // unit tests may rely on that for convenience
 193     if len(res) == 1 {
 194         return res[0], nil
 195     }
 196     return res, nil
 197 }
 198 
 199 // acceptSyntax advances the parser on the first syntactic string matched:
 200 // notice any number of alternatives/options are allowed, as the syntax
 201 // allows/requires at various points
 202 func (p *parser) acceptSyntax(syntax ...string) (match string, ok bool) {
 203     if len(p.tokens) == 0 {
 204         return "", false
 205     }
 206 
 207     t := p.tokens[0]
 208     if t.kind != syntaxToken {
 209         return "", false
 210     }
 211 
 212     for _, s := range syntax {
 213         if t.value == s {
 214             p.advance()
 215             return s, true
 216         }
 217     }
 218     return "", false
 219 }
 220 
 221 // advance skips the current leading token, if there are still any left
 222 func (p *parser) advance() {
 223     if len(p.tokens) == 0 {
 224         return
 225     }
 226 
 227     t := p.tokens[0]
 228     p.tokens = p.tokens[1:]
 229     p.line = t.line
 230     p.pos = t.pos
 231     p.toklen = len(t.value)
 232 }
 233 
 234 // acceptNumeric advances the parser on a numeric value, but only if it's
 235 // the leading token: conversely, any other type of token doesn't advance
 236 // the parser; when matches happen the resulting strings need parsing via
 237 // func parseNumber
 238 func (p *parser) acceptNumeric() (numliteral string, ok bool) {
 239     if len(p.tokens) == 0 {
 240         return "", false
 241     }
 242 
 243     t := p.tokens[0]
 244     if t.kind == numberToken {
 245         p.advance()
 246         return t.value, true
 247     }
 248     return "", false
 249 }
 250 
 251 // demandSyntax imposes a specific syntactic element to follow, or else it's
 252 // an error
 253 func (p *parser) demandSyntax(syntax string) error {
 254     if len(p.tokens) == 0 {
 255         return fmt.Errorf("expected %s instead of the end of source", syntax)
 256     }
 257 
 258     first := p.tokens[0]
 259     if first.kind == syntaxToken && first.value == syntax {
 260         p.advance()
 261         return nil
 262     }
 263     return fmt.Errorf("expected %s instead of %s", syntax, first.value)
 264 }
 265 
 266 func (p *parser) parseExpression() (any, error) {
 267     x, err := p.parseComparison()
 268     if err != nil {
 269         return x, err
 270     }
 271 
 272     // handle assignment statements
 273     if _, ok := p.acceptSyntax(`=`, `:=`); ok {
 274         varname, ok := x.(string)
 275         if !ok {
 276             const fs = "expected a variable name, instead of a %T"
 277             return nil, fmt.Errorf(fs, x)
 278         }
 279 
 280         x, err := p.parseExpression()
 281         expr := assignExpr{name: varname, expr: x}
 282         return expr, err
 283     }
 284 
 285     // handle and/or logical chains
 286     for {
 287         if op, ok := p.acceptSyntax(`&&`, `||`, `&`, `|`); ok {
 288             y, err := p.parseExpression()
 289             if err != nil {
 290                 return y, err
 291             }
 292             x = binaryExpr{op: op, x: x, y: y}
 293             continue
 294         }
 295         break
 296     }
 297 
 298     // handle maybe-properties
 299     if _, ok := p.acceptSyntax(`??`); ok {
 300         y, err := p.parseExpression()
 301         expr := callExpr{name: "??", args: []any{x, y}}
 302         return expr, err
 303     }
 304 
 305     // handle choice/ternary operator
 306     if _, ok := p.acceptSyntax(`?`); ok {
 307         y, err := p.parseExpression()
 308         if err != nil {
 309             expr := callExpr{name: "?:", args: []any{x, nil, nil}}
 310             return expr, err
 311         }
 312 
 313         if _, ok := p.acceptSyntax(`:`); ok {
 314             z, err := p.parseExpression()
 315             expr := callExpr{name: "?:", args: []any{x, y, z}}
 316             return expr, err
 317         }
 318 
 319         if len(p.tokens) == 0 {
 320             expr := callExpr{name: "?:", args: []any{x, y, nil}}
 321             return expr, errors.New("expected `:`")
 322         }
 323 
 324         s := p.tokens[0].value
 325         expr := callExpr{name: "?:", args: []any{x, y, nil}}
 326         err = fmt.Errorf("expected `:`, but got %q instead", s)
 327         return expr, err
 328     }
 329 
 330     // expression was just a comparison, or simpler
 331     return x, nil
 332 }
 333 
 334 func (p *parser) parseComparison() (any, error) {
 335     x, err := p.parseTerm()
 336     if err != nil {
 337         return x, err
 338     }
 339 
 340     op, ok := p.acceptSyntax(`==`, `!=`, `<`, `>`, `<=`, `>=`, `<>`, `===`, `!==`)
 341     if ok {
 342         y, err := p.parseTerm()
 343         return binaryExpr{op: op, x: x, y: y}, err
 344     }
 345     return x, err
 346 }
 347 
 348 // parseBinary handles binary operations, by recursing depth-first on the left
 349 // side of binary expressions; going tail-recursive on these would reverse the
 350 // order of arguments instead, which is obviously wrong
 351 func (p *parser) parseBinary(parse func() (any, error), syntax ...string) (any, error) {
 352     x, err := parse()
 353     if err != nil {
 354         return x, err
 355     }
 356 
 357     for {
 358         op, ok := p.acceptSyntax(syntax...)
 359         if !ok {
 360             return x, nil
 361         }
 362 
 363         y, err := parse()
 364         x = binaryExpr{op: op, x: x, y: y}
 365         if err != nil {
 366             return x, err
 367         }
 368     }
 369 }
 370 
 371 func (p *parser) parseTerm() (any, error) {
 372     return p.parseBinary(func() (any, error) {
 373         return p.parseProduct()
 374     }, `+`, `-`, `^`)
 375 }
 376 
 377 func (p *parser) parseProduct() (any, error) {
 378     return p.parseBinary(func() (any, error) {
 379         return p.parsePower()
 380     }, `*`, `/`, `%`)
 381 }
 382 
 383 func (p *parser) parsePower() (any, error) {
 384     return p.parseBinary(func() (any, error) {
 385         return p.parseValue()
 386     }, `**`, `^`)
 387 }
 388 
 389 func (p *parser) parseValue() (any, error) {
 390     // handle unary operators which can also be considered part of numeric
 391     // literals, and thus should be simplified away
 392     if op, ok := p.acceptSyntax(`+`, `-`); ok {
 393         if s, ok := p.acceptNumeric(); ok {
 394             x, err := strconv.ParseFloat(s, 64)
 395             if err != nil {
 396                 return nil, err
 397             }
 398             if simpler, ok := simplifyNumber(op, x); ok {
 399                 return simpler, nil
 400             }
 401             return unaryExpr{op: op, x: x}, nil
 402         }
 403 
 404         x, err := p.parsePower()
 405         return unaryExpr{op: op, x: x}, err
 406     }
 407 
 408     // handle all other unary operators
 409     if op, ok := p.acceptSyntax(`!`, `&`, `*`, `^`); ok {
 410         x, err := p.parsePower()
 411         return unaryExpr{op: op, x: x}, err
 412     }
 413 
 414     // handle subexpression in parentheses
 415     if _, ok := p.acceptSyntax(`(`); ok {
 416         x, err := p.parseExpression()
 417         if err != nil {
 418             return x, err
 419         }
 420 
 421         if err := p.demandSyntax(`)`); err != nil {
 422             return x, err
 423         }
 424         return p.parseAccessors(x)
 425     }
 426 
 427     // handle subexpression in square brackets: it's just an alternative to
 428     // using parentheses for subexpressions
 429     if _, ok := p.acceptSyntax(`[`); ok {
 430         x, err := p.parseExpression()
 431         if err != nil {
 432             return x, err
 433         }
 434 
 435         if err := p.demandSyntax(`]`); err != nil {
 436             return x, err
 437         }
 438         return p.parseAccessors(x)
 439     }
 440 
 441     // handle all other cases
 442     x, err := p.parseSimpleValue()
 443     if err != nil {
 444         return x, err
 445     }
 446 
 447     // handle arbitrarily-long chains of accessors
 448     return p.parseAccessors(x)
 449 }
 450 
 451 // parseSimpleValue handles a numeric literal or a variable/func name, also
 452 // known as identifier
 453 func (p *parser) parseSimpleValue() (any, error) {
 454     if len(p.tokens) == 0 {
 455         return nil, errEOS
 456     }
 457     t := p.tokens[0]
 458 
 459     switch t.kind {
 460     case identifierToken:
 461         p.advance()
 462         // handle func calls, such as f(...)
 463         if _, ok := p.acceptSyntax(`(`); ok {
 464             args, err := p.parseList(`)`)
 465             expr := callExpr{name: t.value, args: args}
 466             return expr, err
 467         }
 468         // handle func calls, such as f[...]
 469         if _, ok := p.acceptSyntax(`[`); ok {
 470             args, err := p.parseList(`]`)
 471             expr := callExpr{name: t.value, args: args}
 472             return expr, err
 473         }
 474         return t.value, nil
 475 
 476     case numberToken:
 477         p.advance()
 478         return strconv.ParseFloat(t.value, 64)
 479 
 480     default:
 481         const fs = "unexpected %s (token type %d)"
 482         return nil, fmt.Errorf(fs, t.value, t.kind)
 483     }
 484 }
 485 
 486 // parseAccessors handles an arbitrarily-long chain of accessors
 487 func (p *parser) parseAccessors(x any) (any, error) {
 488     for {
 489         s, ok := p.acceptSyntax(`.`, `@`)
 490         if !ok {
 491             // dot-chain is over
 492             return x, nil
 493         }
 494 
 495         // handle property/method accessors
 496         v, err := p.parseDot(s, x)
 497         if err != nil {
 498             return v, err
 499         }
 500         x = v
 501     }
 502 }
 503 
 504 // parseDot handles what follows a syntactic dot, as opposed to a dot which
 505 // may be part of a numeric literal
 506 func (p *parser) parseDot(after string, x any) (any, error) {
 507     if len(p.tokens) == 0 {
 508         const fs = "unexpected end of source after a %q"
 509         return x, fmt.Errorf(fs, after)
 510     }
 511 
 512     t := p.tokens[0]
 513     p.advance()
 514     if t.kind != identifierToken {
 515         const fs = "expected a valid property name, but got %s instead"
 516         return x, fmt.Errorf(fs, t.value)
 517     }
 518 
 519     if _, ok := p.acceptSyntax(`(`); ok {
 520         items, err := p.parseList(`)`)
 521         args := append([]any{x}, items...)
 522         return callExpr{name: t.value, args: args}, err
 523     }
 524 
 525     if _, ok := p.acceptSyntax(`[`); ok {
 526         items, err := p.parseList(`]`)
 527         args := append([]any{x}, items...)
 528         return callExpr{name: t.value, args: args}, err
 529     }
 530 
 531     return callExpr{name: t.value, args: []any{x}}, nil
 532 }
 533 
 534 // parseList handles the argument-list following a `(` or a `[`
 535 func (p *parser) parseList(end string) ([]any, error) {
 536     var arr []any
 537     for len(p.tokens) > 0 {
 538         if _, ok := p.acceptSyntax(`,`); ok {
 539             // ensure extra/trailing commas are allowed/ignored
 540             continue
 541         }
 542 
 543         if _, ok := p.acceptSyntax(end); ok {
 544             return arr, nil
 545         }
 546 
 547         v, err := p.parseExpression()
 548         if err != nil {
 549             return arr, err
 550         }
 551         arr = append(arr, v)
 552     }
 553 
 554     // return an appropriate error for the unexpected end of the source
 555     return arr, p.demandSyntax(`)`)
 556 }
 557 
 558 // simplifyNumber tries to simplify a few trivial unary operations on
 559 // numeric constants
 560 func simplifyNumber(op string, x any) (v any, ok bool) {
 561     if x, ok := x.(float64); ok {
 562         switch op {
 563         case `+`:
 564             return x, true
 565         case `-`:
 566             return -x, true
 567         default:
 568             return x, false
 569         }
 570     }
 571     return x, false
 572 }
     File: ./fmscripts/programs.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 // So far, my apps relying on this package are
  32 //
  33 // - fh, a Function Heatmapper which color-codes f(x, y), seen from above
  34 // - star, a STAtistical Resampler to calculate stats from tabular data
  35 // - waveout, an app to emit sample-by-sample wave-audio by formula
  36 
  37 // Quick Notes on Performance
  38 //
  39 // Note: while these microbenchmarks are far from proper benchmarks, Club Pulse
  40 // can still give a general idea of what relative performance to expect.
  41 //
  42 // While funscripts.Interpreter can do anything a Program can do, and much more,
  43 // a fmscripts.Program is way faster for float64-only tasks: typical speed-ups
  44 // are between 20-to-50 times. Various simple benchmarks suggest running speed
  45 // is between 1/2 and 1/4 of native funcs.
  46 //
  47 // Reimplementations of the Club Pulse benchmark, when run 50 million times,
  48 // suggest this package is starting to measure the relative speed of various
  49 // trigonometric func across JIT-ted script runners, running somewhat slower
  50 // than NodeJS/V8, faster than PyPy, and much faster than Python.
  51 //
  52 // Other scripted tests, which aren't as trigonometry/exponential-heavy, hint
  53 // at even higher speed-ups compared to Python and PyPy, as well as being
  54 // almost as fast as NodeJS/V8.
  55 //
  56 // Being so close to Node's V8 (0.6x - 0.8x its speed) is a surprisingly good
  57 // result for the relatively little effort this package took.
  58 
  59 const (
  60     maxArgsLen = 1<<16 - 1 // max length for the []float64 input of callv
  61     maxOpIndex = 1<<32 - 1 // max index for values
  62 )
  63 
  64 // types to keep compiler code the same, while changing sizes of numOp fields
  65 type (
  66     opcode  uint16
  67     opargs  uint16 // opcode directly affects max correct value of maxArgsLen
  68     opindex uint32 // opcode directly affects max correct value of maxOpIndex
  69 )
  70 
  71 // numOp is a numeric operation in a program
  72 type numOp struct {
  73     What    opcode  // what to do
  74     NumArgs opargs  // vector-length only used for callv and call1v
  75     Index   opindex // index to load a value or call a function
  76 }
  77 
  78 // Since the go 1.19 compiler started compiling dense switch statements using
  79 // jump tables, adding more basic ops doesn't slow things down anymore when
  80 // reaching the next power-of-2 thresholds in the number of cases.
  81 
  82 const (
  83     // load float64 value to top of the stack
  84     load opcode = iota
  85 
  86     // pop top of stack and store it into value
  87     store opcode = iota
  88 
  89     // unary operations
  90     neg    opcode = iota
  91     not    opcode = iota
  92     abs    opcode = iota
  93     square opcode = iota
  94     // 1/x, the reciprocal/inverse value
  95     rec opcode = iota
  96     // x%1, but faster than math.Mod(x, 1)
  97     mod1 opcode = iota
  98 
  99     // arithmetic and logic operations: 2 float64 inputs, 1 float64 output
 100 
 101     add opcode = iota
 102     sub opcode = iota
 103     mul opcode = iota
 104     div opcode = iota
 105     mod opcode = iota
 106     pow opcode = iota
 107     and opcode = iota
 108     or  opcode = iota
 109 
 110     // binary comparisons: 2 float64 inputs, 1 booleanized float64 output
 111 
 112     equal    opcode = iota
 113     notequal opcode = iota
 114     less     opcode = iota
 115     lessoreq opcode = iota
 116     more     opcode = iota
 117     moreoreq opcode = iota
 118 
 119     // function callers with 1..5 float64 inputs and a float64 result
 120 
 121     call0 opcode = iota
 122     call1 opcode = iota
 123     call2 opcode = iota
 124     call3 opcode = iota
 125     call4 opcode = iota
 126     call5 opcode = iota
 127 
 128     // var-arg input function callers
 129 
 130     callv  opcode = iota
 131     call1v opcode = iota
 132 )
 133 
 134 // Program runs a sequence of float64 operations, with no explicit control-flow:
 135 // implicit control-flow is available in the float64-only functions you make
 136 // available to such programs. Such custom funcs must all return a float64, and
 137 // either take from 0 to 5 float64 arguments, or a single float64 array.
 138 //
 139 // As the name suggests, don't create such objects directly, but instead use
 140 // Compiler.Compile to create them. Only a Compiler lets you register variables
 141 // and functions, which then become part of your numeric Program.
 142 //
 143 // A Program lets you change values before each run, using pointers from method
 144 // Program.Get: such pointers are guaranteed never to change before or across
 145 // runs. Just ensure you get a variable defined in the Compiler used to make the
 146 // Program, or the pointer will be to a dummy place which has no effect on final
 147 // results.
 148 //
 149 // Expressions using literals are automatically optimized into their results:
 150 // this also applies to func calls with standard math functions and constants.
 151 //
 152 // The only way to limit such optimizations is to redefine common math funcs and
 153 // const values explicitly when compiling: after doing so, all those value-names
 154 // will stand for externally-updatable values which can change from one run to
 155 // the next. Similarly, there's no guarantee the (re)defined functions will be
 156 // deterministic, like the defaults they replaced.
 157 //
 158 // # Example
 159 //
 160 // var c fmscripts.Compiler
 161 //
 162 //  defs := map[string]any{
 163 //      "x": 0,    // define `x`, and initialize it to 0
 164 //      "k": 4.25, // define `k`, and initialize it to 4.25
 165 //      "b": true, // define `b`, and initialize it to 1.0
 166 //      "n": -23,  // define `n`, and initialize it to -23.0
 167 //      "pi": 3,   // define `pi`, overriding the automatic constant named `pi`
 168 //
 169 //      "f": numericKernel // type is func ([]float64) float64
 170 //      "g": otherFunc     // type is func (float64) float64
 171 //  }
 172 //
 173 // prog, err := c.Compile("log10(k) + f(sqrt(k) * exp(-x), 45, -0.23)", defs)
 174 // // ...
 175 //
 176 // x, _ := prog.Get("x") // Get returns (*float64, bool)
 177 // y, _ := prog.Get("y") // a useless pointer, since program doesn't use `y`
 178 // // ...
 179 //
 180 //  for i := 0; i < n; i++ {
 181 //      *x = float64(i)*dx + minx // you update inputs in place using pointers
 182 //      f := prog.Run()           // method Run gives you a float64 back
 183 //      // ...
 184 //  }
 185 type Program struct {
 186     sp int // stack pointer, even though it's an index
 187 
 188     stack  []float64 // pre-allocated by compiler to max length needed by program
 189     values []float64 // holds all values, whether literals, or variables
 190 
 191     ops   []numOp // all sequential operations for each run
 192     funcs []any   // all funcs used by the program
 193 
 194     names map[string]int // variable-name to index lookup
 195     dummy float64        // all pointers for undefined variables point here
 196 
 197     // data []float64
 198 }
 199 
 200 // Get lets you change parameters/variables before each time you run a program,
 201 // since it doesn't return a value, but a pointer to it, so you can update it
 202 // in place.
 203 //
 204 // If the name given isn't available, the result is a pointer to a dummy place:
 205 // this ensures non-nil pointers, which are always safe to use, even though
 206 // updates to the dummy destination have no effect on program results.
 207 func (p *Program) Get(name string) (ptr *float64, useful bool) {
 208     if i, ok := p.names[name]; ok {
 209         return &p.values[i], true
 210     }
 211     return &p.dummy, false
 212 }
 213 
 214 // Clone creates an exact copy of a Program with all values in their current
 215 // state: this is useful when dispatching embarassingly-parallel tasks to
 216 // multiple programs.
 217 func (p Program) Clone() Program {
 218     // can't share the stack nor the values
 219     stack := make([]float64, len(p.stack))
 220     values := make([]float64, len(p.values))
 221     copy(stack, p.stack)
 222     copy(values, p.values)
 223     p.stack = stack
 224     p.values = values
 225 
 226     // can share everything else as is
 227     return p
 228 }
 229 
 230 // Memo: the command to show all bound checks is
 231 // go test -gcflags="-d=ssa/check_bce/debug=1" fmscripts/programs.go
 232 
 233 // Discussion about go compiler optimizing switch statements into jump tables
 234 // https://go-review.googlesource.com/c/go/+/357330/
 235 
 236 // Run executes the program once. Before each run, update input values using
 237 // pointers from method Get.
 238 func (p *Program) Run() float64 {
 239     // Check for empty programs: these happen either when a compilation error
 240     // was ignored, or when a program was explicitly declared as a variable.
 241     if len(p.ops) == 0 {
 242         return math.NaN()
 243     }
 244 
 245     p.sp = 0
 246     p.runAllOps()
 247     return p.stack[p.sp]
 248 }
 249 
 250 type func4 = func(float64, float64, float64, float64) float64
 251 type func5 = func(float64, float64, float64, float64, float64) float64
 252 
 253 // runAllOps runs all operations in a loop: when done, the program's result is
 254 // ready as the only item left in the stack
 255 func (p *Program) runAllOps() {
 256     for _, op := range p.ops {
 257         // shortcut for the current stack pointer
 258         sp := p.sp
 259 
 260         // Preceding binary ops and func calls by _ = p.stack[i-n] prevents
 261         // an extra bound check for the lhs of assignments, but a quick
 262         // statistical summary of benchmarks doesn't show clear speedups,
 263         // let alone major ones.
 264         //
 265         // Separating different func types into different arrays to avoid
 266         // type-checking at runtime doesn't seem to be worth it either,
 267         // and makes the compiler more complicated.
 268 
 269         switch op.What {
 270         case load:
 271             // store above top of stack
 272             p.stack[sp+1] = p.values[op.Index]
 273             p.sp++
 274         case store:
 275             // store from top of the stack
 276             p.values[op.Index] = p.stack[sp]
 277             p.sp--
 278 
 279         // unary operations
 280         case neg:
 281             p.stack[sp] = -p.stack[sp]
 282         case not:
 283             p.stack[sp] = deboolNot(p.stack[sp])
 284         case abs:
 285             p.stack[sp] = math.Abs(p.stack[sp])
 286         case square:
 287             p.stack[sp] *= p.stack[sp]
 288         case rec:
 289             p.stack[sp] = 1 / p.stack[sp]
 290 
 291         // binary arithmetic ops
 292         case add:
 293             p.stack[sp-1] += p.stack[sp]
 294             p.sp--
 295         case sub:
 296             p.stack[sp-1] -= p.stack[sp]
 297             p.sp--
 298         case mul:
 299             p.stack[sp-1] *= p.stack[sp]
 300             p.sp--
 301         case div:
 302             p.stack[sp-1] /= p.stack[sp]
 303             p.sp--
 304         case mod:
 305             p.stack[sp-1] = math.Mod(p.stack[sp-1], p.stack[sp])
 306             p.sp--
 307         case pow:
 308             p.stack[sp-1] = math.Pow(p.stack[sp-1], p.stack[sp])
 309             p.sp--
 310 
 311         // binary boolean ops / binary comparisons
 312         case and:
 313             p.stack[sp-1] = deboolAnd(p.stack[sp-1], p.stack[sp])
 314             p.sp--
 315         case or:
 316             p.stack[sp-1] = deboolOr(p.stack[sp-1], p.stack[sp])
 317             p.sp--
 318         case equal:
 319             p.stack[sp-1] = debool(p.stack[sp-1] == p.stack[sp])
 320             p.sp--
 321         case notequal:
 322             p.stack[sp-1] = debool(p.stack[sp-1] != p.stack[sp])
 323             p.sp--
 324         case less:
 325             p.stack[sp-1] = debool(p.stack[sp-1] < p.stack[sp])
 326             p.sp--
 327         case lessoreq:
 328             p.stack[sp-1] = debool(p.stack[sp-1] <= p.stack[sp])
 329             p.sp--
 330         case more:
 331             p.stack[sp-1] = debool(p.stack[sp-1] > p.stack[sp])
 332             p.sp--
 333         case moreoreq:
 334             p.stack[sp-1] = debool(p.stack[sp-1] >= p.stack[sp])
 335             p.sp--
 336 
 337         // function calls
 338         case call0:
 339             f := p.funcs[op.Index].(func() float64)
 340             // store above top of stack
 341             p.stack[sp+1] = f()
 342             p.sp++
 343         case call1:
 344             f := p.funcs[op.Index].(func(float64) float64)
 345             p.stack[sp] = f(p.stack[sp])
 346         case call2:
 347             f := p.funcs[op.Index].(func(float64, float64) float64)
 348             p.stack[sp-1] = f(p.stack[sp-1], p.stack[sp])
 349             p.sp--
 350         case call3:
 351             f := p.funcs[op.Index].(func(float64, float64, float64) float64)
 352             p.stack[sp-2] = f(p.stack[sp-2], p.stack[sp-1], p.stack[sp])
 353             p.sp -= 2
 354         case call4:
 355             f := p.funcs[op.Index].(func4)
 356             st := p.stack
 357             p.stack[sp-3] = f(st[sp-3], st[sp-2], st[sp-1], st[sp])
 358             p.sp -= 3
 359         case call5:
 360             f := p.funcs[op.Index].(func5)
 361             st := p.stack
 362             p.stack[sp-4] = f(st[sp-4], st[sp-3], st[sp-2], st[sp-1], st[sp])
 363             p.sp -= 4
 364         case callv:
 365             i := sp - int(op.NumArgs) + 1
 366             f := p.funcs[op.Index].(func(...float64) float64)
 367             p.stack[sp-i+1] = f(p.stack[i : sp+1]...)
 368             p.sp = sp - i + 1
 369         case call1v:
 370             i := sp - int(op.NumArgs) + 1
 371             f := p.funcs[op.Index].(func(float64, ...float64) float64)
 372             p.stack[sp-i+1] = f(p.stack[i], p.stack[i+1:sp+1]...)
 373             p.sp = sp - i + 1
 374         }
 375     }
 376 }
 377 
 378 // debool is only used to turn boolean values used in comparison operations
 379 // into float64s, since those are the only type accepted on a program stack
 380 func debool(b bool) float64 {
 381     if b {
 382         return 1
 383     }
 384     return 0
 385 }
 386 
 387 // deboolNot runs the basic `not` operation
 388 func deboolNot(x float64) float64 {
 389     return debool(x == 0)
 390 }
 391 
 392 // deboolAnd runs the basic `and` operation
 393 func deboolAnd(x, y float64) float64 {
 394     return debool(x != 0 && y != 0)
 395 }
 396 
 397 // deboolOr runs the basic `or` operation
 398 func deboolOr(x, y float64) float64 {
 399     return debool(x != 0 || y != 0)
 400 }
     File: ./fmscripts/programs_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import "testing"
  28 
  29 func TestCallVarFuncs(t *testing.T) {
  30     tests := []struct {
  31         name string
  32         src  string
  33         exp  float64
  34     }{
  35         {"check 1 var arg", "numargs(34)", 1},
  36         {"check 2 var arg", "numargs(34, 33)", 2},
  37         {"check 3 var arg", "numargs(34, 322, 0.3)", 3},
  38         {"check 4 var arg", "numargs(34, -1, 553, 42)", 4},
  39         {"check 5 var arg", "numargs(34, 3, 4, 5, 1)", 5},
  40     }
  41 
  42     numargs := func(args ...float64) float64 { return float64(len(args)) }
  43 
  44     for _, tc := range tests {
  45         t.Run(tc.name, func(t *testing.T) {
  46             var c Compiler
  47             p, err := c.Compile(tc.src, map[string]any{
  48                 "numargs": numargs,
  49             })
  50             if err != nil {
  51                 t.Fatal(err)
  52                 return
  53             }
  54 
  55             got := p.Run()
  56             const fs = "failed check (var-arg): expected %f, got %f instead"
  57             if got != tc.exp {
  58                 t.Fatalf(fs, tc.exp, got)
  59                 return
  60             }
  61         })
  62     }
  63 }
     File: ./fmscripts/random.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29     "math/rand"
  30 )
  31 
  32 // These funcs are kept separate from the larger lookup table so they aren't
  33 // mistaken for deterministic funcs which can be optimized away.
  34 //
  35 // var randomFuncs = map[string]any{
  36 //  "rand":   Random,
  37 //  "rint":   RandomInt,
  38 //  "runif":  RandomUnif,
  39 //  "rexp":   RandomExp,
  40 //  "rnorm":  RandomNorm,
  41 //  "rgamma": RandomGamma,
  42 //  "rbeta":  RandomBeta,
  43 // }
  44 
  45 func Random(r *rand.Rand) float64 {
  46     return r.Float64()
  47 }
  48 
  49 func RandomInt(r *rand.Rand, min, max float64) float64 {
  50     fmin := math.Trunc(min)
  51     fmax := math.Trunc(max)
  52     if fmin == fmax {
  53         return fmin
  54     }
  55 
  56     diff := math.Abs(fmax - fmin)
  57     return float64(r.Intn(int(diff)+1)) + fmin
  58 }
  59 
  60 func RandomUnif(r *rand.Rand, min, max float64) float64 {
  61     return (max-min)*r.Float64() + min
  62 }
  63 
  64 func RandomExp(r *rand.Rand, scale float64) float64 {
  65     return scale * r.ExpFloat64()
  66 }
  67 
  68 func RandomNorm(r *rand.Rand, mu, sigma float64) float64 {
  69     return sigma*r.NormFloat64() + mu
  70 }
  71 
  72 // Gamma generates a gamma-distributed real value, using a scale parameter.
  73 //
  74 // The algorithm is from Marsaglia and Tsang, as described in
  75 //
  76 //  A simple method for generating gamma variables
  77 //  https://dl.acm.org/doi/10.1145/358407.358414
  78 func RandomGamma(r *rand.Rand, scale float64) float64 {
  79     d := scale - 1.0/3.0
  80     c := 1 / math.Sqrt(9/d)
  81 
  82     for {
  83         // generate candidate value
  84         var x, v float64
  85         for {
  86             x = r.NormFloat64()
  87             v = 1 + c*x
  88             if v > 0 {
  89                 break
  90             }
  91         }
  92         v = v * v * v
  93 
  94         // accept or reject candidate value
  95         x2 := x * x
  96         u := r.Float64()
  97         if u < 1-0.0331*x2*x2 {
  98             return d * v
  99         }
 100         if math.Log(u) < 0.5*x2+d*(1-v+math.Log(v)) {
 101             return d * v
 102         }
 103     }
 104 }
 105 
 106 // Beta generates a beta-distributed real value.
 107 func RandomBeta(r *rand.Rand, a, b float64) float64 {
 108     return RandomGamma(r, a) / (RandomGamma(r, a) + RandomGamma(r, b))
 109 }
     File: ./fmscripts/tokens.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "strings"
  31     "unicode"
  32     "unicode/utf8"
  33 )
  34 
  35 // tokenType is the specific type of a token; tokens never represent
  36 // whitespace-like text between recognized tokens
  37 type tokenType int
  38 
  39 const (
  40     // default zero-value type for tokens
  41     unknownToken tokenType = iota
  42 
  43     // a name
  44     identifierToken tokenType = iota
  45 
  46     // a literal numeric value
  47     numberToken tokenType = iota
  48 
  49     // any syntactic element made of 1 or more runes
  50     syntaxToken tokenType = iota
  51 )
  52 
  53 // errEOS signals the end of source code, and is the only token-related error
  54 // which the parser should ignore
  55 var errEOS = errors.New(`no more source code`)
  56 
  57 // token is either a name, value, or syntactic element coming from a script's
  58 // source code
  59 type token struct {
  60     kind  tokenType
  61     value string
  62     line  int
  63     pos   int
  64 }
  65 
  66 // tokenizer splits a string into a stream tokens, via its `next` method
  67 type tokenizer struct {
  68     cur     string
  69     linenum int
  70     linepos int
  71 }
  72 
  73 // newTokenizer is the constructor for type tokenizer
  74 func newTokenizer(src string) tokenizer {
  75     return tokenizer{
  76         cur:     src,
  77         linenum: 1,
  78         linepos: 1,
  79     }
  80 }
  81 
  82 // next advances the tokenizer, giving back a token, unless it's done
  83 func (t *tokenizer) next() (token, error) {
  84     // label to allow looping back after skipping comments, and thus avoid
  85     // an explicit tail-recursion for each commented line
  86 rerun:
  87 
  88     // always ignore any whitespace-like source
  89     if err := t.skipWhitespace(); err != nil {
  90         return token{}, err
  91     }
  92 
  93     if len(t.cur) == 0 {
  94         return token{}, errEOS
  95     }
  96 
  97     // remember starting position, in case of error
  98     line := t.linenum
  99     pos := t.linepos
 100 
 101     // use the leading rune to probe what's next
 102     r, _ := utf8.DecodeRuneInString(t.cur)
 103 
 104     switch r {
 105     case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 106         s, err := t.scanNumber()
 107         return token{kind: numberToken, value: s, line: line, pos: pos}, err
 108 
 109     case '(', ')', '[', ']', '{', '}', ',', '+', '-', '%', '^', '~', '@', ';':
 110         s := t.cur[:1]
 111         t.cur = t.cur[1:]
 112         t.linepos++
 113         res := token{kind: syntaxToken, value: s, line: line, pos: pos}
 114         return res, t.eos()
 115 
 116     case ':':
 117         s, err := t.tryPrefixes(`:=`, `:`)
 118         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 119 
 120     case '*':
 121         s, err := t.tryPrefixes(`**`, `*`)
 122         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 123 
 124     case '/':
 125         s, err := t.tryPrefixes(`//`, `/*`, `/`)
 126         // double-slash starts a comment until the end of the line
 127         if s == `//` {
 128             t.skipLine()
 129             goto rerun
 130         }
 131         // handle comments which can span multiple lines
 132         if s == `/*` {
 133             err := t.skipComment()
 134             if err != nil {
 135                 return token{}, err
 136             }
 137             goto rerun
 138         }
 139         // handle division
 140         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 141 
 142     case '#':
 143         // even hash starts a comment until the end of the line, making the
 144         // syntax more Python-like
 145         t.skipLine()
 146         goto rerun
 147 
 148     case '&':
 149         s, err := t.tryPrefixes(`&&`, `&`)
 150         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 151 
 152     case '|':
 153         s, err := t.tryPrefixes(`||`, `|`)
 154         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 155 
 156     case '?':
 157         s, err := t.tryPrefixes(`??`, `?.`, `?`)
 158         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 159 
 160     case '.':
 161         r, _ := utf8.DecodeRuneInString(t.cur[1:])
 162         if '0' <= r && r <= '9' {
 163             s, err := t.scanNumber()
 164             res := token{kind: numberToken, value: s, line: line, pos: pos}
 165             return res, err
 166         }
 167         s, err := t.tryPrefixes(`...`, `..`, `.?`, `.`)
 168         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 169 
 170     case '=':
 171         // triple-equal makes the syntax even more JavaScript-like
 172         s, err := t.tryPrefixes(`===`, `==`, `=>`, `=`)
 173         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 174 
 175     case '!':
 176         // not-double-equal makes the syntax even more JavaScript-like
 177         s, err := t.tryPrefixes(`!==`, `!=`, `!`)
 178         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 179 
 180     case '<':
 181         // the less-more/diamond syntax is a SQL-like way to say not equal
 182         s, err := t.tryPrefixes(`<=`, `<>`, `<`)
 183         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 184 
 185     case '>':
 186         s, err := t.tryPrefixes(`>=`, `>`)
 187         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 188 
 189     default:
 190         if isIdentStartRune(r) {
 191             s, err := t.scanIdentifier()
 192             res := token{kind: identifierToken, value: s, line: line, pos: pos}
 193             return res, err
 194         }
 195         const fs = `line %d: pos %d: unexpected symbol %c`
 196         return token{}, fmt.Errorf(fs, t.linenum, t.linepos, r)
 197     }
 198 }
 199 
 200 // tryPrefixes tries to greedily match any prefix in the order given: when all
 201 // candidates fail, an empty string is returned; when successful, the tokenizer
 202 // updates its state to account for the matched prefix
 203 func (t *tokenizer) tryPrefixes(prefixes ...string) (string, error) {
 204     for _, pre := range prefixes {
 205         if strings.HasPrefix(t.cur, pre) {
 206             t.linepos += len(pre)
 207             t.cur = t.cur[len(pre):]
 208             return pre, t.eos()
 209         }
 210     }
 211 
 212     return ``, t.eos()
 213 }
 214 
 215 // skipWhitespace updates the tokenizer to ignore runs of consecutive whitespace
 216 // symbols: these are the likes of space, tab, newline, carriage return, etc.
 217 func (t *tokenizer) skipWhitespace() error {
 218     for len(t.cur) > 0 {
 219         r, size := utf8.DecodeRuneInString(t.cur)
 220         if !unicode.IsSpace(r) {
 221             // no more spaces to skip
 222             return nil
 223         }
 224 
 225         t.cur = t.cur[size:]
 226         if r == '\n' {
 227             // reached the next line
 228             t.linenum++
 229             t.linepos = 1
 230             continue
 231         }
 232         // continuing on the same line
 233         t.linepos++
 234     }
 235 
 236     // source code ended
 237     return errEOS
 238 }
 239 
 240 // skipLine updates the tokenizer to the end of the current line, or the end of
 241 // the source code, if it's the last line
 242 func (t *tokenizer) skipLine() error {
 243     for len(t.cur) > 0 {
 244         r, size := utf8.DecodeRuneInString(t.cur)
 245         t.cur = t.cur[size:]
 246         if r == '\n' {
 247             // reached the next line, as expected
 248             t.linenum++
 249             t.linepos = 1
 250             return nil
 251         }
 252     }
 253 
 254     // source code ended
 255     t.linenum++
 256     t.linepos = 1
 257     return errEOS
 258 }
 259 
 260 // skipComment updates the tokenizer to the end of the comment started with a
 261 // `/*` and ending with a `*/`, or to the end of the source code
 262 func (t *tokenizer) skipComment() error {
 263     var prev rune
 264     for len(t.cur) > 0 {
 265         r, size := utf8.DecodeRuneInString(t.cur)
 266         t.cur = t.cur[size:]
 267 
 268         if r == '\n' {
 269             t.linenum++
 270             t.linepos = 1
 271         } else {
 272             t.linepos++
 273         }
 274 
 275         if prev == '*' && r == '/' {
 276             return nil
 277         }
 278         prev = r
 279     }
 280 
 281     // source code ended
 282     const msg = "comment not ended with a `*/` sequence"
 283     return errors.New(msg)
 284 }
 285 
 286 func (t *tokenizer) scanIdentifier() (string, error) {
 287     end := 0
 288     for len(t.cur) > 0 {
 289         r, size := utf8.DecodeRuneInString(t.cur[end:])
 290         if (end == 0 && !isIdentStartRune(r)) || !isIdentRestRune(r) {
 291             // identifier ended, and there's more source code after it
 292             name := t.cur[:end]
 293             t.cur = t.cur[end:]
 294             return name, nil
 295         }
 296         end += size
 297         t.linepos++
 298     }
 299 
 300     // source code ended with an identifier name
 301     name := t.cur
 302     t.cur = ``
 303     return name, nil
 304 }
 305 
 306 func (t *tokenizer) scanNumber() (string, error) {
 307     dots := 0
 308     end := 0
 309     var prev rune
 310 
 311     for len(t.cur) > 0 {
 312         r, size := utf8.DecodeRuneInString(t.cur[end:])
 313 
 314         switch r {
 315         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', 'e':
 316             end += size
 317             t.linepos++
 318             prev = r
 319 
 320         case '+', '-':
 321             if end > 0 && prev != 'e' {
 322                 // number ended, and there's more source code after it
 323                 num := t.cur[:end]
 324                 t.cur = t.cur[end:]
 325                 return num, nil
 326             }
 327             end += size
 328             t.linepos++
 329             prev = r
 330 
 331         case '.':
 332             nr, _ := utf8.DecodeRuneInString(t.cur[end+size:])
 333             if dots == 1 || isIdentStartRune(nr) || unicode.IsSpace(nr) || nr == '.' {
 334                 // number ended, and there's more source code after it
 335                 num := t.cur[:end]
 336                 t.cur = t.cur[end:]
 337                 return num, nil
 338             }
 339             dots++
 340             end += size
 341             t.linepos++
 342             prev = r
 343 
 344         default:
 345             // number ended, and there's more source code after it
 346             num := t.cur[:end]
 347             t.cur = t.cur[end:]
 348             return num, nil
 349         }
 350     }
 351 
 352     // source code ended with a number
 353     name := t.cur
 354     t.cur = ``
 355     return name, nil
 356 }
 357 
 358 // eos checks if the source-code is over: if so, it returns an end-of-file error,
 359 // or a nil error otherwise
 360 func (t *tokenizer) eos() error {
 361     if len(t.cur) == 0 {
 362         return errEOS
 363     }
 364     return nil
 365 }
 366 
 367 func isIdentStartRune(r rune) bool {
 368     return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || r == '_'
 369 }
 370 
 371 func isIdentRestRune(r rune) bool {
 372     return isIdentStartRune(r) || ('0' <= r && r <= '9')
 373 }
     File: ./fmscripts/tokens_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "reflect"
  29     "testing"
  30 )
  31 
  32 func TestTokenizer(t *testing.T) {
  33     tests := map[string][]string{
  34         ``:        nil,
  35         `3.`:      []string{`3.`},
  36         `3.2`:     []string{`3.2`},
  37         `-3.2`:    []string{`-`, `3.2`},
  38         `-3.2+56`: []string{`-`, `3.2`, `+`, `56`},
  39     }
  40 
  41     for script, expected := range tests {
  42         t.Run(script, func(t *testing.T) {
  43             tok := newTokenizer(script)
  44             par, err := newParser(&tok)
  45             if err != nil {
  46                 t.Fatal(err)
  47                 return
  48             }
  49 
  50             var got []string
  51             for _, v := range par.tokens {
  52                 got = append(got, v.value)
  53             }
  54 
  55             if !reflect.DeepEqual(got, expected) {
  56                 const fs = "from %s\nexpected\n%#v\nbut got\n%#v\ninstead"
  57                 t.Fatalf(fs, script, expected, got)
  58                 return
  59             }
  60         })
  61     }
  62 }
     File: ./folders/folders.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package folders
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "io/fs"
  31     "os"
  32     "path/filepath"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 folders [options...] [folders...]
  38 
  39 Find/list all folders in the folders given, without repetitions.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44     -t, -top     turn off recursive behavior; top-level entries only
  45 `
  46 
  47 func Main() {
  48     top := false
  49     buffered := false
  50     args := os.Args[1:]
  51 
  52     for len(args) > 0 {
  53         switch args[0] {
  54         case `-b`, `--b`, `-buffered`, `--buffered`:
  55             buffered = true
  56             args = args[1:]
  57             continue
  58 
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62 
  63         case `-t`, `--t`, `-top`, `--top`:
  64             top = true
  65             args = args[1:]
  66             continue
  67         }
  68 
  69         break
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     liveLines := !buffered
  77     if !buffered {
  78         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  79             liveLines = false
  80         }
  81     }
  82 
  83     var cfg config
  84     cfg.got = make(map[string]struct{})
  85     cfg.recursive = !top
  86     cfg.liveLines = liveLines
  87 
  88     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  89         os.Stderr.WriteString(`folders: `)
  90         os.Stderr.WriteString(err.Error())
  91         os.Stderr.WriteString("\n")
  92         os.Exit(1)
  93         return
  94     }
  95 }
  96 
  97 type config struct {
  98     got       map[string]struct{}
  99     recursive bool
 100     liveLines bool
 101 }
 102 
 103 func run(w io.Writer, paths []string, cfg config) error {
 104     bw := bufio.NewWriterSize(w, 32*1024)
 105     defer bw.Flush()
 106 
 107     if len(paths) == 0 {
 108         paths = []string{`.`}
 109     }
 110 
 111     if cfg.recursive {
 112         return runRecursive(bw, paths, cfg)
 113     }
 114     return runFlat(bw, paths, cfg)
 115 }
 116 
 117 func runRecursive(w *bufio.Writer, paths []string, cfg config) error {
 118     if len(paths) == 0 {
 119         paths = []string{`.`}
 120     }
 121 
 122     // handle is the callback for func filepath.WalkDir
 123     handle := func(path string, e fs.DirEntry, err error) error {
 124         if err != nil {
 125             return err
 126         }
 127 
 128         if _, ok := cfg.got[path]; ok {
 129             return nil
 130         }
 131         cfg.got[path] = struct{}{}
 132 
 133         if e.IsDir() {
 134             if err := handleEntry(w, path, cfg.liveLines); err != nil {
 135                 return err
 136             }
 137         }
 138 
 139         return nil
 140     }
 141 
 142     for _, path := range paths {
 143         if _, ok := cfg.got[path]; ok {
 144             continue
 145         }
 146         cfg.got[path] = struct{}{}
 147 
 148         st, err := os.Stat(path)
 149         if err != nil {
 150             return err
 151         }
 152 
 153         if !strings.HasSuffix(path, `/`) {
 154             path = path + `/`
 155             cfg.got[path] = struct{}{}
 156         }
 157 
 158         if !st.IsDir() {
 159             continue
 160         }
 161 
 162         if err := filepath.WalkDir(path, handle); err != nil {
 163             return err
 164         }
 165     }
 166 
 167     return nil
 168 }
 169 
 170 func runFlat(w *bufio.Writer, paths []string, cfg config) error {
 171     if len(paths) == 0 {
 172         paths = []string{`.`}
 173     }
 174 
 175     for _, path := range paths {
 176         if _, ok := cfg.got[path]; ok {
 177             continue
 178         }
 179         cfg.got[path] = struct{}{}
 180 
 181         st, err := os.Stat(path)
 182         if err != nil {
 183             return err
 184         }
 185 
 186         if !strings.HasSuffix(path, `/`) {
 187             path = path + `/`
 188             cfg.got[path] = struct{}{}
 189         }
 190 
 191         if !st.IsDir() {
 192             continue
 193         }
 194 
 195         entries, err := os.ReadDir(path)
 196         if err != nil {
 197             return err
 198         }
 199 
 200         for _, e := range entries {
 201             if !e.IsDir() {
 202                 continue
 203             }
 204 
 205             path := filepath.Join(path, e.Name())
 206             if err := handleEntry(w, path, cfg.liveLines); err != nil {
 207                 return err
 208             }
 209         }
 210     }
 211 
 212     return nil
 213 }
 214 
 215 func handleEntry(w *bufio.Writer, path string, live bool) error {
 216     abs, err := filepath.Abs(path)
 217     if err != nil {
 218         return err
 219     }
 220 
 221     w.WriteString(abs)
 222     w.WriteByte('\n')
 223 
 224     if live && w.Flush() != nil {
 225         return io.EOF
 226     }
 227     return nil
 228 }
     File: ./gsub/gsub.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package gsub
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 gsub [options...] [regex / replacement pairs...]
  38 
  39 
  40 Named after the AWK function 'gsub' (global substitute), this tool replaces
  41 all matches found with the substitution pattern associated to it.
  42 
  43 Regexes and replacements are given as pairs. Input always comes from standard
  44 input. The regular-expression mode used is "re2", which is a superset of the
  45 commonly-used "extended-mode".
  46 
  47 All ANSI-style sequences are removed before trying to replace all matches, to
  48 avoid messing those up. Each regex replaces all its occurrences on the current
  49 line in the order given among the arguments, so regex-order matters.
  50 
  51 As with the AWK function 'gsub', any '&' symbols substitute into the substring
  52 matched, except for any '&' preceded by a backslash.
  53 
  54 The options are, available both in single and double-dash versions
  55 
  56     -h, -help     show this help message
  57     -e, -erase    all arguments are regexes which are replaced with nothing
  58     -i, -ins      match regexes case-insensitively
  59 `
  60 
  61 type pair struct {
  62     expr *regexp.Regexp
  63     repl []string
  64 }
  65 
  66 func Main() {
  67     args := os.Args[1:]
  68     erase := false
  69     buffered := false
  70     insensitive := false
  71 
  72     for len(args) > 0 {
  73         switch args[0] {
  74         case `-b`, `--b`, `-buffered`, `--buffered`:
  75             buffered = true
  76             args = args[1:]
  77             continue
  78 
  79         case `-e`, `--e`, `-erase`, `--erase`:
  80             erase = true
  81             args = args[1:]
  82             continue
  83 
  84         case `-h`, `--h`, `-help`, `--help`:
  85             os.Stdout.WriteString(info[1:])
  86             return
  87 
  88         case `-i`, `--i`, `-ins`, `--ins`:
  89             insensitive = true
  90             args = args[1:]
  91             continue
  92         }
  93 
  94         break
  95     }
  96 
  97     if len(args) > 0 && args[0] == `--` {
  98         args = args[1:]
  99     }
 100 
 101     errcount := 0
 102     pairs := make([]pair, 0, (len(args)+1)/2)
 103 
 104     for len(args) > 0 {
 105         var err error
 106         var what *regexp.Regexp
 107 
 108         s := args[0]
 109         if insensitive {
 110             what, err = regexp.Compile(`(?i)` + s)
 111         } else {
 112             what, err = regexp.Compile(s)
 113         }
 114 
 115         if err != nil {
 116             os.Stderr.WriteString(`gsub: `)
 117             os.Stderr.WriteString(err.Error())
 118             os.Stderr.WriteString("\n")
 119             errcount++
 120             continue
 121         }
 122 
 123         args = args[1:]
 124         var with []string
 125         if !erase && len(args) > 0 {
 126             with = splitReplacement(args[0])
 127             args = args[1:]
 128         }
 129         pairs = append(pairs, pair{what, with})
 130     }
 131 
 132     // quit right away when given invalid regexes
 133     if errcount > 0 {
 134         os.Exit(1)
 135         return
 136     }
 137 
 138     liveLines := !buffered
 139     if !buffered {
 140         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 141             liveLines = false
 142         }
 143     }
 144 
 145     err := run(os.Stdout, os.Stdin, pairs, liveLines)
 146     if err != nil && err != io.EOF {
 147         os.Stderr.WriteString(`gsub: `)
 148         os.Stderr.WriteString(err.Error())
 149         os.Stderr.WriteString("\n")
 150         os.Exit(1)
 151         return
 152     }
 153 }
 154 
 155 func splitReplacement(s string) []string {
 156     if s == `` {
 157         return nil
 158     }
 159 
 160     n := 1
 161     var prev rune
 162     for _, r := range s {
 163         if prev != '\\' && r == '&' {
 164             n += 2
 165         }
 166         prev = r
 167     }
 168 
 169     repl := make([]string, 0, n)
 170 
 171     for len(s) > 0 {
 172         i := strings.IndexByte(s, '&')
 173         if i == 0 || (i > 0 && s[i-1] != '\\') {
 174             repl = append(repl, unescapeBackslashes(s[:i]))
 175             repl = append(repl, ``)
 176             s = s[i+1:]
 177             continue
 178         }
 179         break
 180     }
 181 
 182     if len(s) > 0 {
 183         repl = append(repl, unescapeBackslashes(s))
 184     }
 185     return repl
 186 }
 187 
 188 func unescapeBackslashes(s string) string {
 189     if strings.IndexByte(s, '\\') < 0 {
 190         return s
 191     }
 192 
 193     var prev byte
 194     unesc := make([]byte, 0, len(s))
 195 
 196     for i := range s {
 197         b := s[i]
 198 
 199         if prev != '\\' {
 200             if b != '\\' {
 201                 unesc = append(unesc, s[i])
 202             }
 203             prev = b
 204             continue
 205         }
 206 
 207         switch b {
 208         case 'e':
 209             unesc = append(unesc, '\x1b')
 210         case 'n':
 211             unesc = append(unesc, '\n')
 212         case 'r':
 213             unesc = append(unesc, '\r')
 214         case 't':
 215             unesc = append(unesc, '\t')
 216         case 'v':
 217             unesc = append(unesc, '\v')
 218         default:
 219             unesc = append(unesc, s[i])
 220         }
 221 
 222         prev = b
 223     }
 224 
 225     return string(unesc)
 226 }
 227 
 228 func run(w io.Writer, r io.Reader, pairs []pair, live bool) error {
 229     var buf []byte
 230     sc := bufio.NewScanner(r)
 231     sc.Buffer(nil, 8*1024*1024*1024)
 232     bw := bufio.NewWriterSize(w, 32*1024)
 233     defer bw.Flush()
 234 
 235     src := make([]byte, 8*1024)
 236     dst := make([]byte, 8*1024)
 237 
 238     for i := 0; sc.Scan(); i++ {
 239         line := sc.Bytes()
 240         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 241             line = line[3:]
 242         }
 243 
 244         s := line
 245         if bytes.IndexByte(s, '\x1b') >= 0 {
 246             buf = plain(buf[:0], s)
 247             s = buf
 248         }
 249 
 250         if len(pairs) > 0 {
 251             src = append(src[:0], s...)
 252             for _, p := range pairs {
 253                 dst = gsub(dst[:0], src, p.expr, p.repl)
 254                 src = append(src[:0], dst...)
 255             }
 256             bw.Write(dst)
 257         } else {
 258             bw.Write(s)
 259         }
 260 
 261         if bw.WriteByte('\n') != nil {
 262             return io.EOF
 263         }
 264 
 265         if live && bw.Flush() != nil {
 266             return io.EOF
 267         }
 268     }
 269 
 270     return sc.Err()
 271 }
 272 
 273 func gsub(dst []byte, src []byte, what *regexp.Regexp, with []string) []byte {
 274     for len(src) > 0 {
 275         span := what.FindIndex(src)
 276         // also ignore empty regex matches to avoid infinite outer loops,
 277         // as skipping empty slices isn't advancing at all, leaving the
 278         // string stuck to being empty-matched forever by the same regex
 279         if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
 280             return append(dst, src...)
 281         }
 282 
 283         start, end := span[0], span[1]
 284         dst = append(dst, src[:start]...)
 285         // avoid infinite loops caused by empty regex matches
 286         if start == end {
 287             if end >= len(src) {
 288                 break
 289             }
 290             dst = append(dst, src[end])
 291             end++
 292             src = src[end:]
 293             continue
 294         }
 295 
 296         match := src[start:end]
 297         for _, sub := range with {
 298             if sub == `` {
 299                 dst = append(dst, match...)
 300                 continue
 301             }
 302             dst = append(dst, sub...)
 303         }
 304 
 305         src = src[end:]
 306     }
 307 
 308     return dst
 309 }
 310 
 311 func plain(dst []byte, src []byte) []byte {
 312     for len(src) > 0 {
 313         i, j := indexEscapeSequence(src)
 314         if i < 0 {
 315             dst = append(dst, src...)
 316             break
 317         }
 318         if j < 0 {
 319             j = len(src)
 320         }
 321 
 322         if i > 0 {
 323             dst = append(dst, src[:i]...)
 324         }
 325 
 326         src = src[j:]
 327     }
 328 
 329     return dst
 330 }
 331 
 332 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 333 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 334 // indices which can be independently negative when either the start/end of
 335 // a sequence isn't found; given their fairly-common use, even the hyperlink
 336 // ESC]8 sequences are supported
 337 func indexEscapeSequence(s []byte) (int, int) {
 338     var prev byte
 339 
 340     for i, b := range s {
 341         if prev == '\x1b' && b == '[' {
 342             j := indexLetter(s[i+1:])
 343             if j < 0 {
 344                 return i, -1
 345             }
 346             return i - 1, i + 1 + j + 1
 347         }
 348 
 349         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 350             j := indexPair(s[i+1:], '\x1b', '\\')
 351             if j < 0 {
 352                 return i, -1
 353             }
 354             return i - 1, i + 1 + j + 2
 355         }
 356 
 357         prev = b
 358     }
 359 
 360     return -1, -1
 361 }
 362 
 363 func indexLetter(s []byte) int {
 364     for i, b := range s {
 365         upper := b &^ 32
 366         if 'A' <= upper && upper <= 'Z' {
 367             return i
 368         }
 369     }
 370 
 371     return -1
 372 }
 373 
 374 func indexPair(s []byte, x byte, y byte) int {
 375     var prev byte
 376 
 377     for i, b := range s {
 378         if prev == x && b == y && i > 0 {
 379             return i
 380         }
 381         prev = b
 382     }
 383 
 384     return -1
 385 }
     File: ./head/head.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package head
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 head [options...] [files...]
  37 
  38 Keep at most the first n lines, or keep the first 10 lines by default. When
  39 not given any filepaths, the standard input is used instead.
  40 
  41 Options
  42 
  43     -n [number]    change max number of lines (default is 10)
  44 `
  45 
  46 type config struct {
  47     max       int
  48     liveLines bool
  49 }
  50 
  51 func Main() {
  52     var cfg config
  53     cfg.max = 10
  54     cfg.liveLines = true
  55 
  56     args := os.Args[1:]
  57     for len(args) > 0 {
  58         switch args[0] {
  59         case `-b`, `--b`, `-buffered`, `--buffered`:
  60             cfg.liveLines = false
  61             args = args[1:]
  62             continue
  63 
  64         case `-n`:
  65             args = args[1:]
  66             if len(args) == 0 {
  67                 os.Stderr.WriteString("missing number of lines\n")
  68                 os.Exit(1)
  69                 return
  70             }
  71 
  72             s := strings.Replace(args[0], `_`, ``, -1)
  73             n, err := strconv.ParseInt(s, 10, 64)
  74             if err != nil {
  75                 os.Stderr.WriteString(`head: invalid number: `)
  76                 os.Stderr.WriteString(err.Error())
  77                 os.Stderr.WriteString("\n")
  78                 os.Exit(1)
  79                 return
  80             }
  81 
  82             args = args[1:]
  83             cfg.max = int(n)
  84             continue
  85 
  86         case `--help`:
  87             os.Stderr.WriteString(info[1:])
  88             return
  89         }
  90 
  91         break
  92     }
  93 
  94     if len(args) > 0 && args[0] == `--` {
  95         args = args[1:]
  96     }
  97 
  98     if cfg.max <= 0 {
  99         return
 100     }
 101 
 102     if cfg.liveLines {
 103         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 104             cfg.liveLines = false
 105         }
 106     }
 107 
 108     if err := run(args, &cfg); err != nil && err != io.EOF {
 109         os.Stderr.WriteString(`head: `)
 110         os.Stderr.WriteString(err.Error())
 111         os.Stderr.WriteString("\n")
 112         os.Exit(1)
 113         return
 114     }
 115 }
 116 
 117 func run(paths []string, cfg *config) error {
 118     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 119     defer w.Flush()
 120 
 121     for _, path := range paths {
 122         if cfg.max <= 0 {
 123             return io.EOF
 124         }
 125         if err := handleFile(w, path, cfg); err != nil {
 126             return err
 127         }
 128     }
 129 
 130     if len(paths) == 0 {
 131         if err := head(w, os.Stdin, cfg); err != nil {
 132             return err
 133         }
 134     }
 135     return nil
 136 }
 137 
 138 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 139     f, err := os.Open(path)
 140     if err != nil {
 141         return err
 142     }
 143     defer f.Close()
 144     return head(w, f, cfg)
 145 }
 146 
 147 func head(w *bufio.Writer, r io.Reader, cfg *config) error {
 148     const gb = 1024 * 1024 * 1024
 149     sc := bufio.NewScanner(r)
 150     sc.Buffer(nil, 8*gb)
 151 
 152     for sc.Scan() {
 153         if cfg.max <= 0 {
 154             return io.EOF
 155         }
 156 
 157         w.Write(sc.Bytes())
 158         if w.WriteByte('\n') != nil {
 159             return io.EOF
 160         }
 161 
 162         cfg.max--
 163 
 164         if cfg.liveLines && w.Flush() != nil {
 165             return io.EOF
 166         }
 167     }
 168 
 169     return sc.Err()
 170 }
     File: ./hima/hima.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package hima
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 hima [options...] [regexes...]
  38 
  39 
  40 HIlight MAtches ANSI-styles matching regular expressions along lines read
  41 from the standard input. The regular-expression mode used is "re2", which
  42 is a superset of the commonly-used "extended-mode".
  43 
  44 Regexes always avoid matching any ANSI-style sequences, to avoid messing
  45 those up. Also, multiple matches in a line never overlap: at each step
  46 along a line, the earliest-starting match among the regexes always wins,
  47 as the order regexes are given among the arguments never matters.
  48 
  49 The options are, available both in single and double-dash versions
  50 
  51     -f, -filter              filter out (ignore) lines with no matches
  52     -h, -help                show this help message
  53     -i, -ins                 match regexes case-insensitively
  54     -l, -line, -w, -whole    highlight whole lines when matching
  55 `
  56 
  57 const highlightStyle = "\x1b[7m"
  58 
  59 type config struct {
  60     filter      bool
  61     insensitive bool
  62     whole       bool
  63     liveLines   bool
  64 }
  65 
  66 func Main() {
  67     var cfg config
  68     buffered := false
  69     args := os.Args[1:]
  70 
  71     for len(args) > 0 {
  72         switch args[0] {
  73         case `-b`, `--b`, `-buffered`, `--buffered`:
  74             buffered = true
  75             args = args[1:]
  76             continue
  77 
  78         case `-f`, `--f`, `-filter`, `--filter`, `-filtered`, `--filtered`:
  79             cfg.filter = true
  80             args = args[1:]
  81             continue
  82 
  83         case `-h`, `--h`, `-help`, `--help`:
  84             os.Stdout.WriteString(info[1:])
  85             return
  86 
  87         case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
  88             cfg.insensitive = true
  89             args = args[1:]
  90             continue
  91 
  92         case `-l`, `--l`, `-line`, `--line`, `-w`, `--w`, `-whole`, `--whole`:
  93             cfg.whole = true
  94             args = args[1:]
  95             continue
  96         }
  97 
  98         if s := args[0]; s == `-` || s == `--` || !strings.HasPrefix(s, `-`) {
  99             break
 100         }
 101 
 102         for s := args[0][1:]; len(s) > 0; s = s[1:] {
 103             switch s[0] {
 104             case 'b':
 105                 buffered = true
 106                 continue
 107             case 'f':
 108                 cfg.filter = true
 109                 continue
 110             case 'h':
 111                 os.Stdout.WriteString(info[1:])
 112                 return
 113             case 'i':
 114                 cfg.insensitive = true
 115                 continue
 116             case 'l':
 117                 cfg.whole = true
 118                 continue
 119             case 'w':
 120                 cfg.whole = true
 121                 continue
 122             default:
 123                 os.Stderr.WriteString(`hima: unsupported option `)
 124                 os.Stderr.WriteString(args[0])
 125                 os.Stderr.WriteString("\n")
 126                 os.Exit(1)
 127             }
 128         }
 129 
 130         args = args[1:]
 131     }
 132 
 133     if len(args) > 0 && args[0] == `--` {
 134         args = args[1:]
 135     }
 136 
 137     patterns := make([]pattern, 0, len(args))
 138 
 139     for _, s := range args {
 140         var err error
 141         var pat pattern
 142 
 143         if cfg.insensitive {
 144             pat, err = compile(`(?i)` + s)
 145         } else {
 146             pat, err = compile(s)
 147         }
 148 
 149         if err != nil {
 150             os.Stderr.WriteString(`hima: `)
 151             os.Stderr.WriteString(err.Error())
 152             os.Stderr.WriteString("\n")
 153             continue
 154         }
 155 
 156         patterns = append(patterns, pat)
 157     }
 158 
 159     // quit right away when given invalid regexes
 160     if len(patterns) < len(args) {
 161         os.Exit(1)
 162         return
 163     }
 164 
 165     cfg.liveLines = !buffered
 166     if !buffered {
 167         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 168             cfg.liveLines = false
 169         }
 170     }
 171 
 172     ok, err := run(os.Stdout, os.Stdin, patterns, cfg)
 173     if err != nil && err != io.EOF {
 174         os.Stderr.WriteString(`hima: `)
 175         os.Stderr.WriteString(err.Error())
 176         os.Stderr.WriteString("\n")
 177         os.Exit(1)
 178         return
 179     }
 180 
 181     if !ok {
 182         os.Exit(1)
 183         return
 184     }
 185 }
 186 
 187 // pattern is a regular-expression pattern which distinguishes between the
 188 // start/end of a line and those of the chunks it can be used to match
 189 type pattern struct {
 190     // expr is the regular-expression
 191     expr *regexp.Regexp
 192 
 193     // begin is whether the regexp refers to the start of a line
 194     begin bool
 195 
 196     // end is whether the regexp refers to the end of a line
 197     end bool
 198 }
 199 
 200 func compile(src string) (pattern, error) {
 201     expr, err := regexp.Compile(src)
 202 
 203     var pat pattern
 204     pat.expr = expr
 205     pat.begin = strings.HasPrefix(src, `^`) || strings.HasPrefix(src, `(?i)^`)
 206     pat.end = strings.HasSuffix(src, `$`) && !strings.HasSuffix(src, `\$`)
 207     return pat, err
 208 }
 209 
 210 func (p pattern) findIndex(s []byte, i int, last int) (start int, stop int) {
 211     if i > 0 && p.begin {
 212         return -1, -1
 213     }
 214     if i != last && p.end {
 215         return -1, -1
 216     }
 217 
 218     span := p.expr.FindIndex(s)
 219     // also ignore empty regex matches to avoid infinite outer loops,
 220     // as skipping empty slices isn't advancing at all, leaving the
 221     // string stuck to being empty-matched forever by the same regex
 222     if len(span) != 2 || span[0] == span[1] {
 223         return -1, -1
 224     }
 225 
 226     return span[0], span[1]
 227 }
 228 
 229 func run(w io.Writer, r io.Reader, pats []pattern, cfg config) (bool, error) {
 230     sc := bufio.NewScanner(r)
 231     sc.Buffer(nil, 8*1024*1024*1024)
 232     bw := bufio.NewWriterSize(w, 32*1024)
 233     defer bw.Flush()
 234 
 235     ok := !cfg.filter
 236 
 237     for i := 0; sc.Scan(); i++ {
 238         s := sc.Bytes()
 239         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 240             s = s[3:]
 241         }
 242 
 243         n := 0
 244         last := countChunks(s) - 1
 245         if last < 0 {
 246             last = 0
 247         }
 248 
 249         anyMatches := matches(s, pats, last)
 250 
 251         if cfg.filter && !anyMatches {
 252             continue
 253         }
 254         if cfg.filter {
 255             ok = true
 256         }
 257 
 258         if cfg.whole && anyMatches {
 259             handleWhole(bw, s)
 260             s = nil
 261         }
 262 
 263         for len(s) > 0 {
 264             i, j := indexEscapeSequence(s)
 265             if i < 0 {
 266                 handleChunk(bw, s, pats, n, last)
 267                 break
 268             }
 269             if j < 0 {
 270                 j = len(s)
 271             }
 272 
 273             handleChunk(bw, s[:i], pats, n, last)
 274             if i > 0 {
 275                 n++
 276             }
 277 
 278             bw.Write(s[i:j])
 279 
 280             s = s[j:]
 281         }
 282 
 283         if bw.WriteByte('\n') != nil {
 284             return ok, io.EOF
 285         }
 286 
 287         if cfg.liveLines && bw.Flush() != nil {
 288             return ok, io.EOF
 289         }
 290     }
 291 
 292     err := sc.Err()
 293     return err == nil && ok, err
 294 }
 295 
 296 // matches finds out if any regex matches any substring around ANSI-sequences
 297 func matches(s []byte, patterns []pattern, last int) bool {
 298     n := 0
 299 
 300     for len(s) > 0 {
 301         i, j := indexEscapeSequence(s)
 302         if i < 0 {
 303             for _, p := range patterns {
 304                 if begin, _ := p.findIndex(s, n, last); begin >= 0 {
 305                     return true
 306                 }
 307             }
 308             return false
 309         }
 310 
 311         if j < 0 {
 312             j = len(s)
 313         }
 314 
 315         for _, p := range patterns {
 316             if begin, _ := p.findIndex(s[:i], n, last); begin >= 0 {
 317                 return true
 318             }
 319         }
 320 
 321         if i > 0 {
 322             n++
 323         }
 324 
 325         s = s[j:]
 326     }
 327 
 328     return false
 329 }
 330 
 331 func countChunks(s []byte) int {
 332     chunks := 0
 333 
 334     for len(s) > 0 {
 335         i, j := indexEscapeSequence(s)
 336         if i < 0 {
 337             break
 338         }
 339 
 340         if i > 0 {
 341             chunks++
 342         }
 343 
 344         if j < 0 {
 345             break
 346         }
 347         s = s[j:]
 348     }
 349 
 350     if len(s) > 0 {
 351         chunks++
 352     }
 353     return chunks
 354 }
 355 
 356 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 357 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 358 // indices which can be independently negative when either the start/end of
 359 // a sequence isn't found; given their fairly-common use, even the hyperlink
 360 // ESC]8 sequences are supported
 361 func indexEscapeSequence(s []byte) (int, int) {
 362     var prev byte
 363 
 364     for i, b := range s {
 365         if prev == '\x1b' && b == '[' {
 366             j := indexLetter(s[i+1:])
 367             if j < 0 {
 368                 return i, -1
 369             }
 370             return i - 1, i + 1 + j + 1
 371         }
 372 
 373         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 374             j := indexPair(s[i+1:], '\x1b', '\\')
 375             if j < 0 {
 376                 return i, -1
 377             }
 378             return i - 1, i + 1 + j + 2
 379         }
 380 
 381         prev = b
 382     }
 383 
 384     return -1, -1
 385 }
 386 
 387 func indexLetter(s []byte) int {
 388     for i, b := range s {
 389         upper := b &^ 32
 390         if 'A' <= upper && upper <= 'Z' {
 391             return i
 392         }
 393     }
 394 
 395     return -1
 396 }
 397 
 398 func indexPair(s []byte, x byte, y byte) int {
 399     var prev byte
 400 
 401     for i, b := range s {
 402         if prev == x && b == y && i > 0 {
 403             return i
 404         }
 405         prev = b
 406     }
 407 
 408     return -1
 409 }
 410 
 411 // note: looking at the results of restoring ANSI-styles after style-resets
 412 // doesn't seem to be worth it, as a previous version used to do
 413 
 414 // handleChunk handles line-slices around any detected ANSI-style sequences,
 415 // or even whole lines, when no ANSI-styles are found in them
 416 func handleChunk(w *bufio.Writer, s []byte, with []pattern, n int, last int) {
 417     for len(s) > 0 {
 418         start, end := -1, -1
 419         for _, p := range with {
 420             i, j := p.findIndex(s, n, last)
 421             if i >= 0 && (i < start || start < 0) {
 422                 start, end = i, j
 423             }
 424         }
 425 
 426         if start < 0 {
 427             w.Write(s)
 428             return
 429         }
 430 
 431         w.Write(s[:start])
 432         w.WriteString(highlightStyle)
 433         w.Write(s[start:end])
 434         w.WriteString("\x1b[0m")
 435 
 436         s = s[end:]
 437     }
 438 }
 439 
 440 func handleWhole(w *bufio.Writer, s []byte) {
 441     if len(s) == 0 {
 442         return
 443     }
 444 
 445     w.WriteString(highlightStyle)
 446 
 447     for len(s) > 0 {
 448         i := bytes.Index(s, []byte{0x1b, '[', '0', 'm'})
 449         if i < 0 {
 450             w.Write(s)
 451             break
 452         }
 453 
 454         j := i + len("\x1b[0m")
 455         w.Write(s[:j])
 456         w.WriteString(highlightStyle)
 457 
 458         s = s[j:]
 459     }
 460 
 461     w.WriteString("\x1b[0m")
 462 }
     File: ./htmlify/htmlify.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package htmlify
  26 
  27 import (
  28     "bufio"
  29     "encoding/base64"
  30     "errors"
  31     "io"
  32     "os"
  33     "regexp"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 htmlify [options...] [filepaths/URIs...]
  39 
  40 
  41 Render plain-text prose into self-contained HTML. Lines which are just a
  42 valid data-URI are turned into pictures, audio, or even video elements.
  43 
  44 All HTTP(s) URIs are autodetected and rendered as hyperlinks, even when
  45 lines have multiple URIs in them.
  46 
  47 If a title isn't given from the cmd-line options, the first line is used
  48 as the title.
  49 
  50 All (optional) leading options start with either single or double-dash,
  51 and most of them change the style/color used. Some of the options are,
  52 shown in their single-dash form:
  53 
  54     -h, -help            show this help message
  55     -mono, -monospace    use a monospace font for text
  56     -t, -title           use the next argument as the webpage title
  57 `
  58 
  59 var links = regexp.MustCompile(`(?i)(https?|ftps?)://[a-zA-Z0-9_%.,?/&=#-]+`)
  60 
  61 type config struct {
  62     Title     string
  63     GotTitle  bool
  64     Started   bool
  65     Monospace bool
  66 }
  67 
  68 func Main() {
  69     var cfg config
  70     args := os.Args[1:]
  71 
  72     for len(args) > 0 {
  73         switch args[0] {
  74         case `-h`, `--h`, `-help`, `--help`:
  75             os.Stdout.WriteString(info[1:])
  76             return
  77 
  78         case `-mono`, `--mono`, `-monospace`, `--monospace`:
  79             cfg.Monospace = true
  80             args = args[1:]
  81             continue
  82 
  83         case `-t`, `--t`, `-title`, `--title`:
  84             if len(args) >= 2 {
  85                 cfg.Title = args[1]
  86                 cfg.GotTitle = true
  87                 args = args[2:]
  88             } else {
  89                 const m = "title option isn't followed by the actual title\n"
  90                 os.Stderr.WriteString(m)
  91                 os.Exit(1)
  92                 return
  93             }
  94             continue
  95         }
  96 
  97         break
  98     }
  99 
 100     if len(args) > 0 && args[0] == `--` {
 101         args = args[1:]
 102     }
 103 
 104     if err := run(os.Stdout, args, &cfg); err != nil && err != io.EOF {
 105         os.Stderr.WriteString(`htmlify: `)
 106         os.Stderr.WriteString(err.Error())
 107         os.Stderr.WriteString("\n")
 108         os.Exit(1)
 109         return
 110     }
 111 }
 112 
 113 func run(w io.Writer, args []string, cfg *config) error {
 114     bw := bufio.NewWriterSize(w, 32*1024)
 115     defer bw.Flush()
 116 
 117     if len(args) == 0 {
 118         if err := handleReader(bw, os.Stdin, cfg); err != nil {
 119             return err
 120         }
 121     }
 122 
 123     for _, name := range args {
 124         if err := handleFile(bw, name, cfg); err != nil {
 125             return err
 126         }
 127     }
 128 
 129     if cfg.Started {
 130         endPage(bw)
 131     }
 132     return nil
 133 }
 134 
 135 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 136     if name == `` || name == `-` {
 137         return handleReader(w, os.Stdin, cfg)
 138     }
 139 
 140     f, err := os.Open(name)
 141     if err != nil {
 142         return errors.New(`can't read from file named "` + name + `"`)
 143     }
 144     defer f.Close()
 145 
 146     return handleReader(w, f, cfg)
 147 }
 148 
 149 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 150     const gb = 1024 * 1024 * 1024
 151     sc := bufio.NewScanner(r)
 152     sc.Buffer(nil, 8*gb)
 153     lines := 0
 154 
 155     for sc.Scan() {
 156         line := sc.Text()
 157 
 158         if lines == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
 159             line = line[3:]
 160         }
 161         if lines == 0 && !cfg.Started {
 162             title := line
 163             if cfg.GotTitle {
 164                 title = cfg.Title
 165             }
 166             startPage(w, title, cfg.Monospace)
 167             cfg.Started = true
 168         }
 169         lines++
 170 
 171         if err := handleLine(w, line, cfg); err != nil {
 172             return err
 173         }
 174     }
 175 
 176     if !cfg.Started && lines > 0 {
 177         startPage(w, cfg.Title, cfg.Monospace)
 178         cfg.Started = true
 179     }
 180     return sc.Err()
 181 }
 182 
 183 const style = `
 184         body {
 185             margin: 1rem auto 2rem auto;
 186             padding: 0.25rem;
 187             font-size: 1.1rem;
 188             line-height: 1.8rem;
 189             font-family: sans-serif;
 190 
 191             max-width: 95vw;
 192             /* width: max-content; */
 193             width: fit-content;
 194 
 195             box-sizing: border-box;
 196             display: block;
 197         }
 198 
 199         a {
 200             color: steelblue;
 201             text-decoration: none;
 202         }
 203 
 204         p {
 205             display: block;
 206             margin: auto;
 207             max-width: 80ch;
 208         }
 209 
 210         img {
 211             margin: none;
 212         }
 213 
 214         audio {
 215             width: 60ch;
 216         }
 217 
 218         table {
 219             margin: 2rem auto;
 220             border-collapse: collapse;
 221         }
 222 
 223         thead>* {
 224             position: sticky;
 225             top: 0;
 226             background-color: white;
 227         }
 228 
 229         tfoot th {
 230             user-select: none;
 231         }
 232 
 233         th, td {
 234             padding: 0.1rem 1ch;
 235             min-width: 4ch;
 236             border-bottom: solid thin transparent;
 237         }
 238 
 239         tr:nth-child(5n) td {
 240             border-bottom: solid thin #ccc;
 241         }
 242 
 243         .monospace {
 244             font-family: monospace;
 245         }
 246 `
 247 
 248 func startPage(w *bufio.Writer, title string, monospace bool) {
 249     w.WriteString("<!DOCTYPE html>\n")
 250     w.WriteString("<html lang=\"en\">\n")
 251     w.WriteString("\n")
 252     w.WriteString("<head>\n")
 253     w.WriteString("    <meta charset=\"UTF-8\">\n")
 254     w.WriteString("    <meta name=\"viewport\" content=\"width=device-width,")
 255     w.WriteString(" initial-scale=1.0\">\n")
 256     w.WriteString("    <meta http-equiv=\"X-UA-Compatible\"")
 257     w.WriteString(" content=\"ie=edge\">\n")
 258     w.WriteString("\n")
 259     w.WriteString("    <link rel=\"icon\" href=\"data:,\">\n")
 260     w.WriteString(`    <title>`)
 261     w.WriteString(title)
 262     w.WriteString("</title>\n")
 263     w.WriteString("\n")
 264     w.WriteString("\n")
 265     w.WriteString("    <style>\n")
 266     w.WriteString(style[1:])
 267     w.WriteString("    </style>\n")
 268     w.WriteString("</head>\n")
 269     if monospace {
 270         w.WriteString("<body class=\"monospace\">\n")
 271     } else {
 272         w.WriteString("<body>\n")
 273     }
 274 }
 275 
 276 func endPage(w *bufio.Writer) {
 277     w.WriteString("</body>\n")
 278     w.WriteString("</html>\n")
 279 }
 280 
 281 func handleLine(w *bufio.Writer, s string, cfg *config) error {
 282     if handleDataURI(w, s) {
 283         if w.WriteByte('\n') != nil {
 284             return io.EOF
 285         }
 286         return nil
 287     }
 288 
 289     for len(s) > 0 {
 290         span := links.FindStringIndex(s)
 291         if span != nil && len(span) == 2 {
 292             i := span[0]
 293             j := span[1]
 294             href := s[i:j]
 295             handleChunk(w, s[:i])
 296             w.WriteString(`<a href="`)
 297             w.WriteString(href)
 298             w.WriteString(`">`)
 299             w.WriteString(href)
 300             w.WriteString(`</a>`)
 301             s = s[j:]
 302             continue
 303         }
 304 
 305         handleChunk(w, s)
 306         break
 307     }
 308 
 309     w.WriteString(`<br>`)
 310     if w.WriteByte('\n') != nil {
 311         return io.EOF
 312     }
 313     return nil
 314 }
 315 
 316 func handleChunk(w *bufio.Writer, s string) {
 317     for len(s) > 0 {
 318         switch b := s[0]; b {
 319         case '&':
 320             w.WriteString("&amp;")
 321         case '<':
 322             w.WriteString("&lt;")
 323         case '>':
 324             w.WriteString("&gt;")
 325         default:
 326             w.WriteByte(b)
 327         }
 328         s = s[1:]
 329     }
 330 }
 331 
 332 func handleDataURI(w *bufio.Writer, s string) bool {
 333     full := s
 334 
 335     if !strings.HasPrefix(s, `data:`) {
 336         return false
 337     }
 338 
 339     s = strings.TrimPrefix(s, `data:`)
 340     i := strings.Index(s, `;base64,`)
 341     if i < 0 {
 342         return false
 343     }
 344 
 345     kind := s[:i]
 346     s = s[i+len(`;base64,`):]
 347 
 348     if strings.HasPrefix(kind, `image/`) {
 349         if !isBase64(s) {
 350             return false
 351         }
 352 
 353         w.WriteString(`<img src="`)
 354         w.WriteString(full)
 355         w.WriteString(`">`)
 356         return true
 357     }
 358 
 359     if strings.HasPrefix(kind, `audio/`) {
 360         if !isBase64(s) {
 361             return false
 362         }
 363 
 364         w.WriteString(`<audio controls src="`)
 365         w.WriteString(full)
 366         w.WriteString(`"></audio>`)
 367         return true
 368     }
 369 
 370     if strings.HasPrefix(kind, `video/`) {
 371         if !isBase64(s) {
 372             return false
 373         }
 374 
 375         w.WriteString(`<video controls src="`)
 376         w.WriteString(full)
 377         w.WriteString(`"></video>`)
 378         return true
 379     }
 380 
 381     return false
 382 }
 383 
 384 func handleMedia(w *bufio.Writer, begin string, s string, end string) bool {
 385     if !isBase64(s) {
 386         return false
 387     }
 388 
 389     w.WriteString(begin)
 390     w.WriteString(s)
 391     w.WriteString(end)
 392     return true
 393 }
 394 
 395 func isBase64(s string) bool {
 396     dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(s))
 397     n, err := io.Copy(io.Discard, dec)
 398     return n > 0 && err == nil
 399 }
     File: ./id3pic/id3pic.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package id3pic
  26 
  27 import (
  28     "bufio"
  29     "encoding/binary"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 id3pic [options...] [file...]
  37 
  38 Extract picture/thumbnail bytes from ID3/MP3 metadata, if available.
  39 
  40 All (optional) leading options start with either single or double-dash:
  41 
  42     -h, -help    show this help message
  43 `
  44 
  45 // errNoThumb is a generic error to handle lack of thumbnails, in case no
  46 // picture-metadata-starters are found at all
  47 var errNoThumb = errors.New(`no thumbnail data found`)
  48 
  49 // errInvalidPIC is a generic error for invalid PIC-format pics
  50 var errInvalidPIC = errors.New(`invalid PIC-format embedded thumbnail`)
  51 
  52 func Main() {
  53     if len(os.Args) > 1 {
  54         switch os.Args[1] {
  55         case `-h`, `--h`, `-help`, `--help`:
  56             os.Stdout.WriteString(info[1:])
  57             return
  58         }
  59     }
  60 
  61     if len(os.Args) > 2 {
  62         showError(`can only handle 1 file`)
  63         os.Exit(1)
  64         return
  65     }
  66 
  67     name := `-`
  68     if len(os.Args) > 1 {
  69         name = os.Args[1]
  70     }
  71 
  72     if err := run(os.Stdout, name); err != nil && err != io.EOF {
  73         showError(err.Error())
  74         os.Exit(1)
  75         return
  76     }
  77 }
  78 
  79 func showError(msg string) {
  80     os.Stderr.WriteString(msg)
  81     os.Stderr.WriteString("\n")
  82 }
  83 
  84 func run(w io.Writer, name string) error {
  85     if name == `-` {
  86         return id3pic(w, bufio.NewReader(os.Stdin))
  87     }
  88 
  89     f, err := os.Open(name)
  90     if err != nil {
  91         return errors.New(`can't read from file named "` + name + `"`)
  92     }
  93     defer f.Close()
  94 
  95     return id3pic(w, bufio.NewReader(f))
  96 }
  97 
  98 func match(r *bufio.Reader, what []byte) bool {
  99     for _, v := range what {
 100         b, err := r.ReadByte()
 101         if b != v || err != nil {
 102             return false
 103         }
 104     }
 105     return true
 106 }
 107 
 108 func id3pic(w io.Writer, r *bufio.Reader) error {
 109     // match the ID3 mark
 110     for {
 111         b, err := r.ReadByte()
 112         if err == io.EOF {
 113             return errNoThumb
 114         }
 115         if err != nil {
 116             return err
 117         }
 118 
 119         if b == 'I' && match(r, []byte{'D', '3'}) {
 120             break
 121         }
 122     }
 123 
 124     for {
 125         b, err := r.ReadByte()
 126         if err == io.EOF {
 127             return errNoThumb
 128         }
 129         if err != nil {
 130             return err
 131         }
 132 
 133         // handle APIC-type chunks
 134         if b == 'A' && match(r, []byte{'P', 'I', 'C'}) {
 135             return handleAPIC(w, r)
 136         }
 137     }
 138 }
 139 
 140 func handleAPIC(w io.Writer, r *bufio.Reader) error {
 141     // section-size seems stored as 4 big-endian bytes
 142     var size uint32
 143     err := binary.Read(r, binary.BigEndian, &size)
 144     if err != nil {
 145         return err
 146     }
 147 
 148     n, err := skipThumbnailTypeAPIC(r)
 149     if err != nil {
 150         return err
 151     }
 152 
 153     _, err = io.Copy(w, io.LimitReader(r, int64(int(size)-n)))
 154     return err
 155 }
 156 
 157 func skipThumbnailTypeAPIC(r *bufio.Reader) (skipped int, err error) {
 158     m, err := r.Discard(2)
 159     if err != nil || m != 2 {
 160         return -1, errors.New(`failed to sync APIC flags`)
 161     }
 162     skipped += m
 163 
 164     m, err = r.Discard(1)
 165     if err != nil || m != 1 {
 166         return -1, errors.New(`failed to sync APIC text-encoding`)
 167     }
 168     skipped += m
 169 
 170     junk, err := r.ReadSlice(0)
 171     if err != nil {
 172         return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`)
 173     }
 174     skipped += len(junk)
 175 
 176     m, err = r.Discard(1)
 177     if err != nil || m != 1 {
 178         return -1, errors.New(`failed to sync APIC picture type`)
 179     }
 180     skipped += m
 181 
 182     junk, err = r.ReadSlice(0)
 183     if err != nil {
 184         return -1, errors.New(`failed to sync to APIC thumbnail description`)
 185     }
 186     skipped += len(junk)
 187 
 188     return skipped, nil
 189 }
     File: ./ignore/ignore.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ignore
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32 )
  33 
  34 const info = `
  35 ignore [options...] [command...] [command args...]
  36 
  37 Ignore the command given, acting as a stdio-passthru instead. This allows
  38 quick editing of pipes to temporarily exclude a pipe-step, while still
  39 having the disabled step show in the pipe-chain as a memo of sorts.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44 `
  45 
  46 func Main() {
  47     buffered := false
  48     args := os.Args[1:]
  49 
  50     if len(args) > 0 {
  51         switch args[0] {
  52         case `-b`, `--b`, `-buffered`, `--buffered`:
  53             buffered = true
  54             args = args[1:]
  55 
  56         case `-h`, `--h`, `-help`, `--help`:
  57             os.Stdout.WriteString(info[1:])
  58             return
  59         }
  60     }
  61 
  62     if len(args) > 0 && args[0] == `--` {
  63         args = args[1:]
  64     }
  65 
  66     liveLines := !buffered
  67     if !buffered {
  68         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  69             liveLines = false
  70         }
  71     }
  72 
  73     err := run(os.Stdout, os.Stdin, liveLines)
  74     if err != nil && err != io.EOF {
  75         os.Stderr.WriteString(`ignore: `)
  76         os.Stderr.WriteString(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79         return
  80     }
  81 }
  82 
  83 func run(w io.Writer, r io.Reader, live bool) error {
  84     bw := bufio.NewWriterSize(w, 32*1024)
  85     defer bw.Flush()
  86 
  87     return catl(bw, r, live)
  88 }
  89 
  90 func catl(w *bufio.Writer, r io.Reader, live bool) error {
  91     if !live {
  92         return catlFast(w, r)
  93     }
  94 
  95     const gb = 1024 * 1024 * 1024
  96     sc := bufio.NewScanner(r)
  97     sc.Buffer(nil, 8*gb)
  98 
  99     for i := 0; sc.Scan(); i++ {
 100         s := sc.Bytes()
 101         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 102             s = s[3:]
 103         }
 104 
 105         w.Write(s)
 106         if w.WriteByte('\n') != nil {
 107             return io.EOF
 108         }
 109 
 110         if live && w.Flush() != nil {
 111             return io.EOF
 112         }
 113     }
 114 
 115     return sc.Err()
 116 }
 117 
 118 func catlFast(w *bufio.Writer, r io.Reader) error {
 119     var buf [32 * 1024]byte
 120     var last byte = '\n'
 121 
 122     for i := 0; true; i++ {
 123         n, err := r.Read(buf[:])
 124         if n > 0 && err == io.EOF {
 125             err = nil
 126         }
 127         if err == io.EOF {
 128             if last != '\n' {
 129                 w.WriteByte('\n')
 130             }
 131             return nil
 132         }
 133 
 134         if err != nil {
 135             return err
 136         }
 137 
 138         chunk := buf[:n]
 139         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 140             chunk = chunk[3:]
 141         }
 142 
 143         if len(chunk) >= 1 {
 144             if _, err := w.Write(chunk); err != nil {
 145                 return io.EOF
 146             }
 147             last = chunk[len(chunk)-1]
 148         }
 149     }
 150 
 151     return nil
 152 }
     File: ./indent/indent.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package indent
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34 )
  35 
  36 const info = `
  37 indent [options...] [leading spaces...] [files...]
  38 
  39 Start each non-empty line with extra n spaces: when a leading number isn't
  40 given, lines are indented using 4 spaces by default.
  41 
  42 All (optional) leading options start with either single or double-dash:
  43 
  44     -h, -help    show this help message
  45 `
  46 
  47 func Main() {
  48     buffered := false
  49     args := os.Args[1:]
  50     indent := 4
  51 
  52     if len(args) > 0 {
  53         switch args[0] {
  54         case `-b`, `--b`, `-buffered`, `--buffered`:
  55             buffered = true
  56             args = args[1:]
  57 
  58         case `-h`, `--h`, `-help`, `--help`:
  59             os.Stdout.WriteString(info[1:])
  60             return
  61         }
  62     }
  63 
  64     if len(args) > 0 && args[0] == `--` {
  65         args = args[1:]
  66     }
  67 
  68     if len(args) > 0 {
  69         if n, err := strconv.Atoi(args[0]); err == nil {
  70             indent = n
  71             args = args[1:]
  72         }
  73     }
  74 
  75     liveLines := !buffered
  76     if !buffered {
  77         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  78             liveLines = false
  79         }
  80     }
  81 
  82     err := run(os.Stdout, args, indent, liveLines)
  83     if err != nil && err != io.EOF {
  84         os.Stderr.WriteString(`indent: `)
  85         os.Stderr.WriteString(err.Error())
  86         os.Stderr.WriteString("\n")
  87         os.Exit(1)
  88         return
  89     }
  90 }
  91 
  92 func run(w io.Writer, args []string, level int, live bool) error {
  93     bw := bufio.NewWriterSize(w, 32*1024)
  94     defer bw.Flush()
  95 
  96     if len(args) == 0 {
  97         return indent(bw, os.Stdin, level, live)
  98     }
  99 
 100     for _, name := range args {
 101         if err := handleFile(bw, name, level, live); err != nil {
 102             return err
 103         }
 104     }
 105     return nil
 106 }
 107 
 108 func handleFile(w *bufio.Writer, name string, level int, live bool) error {
 109     if name == `` || name == `-` {
 110         return indent(w, os.Stdin, level, live)
 111     }
 112 
 113     f, err := os.Open(name)
 114     if err != nil {
 115         return errors.New(`can't read from file named "` + name + `"`)
 116     }
 117     defer f.Close()
 118 
 119     return indent(w, f, level, live)
 120 }
 121 
 122 func indent(w *bufio.Writer, r io.Reader, level int, live bool) error {
 123     const gb = 1024 * 1024 * 1024
 124     sc := bufio.NewScanner(r)
 125     sc.Buffer(nil, 8*gb)
 126 
 127     for i := 0; sc.Scan(); i++ {
 128         s := sc.Bytes()
 129         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 130             s = s[3:]
 131         }
 132 
 133         writeSpaces(w, level)
 134         w.Write(s)
 135 
 136         if w.WriteByte('\n') != nil {
 137             return io.EOF
 138         }
 139 
 140         if live && w.Flush() != nil {
 141             return io.EOF
 142         }
 143     }
 144 
 145     return sc.Err()
 146 }
 147 
 148 func writeSpaces(w *bufio.Writer, n int) {
 149     const (
 150         half   = `                                `
 151         spaces = half + half
 152     )
 153 
 154     for n >= len(spaces) {
 155         w.WriteString(spaces)
 156         n -= len(spaces)
 157     }
 158     if n > 0 {
 159         w.WriteString(spaces[:n])
 160     }
 161 }
     File: ./info.txt
   1 easybox [options...] [tool] [arguments...]
   2 
   3 This is a collection of many specialized app-like tools, similar to "busybox".
   4 
   5 You can either run it with the tool name as its first argument, or run a link
   6 to it whose name is one of those same tools, avoiding the tool-name argument
   7 in that case.
   8 
   9 Tool "help" shows you all tools available, as well as all their aliases, and
  10 tool "tools" merely lists all main tool-names.
     File: ./items/items.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package items
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 items [options...] [files...]
  37 
  38 Emit each word/item from each input line into its own output line.
  39 
  40 All (optional) leading options start with either single or double-dash:
  41 
  42     -h, -help    show this help message
  43     -tsv         separate items from input-lines using individual tabs
  44 `
  45 
  46 type config struct {
  47     tsv       bool
  48     liveLines bool
  49 }
  50 
  51 func Main() {
  52     var cfg config
  53     cfg.liveLines = true
  54     args := os.Args[1:]
  55 
  56     for len(args) > 0 {
  57         switch args[0] {
  58         case `-b`, `--b`, `-buffered`, `--buffered`:
  59             cfg.liveLines = false
  60             args = args[1:]
  61             continue
  62 
  63         case `-h`, `--h`, `-help`, `--help`:
  64             os.Stdout.WriteString(info[1:])
  65             return
  66 
  67         case `-tsv`, `--tsv`:
  68             cfg.tsv = true
  69             args = args[1:]
  70             continue
  71         }
  72 
  73         break
  74     }
  75 
  76     if len(args) > 0 && args[0] == `--` {
  77         args = args[1:]
  78     }
  79 
  80     if cfg.liveLines {
  81         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  82             cfg.liveLines = false
  83         }
  84     }
  85 
  86     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  87         os.Stderr.WriteString(`items: `)
  88         os.Stderr.WriteString(err.Error())
  89         os.Stderr.WriteString("\n")
  90         os.Exit(1)
  91         return
  92     }
  93 }
  94 
  95 func run(w io.Writer, args []string, cfg config) error {
  96     bw := bufio.NewWriterSize(w, 32*1024)
  97     defer bw.Flush()
  98 
  99     dashes := 0
 100     for _, name := range args {
 101         if name == `-` {
 102             dashes++
 103         }
 104         if dashes > 1 {
 105             return errors.New(`can't read stdin (dash) more than once`)
 106         }
 107     }
 108 
 109     if len(args) == 0 {
 110         return items(bw, os.Stdin, cfg)
 111     }
 112 
 113     for _, name := range args {
 114         if name == `-` {
 115             if err := items(bw, os.Stdin, cfg); err != nil {
 116                 return err
 117             }
 118             continue
 119         }
 120 
 121         if err := handleFile(bw, name, cfg); err != nil {
 122             return err
 123         }
 124     }
 125     return nil
 126 }
 127 
 128 func handleFile(w *bufio.Writer, name string, cfg config) error {
 129     if name == `` || name == `-` {
 130         return items(w, os.Stdin, cfg)
 131     }
 132 
 133     f, err := os.Open(name)
 134     if err != nil {
 135         return errors.New(`can't read from file named "` + name + `"`)
 136     }
 137     defer f.Close()
 138 
 139     return items(w, f, cfg)
 140 }
 141 
 142 func items(w *bufio.Writer, r io.Reader, cfg config) error {
 143     const gb = 1024 * 1024 * 1024
 144     sc := bufio.NewScanner(r)
 145     sc.Buffer(nil, 8*gb)
 146 
 147     handleLine := handleSSV
 148     if cfg.tsv {
 149         handleLine = handleTSV
 150     }
 151 
 152     for i := 0; sc.Scan(); i++ {
 153         s := sc.Bytes()
 154         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 155             s = s[3:]
 156         }
 157 
 158         if handleLine(w, s) != nil {
 159             return io.EOF
 160         }
 161 
 162         if cfg.liveLines && w.Flush() != nil {
 163             return io.EOF
 164         }
 165     }
 166 
 167     return sc.Err()
 168 }
 169 
 170 func handleSSV(w *bufio.Writer, line []byte) error {
 171     for len(line) > 0 {
 172         if i := indexNonWhitespace(line); i >= 0 {
 173             line = line[i:]
 174         }
 175         if len(line) == 0 {
 176             break
 177         }
 178 
 179         var item []byte
 180         skip := 0
 181 
 182         i := indexWhitespace(line)
 183         if i >= 0 {
 184             item = line[:i]
 185             skip = i + 1
 186         } else {
 187             item = line
 188             skip = len(line)
 189         }
 190 
 191         w.Write(item)
 192         if w.WriteByte('\n') != nil {
 193             return io.EOF
 194         }
 195 
 196         line = line[skip:]
 197     }
 198 
 199     return nil
 200 }
 201 
 202 func handleTSV(w *bufio.Writer, line []byte) error {
 203     for len(line) > 0 {
 204         var item []byte
 205         skip := 0
 206 
 207         i := bytes.IndexByte(line, '\t')
 208         if i >= 0 {
 209             item = line[:i]
 210             skip = i + 1
 211         } else {
 212             item = line
 213             skip = len(line)
 214         }
 215 
 216         w.Write(item)
 217         if w.WriteByte('\n') != nil {
 218             return io.EOF
 219         }
 220 
 221         line = line[skip:]
 222     }
 223 
 224     return nil
 225 }
 226 
 227 func indexNonWhitespace(s []byte) int {
 228     for i, b := range s {
 229         switch b {
 230         case ' ', '\t':
 231             continue
 232         }
 233         return i
 234     }
 235 
 236     return -1
 237 }
 238 
 239 func indexWhitespace(s []byte) int {
 240     for i, b := range s {
 241         switch b {
 242         case ' ', '\t':
 243             return i
 244         }
 245     }
 246 
 247     return -1
 248 }
     File: ./items/items_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package items
  26 
  27 import (
  28     "bufio"
  29     "strings"
  30     "testing"
  31 )
  32 
  33 func TestSingleLineItems(t *testing.T) {
  34     tests := []struct {
  35         line     string
  36         expected []string
  37         tsv      bool
  38     }{
  39         {``, nil, false},
  40         {``, nil, true},
  41         {`    abc  def `, []string{`abc`, `def`}, false},
  42         {`    abc  def `, []string{`    abc  def `}, true},
  43         {"    abc \t def ", []string{`    abc `, ` def `}, true},
  44         {"    abc \t def ", []string{`abc`, `def`}, false},
  45     }
  46 
  47     for _, tc := range tests {
  48         var prefix string
  49         if tc.tsv {
  50             prefix = `TSV: `
  51         } else {
  52             prefix = `SSV: `
  53         }
  54 
  55         t.Run(prefix+tc.line, func(t *testing.T) {
  56             var sb strings.Builder
  57             w := bufio.NewWriter(&sb)
  58 
  59             var cfg config
  60             cfg.tsv = tc.tsv
  61             if err := items(w, strings.NewReader(tc.line), cfg); err != nil {
  62                 t.Fatal(err)
  63                 return
  64             }
  65             w.Flush()
  66 
  67             out := strings.TrimSuffix(sb.String(), "\n")
  68             got := strings.Split(out, "\n")
  69             if len(got) == 1 && got[0] == `` {
  70                 got = nil
  71             }
  72 
  73             if len(tc.expected) != len(got) {
  74                 const fs = "len %d vs. %d: got\n%v\nexp\n%v\n"
  75                 t.Fatalf(fs, len(got), len(tc.expected), got, tc.expected)
  76                 return
  77             }
  78 
  79             for i, s := range got {
  80                 if s != tc.expected[i] {
  81                     t.Logf("#%d: %q vs. %q", i, s, tc.expected[i])
  82                     t.Fatalf("got\n%s", out)
  83                     return
  84                 }
  85             }
  86         })
  87     }
  88 }
     File: ./json0/json0.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package json0
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strconv"
  33     "unicode"
  34 )
  35 
  36 const info = `
  37 json0 [options...] [file...]
  38 
  39 
  40 JSON-0 converts/fixes JSON/pseudo-JSON input into minimal JSON output.
  41 Its output is always a single line, which ends with a line-feed.
  42 
  43 Besides minimizing bytes, this tool also adapts almost-JSON input into
  44 valid JSON, since it
  45 
  46     - ignores extra/trailing commas in arrays and objects
  47     - ignores rest-of-line, multi-line, and even unix-style comments
  48     - ignores final 'n' in javascript bigint literals
  49     - ignores final trailing semicolon
  50     - turns single-quoted strings/keys into double-quoted strings
  51     - turns parentheses into proper square-bracket array delimiters
  52     - turns PYON (PYthon Object Notation) into its valid JSON counterpart
  53     - double-quotes unquoted object keys
  54     - changes \x 2-hex-digit into \u 4-hex-digit string-escapes
  55 
  56 All options available can either start with a single or a double-dash
  57 
  58     -h, -help    show this help message
  59     -jsonl       emit JSON Lines, when top-level value is an array
  60     -max-rec     next argument limits maximum-recursion allowed
  61 `
  62 
  63 const (
  64     bufSize       = 32 * 1024
  65     chunkPeekSize = 32
  66 )
  67 
  68 // escapedStringBytes helps funcs like handleString treat all strings
  69 // quickly and correctly, using their officially-supported JSON escape
  70 // sequences
  71 //
  72 // https://www.rfc-editor.org/rfc/rfc8259#section-7
  73 var escapedStringBytes = [128][]byte{
  74     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
  75     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
  76     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
  77     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
  78     {'\\', 'b'}, {'\\', 't'},
  79     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
  80     {'\\', 'f'}, {'\\', 'r'},
  81     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
  82     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
  83     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
  84     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
  85     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
  86     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
  87     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
  88     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
  89     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
  90     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
  91     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
  92     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
  93     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
  94     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
  95     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
  96     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
  97     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
  98     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
  99     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 100     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 101     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 102 }
 103 
 104 type config struct {
 105     maxNesting int
 106     liveLines  bool
 107 }
 108 
 109 func Main() {
 110     args := os.Args[1:]
 111     buffered := true // avoid checking stdout by default, when emitting 1 line
 112     handler := json0
 113     var cfg config
 114 
 115     for len(args) > 0 {
 116         switch args[0] {
 117         case `-b`, `--b`, `-buffered`, `--buffered`:
 118             buffered = true
 119             args = args[1:]
 120             continue
 121 
 122         case `-h`, `--h`, `-help`, `--help`:
 123             os.Stdout.WriteString(info[1:])
 124             return
 125 
 126         case `-jsonl`, `--jsonl`:
 127             handler = jsonl
 128             args = args[1:]
 129             continue
 130 
 131         case
 132             `-max-nesting`, `--max-nesting`,
 133             `-max-rec`, `--max-rec`,
 134             `-max-recursion`, `--max-recursion`:
 135             if len(args) >= 2 {
 136                 s := args[1]
 137                 n, err := strconv.ParseInt(s, 10, 64)
 138                 if err != nil {
 139                     os.Stderr.WriteString("invalid number '" + s + "'\n")
 140                     os.Exit(1)
 141                     return
 142                 }
 143 
 144                 cfg.maxNesting = int(n)
 145                 args = args[2:]
 146                 continue
 147             }
 148 
 149             os.Stderr.WriteString("forgot the max-recursion limit\n")
 150             os.Exit(1)
 151             return
 152         }
 153 
 154         break
 155     }
 156 
 157     if len(args) > 0 && args[0] == `--` {
 158         args = args[1:]
 159     }
 160 
 161     if len(args) > 1 {
 162         os.Stderr.WriteString("multiple inputs aren't allowed\n")
 163         os.Exit(1)
 164         return
 165     }
 166 
 167     cfg.liveLines = !buffered
 168     if !buffered {
 169         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 170             cfg.liveLines = false
 171         }
 172     }
 173 
 174     name := `-`
 175     if len(args) == 1 {
 176         name = args[0]
 177     }
 178 
 179     err := run(os.Stdout, name, handler, cfg)
 180     if err != nil && err != io.EOF {
 181         os.Stderr.WriteString(`json0: `)
 182         os.Stderr.WriteString(err.Error())
 183         os.Stderr.WriteString("\n")
 184         os.Exit(1)
 185         return
 186     }
 187 }
 188 
 189 type handlerFunc func(w *bufio.Writer, r *bufio.Reader, cfg config) error
 190 
 191 func run(w io.Writer, name string, handler handlerFunc, cfg config) error {
 192     // f, _ := os.Create(`json0.prof`)
 193     // defer f.Close()
 194     // pprof.StartCPUProfile(f)
 195     // defer pprof.StopCPUProfile()
 196 
 197     if name == `` || name == `-` {
 198         bw := bufio.NewWriterSize(w, bufSize)
 199         br := bufio.NewReaderSize(os.Stdin, bufSize)
 200         defer bw.Flush()
 201         return handler(bw, br, cfg)
 202     }
 203 
 204     f, err := os.Open(name)
 205     if err != nil {
 206         return errors.New(`can't read from file named "` + name + `"`)
 207     }
 208     defer f.Close()
 209 
 210     bw := bufio.NewWriterSize(w, bufSize)
 211     br := bufio.NewReaderSize(f, bufSize)
 212     defer bw.Flush()
 213     return handler(bw, br, cfg)
 214 }
 215 
 216 var (
 217     errCommentEarlyEnd  = errors.New(`unexpected early-end of comment`)
 218     errInputEarlyEnd    = errors.New(`expected end of input data`)
 219     errInvalidComment   = errors.New(`expected / or *`)
 220     errInvalidHex       = errors.New(`expected a base-16 digit`)
 221     errInvalidRune      = errors.New(`invalid UTF-8 bytes`)
 222     errInvalidToken     = errors.New(`invalid JSON token`)
 223     errNoDigits         = errors.New(`expected numeric digits`)
 224     errNoStringQuote    = errors.New(`expected " or '`)
 225     errNoArrayComma     = errors.New(`missing comma between array values`)
 226     errNoObjectComma    = errors.New(`missing comma between key-value pairs`)
 227     errStringEarlyEnd   = errors.New(`unexpected early-end of string`)
 228     errTooMuchRecursion = errors.New(`exceeded recursion-limit given`)
 229     errExtraBytes       = errors.New(`unexpected extra input bytes`)
 230 )
 231 
 232 // linePosError is a more descriptive kind of error, showing the source of
 233 // the input-related problem, as 1-based a line/pos number pair in front
 234 // of the error message
 235 type linePosError struct {
 236     // line is the 1-based line count from the input
 237     line int
 238 
 239     // pos is the 1-based `horizontal` position in its line
 240     pos int
 241 
 242     // err is the error message to `decorate` with the position info
 243     err error
 244 }
 245 
 246 // Error satisfies the error interface
 247 func (lpe linePosError) Error() string {
 248     where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
 249     return where + `: ` + lpe.err.Error()
 250 }
 251 
 252 // isIdentifier improves control-flow of func handleKey, when it handles
 253 // unquoted object keys
 254 var isIdentifier = [256]bool{
 255     '_': true,
 256 
 257     '0': true, '1': true, '2': true, '3': true, '4': true,
 258     '5': true, '6': true, '7': true, '8': true, '9': true,
 259 
 260     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
 261     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
 262     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
 263     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
 264     'Y': true, 'Z': true,
 265 
 266     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
 267     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
 268     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
 269     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
 270     'y': true, 'z': true,
 271 }
 272 
 273 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
 274 // being 0, and normalizes letter-case for the hex letters
 275 var matchHex = [256]byte{
 276     '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
 277     '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
 278     'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
 279     'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
 280 }
 281 
 282 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; final boolean
 283 // value isn't used, and is just there to match the signature of func jsonl
 284 func json0(w *bufio.Writer, r *bufio.Reader, cfg config) error {
 285     jr := jsonReader{r: r, line: 1, pos: 1, maxNesting: cfg.maxNesting}
 286     defer w.Flush()
 287 
 288     if err := jr.handleLeadingJunk(); err != nil {
 289         return err
 290     }
 291 
 292     // handle a single top-level JSON value
 293     err := handleValue(w, &jr)
 294 
 295     // end the only output-line with a line-feed; this also avoids showing
 296     // error messages on the same line as the main output, since JSON-0
 297     // output has no line-feeds before its last byte
 298     outputByte(w, '\n')
 299 
 300     if err != nil {
 301         return err
 302     }
 303     return jr.handleTrailingJunk()
 304 }
 305 
 306 // jsonl converts JSON/pseudo-JSON into (valid) minimal JSON Lines; this func
 307 // avoids writing a trailing line-feed, leaving that up to its caller
 308 func jsonl(w *bufio.Writer, r *bufio.Reader, cfg config) error {
 309     jr := jsonReader{r: r, line: 1, pos: 1, maxNesting: cfg.maxNesting}
 310 
 311     if err := jr.handleLeadingJunk(); err != nil {
 312         return err
 313     }
 314 
 315     chunk, err := jr.r.Peek(1)
 316     if err == nil && len(chunk) >= 1 {
 317         switch b := chunk[0]; b {
 318         case '[', '(':
 319             return handleArrayJSONL(w, &jr, b, cfg.liveLines)
 320         }
 321     }
 322 
 323     // handle a single top-level JSON value
 324     err = handleValue(w, &jr)
 325 
 326     // end the only output-line with a line-feed; this also avoids showing
 327     // error messages on the same line as the main output, since JSON-0
 328     // output has no line-feeds before its last byte
 329     outputByte(w, '\n')
 330 
 331     if err != nil {
 332         return err
 333     }
 334     return jr.handleTrailingJunk()
 335 }
 336 
 337 // handleArrayJSONL handles top-level arrays for func jsonl
 338 func handleArrayJSONL(w *bufio.Writer, jr *jsonReader, start byte, live bool) error {
 339     if err := jr.demandSyntax(start); err != nil {
 340         return err
 341     }
 342 
 343     var end byte = ']'
 344     if start == '(' {
 345         end = ')'
 346     }
 347 
 348     for n := 0; true; n++ {
 349         // there may be whitespace/comments before the next comma
 350         if err := jr.seekNext(); err != nil {
 351             return err
 352         }
 353 
 354         // handle commas between values, as well as trailing ones
 355         comma := false
 356         b, _ := jr.peekByte()
 357         if b == ',' {
 358             jr.discardSyntaxByte()
 359             comma = true
 360 
 361             // there may be whitespace/comments before an ending ']'
 362             if err := jr.seekNext(); err != nil {
 363                 return err
 364             }
 365             b, _ = jr.peekByte()
 366         }
 367 
 368         // handle end of array
 369         if b == end {
 370             jr.discardSyntaxByte()
 371             if n > 0 {
 372                 err := outputByte(w, '\n')
 373                 if live && w.Flush() != nil {
 374                     return io.EOF
 375                 }
 376                 return err
 377             }
 378             return nil
 379         }
 380 
 381         // turn commas between adjacent values into line-feeds, as the
 382         // output for this custom func is supposed to be JSON Lines
 383         if n > 0 {
 384             if !comma {
 385                 return errNoArrayComma
 386             }
 387             if err := outputByte(w, '\n'); err != nil {
 388                 return err
 389             }
 390             if live && w.Flush() != nil {
 391                 return io.EOF
 392             }
 393         }
 394 
 395         // handle the next value
 396         if err := jr.seekNext(); err != nil {
 397             return err
 398         }
 399         if err := handleValue(w, jr); err != nil {
 400             return err
 401         }
 402     }
 403 
 404     // make the compiler happy
 405     return nil
 406 }
 407 
 408 // jsonReader reads data via a buffer, keeping track of the input position:
 409 // this in turn allows showing much more useful errors, when these happen
 410 type jsonReader struct {
 411     // r is the actual reader
 412     r *bufio.Reader
 413 
 414     // line is the 1-based line-counter for input bytes, and gives errors
 415     // useful position info
 416     line int
 417 
 418     // pos is the 1-based `horizontal` position in its line, and gives
 419     // errors useful position info
 420     pos int
 421 
 422     // maxNesting optionally controls recursion when parsing the input;
 423     // checking recursion is disabled when this value is 0 or negative
 424     maxNesting int
 425 
 426     // nesting is the current recursive-nesting level, which can only
 427     // increase when handling arrays and/or objects
 428     nesting int
 429 }
 430 
 431 // exitRec decreases the recursion counter: this func is handy to defer
 432 func (jr *jsonReader) exitRec() {
 433     jr.nesting--
 434 }
 435 
 436 // improveError makes any error more useful, by giving it info about the
 437 // current input-position, as a 1-based line/within-line-position pair
 438 func (jr jsonReader) improveError(err error) error {
 439     if _, ok := err.(linePosError); ok {
 440         return err
 441     }
 442 
 443     if err == io.EOF {
 444         return linePosError{jr.line, jr.pos, errInputEarlyEnd}
 445     }
 446     if err != nil {
 447         return linePosError{jr.line, jr.pos, err}
 448     }
 449     return nil
 450 }
 451 
 452 // enterRec increases the recursion counter, checking if new recursion-level
 453 // is allowed
 454 func (jr *jsonReader) enterRec() error {
 455     jr.nesting++
 456     if jr.maxNesting > 0 && jr.maxNesting < jr.nesting {
 457         return errTooMuchRecursion
 458     }
 459     return nil
 460 }
 461 
 462 func (jr *jsonReader) handleLeadingJunk() error {
 463     // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
 464     // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
 465     // about byte-order by design
 466     jr.skipUTF8BOM()
 467 
 468     // ignore leading whitespace and/or comments
 469     return jr.seekNext()
 470 }
 471 
 472 func (jr *jsonReader) handleTrailingJunk() error {
 473     // ignore trailing whitespace and/or comments
 474     if err := jr.seekNext(); err != nil {
 475         return err
 476     }
 477 
 478     // ignore trailing semicolons
 479     for {
 480         if b, ok := jr.peekByte(); !ok || b != ';' {
 481             break
 482         }
 483 
 484         jr.discardSyntaxByte()
 485         // ignore trailing whitespace and/or comments
 486         if err := jr.seekNext(); err != nil {
 487             return err
 488         }
 489     }
 490 
 491     // beyond trailing whitespace and/or comments, any more bytes
 492     // make the whole input data invalid JSON
 493     if _, ok := jr.peekByte(); ok {
 494         return jr.improveError(errExtraBytes)
 495     }
 496     return nil
 497 }
 498 
 499 // demandSyntax fails with an error when the next byte isn't the one given;
 500 // when it is, the byte is then read/skipped, and a nil error is returned
 501 func (jr *jsonReader) demandSyntax(syntax byte) error {
 502     chunk, err := jr.r.Peek(1)
 503     if err == io.EOF {
 504         return jr.improveError(errInputEarlyEnd)
 505     }
 506     if err != nil {
 507         return jr.improveError(err)
 508     }
 509 
 510     if len(chunk) < 1 || chunk[0] != syntax {
 511         msg := `expected ` + string(rune(syntax))
 512         return jr.improveError(errors.New(msg))
 513     }
 514 
 515     jr.discardSyntaxByte()
 516     return nil
 517 }
 518 
 519 // discardSyntaxByte discards 1 byte, updating the reader's position info
 520 func (jr *jsonReader) discardSyntaxByte() {
 521     jr.readByte()
 522 }
 523 
 524 // peekByte simplifies control-flow for various other funcs
 525 func (jr jsonReader) peekByte() (b byte, ok bool) {
 526     chunk, err := jr.r.Peek(1)
 527     if err == nil && len(chunk) >= 1 {
 528         return chunk[0], true
 529     }
 530     return 0, false
 531 }
 532 
 533 // readByte does what it says, updating the reader's position info
 534 func (jr *jsonReader) readByte() (b byte, err error) {
 535     b, err = jr.r.ReadByte()
 536     if err == nil {
 537         if b == '\n' {
 538             jr.line += 1
 539             jr.pos = 1
 540         } else {
 541             jr.pos++
 542         }
 543         return b, nil
 544     }
 545     return b, jr.improveError(err)
 546 }
 547 
 548 // readRune does what it says, updating the reader's position info
 549 func (jr *jsonReader) readRune() (r rune, err error) {
 550     r, _, err = jr.r.ReadRune()
 551     if err == nil {
 552         if r == '\n' {
 553             jr.line += 1
 554             jr.pos = 1
 555         } else {
 556             jr.pos++
 557         }
 558         return r, nil
 559     }
 560     return r, jr.improveError(err)
 561 }
 562 
 563 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
 564 // and comments, either single-line (starting with //) or general (starting
 565 // with /* and ending with */)
 566 func (jr *jsonReader) seekNext() error {
 567     for {
 568         b, err := jr.r.ReadByte()
 569         if err != nil {
 570             return nil
 571         }
 572 
 573         switch b {
 574         case '\n':
 575             jr.line++
 576             jr.pos = 1
 577 
 578         case '#':
 579             jr.pos++
 580             if err := jr.skipLine(); err != nil {
 581                 return err
 582             }
 583 
 584         case '/':
 585             jr.r.UnreadByte()
 586             if err := jr.skipComment(); err != nil {
 587                 return err
 588             }
 589 
 590         default:
 591             if b <= 32 {
 592                 // keep skipping whitespace bytes
 593                 jr.pos++
 594             } else {
 595                 // reached the next token
 596                 jr.r.UnreadByte()
 597                 return nil
 598             }
 599         }
 600 
 601         // after comments, keep looking for more whitespace and/or comments
 602     }
 603 }
 604 
 605 // skipComment helps func seekNext skip over comments, simplifying the latter
 606 // func's control-flow
 607 func (jr *jsonReader) skipComment() error {
 608     err := jr.demandSyntax('/')
 609     if err != nil {
 610         return err
 611     }
 612 
 613     b, ok := jr.peekByte()
 614     if !ok {
 615         return nil
 616     }
 617 
 618     switch b {
 619     case '/':
 620         // handle single-line comments
 621         return jr.skipLine()
 622 
 623     case '*':
 624         // handle (potentially) multi-line comments
 625         return jr.skipGeneralComment()
 626 
 627     default:
 628         return jr.improveError(errInvalidComment)
 629     }
 630 }
 631 
 632 // skipLine handles single-line comments for func skipComment
 633 func (jr *jsonReader) skipLine() error {
 634     for {
 635         b, err := jr.readByte()
 636         if err == io.EOF {
 637             // end of input is fine in this case
 638             return nil
 639         }
 640         if err != nil {
 641             return err
 642         }
 643 
 644         if b == '\n' {
 645             return nil
 646         }
 647     }
 648 }
 649 
 650 // skipGeneralComment handles (potentially) multi-line comments for func
 651 // skipComment
 652 func (jr *jsonReader) skipGeneralComment() error {
 653     var prev byte
 654     for {
 655         b, err := jr.readByte()
 656         if err != nil {
 657             return jr.improveError(errCommentEarlyEnd)
 658         }
 659 
 660         if prev == '*' && b == '/' {
 661             return nil
 662         }
 663         if b == '\n' {
 664             jr.line++
 665             jr.pos = 1
 666         }
 667         prev = b
 668     }
 669 }
 670 
 671 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
 672 func (jr *jsonReader) skipUTF8BOM() {
 673     lead, err := jr.r.Peek(3)
 674     if err != nil {
 675         return
 676     }
 677 
 678     if len(lead) > 2 && lead[0] == 0xef && lead[1] == 0xbb && lead[2] == 0xbf {
 679         jr.r.Discard(3)
 680         jr.pos += 1
 681     }
 682 }
 683 
 684 // outputByte is a small wrapper on func WriteByte, which adapts any error
 685 // into a custom dummy output-error, which is in turn meant to be ignored,
 686 // being just an excuse to quit the app immediately and successfully
 687 func outputByte(w *bufio.Writer, b byte) error {
 688     err := w.WriteByte(b)
 689     if err == nil {
 690         return nil
 691     }
 692     return io.EOF
 693 }
 694 
 695 // handleArray handles arrays for func handleValue
 696 func handleArray(w *bufio.Writer, jr *jsonReader, start byte) error {
 697     jr.enterRec()
 698     defer jr.exitRec()
 699 
 700     if err := jr.demandSyntax(start); err != nil {
 701         return err
 702     }
 703 
 704     var end byte = ']'
 705     if start == '(' {
 706         end = ')'
 707     }
 708 
 709     w.WriteByte('[')
 710 
 711     for n := 0; true; n++ {
 712         // there may be whitespace/comments before the next comma
 713         if err := jr.seekNext(); err != nil {
 714             return err
 715         }
 716 
 717         // handle commas between values, as well as trailing ones
 718         comma := false
 719         b, _ := jr.peekByte()
 720         if b == ',' {
 721             jr.discardSyntaxByte()
 722             comma = true
 723 
 724             // there may be whitespace/comments before an ending ']'
 725             if err := jr.seekNext(); err != nil {
 726                 return err
 727             }
 728             b, _ = jr.peekByte()
 729         }
 730 
 731         // handle end of array
 732         if b == end {
 733             jr.discardSyntaxByte()
 734             w.WriteByte(']')
 735             return nil
 736         }
 737 
 738         // don't forget commas between adjacent values
 739         if n > 0 {
 740             if !comma {
 741                 return errNoArrayComma
 742             }
 743             if err := outputByte(w, ','); err != nil {
 744                 return err
 745             }
 746         }
 747 
 748         // handle the next value
 749         if err := jr.seekNext(); err != nil {
 750             return err
 751         }
 752         if err := handleValue(w, jr); err != nil {
 753             return err
 754         }
 755     }
 756 
 757     // make the compiler happy
 758     return nil
 759 }
 760 
 761 // handleDigits helps various number-handling funcs do their job
 762 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
 763     if trySimpleDigits(w, jr) {
 764         return nil
 765     }
 766 
 767     for n := 0; true; n++ {
 768         b, _ := jr.peekByte()
 769 
 770         // support `nice` long numbers by ignoring their underscores
 771         if b == '_' {
 772             jr.discardSyntaxByte()
 773             continue
 774         }
 775 
 776         if '0' <= b && b <= '9' {
 777             jr.discardSyntaxByte()
 778             w.WriteByte(b)
 779             continue
 780         }
 781 
 782         if n == 0 {
 783             return errNoDigits
 784         }
 785         return nil
 786     }
 787 
 788     // make the compiler happy
 789     return nil
 790 }
 791 
 792 // trySimpleDigits tries to handle (more quickly) digit-runs where all bytes
 793 // are just digits: this is a very common case for numbers; returns whether
 794 // it succeeded, so this func's caller knows knows if it needs to do anything,
 795 // the slower way
 796 func trySimpleDigits(w *bufio.Writer, jr *jsonReader) (gotIt bool) {
 797     chunk, _ := jr.r.Peek(chunkPeekSize)
 798 
 799     for i, b := range chunk {
 800         if '0' <= b && b <= '9' {
 801             continue
 802         }
 803 
 804         if i == 0 || b == '_' {
 805             return false
 806         }
 807 
 808         // bulk-writing the chunk is this func's whole point
 809         w.Write(chunk[:i])
 810 
 811         jr.r.Discard(i)
 812         jr.pos += i
 813         return true
 814     }
 815 
 816     // maybe the digits-run is ok, but it's just longer than the chunk
 817     return false
 818 }
 819 
 820 // handleDot handles pseudo-JSON numbers which start with a decimal dot
 821 func handleDot(w *bufio.Writer, jr *jsonReader) error {
 822     if err := jr.demandSyntax('.'); err != nil {
 823         return err
 824     }
 825     w.Write([]byte{'0', '.'})
 826     return handleDigits(w, jr)
 827 }
 828 
 829 // handleKey is used by func handleObjects and generalizes func handleString,
 830 // by allowing unquoted object keys; it's not used anywhere else, as allowing
 831 // unquoted string values is ambiguous with actual JSON-keyword values null,
 832 // false, and true.
 833 func handleKey(w *bufio.Writer, jr *jsonReader) error {
 834     quote, ok := jr.peekByte()
 835     if !ok {
 836         return jr.improveError(errStringEarlyEnd)
 837     }
 838 
 839     if quote == '"' || quote == '\'' {
 840         return handleString(w, jr, quote)
 841     }
 842 
 843     w.WriteByte('"')
 844     for {
 845         if b, _ := jr.peekByte(); isIdentifier[b] {
 846             jr.discardSyntaxByte()
 847             w.WriteByte(b)
 848             continue
 849         }
 850 
 851         w.WriteByte('"')
 852         return nil
 853     }
 854 }
 855 
 856 // trySimpleString tries to handle (more quickly) inner-strings where all bytes
 857 // are unescaped ASCII symbols: this is a very common case for strings, and is
 858 // almost always the case for object keys; returns whether it succeeded, so
 859 // this func's caller knows knows if it needs to do anything, the slower way
 860 func trySimpleString(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
 861     end := -1
 862     chunk, _ := jr.r.Peek(chunkPeekSize)
 863 
 864     for i, b := range chunk {
 865         if b == byte(quote) {
 866             end = i
 867             break
 868         }
 869 
 870         if b < ' ' || b > 127 || b == '\\' || b == '"' {
 871             return false
 872         }
 873     }
 874 
 875     if end < 0 {
 876         return false
 877     }
 878 
 879     // bulk-writing the chunk is this func's whole point
 880     w.WriteByte('"')
 881     w.Write(chunk[:end])
 882     w.WriteByte('"')
 883 
 884     jr.r.Discard(end + 1)
 885     jr.pos += end + 1
 886     return true
 887 }
 888 
 889 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
 890 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
 891     for rest := kw; len(rest) > 0; rest = rest[1:] {
 892         b, err := jr.readByte()
 893         if err == nil && b == rest[0] {
 894             // keywords given to this func have no line-feeds
 895             jr.pos++
 896             continue
 897         }
 898 
 899         msg := `expected JSON value ` + string(kw)
 900         return jr.improveError(errors.New(msg))
 901     }
 902 
 903     w.Write(kw)
 904     return nil
 905 }
 906 
 907 func replaceKeyword(w *bufio.Writer, jr *jsonReader, kw, with []byte) error {
 908     for rest := kw; len(rest) > 0; rest = rest[1:] {
 909         b, err := jr.readByte()
 910         if err == nil && b == rest[0] {
 911             // keywords given to this func have no line-feeds
 912             jr.pos++
 913             continue
 914         }
 915 
 916         msg := `expected JSON value ` + string(kw)
 917         return jr.improveError(errors.New(msg))
 918     }
 919 
 920     w.Write(with)
 921     return nil
 922 }
 923 
 924 // handleNegative handles numbers starting with a negative sign for func
 925 // handleValue
 926 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
 927     if err := jr.demandSyntax('-'); err != nil {
 928         return err
 929     }
 930 
 931     w.WriteByte('-')
 932     if b, _ := jr.peekByte(); b == '.' {
 933         jr.discardSyntaxByte()
 934         w.Write([]byte{'0', '.'})
 935         return handleDigits(w, jr)
 936     }
 937     return handleNumber(w, jr)
 938 }
 939 
 940 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
 941 // such as values starting with a decimal dot
 942 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
 943     // handle integer digits
 944     if err := handleDigits(w, jr); err != nil {
 945         return err
 946     }
 947 
 948     switch b, _ := jr.peekByte(); b {
 949     case 'n':
 950         // handle optional trailing 'n', used in javascript's bigint literals
 951         jr.discardSyntaxByte()
 952         return nil
 953 
 954     case '.':
 955         // handle optional decimal digits, starting with a leading dot
 956         jr.discardSyntaxByte()
 957         w.WriteByte('.')
 958         return handleDigits(w, jr)
 959 
 960     case 'e', 'E':
 961         // handle optional exponent digits
 962         jr.discardSyntaxByte()
 963         w.WriteByte(b)
 964         b, _ = jr.peekByte()
 965         if b == '+' {
 966             jr.discardSyntaxByte()
 967         } else if b == '-' {
 968             w.WriteByte('-')
 969             jr.discardSyntaxByte()
 970         }
 971         return handleDigits(w, jr)
 972 
 973     default:
 974         // nothing special after some digits
 975         return nil
 976     }
 977 }
 978 
 979 // handleObject handles objects for func handleValue
 980 func handleObject(w *bufio.Writer, jr *jsonReader) error {
 981     jr.enterRec()
 982     defer jr.exitRec()
 983 
 984     if err := jr.demandSyntax('{'); err != nil {
 985         return err
 986     }
 987 
 988     w.WriteByte('{')
 989 
 990     for npairs := 0; true; npairs++ {
 991         // there may be whitespace/comments before the next comma
 992         if err := jr.seekNext(); err != nil {
 993             return err
 994         }
 995 
 996         // handle commas between key-value pairs, as well as trailing ones
 997         comma := false
 998         b, _ := jr.peekByte()
 999         if b == ',' {
1000             jr.discardSyntaxByte()
1001             comma = true
1002 
1003             // there may be whitespace/comments before an ending '}'
1004             if err := jr.seekNext(); err != nil {
1005                 return err
1006             }
1007             b, _ = jr.peekByte()
1008         }
1009 
1010         // handle end of object
1011         if b == '}' {
1012             jr.discardSyntaxByte()
1013             w.WriteByte('}')
1014             return nil
1015         }
1016 
1017         // don't forget commas between adjacent key-value pairs
1018         if npairs > 0 {
1019             if !comma {
1020                 return errNoObjectComma
1021             }
1022             if err := outputByte(w, ','); err != nil {
1023                 return err
1024             }
1025         }
1026 
1027         // handle the next pair's key
1028         if err := jr.seekNext(); err != nil {
1029             return err
1030         }
1031         if err := handleKey(w, jr); err != nil {
1032             return err
1033         }
1034 
1035         // demand a colon right after the key
1036         if err := jr.seekNext(); err != nil {
1037             return err
1038         }
1039         if err := jr.demandSyntax(':'); err != nil {
1040             return err
1041         }
1042         w.WriteByte(':')
1043 
1044         // handle the next pair's value
1045         if err := jr.seekNext(); err != nil {
1046             return err
1047         }
1048         if err := handleValue(w, jr); err != nil {
1049             return err
1050         }
1051     }
1052 
1053     // make the compiler happy
1054     return nil
1055 }
1056 
1057 // handlePositive handles numbers starting with a positive sign for func
1058 // handleValue
1059 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
1060     if err := jr.demandSyntax('+'); err != nil {
1061         return err
1062     }
1063 
1064     // valid JSON isn't supposed to have leading pluses on numbers, so
1065     // emit nothing for it, unlike for negative numbers
1066 
1067     if b, _ := jr.peekByte(); b == '.' {
1068         jr.discardSyntaxByte()
1069         w.Write([]byte{'0', '.'})
1070         return handleDigits(w, jr)
1071     }
1072     return handleNumber(w, jr)
1073 }
1074 
1075 // handleString handles strings for funcs handleValue and handleObject, and
1076 // supports both single-quotes and double-quotes, always emitting the latter
1077 // in the output, of course
1078 func handleString(w *bufio.Writer, jr *jsonReader, quote byte) error {
1079     if quote != '"' && quote != '\'' {
1080         return errNoStringQuote
1081     }
1082 
1083     jr.discardSyntaxByte()
1084 
1085     // try the quicker no-escapes ASCII handler
1086     if trySimpleString(w, jr, quote) {
1087         return nil
1088     }
1089 
1090     // it's a non-trivial inner-string, so handle it byte-by-byte
1091     w.WriteByte('"')
1092     escaped := false
1093 
1094     for quote := rune(quote); true; {
1095         r, err := jr.readRune()
1096         if r == unicode.ReplacementChar {
1097             return jr.improveError(errInvalidRune)
1098         }
1099         if err != nil {
1100             if err == io.EOF {
1101                 return jr.improveError(errStringEarlyEnd)
1102             }
1103             return jr.improveError(err)
1104         }
1105 
1106         if !escaped {
1107             if r == '\\' {
1108                 escaped = true
1109                 continue
1110             }
1111 
1112             // handle end of string
1113             if r == quote {
1114                 return outputByte(w, '"')
1115             }
1116 
1117             if int(r) <= len(escapedStringBytes) {
1118                 w.Write(escapedStringBytes[r])
1119             } else {
1120                 w.WriteRune(r)
1121             }
1122             continue
1123         }
1124 
1125         // handle escaped items
1126         escaped = false
1127 
1128         switch r {
1129         case 'u':
1130             // \u needs exactly 4 hex-digits to follow it
1131             w.Write([]byte{'\\', 'u'})
1132             if err := copyHex(w, 4, jr); err != nil {
1133                 return jr.improveError(err)
1134             }
1135 
1136         case 'x':
1137             // JSON only supports 4 escaped hex-digits, so pad the 2
1138             // expected hex-digits with 2 zeros
1139             w.Write([]byte{'\\', 'u', '0', '0'})
1140             if err := copyHex(w, 2, jr); err != nil {
1141                 return jr.improveError(err)
1142             }
1143 
1144         case 't', 'f', 'r', 'n', 'b', '\\', '"':
1145             // handle valid-JSON escaped string sequences
1146             w.WriteByte('\\')
1147             w.WriteByte(byte(r))
1148 
1149         case '\'':
1150             // escaped single-quotes aren't standard JSON, but are needed
1151             // when the input uses non-standard single-quoted strings
1152             w.WriteByte('\'')
1153 
1154         default:
1155             if int(r) < len(escapedStringBytes) {
1156                 w.Write(escapedStringBytes[r])
1157             } else {
1158                 w.WriteRune(r)
1159             }
1160         }
1161     }
1162 
1163     if escaped {
1164         return jr.improveError(errStringEarlyEnd)
1165     }
1166     return nil
1167 }
1168 
1169 // copyHex handles a run of hex-digits for func handleString, starting right
1170 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
1171 // errors with position info: that's up to the caller
1172 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
1173     for i := 0; i < n; i++ {
1174         b, err := jr.readByte()
1175         if err == io.EOF {
1176             return errStringEarlyEnd
1177         }
1178         if err != nil {
1179             return err
1180         }
1181 
1182         if b >= 128 {
1183             return errInvalidHex
1184         }
1185 
1186         if b := matchHex[b]; b != 0 {
1187             w.WriteByte(b)
1188             continue
1189         }
1190 
1191         return errInvalidHex
1192     }
1193 
1194     return nil
1195 }
1196 
1197 // handleValue is a generic JSON-token handler, which allows the recursive
1198 // behavior to handle any kind of JSON/pseudo-JSON input
1199 func handleValue(w *bufio.Writer, jr *jsonReader) error {
1200     chunk, err := jr.r.Peek(1)
1201     if err == nil && len(chunk) >= 1 {
1202         return handleValueDispatch(w, jr, chunk[0])
1203     }
1204 
1205     if err == io.EOF {
1206         return jr.improveError(errInputEarlyEnd)
1207     }
1208     return jr.improveError(errInputEarlyEnd)
1209 }
1210 
1211 // handleValueDispatch simplifies control-flow for func handleValue
1212 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
1213     switch b {
1214     case '#':
1215         return jr.skipLine()
1216     case 'f':
1217         return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
1218     case 'n':
1219         return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
1220     case 't':
1221         return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
1222     case 'F':
1223         return replaceKeyword(w, jr, []byte(`False`), []byte(`false`))
1224     case 'N':
1225         return replaceKeyword(w, jr, []byte(`None`), []byte(`null`))
1226     case 'T':
1227         return replaceKeyword(w, jr, []byte(`True`), []byte(`true`))
1228     case '.':
1229         return handleDot(w, jr)
1230     case '+':
1231         return handlePositive(w, jr)
1232     case '-':
1233         return handleNegative(w, jr)
1234     case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
1235         return handleNumber(w, jr)
1236     case '\'', '"':
1237         return handleString(w, jr, b)
1238     case '[', '(':
1239         return handleArray(w, jr, b)
1240     case '{':
1241         return handleObject(w, jr)
1242     default:
1243         return jr.improveError(errInvalidToken)
1244     }
1245 }
     File: ./json0/json_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package json0
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "strings"
  32     "testing"
  33 )
  34 
  35 func TestJSON0(t *testing.T) {
  36     tests := map[string]string{
  37         `false`:    `false`,
  38         `null`:     `null`,
  39         `  true  `: `true`,
  40 
  41         `False`:    `false`,
  42         `None`:     `null`,
  43         `  True  `: `true`,
  44 
  45         `0`: `0`,
  46         `1`: `1`,
  47         `2`: `2`,
  48         `3`: `3`,
  49         `4`: `4`,
  50         `5`: `5`,
  51         `6`: `6`,
  52         `7`: `7`,
  53         `8`: `8`,
  54         `9`: `9`,
  55 
  56         `  .345`:       `0.345`,
  57         ` -.345`:       `-0.345`,
  58         ` +.345`:       `0.345`,
  59         ` +123.345`:    `123.345`,
  60         ` 123.34523`:   `123.34523`,
  61         ` 123.34_523`:  `123.34523`,
  62         ` 123_456.123`: `123456.123`,
  63         ` 123n `:       `123`,
  64         ` +123n `:      `123`,
  65         ` -123n `:      `-123`,
  66         ` -123_456n `:  `-123456`,
  67 
  68         `""`:           `""`,
  69         `''`:           `""`,
  70         `"\""`:         `"\""`,
  71         `'\"'`:         `"\""`,
  72         `'\''`:         `"'"`,
  73         `'abc\u0e9A'`:  `"abc\u0E9A"`,
  74         `'abc\x1f[0m'`: `"abc\u001F[0m"`,
  75         `"abc●def"`:    `"abc●def"`,
  76 
  77         `[  ]`:                  `[]`,
  78         `[ , ]`:                 `[]`,
  79         `[.345, false,null , ]`: `[0.345,false,null]`,
  80 
  81         `(  )`:                  `[]`,
  82         `( , )`:                 `[]`,
  83         `(.345, false,null , )`: `[0.345,false,null]`,
  84 
  85         `{  }`:  `{}`,
  86         `{ , }`: `{}`,
  87 
  88         `{ 'abc': .345, "def"  : false, 'xyz':null , }`: `{"abc":0.345,"def":false,"xyz":null}`,
  89 
  90         `{0problems:123,}`: `{"0problems":123}`,
  91         `{0_problems:123}`: `{"0_problems":123}`,
  92     }
  93 
  94     for input, expected := range tests {
  95         t.Run(input, func(t *testing.T) {
  96             var out strings.Builder
  97             w := bufio.NewWriter(&out)
  98             r := bufio.NewReader(strings.NewReader(input))
  99             if err := json0(w, r, config{}); err != nil && err != io.EOF {
 100                 t.Fatal(err)
 101                 return
 102             }
 103             // don't forget to flush the buffer, or output will be empty
 104             w.Flush()
 105 
 106             // output may have a final line-feed: get rid of it, or every
 107             // single test-case will fail
 108             got := out.String()
 109             if len(got) > 0 && got[len(got)-1] == '\n' {
 110                 got = got[:len(got)-1]
 111             }
 112 
 113             if got != expected {
 114                 t.Fatalf("<got>\n%s\n<expected>\n%s", got, expected)
 115                 return
 116             }
 117         })
 118     }
 119 }
 120 
 121 func TestEscapedStringBytes(t *testing.T) {
 122     var escaped = map[rune][]byte{
 123         '\x00': {'\\', 'u', '0', '0', '0', '0'},
 124         '\x01': {'\\', 'u', '0', '0', '0', '1'},
 125         '\x02': {'\\', 'u', '0', '0', '0', '2'},
 126         '\x03': {'\\', 'u', '0', '0', '0', '3'},
 127         '\x04': {'\\', 'u', '0', '0', '0', '4'},
 128         '\x05': {'\\', 'u', '0', '0', '0', '5'},
 129         '\x06': {'\\', 'u', '0', '0', '0', '6'},
 130         '\x07': {'\\', 'u', '0', '0', '0', '7'},
 131         '\x0b': {'\\', 'u', '0', '0', '0', 'b'},
 132         '\x0e': {'\\', 'u', '0', '0', '0', 'e'},
 133         '\x0f': {'\\', 'u', '0', '0', '0', 'f'},
 134         '\x10': {'\\', 'u', '0', '0', '1', '0'},
 135         '\x11': {'\\', 'u', '0', '0', '1', '1'},
 136         '\x12': {'\\', 'u', '0', '0', '1', '2'},
 137         '\x13': {'\\', 'u', '0', '0', '1', '3'},
 138         '\x14': {'\\', 'u', '0', '0', '1', '4'},
 139         '\x15': {'\\', 'u', '0', '0', '1', '5'},
 140         '\x16': {'\\', 'u', '0', '0', '1', '6'},
 141         '\x17': {'\\', 'u', '0', '0', '1', '7'},
 142         '\x18': {'\\', 'u', '0', '0', '1', '8'},
 143         '\x19': {'\\', 'u', '0', '0', '1', '9'},
 144         '\x1a': {'\\', 'u', '0', '0', '1', 'a'},
 145         '\x1b': {'\\', 'u', '0', '0', '1', 'b'},
 146         '\x1c': {'\\', 'u', '0', '0', '1', 'c'},
 147         '\x1d': {'\\', 'u', '0', '0', '1', 'd'},
 148         '\x1e': {'\\', 'u', '0', '0', '1', 'e'},
 149         '\x1f': {'\\', 'u', '0', '0', '1', 'f'},
 150 
 151         '\t': {'\\', 't'},
 152         '\f': {'\\', 'f'},
 153         '\b': {'\\', 'b'},
 154         '\r': {'\\', 'r'},
 155         '\n': {'\\', 'n'},
 156         '\\': {'\\', '\\'},
 157         '"':  {'\\', '"'},
 158     }
 159 
 160     if n := len(escapedStringBytes); n != 128 {
 161         t.Errorf(`expected 128 entries, instead of %d`, n)
 162     }
 163 
 164     for i, v := range escapedStringBytes {
 165         exp := []byte{byte(i)}
 166         if esc, ok := escaped[rune(i)]; ok {
 167             exp = esc
 168         }
 169 
 170         if !bytes.Equal(v, exp) {
 171             t.Errorf("%d: expected %#v, got %#v", i, exp, v)
 172         }
 173     }
 174 }
     File: ./json0/tables_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package json0
  26 
  27 import (
  28     "bytes"
  29     "encoding/json"
  30     "testing"
  31 )
  32 
  33 func TestTables(t *testing.T) {
  34     var buf []byte
  35 
  36     for i := 0; i < 128; i++ {
  37         r := rune(i)
  38         exp := buf[:0]
  39         exp = append(exp, '"')
  40         exp = append(exp, escapedStringBytes[i]...)
  41         exp = append(exp, '"')
  42 
  43         got, err := json.Marshal(string(r))
  44         if err != nil {
  45             t.Error(err)
  46             continue
  47         }
  48 
  49         if bytes.Equal(got, exp) {
  50             continue
  51         }
  52 
  53         // can't fully rely on the JSON string-encoder from the stdlib
  54         switch r {
  55         case '&', '<', '>':
  56             continue
  57         }
  58 
  59         const fs = "escaped-strings entry @ %d: expected %q, got %q"
  60         t.Errorf(fs, i, string(exp), string(got))
  61     }
  62 }
     File: ./json2/json2.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package json2
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 json2 [filepath...]
  37 
  38 JSON-2 indents valid JSON input into multi-line JSON which uses 2 spaces for
  39 each indentation level.
  40 `
  41 
  42 // escapedStringBytes helps funcs like handleString treat all strings
  43 // quickly and correctly, using their officially-supported JSON escape
  44 // sequences
  45 //
  46 // https://www.rfc-editor.org/rfc/rfc8259#section-7
  47 var escapedStringBytes = [128][]byte{
  48     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
  49     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
  50     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
  51     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
  52     {'\\', 'b'}, {'\\', 't'},
  53     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
  54     {'\\', 'f'}, {'\\', 'r'},
  55     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
  56     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
  57     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
  58     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
  59     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
  60     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
  61     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
  62     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
  63     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
  64     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
  65     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
  66     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
  67     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
  68     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
  69     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
  70     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
  71     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
  72     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
  73     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
  74     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
  75     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
  76 }
  77 
  78 func Main() {
  79     args := os.Args[1:]
  80 
  81     if len(args) > 0 {
  82         switch args[0] {
  83         case `-h`, `--h`, `-help`, `--help`:
  84             os.Stdout.WriteString(info[1:])
  85             return
  86         }
  87     }
  88 
  89     if len(args) > 0 && args[0] == `--` {
  90         args = args[1:]
  91     }
  92 
  93     if len(args) > 1 {
  94         const msg = "multiple inputs aren't allowed\n"
  95         os.Stderr.WriteString(msg)
  96         os.Exit(1)
  97         return
  98     }
  99 
 100     // figure out whether input should come from a named file or from stdin
 101     name := `-`
 102     if len(args) == 1 {
 103         name = args[0]
 104     }
 105 
 106     if err := handleInput(os.Stdout, name); err != nil && err != io.EOF {
 107         os.Stderr.WriteString(`json2: `)
 108         os.Stderr.WriteString(err.Error())
 109         os.Stderr.WriteString("\n")
 110         os.Exit(1)
 111         return
 112     }
 113 }
 114 
 115 // handleInput simplifies control-flow for func main
 116 func handleInput(w io.Writer, path string) error {
 117     if path == `-` {
 118         return convert(w, os.Stdin)
 119     }
 120 
 121     f, err := os.Open(path)
 122     if err != nil {
 123         // on windows, file-not-found error messages may mention `CreateFile`,
 124         // even when trying to open files in read-only mode
 125         return errors.New(`can't open file named ` + path)
 126     }
 127     defer f.Close()
 128     return convert(w, f)
 129 }
 130 
 131 // convert simplifies control-flow for func handleInput
 132 func convert(w io.Writer, r io.Reader) error {
 133     bw := bufio.NewWriterSize(w, 32*1024)
 134     defer bw.Flush()
 135     return json2(bw, r)
 136 }
 137 
 138 // writeSpaces does what it says, minimizing calls to write-like funcs
 139 func writeSpaces(w *bufio.Writer, n int) {
 140     const spaces = `                                `
 141     if n < 1 {
 142         return
 143     }
 144 
 145     for n >= len(spaces) {
 146         w.WriteString(spaces)
 147         n -= len(spaces)
 148     }
 149     w.WriteString(spaces[:n])
 150 }
 151 
 152 // json2 does it all, given a reader and a writer
 153 func json2(w *bufio.Writer, r io.Reader) error {
 154     dec := json.NewDecoder(r)
 155     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 156     // even if JSON parsers aren't required to guarantee such input-fidelity
 157     // for numbers
 158     dec.UseNumber()
 159 
 160     t, err := dec.Token()
 161     if err == io.EOF {
 162         return errors.New(`input has no JSON values`)
 163     }
 164 
 165     if err = handleToken(w, dec, t, 0, 0); err != nil {
 166         return err
 167     }
 168     // don't forget ending the last line for the last value
 169     w.WriteByte('\n')
 170 
 171     _, err = dec.Token()
 172     if err == io.EOF {
 173         // input is over, so it's a success
 174         return nil
 175     }
 176 
 177     if err == nil {
 178         // a successful `read` is a failure, as it means there are
 179         // trailing JSON tokens
 180         return errors.New(`unexpected trailing data`)
 181     }
 182 
 183     // any other error, perhaps some invalid-JSON-syntax-type error
 184     return err
 185 }
 186 
 187 // handleToken handles recursion for func json2
 188 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
 189     switch t := t.(type) {
 190     case json.Delim:
 191         switch t {
 192         case json.Delim('['):
 193             return handleArray(w, dec, pre, level)
 194         case json.Delim('{'):
 195             return handleObject(w, dec, pre, level)
 196         default:
 197             return errors.New(`unsupported JSON syntax ` + string(t))
 198         }
 199 
 200     case nil:
 201         writeSpaces(w, 2*pre)
 202         w.WriteString(`null`)
 203         return nil
 204 
 205     case bool:
 206         writeSpaces(w, 2*pre)
 207         if t {
 208             w.WriteString(`true`)
 209         } else {
 210             w.WriteString(`false`)
 211         }
 212         return nil
 213 
 214     case json.Number:
 215         writeSpaces(w, 2*pre)
 216         w.WriteString(t.String())
 217         return nil
 218 
 219     case string:
 220         return handleString(w, t, pre)
 221 
 222     default:
 223         // return fmt.Errorf(`unsupported token type %T`, t)
 224         return errors.New(`invalid JSON token`)
 225     }
 226 }
 227 
 228 // handleArray handles arrays for func handleToken
 229 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 230     for i := 0; true; i++ {
 231         t, err := dec.Token()
 232         if err == io.EOF {
 233             return errors.New(`end of JSON before array was closed`)
 234         }
 235         if err != nil {
 236             return err
 237         }
 238 
 239         if t == json.Delim(']') {
 240             if i == 0 {
 241                 writeSpaces(w, 2*pre)
 242                 w.WriteByte('[')
 243                 w.WriteByte(']')
 244             } else {
 245                 w.WriteByte('\n')
 246                 writeSpaces(w, 2*level)
 247                 w.WriteByte(']')
 248             }
 249             return nil
 250         }
 251 
 252         if i == 0 {
 253             writeSpaces(w, 2*pre)
 254             w.WriteByte('[')
 255             w.WriteByte('\n')
 256         } else {
 257             w.WriteByte(',')
 258             w.WriteByte('\n')
 259         }
 260 
 261         err = handleToken(w, dec, t, level+1, level+1)
 262         if err != nil {
 263             return err
 264         }
 265     }
 266 
 267     // make the compiler happy
 268     return nil
 269 }
 270 
 271 // handleObject handles objects for func handleToken
 272 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 273     for i := 0; true; i++ {
 274         t, err := dec.Token()
 275         if err == io.EOF {
 276             return errors.New(`end of JSON before object was closed`)
 277         }
 278         if err != nil {
 279             return err
 280         }
 281 
 282         if t == json.Delim('}') {
 283             if i == 0 {
 284                 writeSpaces(w, 2*pre)
 285                 w.WriteByte('{')
 286                 w.WriteByte('}')
 287             } else {
 288                 w.WriteByte('\n')
 289                 writeSpaces(w, 2*level)
 290                 w.WriteByte('}')
 291             }
 292             return nil
 293         }
 294 
 295         if i == 0 {
 296             writeSpaces(w, 2*pre)
 297             w.WriteByte('{')
 298             w.WriteByte('\n')
 299         } else {
 300             w.WriteByte(',')
 301             w.WriteByte('\n')
 302         }
 303 
 304         k, ok := t.(string)
 305         if !ok {
 306             return errors.New(`expected a string for a key-value pair`)
 307         }
 308 
 309         err = handleString(w, k, level+1)
 310         if err != nil {
 311             return err
 312         }
 313 
 314         w.WriteString(": ")
 315 
 316         t, err = dec.Token()
 317         if err == io.EOF {
 318             return errors.New(`expected a value for a key-value pair`)
 319         }
 320 
 321         err = handleToken(w, dec, t, 0, level+1)
 322         if err != nil {
 323             return err
 324         }
 325     }
 326 
 327     // make the compiler happy
 328     return nil
 329 }
 330 
 331 // handleString handles strings for func handleToken, and keys for func
 332 // handleObject
 333 func handleString(w *bufio.Writer, s string, level int) error {
 334     writeSpaces(w, 2*level)
 335 
 336     for _, r := range s {
 337         if r < ' ' || r == '"' || r == '\\' {
 338             w.WriteByte('"')
 339             for _, r := range s {
 340                 if int(r) < len(escapedStringBytes) {
 341                     w.Write(escapedStringBytes[r])
 342                 } else {
 343                     w.WriteRune(r)
 344                 }
 345             }
 346             w.WriteByte('"')
 347             return nil
 348         }
 349     }
 350 
 351     w.WriteByte('"')
 352     w.WriteString(s)
 353     w.WriteByte('"')
 354     return nil
 355 }
     File: ./jsonl/jsonl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package jsonl
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 jsonl [options...] [filepaths...]
  37 
  38 JSON Lines turns valid JSON-input arrays into separate JSON lines, one for
  39 each top-level item. Non-arrays result in a single JSON-line.
  40 
  41 When not given a filepath to load, standard input is used instead. Every
  42 output line is always a single top-level item from the input.
  43 `
  44 
  45 func Main() {
  46     args := os.Args[1:]
  47     buffered := false
  48 
  49     for len(args) > 0 {
  50         switch args[0] {
  51         case `-b`, `--b`, `-buffered`, `--buffered`:
  52             buffered = true
  53             args = args[1:]
  54             continue
  55 
  56         case `-h`, `--h`, `-help`, `--help`:
  57             os.Stdout.WriteString(info[1:])
  58             return
  59         }
  60 
  61         break
  62     }
  63 
  64     if len(args) > 0 && args[0] == `--` {
  65         args = args[1:]
  66     }
  67 
  68     liveLines := !buffered
  69     if !buffered {
  70         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  71             liveLines = false
  72         }
  73     }
  74 
  75     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  76         os.Stderr.WriteString(`jsonl: `)
  77         os.Stderr.WriteString(err.Error())
  78         os.Stderr.WriteString("\n")
  79         os.Exit(1)
  80         return
  81     }
  82 }
  83 
  84 func run(w io.Writer, args []string, liveLines bool) error {
  85     dashes := 0
  86     for _, path := range args {
  87         if path == `-` {
  88             dashes++
  89         }
  90         if dashes > 1 {
  91             return errors.New(`can't use stdin (dash) more than once`)
  92         }
  93     }
  94 
  95     bw := bufio.NewWriterSize(w, 32*1024)
  96     defer bw.Flush()
  97 
  98     if len(args) == 0 {
  99         return handleInput(bw, `-`, liveLines)
 100     }
 101 
 102     for _, path := range args {
 103         if err := handleInput(bw, path, liveLines); err != nil {
 104             return err
 105         }
 106     }
 107     return nil
 108 }
 109 
 110 // handleInput simplifies control-flow for func main
 111 func handleInput(w *bufio.Writer, path string, liveLines bool) error {
 112     if path == `-` {
 113         return jsonl(w, os.Stdin, liveLines)
 114     }
 115 
 116     f, err := os.Open(path)
 117     if err != nil {
 118         // on windows, file-not-found error messages may mention `CreateFile`,
 119         // even when trying to open files in read-only mode
 120         return errors.New(`can't open file named ` + path)
 121     }
 122     defer f.Close()
 123     return jsonl(w, f, liveLines)
 124 }
 125 
 126 // escapedStringBytes helps funcs like handleString treat all strings
 127 // quickly and correctly, using their officially-supported JSON escape
 128 // sequences
 129 //
 130 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 131 var escapedStringBytes = [128][]byte{
 132     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 133     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 134     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 135     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 136     {'\\', 'b'}, {'\\', 't'},
 137     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 138     {'\\', 'f'}, {'\\', 'r'},
 139     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 140     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 141     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 142     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 143     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 144     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 145     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 146     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 147     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 148     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 149     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 150     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 151     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 152     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 153     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 154     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 155     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 156     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 157     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 158     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 159     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 160 }
 161 
 162 // jsonl does it all, given a reader and a writer
 163 func jsonl(w *bufio.Writer, r io.Reader, live bool) error {
 164     dec := json.NewDecoder(r)
 165     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 166     // even if JSON parsers aren't required to guarantee such input-fidelity
 167     // for numbers
 168     dec.UseNumber()
 169 
 170     t, err := dec.Token()
 171     if err == io.EOF {
 172         // return errors.New(`input has no JSON values`)
 173         return nil
 174     }
 175 
 176     if t == json.Delim('[') {
 177         if err := handleTopLevelArray(w, dec, live); err != nil {
 178             return err
 179         }
 180     } else {
 181         if err := handleToken(w, dec, t); err != nil {
 182             return err
 183         }
 184         w.WriteByte('\n')
 185     }
 186 
 187     _, err = dec.Token()
 188     if err == io.EOF {
 189         // input is over, so it's a success
 190         return nil
 191     }
 192 
 193     if err == nil {
 194         // a successful `read` is a failure, as it means there are
 195         // trailing JSON tokens
 196         return errors.New(`unexpected trailing data`)
 197     }
 198 
 199     // any other error, perhaps some invalid-JSON-syntax-type error
 200     return err
 201 }
 202 
 203 // handleToken handles recursion for func json2
 204 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token) error {
 205     switch t := t.(type) {
 206     case json.Delim:
 207         switch t {
 208         case json.Delim('['):
 209             return handleArray(w, dec)
 210         case json.Delim('{'):
 211             return handleObject(w, dec)
 212         default:
 213             return errors.New(`unsupported JSON syntax ` + string(t))
 214         }
 215 
 216     case nil:
 217         w.WriteString(`null`)
 218         return nil
 219 
 220     case bool:
 221         if t {
 222             w.WriteString(`true`)
 223         } else {
 224             w.WriteString(`false`)
 225         }
 226         return nil
 227 
 228     case json.Number:
 229         w.WriteString(t.String())
 230         return nil
 231 
 232     case string:
 233         return handleString(w, t)
 234 
 235     default:
 236         // return fmt.Errorf(`unsupported token type %T`, t)
 237         return errors.New(`invalid JSON token`)
 238     }
 239 }
 240 
 241 func handleTopLevelArray(w *bufio.Writer, dec *json.Decoder, live bool) error {
 242     for i := 0; true; i++ {
 243         t, err := dec.Token()
 244         if err == io.EOF {
 245             return nil
 246         }
 247 
 248         if err != nil {
 249             return err
 250         }
 251 
 252         if t == json.Delim(']') {
 253             return nil
 254         }
 255 
 256         err = handleToken(w, dec, t)
 257         if err != nil {
 258             return err
 259         }
 260 
 261         if w.WriteByte('\n') != nil {
 262             return io.EOF
 263         }
 264 
 265         if live && w.Flush() != nil {
 266             return io.EOF
 267         }
 268     }
 269 
 270     // make the compiler happy
 271     return nil
 272 }
 273 
 274 // handleArray handles arrays for func handleToken
 275 func handleArray(w *bufio.Writer, dec *json.Decoder) error {
 276     w.WriteByte('[')
 277 
 278     for i := 0; true; i++ {
 279         t, err := dec.Token()
 280         if err == io.EOF {
 281             return errors.New(`end of JSON before array was closed`)
 282         }
 283         if err != nil {
 284             return err
 285         }
 286 
 287         if t == json.Delim(']') {
 288             w.WriteByte(']')
 289             return nil
 290         }
 291 
 292         if i > 0 {
 293             _, err := w.WriteString(", ")
 294             if err != nil {
 295                 return io.EOF
 296             }
 297         }
 298 
 299         err = handleToken(w, dec, t)
 300         if err != nil {
 301             return err
 302         }
 303     }
 304 
 305     // make the compiler happy
 306     return nil
 307 }
 308 
 309 // handleObject handles objects for func handleToken
 310 func handleObject(w *bufio.Writer, dec *json.Decoder) error {
 311     w.WriteByte('{')
 312 
 313     for i := 0; true; i++ {
 314         t, err := dec.Token()
 315         if err == io.EOF {
 316             return errors.New(`end of JSON before object was closed`)
 317         }
 318         if err != nil {
 319             return err
 320         }
 321 
 322         if t == json.Delim('}') {
 323             w.WriteByte('}')
 324             return nil
 325         }
 326 
 327         if i > 0 {
 328             _, err := w.WriteString(", ")
 329             if err != nil {
 330                 return io.EOF
 331             }
 332         }
 333 
 334         k, ok := t.(string)
 335         if !ok {
 336             return errors.New(`expected a string for a key-value pair`)
 337         }
 338 
 339         err = handleString(w, k)
 340         if err != nil {
 341             return err
 342         }
 343 
 344         w.WriteString(": ")
 345 
 346         t, err = dec.Token()
 347         if err == io.EOF {
 348             return errors.New(`expected a value for a key-value pair`)
 349         }
 350 
 351         err = handleToken(w, dec, t)
 352         if err != nil {
 353             return err
 354         }
 355     }
 356 
 357     // make the compiler happy
 358     return nil
 359 }
 360 
 361 // handleString handles strings for func handleToken, and keys for func
 362 // handleObject
 363 func handleString(w *bufio.Writer, s string) error {
 364     for _, r := range s {
 365         if r < ' ' || r == '"' || r == '\\' {
 366             w.WriteByte('"')
 367             for _, r := range s {
 368                 if int(r) < len(escapedStringBytes) {
 369                     w.Write(escapedStringBytes[r])
 370                 } else {
 371                     w.WriteRune(r)
 372                 }
 373             }
 374             w.WriteByte('"')
 375             return nil
 376         }
 377     }
 378 
 379     w.WriteByte('"')
 380     w.WriteString(s)
 381     w.WriteByte('"')
 382     return nil
 383 }
     File: ./jsons/jsons.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package jsons
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strings"
  32 )
  33 
  34 const info = `
  35 jsons [options...] [filenames...]
  36 
  37 JSON Strings turns TSV (tab-separated values) data into a JSON array of
  38 objects whose values are strings or nulls, the latter being used for
  39 missing trailing values.
  40 `
  41 
  42 func Main() {
  43     if len(os.Args) > 1 {
  44         switch os.Args[1] {
  45         case `-h`, `--h`, `-help`, `--help`:
  46             os.Stdout.WriteString(info[1:])
  47             return
  48         }
  49     }
  50 
  51     err := run(os.Args[1:])
  52     if err == io.EOF {
  53         err = nil
  54     }
  55 
  56     if err != nil {
  57         os.Stderr.WriteString(`jsons: `)
  58         os.Stderr.WriteString(err.Error())
  59         os.Stderr.WriteString("\n")
  60         os.Exit(1)
  61         return
  62     }
  63 }
  64 
  65 type runConfig struct {
  66     lines int
  67     keys  []string
  68 }
  69 
  70 func run(paths []string) error {
  71     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  72     defer bw.Flush()
  73 
  74     dashes := 0
  75     var cfg runConfig
  76 
  77     for _, path := range paths {
  78         if path == `-` {
  79             dashes++
  80             if dashes > 1 {
  81                 continue
  82             }
  83 
  84             if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  85                 return err
  86             }
  87 
  88             continue
  89         }
  90 
  91         if err := handleFile(bw, path, &cfg); err != nil {
  92             return err
  93         }
  94     }
  95 
  96     if len(paths) == 0 {
  97         if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  98             return err
  99         }
 100     }
 101 
 102     if cfg.lines > 1 {
 103         bw.WriteString("\n]\n")
 104     } else {
 105         bw.WriteString("[]\n")
 106     }
 107     return nil
 108 }
 109 
 110 func handleFile(w *bufio.Writer, path string, cfg *runConfig) error {
 111     f, err := os.Open(path)
 112     if err != nil {
 113         return err
 114     }
 115     defer f.Close()
 116     return handleInput(w, f, cfg)
 117 }
 118 
 119 func escapeKeys(line string) []string {
 120     var keys []string
 121     var sb strings.Builder
 122 
 123     loopTSV(line, func(i int, s string) {
 124         sb.WriteByte('"')
 125         for _, r := range s {
 126             if r == '\\' || r == '"' {
 127                 sb.WriteByte('\\')
 128             }
 129             sb.WriteRune(r)
 130         }
 131         sb.WriteByte('"')
 132 
 133         keys = append(keys, sb.String())
 134         sb.Reset()
 135     })
 136 
 137     return keys
 138 }
 139 
 140 func emitRow(w *bufio.Writer, line string, keys []string) {
 141     j := 0
 142     w.WriteByte('{')
 143 
 144     loopTSV(line, func(i int, s string) {
 145         j = i
 146         if i > 0 {
 147             w.WriteString(", ")
 148         }
 149 
 150         w.WriteString(keys[i])
 151         w.WriteString(": \"")
 152 
 153         for _, r := range s {
 154             if r == '\\' || r == '"' {
 155                 w.WriteByte('\\')
 156             }
 157             w.WriteRune(r)
 158         }
 159         w.WriteByte('"')
 160     })
 161 
 162     for i := j + 1; i < len(keys); i++ {
 163         if i > 0 {
 164             w.WriteString(", ")
 165         }
 166         w.WriteString(keys[i])
 167         w.WriteString(": null")
 168     }
 169     w.WriteByte('}')
 170 }
 171 
 172 func loopTSV(line string, f func(i int, s string)) {
 173     for i := 0; len(line) > 0; i++ {
 174         pos := strings.IndexByte(line, '\t')
 175         if pos < 0 {
 176             f(i, line)
 177             return
 178         }
 179 
 180         f(i, line[:pos])
 181         line = line[pos+1:]
 182     }
 183 }
 184 
 185 func handleInput(w *bufio.Writer, r io.Reader, cfg *runConfig) error {
 186     const gb = 1024 * 1024 * 1024
 187     sc := bufio.NewScanner(r)
 188     sc.Buffer(nil, 8*gb)
 189 
 190     for i := 0; sc.Scan(); i++ {
 191         s := sc.Text()
 192         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 193             s = s[3:]
 194         }
 195 
 196         if cfg.lines == 0 {
 197             cfg.keys = escapeKeys(s)
 198             w.WriteByte('[')
 199             cfg.lines++
 200             continue
 201         }
 202 
 203         if cfg.lines == 1 {
 204             w.WriteString("\n  ")
 205         } else {
 206             if _, err := w.WriteString(",\n  "); err != nil {
 207                 return io.EOF
 208             }
 209         }
 210 
 211         emitRow(w, s, cfg.keys)
 212         cfg.lines++
 213     }
 214 
 215     return sc.Err()
 216 }
     File: ./junk/junk.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package junk
  26 
  27 import (
  28     "crypto/rand"
  29     "io"
  30     "os"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 junk [byte-count...]
  37 
  38 Emit a given count of random bytes, or emit 1024 random bytes by default.
  39 
  40 All options available can either start with a single or a double-dash
  41 
  42     -h, -help    show this help message
  43 `
  44 
  45 type config struct {
  46     all  bool
  47     long bool
  48 }
  49 
  50 func Main() {
  51     args := os.Args[1:]
  52     if len(args) > 0 {
  53         switch args[0] {
  54         case `-h`, `--h`, `-help`, `--help`:
  55             os.Stdout.WriteString(info[1:])
  56             return
  57         }
  58     }
  59 
  60     n := 1024
  61     if len(args) > 0 {
  62         s := strings.Replace(args[0], `_`, ``, -1)
  63         if v, err := strconv.ParseInt(s, 10, 64); err == nil {
  64             n = int(v)
  65             args = args[1:]
  66         }
  67     }
  68 
  69     if err := writeJunk(os.Stdout, n); err != nil && err != io.EOF {
  70         os.Stderr.WriteString(`junk: `)
  71         os.Stderr.WriteString(err.Error())
  72         os.Stderr.WriteString("\n")
  73         os.Exit(1)
  74         return
  75     }
  76 }
  77 
  78 func writeJunk(w io.Writer, n int) error {
  79     var buf [32 * 1024]byte
  80 
  81     for n > 0 {
  82         max := n
  83         if max > cap(buf) {
  84             max = cap(buf)
  85         }
  86 
  87         got, err := io.ReadFull(rand.Reader, buf[:max])
  88         if err == io.EOF && got > 0 {
  89             err = nil
  90         }
  91 
  92         if err != nil {
  93             return err
  94         }
  95 
  96         if _, err := w.Write(buf[:got]); err != nil {
  97             return io.EOF
  98         }
  99 
 100         n -= got
 101     }
 102 
 103     return nil
 104 }
     File: ./label/label.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package label
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "unicode/utf8"
  33 )
  34 
  35 const info = `
  36 label [options...] [words...]
  37 
  38 Emit an ANSI-reverse-styled right-padded line with the words given as
  39 command-line arguments, then copies all bytes from the standard input
  40 into the standard output.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help       show this help message
  45 `
  46 
  47 func Main() {
  48     buffered := false
  49     args := os.Args[1:]
  50 
  51     if len(args) > 0 {
  52         switch args[0] {
  53         case `-b`, `--b`, `-buffered`, `--buffered`:
  54             buffered = true
  55             args = args[1:]
  56 
  57         case `-h`, `--h`, `-help`, `--help`, `help`:
  58             os.Stdout.WriteString(info[1:])
  59             return
  60         }
  61     }
  62 
  63     if len(args) > 0 && args[0] == `--` {
  64         args = args[1:]
  65     }
  66 
  67     liveLines := !buffered
  68     if !buffered {
  69         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  70             liveLines = false
  71         }
  72     }
  73 
  74     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  75         os.Stderr.WriteString(`label: `)
  76         os.Stderr.WriteString(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79         return
  80     }
  81 }
  82 
  83 func run(w io.Writer, args []string, live bool) error {
  84     const (
  85         half   = `                                        `
  86         spaces = half + half
  87     )
  88 
  89     bw := bufio.NewWriterSize(w, 32*1024)
  90     defer bw.Flush()
  91 
  92     bw.WriteString("\x1b[7m")
  93 
  94     left := len(spaces)
  95 
  96     for i, s := range args {
  97         if i > 0 {
  98             if err := bw.WriteByte(' '); err != nil {
  99                 return io.EOF
 100             }
 101             left--
 102         }
 103         bw.WriteString(s)
 104         left -= utf8.RuneCountInString(s)
 105     }
 106 
 107     if 0 < left && left < len(spaces) {
 108         bw.WriteString(spaces[:left])
 109     }
 110 
 111     if _, err := bw.WriteString("\x1b[0m\n"); err != nil {
 112         return io.EOF
 113     }
 114 
 115     if live && bw.Flush() != nil {
 116         return io.EOF
 117     }
 118 
 119     return catl(bw, os.Stdin, live)
 120 }
 121 
 122 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 123     const gb = 1024 * 1024 * 1024
 124     sc := bufio.NewScanner(r)
 125     sc.Buffer(nil, 8*gb)
 126 
 127     for i := 0; sc.Scan(); i++ {
 128         s := sc.Bytes()
 129         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 130             s = s[3:]
 131         }
 132 
 133         w.Write(s)
 134         if w.WriteByte('\n') != nil {
 135             return io.EOF
 136         }
 137 
 138         if live && w.Flush() != nil {
 139             return io.EOF
 140         }
 141     }
 142 
 143     return sc.Err()
 144 }
     File: ./last/last.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 Note
  27 
  28     A previous attempt was trying to act more like the standard `tail` app
  29     for efficiency, by going backwards on the list of files, reading chunks
  30     backwards to reach the starting position in the right file for (up to)
  31     the last n lines.
  32 
  33     That attempt had an intermittent bug, and was scrapped in favor of the
  34     naive/slow approach used here, forward-scanning lines and keeping them
  35     into a ring-buffer.
  36 */
  37 
  38 package last
  39 
  40 import (
  41     "bufio"
  42     "io"
  43     "os"
  44     "strconv"
  45     "strings"
  46 )
  47 
  48 const info = `
  49 last [options...] [max lines...] [files...]
  50 
  51 Keep at most the last n lines, or keep the last line by default. When not
  52 given any filepaths, the standard input is used instead.
  53 
  54 All (optional) leading options start with either single or double-dash:
  55 
  56     -h, -help    show this help message
  57 `
  58 
  59 type ringBuffer struct {
  60     next  int
  61     max   int
  62     items []string
  63 }
  64 
  65 func (rb *ringBuffer) append(s string) {
  66     if rb.next < len(rb.items) {
  67         rb.items[rb.next] = s
  68     } else if len(rb.items) < rb.max {
  69         rb.items = append(rb.items, s)
  70     } else if len(rb.items) > 0 {
  71         rb.items[0] = s
  72     }
  73 
  74     rb.next++
  75     if rb.next >= rb.max {
  76         rb.next = 0
  77     }
  78 }
  79 
  80 func Main() {
  81     var latest ringBuffer
  82     latest.max = 1
  83     args := os.Args[1:]
  84 
  85     if len(args) > 0 {
  86         switch args[0] {
  87         case `-h`, `--h`, `-help`, `--help`:
  88             os.Stderr.WriteString(info[1:])
  89             return
  90         }
  91     }
  92 
  93     if len(args) > 0 {
  94         s := strings.Replace(args[0], `_`, ``, -1)
  95         n, err := strconv.ParseInt(s, 10, 64)
  96         if err == nil {
  97             latest.max = int(n)
  98             args = args[1:]
  99         }
 100     }
 101 
 102     if len(args) > 0 && args[0] == `--` {
 103         args = args[1:]
 104     }
 105 
 106     if latest.max <= 0 {
 107         return
 108     }
 109 
 110     if latest.max <= 1_000 {
 111         latest.items = make([]string, 0, latest.max)
 112     }
 113 
 114     if err := run(args, &latest); err != nil {
 115         os.Stderr.WriteString(`last: `)
 116         os.Stderr.WriteString(err.Error())
 117         os.Stderr.WriteString("\n")
 118         os.Exit(1)
 119         return
 120     }
 121 
 122     show(os.Stdout, latest)
 123 }
 124 
 125 func run(paths []string, rb *ringBuffer) error {
 126     for _, path := range paths {
 127         if err := handleFile(path, rb); err != nil {
 128             return err
 129         }
 130     }
 131 
 132     if len(paths) == 0 {
 133         if err := visit(os.Stdin, rb); err != nil {
 134             return err
 135         }
 136     }
 137     return nil
 138 }
 139 
 140 func show(w io.Writer, rb ringBuffer) {
 141     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 142     defer bw.Flush()
 143 
 144     for i := rb.next; i < len(rb.items); i++ {
 145         bw.WriteString(rb.items[i])
 146         if err := bw.WriteByte('\n'); err != nil {
 147             return
 148         }
 149     }
 150 
 151     for i := 0; i < len(rb.items) && i < rb.next; i++ {
 152         bw.WriteString(rb.items[i])
 153         if err := bw.WriteByte('\n'); err != nil {
 154             return
 155         }
 156     }
 157 }
 158 
 159 func handleFile(path string, rb *ringBuffer) error {
 160     f, err := os.Open(path)
 161     if err != nil {
 162         return err
 163     }
 164     defer f.Close()
 165     return visit(f, rb)
 166 }
 167 
 168 func visit(r io.Reader, rb *ringBuffer) error {
 169     const gb = 1024 * 1024 * 1024
 170     sc := bufio.NewScanner(r)
 171     sc.Buffer(nil, 8*gb)
 172 
 173     for sc.Scan() {
 174         rb.append(sc.Text())
 175     }
 176     return sc.Err()
 177 }
     File: ./leak/leak.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package leak
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "os/exec"
  33 )
  34 
  35 const info = `
  36 leak [options...] [command...] [command args...]
  37 
  38 
  39 Emit copies of input lines both to stdout and to stderr, thus 'leaking'
  40 the contents going through a pipe of commands. If a command is given, the
  41 standard-error output is the standard output of that command, instead.
  42 
  43 This tool's main use-case is to inspect/debug the intermediate stages of
  44 a 'pipelined' shell command.
  45 
  46 All (optional) leading options start with either single or double-dash:
  47 
  48     -h, -help    show this help message
  49 `
  50 
  51 type config struct {
  52     // live is whether lines are flushed each time
  53     live bool
  54 
  55     inverted bool
  56 }
  57 
  58 func Main() {
  59     var cfg config
  60     cfg.live = true
  61     cfg.inverted = false
  62     args := os.Args[1:]
  63 
  64     for len(args) > 0 {
  65         switch args[0] {
  66         case `-b`, `--b`, `-buffered`, `--buffered`:
  67             cfg.live = false
  68             args = args[1:]
  69             continue
  70 
  71         case `-h`, `--h`, `-help`, `--help`:
  72             os.Stdout.WriteString(info[1:])
  73             return
  74 
  75         case `-i`, `--i`, `-inv`, `--inv`, `-hi`, `--hi`:
  76             cfg.inverted = true
  77             args = args[1:]
  78             continue
  79         }
  80 
  81         break
  82     }
  83 
  84     if len(args) > 0 && args[0] == `--` {
  85         args = args[1:]
  86     }
  87 
  88     if cfg.live {
  89         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  90             cfg.live = false
  91         }
  92     }
  93 
  94     err := run(os.Stdout, os.Stderr, os.Stdin, args, cfg)
  95     if ee, ok := err.(*exec.ExitError); ok {
  96         os.Exit(ee.ExitCode())
  97         return
  98     }
  99     if err != nil && err != io.EOF {
 100         os.Stderr.WriteString(`leak: `)
 101         os.Stderr.WriteString(err.Error())
 102         os.Stderr.WriteString("\n")
 103         os.Exit(1)
 104         return
 105     }
 106 }
 107 
 108 func run(w1, w2 io.Writer, r io.Reader, args []string, cfg config) error {
 109     bw1 := bufio.NewWriterSize(w1, 32*1024)
 110     defer bw1.Flush()
 111 
 112     bw2 := bufio.NewWriterSize(w2, 32*1024)
 113     defer bw2.Flush()
 114 
 115     if len(args) == 0 {
 116         return tee(bw1, bw2, r, cfg)
 117     }
 118 
 119     cmd := exec.Command(args[0], args[1:]...)
 120     if cfg.live {
 121         cmd.Stdin = ioDuplicator{r, lineFlusher{bw1}}
 122         cmd.Stdout = lineFlusher{bw2}
 123     } else {
 124         cmd.Stdin = ioDuplicator{r, bw1}
 125         cmd.Stdout = bw2
 126     }
 127     cmd.Stderr = cmd.Stdout
 128 
 129     if err := cmd.Start(); err != nil {
 130         return err
 131     }
 132 
 133     return cmd.Wait()
 134 }
 135 
 136 func tee(w1 *bufio.Writer, w2 *bufio.Writer, r io.Reader, cfg config) error {
 137     const gb = 1024 * 1024 * 1024
 138     sc := bufio.NewScanner(r)
 139     sc.Buffer(nil, 8*gb)
 140 
 141     var buf []byte
 142 
 143     for i := 0; sc.Scan(); i++ {
 144         s := sc.Bytes()
 145         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 146             s = s[3:]
 147         }
 148 
 149         w1.Write(s)
 150         if w1.WriteByte('\n') != nil {
 151             return io.EOF
 152         }
 153 
 154         // leak the (re)styled line to the second writer
 155         if cfg.inverted {
 156             buf = buf[:0]
 157             buf = append(buf, "\x1b[7m"...)
 158             // buf = appendPlain(buf, s)
 159             buf = appendInvertedInner(buf, s)
 160             buf = append(buf, "\x1b[0m"...)
 161             w2.Write(buf)
 162         } else {
 163             w2.Write(s)
 164         }
 165 
 166         if w2.WriteByte('\n') != nil {
 167             return io.EOF
 168         }
 169 
 170         if !cfg.live {
 171             continue
 172         }
 173 
 174         // a write error may be the consequence of an output stream being
 175         // closed, perhaps by another app along a pipe
 176         if err := w1.Flush(); err != nil {
 177             return io.EOF
 178         }
 179         if err := w2.Flush(); err != nil {
 180             return io.EOF
 181         }
 182     }
 183 
 184     return sc.Err()
 185 }
 186 
 187 func appendInvertedInner(dst []byte, src []byte) []byte {
 188     for len(src) > 0 {
 189         j := bytes.Index(src, []byte{0x1b, '[', '0', 'm'})
 190         if j < 0 {
 191             dst = append(dst, src...)
 192             break
 193         }
 194 
 195         dst = append(dst, src[:j]...)
 196         dst = append(dst, "\x1b[0m\x1b[7m"...)
 197         j += len("\x1b[0m")
 198         src = src[j:]
 199     }
 200 
 201     return dst
 202 }
 203 
 204 // appendPlain extends the slice given using the non-ANSI parts of a string
 205 func appendPlain(dst []byte, src []byte) []byte {
 206     for len(src) > 0 {
 207         i, j := indexEscapeSequence(src)
 208         if i < 0 {
 209             dst = append(dst, src...)
 210             break
 211         }
 212         if j < 0 {
 213             j = len(src)
 214         }
 215 
 216         if i > 0 {
 217             dst = append(dst, src[:i]...)
 218         }
 219 
 220         src = src[j:]
 221     }
 222 
 223     return dst
 224 }
 225 
 226 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 227 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 228 // indices which can be independently negative when either the start/end of
 229 // a sequence isn't found; given their fairly-common use, even the hyperlink
 230 // ESC]8 sequences are supported
 231 func indexEscapeSequence(s []byte) (int, int) {
 232     var prev byte
 233 
 234     for i, b := range s {
 235         if prev == '\x1b' && b == '[' {
 236             j := indexLetter(s[i+1:])
 237             if j < 0 {
 238                 return i, -1
 239             }
 240             return i - 1, i + 1 + j + 1
 241         }
 242 
 243         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 244             j := indexPair(s[i+1:], '\x1b', '\\')
 245             if j < 0 {
 246                 return i, -1
 247             }
 248             return i - 1, i + 1 + j + 2
 249         }
 250 
 251         prev = b
 252     }
 253 
 254     return -1, -1
 255 }
 256 
 257 func indexLetter(s []byte) int {
 258     for i, b := range s {
 259         upper := b &^ 32
 260         if 'A' <= upper && upper <= 'Z' {
 261             return i
 262         }
 263     }
 264 
 265     return -1
 266 }
 267 
 268 func indexPair(s []byte, x byte, y byte) int {
 269     var prev byte
 270 
 271     for i, b := range s {
 272         if prev == x && b == y && i > 0 {
 273             return i
 274         }
 275         prev = b
 276     }
 277 
 278     return -1
 279 }
 280 
 281 type ioDuplicator struct {
 282     r io.Reader
 283     w io.Writer
 284 }
 285 
 286 func (iod ioDuplicator) Read(p []byte) (n int, err error) {
 287     n, err = iod.r.Read(p)
 288     if n > 0 {
 289         iod.w.Write(p[:n])
 290     }
 291     return n, err
 292 }
 293 
 294 type lineFlusher struct {
 295     bw *bufio.Writer
 296 }
 297 
 298 func (lf lineFlusher) Write(p []byte) (n int, err error) {
 299     n, err = lf.bw.Write(p)
 300     if err != nil {
 301         return n, err
 302     }
 303 
 304     if bytes.IndexByte(p, '\n') >= 0 {
 305         err = lf.bw.Flush()
 306     }
 307 
 308     return n, err
 309 }
     File: ./limit/limit.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package limit
  26 
  27 import (
  28     "errors"
  29     "io"
  30     "os"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 limit [options...] [max bytes...] [files...]
  37 
  38 Limit output to only the first n given bytes, using the leading number given,
  39 or the first 1024 bytes by default. This is just a more intuitive alternative
  40 to using the basic command "head -c".
  41 
  42 All (optional) leading options start with either single or double-dash:
  43 
  44     -h, -help    show this help message
  45 `
  46 
  47 var multipliers = []struct {
  48     suffix string
  49     factor int
  50 }{
  51     {`k`, 1024},
  52     {`K`, 1024},
  53     {`KB`, 1024},
  54     {`KiB`, 1024},
  55     {`m`, 1024 * 1024},
  56     {`M`, 1024 * 1024},
  57     {`MB`, 1024 * 1024},
  58     {`MiB`, 1024 * 1024},
  59 }
  60 
  61 func Main() {
  62     args := os.Args[1:]
  63 
  64     if len(args) > 0 {
  65         switch args[0] {
  66         case `-h`, `--h`, `-help`, `--help`, `help`:
  67             os.Stdout.WriteString(info[1:])
  68             return
  69         }
  70     }
  71 
  72     n := 1024
  73     if len(args) > 0 {
  74         mul := 1
  75         s := strings.ReplaceAll(args[0], `_`, ``)
  76         for _, m := range multipliers {
  77             if strings.HasSuffix(s, m.suffix) {
  78                 mul = m.factor
  79                 break
  80             }
  81         }
  82 
  83         if v, err := strconv.Atoi(s); err == nil {
  84             n = mul * v
  85             args = args[1:]
  86         }
  87     }
  88 
  89     if n < 1 {
  90         return
  91     }
  92 
  93     if len(args) > 0 && args[0] == `--` {
  94         args = args[1:]
  95     }
  96 
  97     if err := run(args, &n); err != nil && err != io.EOF {
  98         os.Stderr.WriteString(`limit: `)
  99         os.Stderr.WriteString(err.Error())
 100         os.Stderr.WriteString("\n")
 101         os.Exit(1)
 102         return
 103     }
 104 }
 105 
 106 func run(args []string, n *int) error {
 107     dashes := 0
 108     for _, name := range args {
 109         if name == `-` {
 110             dashes++
 111         }
 112         if dashes > 1 {
 113             return errors.New(`can't read stdin (dash) more than once`)
 114         }
 115     }
 116 
 117     if len(args) == 0 {
 118         if err := handleReader(os.Stdout, os.Stdin, n); err != nil {
 119             return err
 120         }
 121     }
 122 
 123     for _, name := range args {
 124         if name == `-` {
 125             if err := handleReader(os.Stdout, os.Stdin, n); err != nil {
 126                 return err
 127             }
 128             continue
 129         }
 130 
 131         if err := handleFile(os.Stdout, name, n); err != nil {
 132             return err
 133         }
 134     }
 135 
 136     return nil
 137 }
 138 
 139 func handleFile(w io.Writer, name string, n *int) error {
 140     if name == `` || name == `-` {
 141         return handleReader(w, os.Stdin, n)
 142     }
 143 
 144     f, err := os.Open(name)
 145     if err != nil {
 146         return errors.New(`can't read from file named "` + name + `"`)
 147     }
 148     defer f.Close()
 149 
 150     return handleReader(w, f, n)
 151 }
 152 
 153 func handleReader(w io.Writer, r io.Reader, n *int) error {
 154     var buf [32 * 1024]byte
 155 
 156     for *n > 0 {
 157         max := *n
 158         if max > cap(buf) {
 159             max = cap(buf)
 160         }
 161 
 162         got, err := os.Stdin.Read(buf[:max])
 163         if got > 0 {
 164             if _, err := os.Stdout.Write(buf[:got]); err != nil {
 165                 return io.EOF
 166             }
 167         }
 168         *n -= got
 169 
 170         if err != nil {
 171             return io.EOF
 172         }
 173     }
 174 
 175     return nil
 176 }
     File: ./lineup/lineup.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package lineup
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34 )
  35 
  36 const info = `
  37 lineup [options...] [max-item-count...] [files...]
  38 
  39 Put lines side-by-side up the item-count given per line, joining the items
  40 using tabs. If not item-count is given, or the item-count is less than 1,
  41 all lines will be tab-joined into a single output line.
  42 
  43 All (optional) leading options start with either single or double-dash:
  44 
  45     -h, -help       show this help message
  46 `
  47 
  48 type config struct {
  49     maxItems  int
  50     count     int
  51     remainder int
  52     liveLines bool
  53 }
  54 
  55 func Main() {
  56     var cfg config
  57     cfg.maxItems = 0
  58     cfg.liveLines = true
  59     args := os.Args[1:]
  60 
  61     for len(args) > 0 {
  62         switch args[0] {
  63         case `-b`, `--b`, `-buffered`, `--buffered`:
  64             cfg.liveLines = false
  65             args = args[1:]
  66             continue
  67 
  68         case `-h`, `--h`, `-help`, `--help`:
  69             os.Stdout.WriteString(info[1:])
  70             return
  71         }
  72 
  73         break
  74     }
  75 
  76     if len(args) > 0 {
  77         if n, err := strconv.ParseInt(args[0], 10, 64); err == nil {
  78             if n < 1 {
  79                 n = 0
  80             }
  81             cfg.maxItems = int(n)
  82             args = args[1:]
  83         }
  84     }
  85 
  86     if len(args) > 0 && args[0] == `--` {
  87         args = args[1:]
  88     }
  89 
  90     if cfg.liveLines {
  91         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  92             cfg.liveLines = false
  93         }
  94     }
  95 
  96     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  97         os.Stderr.WriteString(`lineup: `)
  98         os.Stderr.WriteString(err.Error())
  99         os.Stderr.WriteString("\n")
 100         os.Exit(1)
 101         return
 102     }
 103 }
 104 
 105 func run(w io.Writer, args []string, cfg config) error {
 106     bw := bufio.NewWriterSize(w, 32*1024)
 107     defer bw.Flush()
 108 
 109     dashes := 0
 110     for _, name := range args {
 111         if name == `-` {
 112             dashes++
 113         }
 114         if dashes > 1 {
 115             return errors.New(`can't read stdin (dash) more than once`)
 116         }
 117     }
 118 
 119     if len(args) == 0 {
 120         if err := lineUp(bw, os.Stdin, &cfg); err != nil {
 121             return err
 122         }
 123     }
 124 
 125     for _, name := range args {
 126         if name == `-` {
 127             if err := lineUp(bw, os.Stdin, &cfg); err != nil {
 128                 return err
 129             }
 130             continue
 131         }
 132 
 133         if err := handleFile(bw, name, &cfg); err != nil {
 134             return err
 135         }
 136     }
 137 
 138     if cfg.count > 0 {
 139         bw.WriteByte('\n')
 140     }
 141     return nil
 142 }
 143 
 144 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 145     if name == `` || name == `-` {
 146         return lineUp(w, os.Stdin, cfg)
 147     }
 148 
 149     f, err := os.Open(name)
 150     if err != nil {
 151         return errors.New(`can't read from file named "` + name + `"`)
 152     }
 153     defer f.Close()
 154 
 155     return lineUp(w, f, cfg)
 156 }
 157 
 158 func lineUp(w *bufio.Writer, r io.Reader, cfg *config) error {
 159     const gb = 1024 * 1024 * 1024
 160     sc := bufio.NewScanner(r)
 161     sc.Buffer(nil, 8*gb)
 162 
 163     for i := 0; sc.Scan(); i++ {
 164         s := sc.Bytes()
 165         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 166             s = s[3:]
 167         }
 168 
 169         if cfg.count > 0 {
 170             var sep byte
 171             if cfg.remainder == 0 {
 172                 sep = '\n'
 173             } else {
 174                 sep = '\t'
 175             }
 176 
 177             if w.WriteByte(sep) != nil {
 178                 return io.EOF
 179             }
 180         }
 181 
 182         w.Write(s)
 183         cfg.count++
 184 
 185         cfg.remainder++
 186         if cfg.remainder >= cfg.maxItems && cfg.maxItems > 1 {
 187             cfg.remainder = 0
 188         }
 189 
 190         if cfg.liveLines && w.Flush() != nil {
 191             return io.EOF
 192         }
 193     }
 194 
 195     return sc.Err()
 196 }
     File: ./ls/ls.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ls
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "sort"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 ls [options...] [folders...]
  37 
  38 List top-level entries in the folder paths given: if no paths are given, the
  39 current folder is listed by default.
  40 
  41 Options
  42 
  43     -a        show all files, including those whose name starts with a dot
  44     --help    show this help message
  45 `
  46 
  47 type config struct {
  48     all  bool
  49     long bool
  50 }
  51 
  52 func Main() {
  53     args := os.Args[1:]
  54 
  55     var cfg config
  56     for len(args) > 0 {
  57         switch args[0] {
  58         case `-a`:
  59             cfg.all = true
  60             args = args[1:]
  61             continue
  62 
  63         case `--help`:
  64             os.Stderr.WriteString(info[1:])
  65             return
  66 
  67         case `-l`:
  68             cfg.long = true
  69             args = args[1:]
  70             continue
  71         }
  72 
  73         break
  74     }
  75 
  76     if len(args) > 0 && args[0] == `--` {
  77         args = args[1:]
  78     }
  79 
  80     if err := run(args, cfg); err != nil && err != io.EOF {
  81         os.Stderr.WriteString(`ls: `)
  82         os.Stderr.WriteString(err.Error())
  83         os.Stderr.WriteString("\n")
  84         os.Exit(1)
  85         return
  86     }
  87 }
  88 
  89 func run(paths []string, cfg config) error {
  90     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  91     defer w.Flush()
  92 
  93     for i, path := range paths {
  94         if len(paths) > 1 {
  95             w.WriteString(path)
  96             w.WriteString(":\n")
  97             if i > 0 {
  98                 w.WriteString("\n")
  99             }
 100         }
 101 
 102         if err := ls(w, path, cfg); err != nil {
 103             return err
 104         }
 105     }
 106 
 107     if len(paths) == 0 {
 108         return ls(w, `.`, cfg)
 109     }
 110     return nil
 111 }
 112 
 113 func ls(w *bufio.Writer, path string, cfg config) error {
 114     defer w.Flush()
 115 
 116     entries, err := os.ReadDir(path)
 117     if err != nil {
 118         return err
 119     }
 120 
 121     sort.SliceStable(entries, func(i, j int) bool {
 122         return compareNames(entries[i].Name(), entries[j].Name()) < 0
 123     })
 124 
 125     for _, e := range entries {
 126         name := e.Name()
 127 
 128         if !cfg.all && len(name) > 0 && name[0] == '.' {
 129             continue
 130         }
 131 
 132         w.WriteString(name)
 133         if _, err := w.WriteString("\n"); err != nil {
 134             return io.EOF
 135         }
 136     }
 137 
 138     return nil
 139 }
 140 
 141 func compareNames(x, y string) int {
 142     if len(x) < len(y) && strings.HasPrefix(y, x) {
 143         return -1
 144     }
 145     if len(y) < len(x) && strings.HasPrefix(x, y) {
 146         return +1
 147     }
 148     return strings.Compare(x, y)
 149 }
     File: ./main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29     go build -ldflags "-s -w" -trimpath
  30 
  31 If you have `tinygo`, you can do even better by instead using:
  32 
  33     tinygo build -no-debug -opt=2 easybox
  34 */
  35 
  36 package main
  37 
  38 import (
  39     "fmt"
  40     "io"
  41     "os"
  42     "path"
  43     "sort"
  44     "strings"
  45     "unicode/utf8"
  46 
  47     "./avoid"
  48     "./backsort"
  49     "./base64"
  50     "./bitdump"
  51     "./breakdown"
  52     "./bytedump"
  53     "./calc"
  54     "./cat"
  55     "./catl"
  56     "./cbl"
  57     "./chopleads"
  58     "./clock"
  59     "./coby"
  60     "./compress"
  61     "./countdown"
  62     "./datauri"
  63     "./dc"
  64     "./dcol"
  65     "./debase64"
  66     "./decap"
  67     "./decsv"
  68     "./dedent"
  69     "./dedup"
  70     "./dejsonl"
  71     "./delay"
  72     "./dessv"
  73     "./detab"
  74     "./dtsv"
  75     "./ecoli"
  76     "./erase"
  77     "./expand"
  78     "./factor"
  79     "./fail"
  80     "./fh"
  81     "./files"
  82     "./filesizes"
  83     "./finfo"
  84     "./first"
  85     "./fixlines"
  86     "./folders"
  87     "./gsub"
  88     "./head"
  89     "./hima"
  90     "./htmlify"
  91     "./id3pic"
  92     "./ignore"
  93     "./indent"
  94     "./items"
  95     "./json0"
  96     "./json2"
  97     "./jsonl"
  98     "./jsons"
  99     "./junk"
 100     "./label"
 101     "./last"
 102     "./leak"
 103     "./limit"
 104     "./lineup"
 105     "./ls"
 106     "./match"
 107     "./mimetype"
 108     "./n"
 109     "./ncol"
 110     "./ngron"
 111     "./nhex"
 112     "./njson"
 113     "./nl"
 114     "./nn"
 115     "./now"
 116     "./nts"
 117     "./null"
 118     "./pac"
 119     "./pad"
 120     "./paramatch"
 121     "./pbn"
 122     "./pcol"
 123     "./plain"
 124     "./precho"
 125     "./pretsv"
 126     "./primes"
 127     "./ptsv"
 128     "./realign"
 129     "./reprose"
 130     "./restyle"
 131     "./same"
 132     "./sbs"
 133     "./seq"
 134     "./setint"
 135     "./setsub"
 136     "./shame512"
 137     "./skip"
 138     "./skiplast"
 139     "./squeeze"
 140     "./squomp"
 141     "./stomp"
 142     "./sym"
 143     "./tacl"
 144     "./tail"
 145     "./tam"
 146     "./tcatl"
 147     "./teletype"
 148     "./timer"
 149     "./tolower"
 150     "./transpose"
 151     "./underline"
 152     "./units"
 153     "./utfate"
 154     "./waveout"
 155     "./zcat"
 156     "./zj"
 157 
 158     _ "embed"
 159 )
 160 
 161 //go:embed info.txt
 162 var info string
 163 
 164 // mains has some entries starting as nil to avoid circular-dependency errors
 165 var mains = map[string]func(){
 166     `args`:      args,
 167     `avoid`:     avoid.Main,
 168     `bitdump`:   bitdump.Main,
 169     `breakdown`: breakdown.Main,
 170     `bytedump`:  bytedump.Main,
 171     `calc`:      calc.Main,
 172     `catl`:      catl.Main,
 173     `cbl`:       cbl.Main,
 174     `chopleads`: chopleads.Main,
 175     `clock`:     clock.Main,
 176     `cls`:       cls,
 177     `coby`:      coby.Main,
 178     `compress`:  compress.Main,
 179     `countdown`: countdown.Main,
 180     `datauri`:   datauri.Main,
 181     `dcol`:      dcol.Main,
 182     `debase64`:  debase64.Main,
 183     `decap`:     decap.Main,
 184     `decsv`:     decsv.Main,
 185     `dedent`:    dedent.Main,
 186     `dedup`:     dedup.Main,
 187     `dejsonl`:   dejsonl.Main,
 188     `delay`:     delay.Main,
 189     `dessv`:     dessv.Main,
 190     `detab`:     detab.Main,
 191     `dtsv`:      dtsv.Main,
 192     `echobar`:   echobar,
 193     `ecoli`:     ecoli.Main,
 194     `erase`:     erase.Main,
 195     `fail`:      fail.Main,
 196     `files`:     files.Main,
 197     `filesizes`: filesizes.Main,
 198     `finfo`:     finfo.Main,
 199     `first`:     first.Main,
 200     `fixlines`:  fixlines.Main,
 201     `folders`:   folders.Main,
 202     `gsub`:      gsub.Main,
 203     `hecho`:     hecho,
 204     `hima`:      hima.Main,
 205     `htmlify`:   htmlify.Main,
 206     `id3pic`:    id3pic.Main,
 207     `ignore`:    ignore.Main,
 208     `indent`:    indent.Main,
 209     `items`:     items.Main,
 210     `json0`:     json0.Main,
 211     `json2`:     json2.Main,
 212     `jsonl`:     jsonl.Main,
 213     `jsons`:     jsons.Main,
 214     `junk`:      junk.Main,
 215     `label`:     label.Main,
 216     `last`:      last.Main,
 217     `leak`:      leak.Main,
 218     `limit`:     limit.Main,
 219     `lineup`:    lineup.Main,
 220     `match`:     match.Main,
 221     `mimetype`:  mimetype.Main,
 222     `n`:         n.Main,
 223     `ncol`:      ncol.Main,
 224     `ngron`:     ngron.Main,
 225     `nhex`:      nhex.Main,
 226     `njson`:     njson.Main,
 227     `nn`:        nn.Main,
 228     `now`:       now.Main,
 229     `nts`:       nts.Main,
 230     `pad`:       pad.Main,
 231     `paramatch`: paramatch.Main,
 232     `pbn`:       pbn.Main,
 233     `pcol`:      pcol.Main,
 234     `plain`:     plain.Main,
 235     `precho`:    precho.Main,
 236     `pretsv`:    pretsv.Main,
 237     `primes`:    primes.Main,
 238     `ptsv`:      ptsv.Main,
 239     `realign`:   realign.Main,
 240     `reprose`:   reprose.Main,
 241     `restyle`:   restyle.Main,
 242     `ruler`:     ruler,
 243     `same`:      same.Main,
 244     `sbs`:       sbs.Main,
 245     `setint`:    setint.Main,
 246     `setsub`:    setsub.Main,
 247     `shame512`:  shame512.Main,
 248     `skip`:      skip.Main,
 249     `skiplast`:  skiplast.Main,
 250     `squeeze`:   squeeze.Main,
 251     `squomp`:    squomp.Main,
 252     `stomp`:     stomp.Main,
 253     `sym`:       sym.Main,
 254     `tacl`:      tacl.Main,
 255     `tam`:       tam.Main,
 256     `tcatl`:     tcatl.Main,
 257     `teletype`:  teletype.Main,
 258     `timer`:     timer.Main,
 259     `timezones`: timezones,
 260     `tolower`:   tolower.Main,
 261     `transpose`: transpose.Main,
 262     `underline`: underline.Main,
 263     `units`:     units.Main,
 264     `utfate`:    utfate.Main,
 265     `waveout`:   waveout.Main,
 266 }
 267 
 268 var experimental = map[string]func(){
 269     `backsort`: backsort.Main,
 270     `div`:      div,
 271     `fh`:       fh.Main,
 272     `mop`:      mop,
 273     `nil`:      null.Main,
 274     `pac`:      pac.Main,
 275     `nothing`:  nothing,
 276     `tinker`:   tinker,
 277     `ungron`:   ungronMain,
 278     `zj`:       zj.Main,
 279 }
 280 
 281 var remakes = map[string]func(){
 282     `base64`: base64.Main,
 283     `cat`:    cat.Main,
 284     `clear`:  clear,
 285     `dc`:     dc.Main,
 286     `echo`:   echo,
 287     `expand`: expand.Main,
 288     `factor`: factor.Main,
 289     `fail`:   fail.Main,
 290     `false`:  falseMain,
 291     `head`:   head.Main,
 292     `ls`:     ls.Main,
 293     `nl`:     nl.Main,
 294     `seq`:    seq.Main,
 295     `sleep`:  sleep,
 296     `tail`:   tail.Main,
 297     `true`:   trueMain,
 298     `yes`:    yes,
 299     `zcat`:   zcat.Main,
 300 }
 301 
 302 var aliases = map[string]string{
 303     `a`:            `avoid`,
 304     `break`:        `breakdown`,
 305     `breaklines`:   `breakdown`,
 306     `ca`:           `calc`,
 307     `calculate`:    `calc`,
 308     `calculator`:   `calc`,
 309     `fc`:           `calc`,
 310     `frac`:         `calc`,
 311     `fraca`:        `calc`,
 312     `fracalc`:      `calc`,
 313     `bytes`:        `cat`,
 314     `c`:            `cat`,
 315     `catenate`:     `cat`,
 316     `concat`:       `cat`,
 317     `concatenate`:  `cat`,
 318     `load`:         `cat`,
 319     `lines`:        `catl`,
 320     `dcols`:        `dcol`,
 321     `dropcol`:      `dcol`,
 322     `dropcols`:     `dcol`,
 323     `dropcolumns`:  `dcol`,
 324     `unbase64`:     `debase64`,
 325     `uncsv`:        `decsv`,
 326     `deduplicate`:  `dedup`,
 327     `undup`:        `dedup`,
 328     `unique`:       `dedup`,
 329     `unjsonl`:      `dejsonl`,
 330     `slowlines`:    `delay`,
 331     `unssv`:        `dessv`,
 332     `untab`:        `detab`,
 333     `e`:            `echo`,
 334     `b`:            `echobar`,
 335     `bar`:          `echobar`,
 336     `fileinfo`:     `finfo`,
 337     `detrail`:      `fixlines`,
 338     `id3picture`:   `id3pic`,
 339     `id3thumb`:     `id3pic`,
 340     `id3thumbnail`: `id3pic`,
 341     `aside`:        `leak`,
 342     `sideshow`:     `leak`,
 343     `automime`:     `mimetype`,
 344     `filemime`:     `mimetype`,
 345     `filetype`:     `mimetype`,
 346     `mime`:         `mimetype`,
 347     `mp3pic`:       `id3pic`,
 348     `mp3picture`:   `id3pic`,
 349     `mp3thumb`:     `id3pic`,
 350     `mp3thumbnail`: `id3pic`,
 351     `idem`:         `ignore`,
 352     `iden`:         `ignore`,
 353     `identity`:     `ignore`,
 354     `j0`:           `json0`,
 355     `j2`:           `json2`,
 356     `jl`:           `jsonl`,
 357     `detsv`:        `jsons`,
 358     `prelabel`:     `label`,
 359     `prememo`:      `label`,
 360     `k`:            `match`,
 361     `keep`:         `match`,
 362     `m`:            `match`,
 363     `mops`:         `mop`,
 364     `ncols`:        `ncol`,
 365     `nicecol`:      `ncol`,
 366     `nicecols`:     `ncol`,
 367     `nicecolumns`:  `ncol`,
 368     `gron`:         `ngron`,
 369     `nicegron`:     `ngron`,
 370     `nh`:           `nhex`,
 371     `nicehex`:      `nhex`,
 372     `null`:         `nil`,
 373     `nicejson`:     `njson`,
 374     `nj`:           `njson`,
 375     `nicedigits`:   `nn`,
 376     `nicenum`:      `nn`,
 377     `nicenumbers`:  `nn`,
 378     `nicenums`:     `nn`,
 379     `ok`:           `nothing`,
 380     `rpn`:          `pac`,
 381     `parmatch`:     `paramatch`,
 382     `pmatch`:       `paramatch`,
 383     `mapa`:         `paramatch`,
 384     `mapara`:       `paramatch`,
 385     `matchpar`:     `paramatch`,
 386     `matchpara`:    `paramatch`,
 387     `pcols`:        `pcol`,
 388     `pickcol`:      `pcol`,
 389     `pickcols`:     `pcol`,
 390     `pickcolumns`:  `pcol`,
 391     `p`:            `plain`,
 392     `color`:        `restyle`,
 393     `recolor`:      `restyle`,
 394     `style`:        `restyle`,
 395     `symbol`:       `sym`,
 396     `symbols`:      `sym`,
 397     `syms`:         `sym`,
 398     `tty`:          `teletype`,
 399     `chrono`:       `timer`,
 400     `chronograph`:  `timer`,
 401     `timezone`:     `timezones`,
 402     `lower`:        `tolower`,
 403     `lowercase`:    `tolower`,
 404     `t`:            `transpose`,
 405     `tran`:         `transpose`,
 406     `trans`:        `transpose`,
 407     `transp`:       `transpose`,
 408     `u`:            `underline`,
 409     `uline`:        `underline`,
 410     `degron`:       `ungron`,
 411     `utf8`:         `utfate`,
 412     `zoomjson`:     `zj`,
 413     `decompress`:   `zcat`,
 414     `degzip`:       `zcat`,
 415     `uncompress`:   `zcat`,
 416     `ungzip`:       `zcat`,
 417 }
 418 
 419 var blurbs = map[string]string{
 420     `args`:       `show all ARGumentS given after the tool name, one per line`,
 421     `avoid`:      `ignore lines matching any of the regexes given`,
 422     `bitdump`:    `show all bits for all input bytes`,
 423     `breakdown`:  `break input lines into multiple output lines by regexes`,
 424     `bytedump`:   `show bytes as hex values, with a wide ASCII panel`,
 425     `calc`:       `fractional calculator, with floating-point powers`,
 426     `catl`:       `conCATenate Lines, ensures text ends with a line-feed`,
 427     `cbl`:        `Count Bytes and Lines`,
 428     `chopleads`:  `ignore common leading starts of all non-empty lines`,
 429     `clock`:      `keep showing the current time, updating every new minute`,
 430     `cls`:        `CLear the Screen`,
 431     `coby`:       `COunt BYtes, and many other byte/text-related stats`,
 432     `compress`:   `gzip-compress all concatenated inputs given`,
 433     `countdown`:  `countdown the seconds/minutes/hours given`,
 434     `datauri`:    `turn bytes into data-URIs, auto-detecting MIME types`,
 435     `dcol`:       `Drop COLumns by name or by 1-based index`,
 436     `decap`:      `DECAPitate the first n lines from stdin, or 1 by default`,
 437     `debase64`:   `decode base64 text and data-URIs`,
 438     `decompress`: `gzip-decompress concatenated bytes from all the inputs`,
 439     `decsv`:      `convert CSV tables into TSV tables, or into JSON`,
 440     `dedent`:     `ignore the common leading-space indentation`,
 441     `dedup`:      `deduplicate lines, emitting each unique line only once`,
 442     `dejsonl`:    `turn JSON Lines into proper JSON`,
 443     `delay`:      `wait the seconds given, before emitting each input line`,
 444     `dessv`:      `turn tables of space-separated values into TSV tables`,
 445     `detab`:      `expand tabs into runs of up to n spaces, or up to 4`,
 446     `div`:        `DIVide 2 numbers, or invert 1 number`,
 447     `dtsv`:       `Drop Tab-Separated Values, matching args to column names`,
 448     `echobar`:    `like 'echo', but right-pads and highlights its output`,
 449     `ecoli`:      `Expressions COloring LInes color-codes matching lines`,
 450     `erase`:      `ignore/erase all matching regexes away from each line`,
 451     `fail`:       `fail with the exit code given, or using 1 by default`,
 452     `fh`:         `Function Heatmapper draws 2-input (x, y) functions`,
 453     `files`:      `list all files in the folder(s) given`,
 454     `filesizes`:  `show sizes of files and block-counts (4K by default)`,
 455     `finfo`:      `show various file info, for plain-text and/or media files`,
 456     `first`:      `keep only the first n lines, or the first line by default`,
 457     `fixlines`:   `ignore carriage-returns, or even trailing spaces`,
 458     `folders`:    `list all folders in the folder(s) given`,
 459     `gsub`:       `Globally SUBstitute all regular expression matches`,
 460     `hecho`:      `Highlighted ECHO shows an ANSI-styled line`,
 461     `help`:       `show the help message for "easybox"`,
 462     `hima`:       `HIlight MAtches using the regexes given`,
 463     `htmlify`:    `turn plain-text lines into HTML documents`,
 464     `id3pic`:     `get the encoded picture out of audio files, if present`,
 465     `ignore`:     `ignore all arguments, copying stdin into stdout instead`,
 466     `indent`:     `indent lines, using 4 leading spaces by default`,
 467     `items`:      `emit words/items from input lines as single-item lines`,
 468     `json0`:      `minimize/fix JSON into the smallest-possible size`,
 469     `json2`:      `indent JSON into multiple lines, using 2 spaces per level`,
 470     `jsonl`:      `turn items from top-level JSON arrays into JSON Lines`,
 471     `jsons`:      `JSON Strings turns TSV into arrays of objects of strings`,
 472     `junk`:       `emit a given count of random bytes, or 1024 by default`,
 473     `label`:      `precede input lines with a styled line`,
 474     `last`:       `keep only the last n lines, or the last line by default`,
 475     `leak`:       `emit lines both to stdout and stderr; good to debug pipes`,
 476     `limit`:      `emit up to the first n bytes, or the first 1024 by default`,
 477     `lineup`:     `put input lines into (up to) n-items tab-separated lines`,
 478     `match`:      `only keep lines matching any of the regexes given`,
 479     `mimetype`:   `try to auto-detect the MIME-TYPE of all files given`,
 480     `mop`:        `Multiple OPerations, using the 2 numbers given`,
 481     `n`:          `Number lines, putting tabs between numbers and contents`,
 482     `ncol`:       `Nice COLumns realigns tables, color-coding their values`,
 483     `ngron`:      `Nice GRON mimics a subset of "gron", using better colors`,
 484     `nhex`:       `Nice HEXadecimal shows bytes as hex values and ASCII`,
 485     `nil`:        `emit nothing, also discarding all stdin bytes, if piped`,
 486     `njson`:      `Nice JSON indents and color-codes JSON data`,
 487     `nn`:         `Nice Numbers color-codes groups of digits for legibility`,
 488     `now`:        `show the current date and time, also for other timezones`,
 489     `nts`:        `Nice TimeStamp precedes each line with styled date/time`,
 490     `pac`:        `Postfix Array Calculator is an RPN-style calculator`,
 491     `pad`:        `right-pad lines, so they all have the same symbol-count`,
 492     `pbn`:        `Pick (lines) By Number, using 1-based or negative indices`,
 493     `paramatch`:  `PARAgraphs MATCHing any of the regexes given`,
 494     `pcol`:       `Pick COLumns by name or by 1-based index`,
 495     `plain`:      `ignore all ANSI-sequences, leaving unstyled text`,
 496     `precho`:     `PRecede ECHO, precedes input lines with an SSV line`,
 497     `pretsv`:     `PREcede TSV, precedes input lines with a TSV line`,
 498     `primes`:     `find prime numbers, up to the first million by default`,
 499     `ptsv`:       `Pick Tab-Separated Values, matching args to column names`,
 500     `realign`:    `realign items from the SSV/TSV tables given`,
 501     `reprose`:    `reflow/trim lines of text/prose to improve its legibility`,
 502     `restyle`:    `restyle lines using ANSI-codes for the style name given`,
 503     `ruler`:      `show a ruler-like pattern on a single line`,
 504     `same`:       `fail if the 2 files given have different contents`,
 505     `sbs`:        `Side By Side, wraps input lines in multiple columns`,
 506     `seq`:        `emit a sequence of numbers, one per line`,
 507     `setint`:     `SET INTersection of (unique) lines from the inputs given`,
 508     `setsub`:     `SET SUBtraction of (unique) lines from the first input`,
 509     `shame512`:   `group filepaths by common SHA-512 checksums`,
 510     `skip`:       `skip at most the first n lines, or the first by default`,
 511     `skiplast`:   `skip at most the last n lines, or the last by default`,
 512     `squeeze`:    `aggressively ignore spaces, especially runs of spaces`,
 513     `squomp`:     `squeeze non-empty lines and stomp empty(-ish) lines`,
 514     `stomp`:      `ignore leading/trailing empty lines, squeezing empty runs`,
 515     `sym`:        `lookup hard-to-type symbols by their common names/aliases`,
 516     `tacl`:       `TAC Lines emits text lines in backward-order`,
 517     `tam`:        `Time And Memory (and exit) runs/monitors commands`,
 518     `tcatl`:      `Titled conCATenate Lines, is like "catl" but with names`,
 519     `teletype`:   `mimic old-fashioned teletype devices, by delaying output`,
 520     `timer`:      `run a live chronograph, optionally timing a command`,
 521     `timezones`:  `lookup full timezone names from the city/place names given`,
 522     `tolower`:    `turn all uppercase letters into lowercase ones`,
 523     `transpose`:  `switch rows and columns from tables; output is always TSV`,
 524     `tools`:      `list all tools available`,
 525     `underline`:  `underline every nth line, or every 5th line by default`,
 526     `ungron`:     `turn GRON-style lines into valid JSON`,
 527     `units`:      `convert weird units into the international standard ones`,
 528     `utfate`:     `decode all other types of UTF text into UTF-8`,
 529     `waveout`:    `emit/calculate WAV-format sounds by formula`,
 530     `yes`:        `keep emitting a line with "yes", or the message given`,
 531     `zj`:         `Zoom Json, using the keys/indices given as arguments`,
 532 }
 533 
 534 func main() {
 535     appName := os.Args[0]
 536 
 537     // try to use the app's `name`, in case it's being called from a file-link
 538     // named after one of the tools
 539     if tool, ok := lookupTool(path.Base(appName)); ok {
 540         tool()
 541         return
 542     }
 543 
 544     if len(os.Args) < 2 {
 545         showHelp(os.Stderr)
 546         fmt.Fprintln(os.Stderr, ``)
 547         fmt.Fprintln(os.Stderr, `easybox: no tool name given`)
 548         os.Exit(1)
 549         return
 550     }
 551 
 552     // try normal tool-lookup using the first command-line argument
 553 
 554     toolName := os.Args[1]
 555     os.Args = os.Args[1:]
 556 
 557     if tool, ok := lookupTool(toolName); ok {
 558         tool()
 559         return
 560     }
 561 
 562     switch toolName {
 563     case `-h`, `--h`, `-help`, `--help`, `help`:
 564         showHelp(os.Stdout)
 565 
 566     case `-l`, `--l`, `-list`, `--list`:
 567         tools()
 568 
 569     case `-links`, `--links`:
 570         showLinksCommands(os.Stdout, appName, os.Args[1:])
 571 
 572     case `-t`, `--t`, `-tools`, `--tools`, `tools`:
 573         tools()
 574 
 575     default:
 576         const fs = "easybox: tool/alias named %q not found\n"
 577         fmt.Fprintf(os.Stderr, fs, toolName)
 578         os.Exit(1)
 579         return
 580     }
 581 }
 582 
 583 // dealias tries to lookup a string to the aliases given, returning the name
 584 // given if the lookup fails
 585 func dealias(aliases map[string]string, name string) string {
 586     if s, ok := aliases[name]; ok {
 587         return s
 588     }
 589     return name
 590 }
 591 
 592 func help() {
 593     showHelp(os.Stdout)
 594 }
 595 
 596 func lookupTool(name string) (tool func(), ok bool) {
 597     name = strings.ReplaceAll(name, `-`, ``)
 598     name = strings.ReplaceAll(name, `_`, ``)
 599     key := dealias(aliases, name)
 600 
 601     if tool, ok := mains[key]; ok {
 602         return tool, ok
 603     }
 604 
 605     if tool, ok := remakes[key]; ok {
 606         return tool, ok
 607     }
 608 
 609     tool, ok = experimental[key]
 610     return tool, ok
 611 }
 612 
 613 // showHelp has a parameter to write either to stdout or stderr
 614 func showHelp(w io.Writer) {
 615     fmt.Fprintln(w, info)
 616 
 617     fmt.Fprintln(w, "\nTools Available")
 618 
 619     maxlen := 0
 620     names := make([]string, 0, max(len(mains), len(aliases)))
 621     for k := range mains {
 622         names = append(names, k)
 623         maxlen = max(maxlen, utf8.RuneCountInString(k))
 624     }
 625     sort.Strings(names)
 626 
 627     for _, s := range names {
 628         fmt.Fprintf(w, "  - %-*s  %s\n", maxlen, s, blurbs[s])
 629     }
 630 
 631     fmt.Fprintln(w, "\nAliases Available")
 632 
 633     maxlen = 0
 634     names = names[:0]
 635     for k := range aliases {
 636         names = append(names, k)
 637         maxlen = max(maxlen, utf8.RuneCountInString(k))
 638     }
 639     sort.Strings(names)
 640 
 641     for _, k := range names {
 642         fmt.Fprintf(w, "  - %-*s -> %s\n", maxlen, k, aliases[k])
 643     }
 644 
 645     names = names[:0]
 646     for k := range experimental {
 647         names = append(names, k)
 648     }
 649     sort.Strings(names)
 650 
 651     if len(names) == 0 {
 652         return
 653     }
 654 
 655     fmt.Fprintln(w, "\nExperimental Tools Available")
 656 
 657     for _, k := range names {
 658         fmt.Fprintf(w, "  - %s\n", k)
 659     }
 660 }
 661 
 662 func showLinksCommands(w io.Writer, appName string, args []string) {
 663     prefix := `.`
 664     if len(args) > 0 {
 665         prefix = args[0]
 666     }
 667     if len(prefix) > 0 && prefix[len(prefix)-1] == '/' {
 668         prefix = prefix[:len(prefix)-1]
 669     }
 670     if len(prefix) == 0 {
 671         prefix = `.`
 672     }
 673 
 674     names := make([]string, 0, len(mains))
 675     for k := range mains {
 676         names = append(names, k)
 677     }
 678 
 679     sort.Strings(names)
 680 
 681     for _, s := range names {
 682         const fs = "ln -s \"$(realpath \"$(which %s)\")\" %s/%s\n"
 683         fmt.Fprintf(w, fs, appName, prefix, s)
 684     }
 685 }
 686 
 687 // tinker is for little throw-away experiments
 688 func tinker() {
 689 }
     File: ./main_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package main
  26 
  27 import "testing"
  28 
  29 func TestAliases(t *testing.T) {
  30     for alias, name := range aliases {
  31         if _, ok := mains[name]; ok {
  32             continue
  33         }
  34         if _, ok := remakes[name]; ok {
  35             continue
  36         }
  37         if _, ok := experimental[name]; ok {
  38             continue
  39         }
  40 
  41         t.Errorf("alias %q leads nowhere", alias)
  42     }
  43 }
  44 
  45 func TestBlurbs(t *testing.T) {
  46     for name := range mains {
  47         if blurbs[name] != `` {
  48             continue
  49         }
  50         t.Errorf("no description/blurb for tool %q", name)
  51     }
  52 
  53     for name := range blurbs {
  54         name = dealias(aliases, name)
  55 
  56         if _, ok := mains[name]; ok {
  57             continue
  58         }
  59         if _, ok := remakes[name]; ok {
  60             continue
  61         }
  62         if _, ok := experimental[name]; ok {
  63             continue
  64         }
  65         if name == `help` || name == `tools` {
  66             continue
  67         }
  68 
  69         t.Errorf("description/blurb for name %q, but no tool", name)
  70     }
  71 }
     File: ./match/match.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package match
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 match [options...] [regular expressions...]
  38 
  39 Only keep lines which match any of the extended-mode regular expressions
  40 given. When not given any regex, match non-empty lines by default.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help     show this help message
  45     -i, -ins      match regexes case-insensitively
  46     -l, -links    add a regex to match HTTP/HTTPS links case-insensitively
  47     -q, -quiet    never emit output: exit code still succeeds/fails
  48 `
  49 
  50 const linkRegexp = `(?i)https?://[a-z0-9+_.:%-]+(/[a-z0-9+_.%/,#?&=-]*)*`
  51 
  52 func Main() {
  53     nerr := 0
  54     links := false
  55     buffered := false
  56     avoid := false
  57     sensitive := true
  58     quiet := false
  59     args := os.Args[1:]
  60 
  61     for len(args) > 0 {
  62         switch args[0] {
  63         case `-b`, `--b`, `-buffered`, `--buffered`:
  64             buffered = true
  65             args = args[1:]
  66             continue
  67 
  68         case `-h`, `--h`, `-help`, `--help`:
  69             os.Stdout.WriteString(info[1:])
  70             return
  71 
  72         case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
  73             sensitive = false
  74             args = args[1:]
  75             continue
  76 
  77         case `-l`, `--l`, `-links`, `--links`:
  78             links = true
  79             args = args[1:]
  80             continue
  81 
  82         case `-q`, `--q`, `-quiet`, `--quiet`:
  83             quiet = true
  84             args = args[1:]
  85             continue
  86 
  87         case `-v`, `--v`, `-inv`, `--inv`, `-neg`, `--neg`:
  88             avoid = true
  89             args = args[1:]
  90             continue
  91         }
  92 
  93         if s := args[0]; s == `-` || s == `--` || !strings.HasPrefix(s, `-`) {
  94             break
  95         }
  96 
  97         for s := args[0][1:]; len(s) > 0; s = s[1:] {
  98             switch s[0] {
  99             case 'a':
 100                 avoid = true
 101                 continue
 102             case 'b':
 103                 buffered = true
 104                 continue
 105             case 'h':
 106                 os.Stdout.WriteString(info[1:])
 107                 return
 108             case 'i':
 109                 sensitive = false
 110                 continue
 111             case 'l':
 112                 links = true
 113                 continue
 114             case 'q':
 115                 quiet = true
 116                 continue
 117             case 'v':
 118                 avoid = true
 119                 continue
 120             default:
 121                 os.Stderr.WriteString(`match: unsupported option `)
 122                 os.Stderr.WriteString(args[0])
 123                 os.Stderr.WriteString("\n")
 124                 os.Exit(1)
 125             }
 126         }
 127 
 128         args = args[1:]
 129     }
 130 
 131     if len(args) > 0 && args[0] == `--` {
 132         args = args[1:]
 133     }
 134 
 135     liveLines := false
 136     if !quiet {
 137         liveLines = !buffered
 138         if !buffered {
 139             if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 140                 liveLines = false
 141             }
 142         }
 143     }
 144 
 145     if len(args) == 0 {
 146         args = []string{`.`}
 147     }
 148 
 149     var exprs []*regexp.Regexp
 150     if links {
 151         exprs = make([]*regexp.Regexp, 0, len(args)+1)
 152         exprs = append(exprs, regexp.MustCompile(linkRegexp))
 153     } else {
 154         exprs = make([]*regexp.Regexp, 0, len(args))
 155     }
 156 
 157     for _, src := range args {
 158         var err error
 159         var exp *regexp.Regexp
 160         if !sensitive {
 161             exp, err = regexp.Compile(`(?i)` + src)
 162         } else {
 163             exp, err = regexp.Compile(src)
 164         }
 165 
 166         if err != nil {
 167             os.Stderr.WriteString(`match: `)
 168             os.Stderr.WriteString(err.Error())
 169             os.Stderr.WriteString("\n")
 170             nerr++
 171         }
 172 
 173         exprs = append(exprs, exp)
 174     }
 175 
 176     if nerr > 0 {
 177         os.Exit(1)
 178         return
 179     }
 180 
 181     var buf []byte
 182     sc := bufio.NewScanner(os.Stdin)
 183     sc.Buffer(nil, 8*1024*1024*1024)
 184     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 185 
 186     ok := false
 187 
 188     for i := 0; sc.Scan(); i++ {
 189         line := sc.Bytes()
 190         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 191             line = line[3:]
 192         }
 193 
 194         s := line
 195         if bytes.IndexByte(s, '\x1b') >= 0 {
 196             buf = plain(buf[:0], s)
 197             s = buf
 198         }
 199 
 200         if match(s, exprs) {
 201             if avoid {
 202                 continue
 203             }
 204 
 205             ok = true
 206             if quiet {
 207                 continue
 208             }
 209             if err := emit(bw, line, liveLines); err != nil {
 210                 break
 211             }
 212         }
 213 
 214         if avoid {
 215             ok = true
 216             if quiet {
 217                 continue
 218             }
 219             if err := emit(bw, line, liveLines); err != nil {
 220                 break
 221             }
 222         }
 223     }
 224 
 225     if !ok {
 226         os.Exit(1)
 227         return
 228     }
 229 }
 230 
 231 func emit(w *bufio.Writer, line []byte, live bool) error {
 232     w.Write(line)
 233     w.WriteByte('\n')
 234 
 235     if live && w.Flush() != nil {
 236         return io.EOF
 237     }
 238     return nil
 239 }
 240 
 241 func match(what []byte, with []*regexp.Regexp) bool {
 242     for _, e := range with {
 243         if e.Match(what) {
 244             return true
 245         }
 246     }
 247     return false
 248 }
 249 
 250 func plain(dst []byte, src []byte) []byte {
 251     for len(src) > 0 {
 252         i, j := indexEscapeSequence(src)
 253         if i < 0 {
 254             dst = append(dst, src...)
 255             break
 256         }
 257         if j < 0 {
 258             j = len(src)
 259         }
 260 
 261         if i > 0 {
 262             dst = append(dst, src[:i]...)
 263         }
 264 
 265         src = src[j:]
 266     }
 267 
 268     return dst
 269 }
 270 
 271 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 272 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 273 // indices which can be independently negative when either the start/end of
 274 // a sequence isn't found; given their fairly-common use, even the hyperlink
 275 // ESC]8 sequences are supported
 276 func indexEscapeSequence(s []byte) (int, int) {
 277     var prev byte
 278 
 279     for i, b := range s {
 280         if prev == '\x1b' && b == '[' {
 281             j := indexLetter(s[i+1:])
 282             if j < 0 {
 283                 return i, -1
 284             }
 285             return i - 1, i + 1 + j + 1
 286         }
 287 
 288         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 289             j := indexPair(s[i+1:], '\x1b', '\\')
 290             if j < 0 {
 291                 return i, -1
 292             }
 293             return i - 1, i + 1 + j + 2
 294         }
 295 
 296         prev = b
 297     }
 298 
 299     return -1, -1
 300 }
 301 
 302 func indexLetter(s []byte) int {
 303     for i, b := range s {
 304         upper := b &^ 32
 305         if 'A' <= upper && upper <= 'Z' {
 306             return i
 307         }
 308     }
 309 
 310     return -1
 311 }
 312 
 313 func indexPair(s []byte, x byte, y byte) int {
 314     var prev byte
 315 
 316     for i, b := range s {
 317         if prev == x && b == y && i > 0 {
 318             return i
 319         }
 320         prev = b
 321     }
 322 
 323     return -1
 324 }
     File: ./mathplus/doc.go
   1 /*
   2 # mathplus
   3 
   4 This is an add-on to the stdlib package `math`, with statistics-gathering
   5 functionality and plenty more math-related functions.
   6 
   7 This package also wraps types/methods from math/rand with nil-safe ones, which
   8 act on the default value-generator when nil pointers are used.
   9 */
  10 
  11 package mathplus
     File: ./mathplus/functions.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import "math"
  28 
  29 func Decade(x float64) float64  { return 10 * math.Floor(0.1*x) }
  30 func Century(x float64) float64 { return 100 * math.Floor(0.01*x) }
  31 
  32 func Round1(x float64) float64 { return 1e-1 * math.Round(1e+1*x) }
  33 func Round2(x float64) float64 { return 1e-2 * math.Round(1e+2*x) }
  34 func Round3(x float64) float64 { return 1e-3 * math.Round(1e+3*x) }
  35 func Round4(x float64) float64 { return 1e-4 * math.Round(1e+4*x) }
  36 func Round5(x float64) float64 { return 1e-5 * math.Round(1e+5*x) }
  37 func Round6(x float64) float64 { return 1e-6 * math.Round(1e+6*x) }
  38 func Round7(x float64) float64 { return 1e-7 * math.Round(1e+7*x) }
  39 func Round8(x float64) float64 { return 1e-8 * math.Round(1e+8*x) }
  40 func Round9(x float64) float64 { return 1e-9 * math.Round(1e+9*x) }
  41 
  42 // Ln calculates the natural logarithm.
  43 func Ln(x float64) float64 {
  44     return math.Log(x)
  45 }
  46 
  47 // Log calculates logarithms using the base given.
  48 // func Log(base float64, x float64) float64 {
  49 //  return math.Log(x) / math.Log(base)
  50 // }
  51 
  52 // Round rounds the number given to the number of decimal places given.
  53 func Round(x float64, decimals int) float64 {
  54     k := math.Pow(10, float64(decimals))
  55     return math.Round(k*x) / k
  56 }
  57 
  58 // RoundBy rounds the number given to the unit-size given
  59 // func RoundBy(x float64, by float64) float64 {
  60 //  return math.Round(by*x) / by
  61 // }
  62 
  63 // FloorBy quantizes a number downward by the unit-size given
  64 // func FloorBy(x float64, by float64) float64 {
  65 //  mod := math.Mod(x, by)
  66 //  if x < 0 {
  67 //      if mod == 0 {
  68 //          return x - mod
  69 //      }
  70 //      return x - mod - by
  71 //  }
  72 //  return x - mod
  73 // }
  74 
  75 // Wrap linearly interpolates the number given to the range [0...1],
  76 // according to its continuous domain, specified by the limits given
  77 func Wrap(x float64, min, max float64) float64 {
  78     return (x - min) / (max - min)
  79 }
  80 
  81 // AnchoredWrap works like Wrap, except when both domain boundaries are positive,
  82 // the minimum becomes 0, and when both domain boundaries are negative, the
  83 // maximum becomes 0.
  84 func AnchoredWrap(x float64, min, max float64) float64 {
  85     if min > 0 && max > 0 {
  86         min = 0
  87     } else if max < 0 && min < 0 {
  88         max = 0
  89     }
  90     return (x - min) / (max - min)
  91 }
  92 
  93 // Unwrap undoes Wrap, by turning the [0...1] number given into its equivalent
  94 // in the new domain given.
  95 func Unwrap(x float64, min, max float64) float64 {
  96     return (max-min)*x + min
  97 }
  98 
  99 // Clamp constrains the domain of the value given
 100 func Clamp(x float64, min, max float64) float64 {
 101     return math.Min(math.Max(x, min), max)
 102 }
 103 
 104 // Scale transforms a number according to its position in [xMin, xMax] into its
 105 // correspondingly-positioned counterpart in [yMin, yMax]: if value isn't in its
 106 // assumed domain, its result will be extrapolated accordingly
 107 func Scale(x float64, xmin, xmax, ymin, ymax float64) float64 {
 108     k := (x - xmin) / (xmax - xmin)
 109     return (ymax-ymin)*k + ymin
 110 }
 111 
 112 // AnchoredScale works like Scale, except when both domain boundaries are positive,
 113 // the minimum becomes 0, and when both domain boundaries are negative, the maximum
 114 // becomes 0. This allows for proportionally-correct scaling of quantities, such
 115 // as when showing data visually.
 116 func AnchoredScale(x float64, xmin, xmax, ymin, ymax float64) float64 {
 117     if xmin > 0 && xmax > 0 {
 118         xmin = 0
 119     } else if xmax < 0 && xmin < 0 {
 120         xmax = 0
 121     }
 122     k := (x - xmin) / (xmax - xmin)
 123     return (ymax-ymin)*k + ymin
 124 }
 125 
 126 // ForceRange is handy when trying to mold floating-point values into numbers
 127 // valid for JSON, since NaN and Infinity are replaced by the values given;
 128 // the infinity-replacement value is negated for negative infinity.
 129 func ForceRange(x float64, nan, inf float64) float64 {
 130     if math.IsNaN(x) {
 131         return nan
 132     }
 133     if math.IsInf(x, -1) {
 134         return -inf
 135     }
 136     if math.IsInf(x, +1) {
 137         return inf
 138     }
 139     return x
 140 }
 141 
 142 // Sign returns the standardized sign of a value, either as -1, 0, or +1: NaN
 143 // values stay as NaN, as is expected when using floating-point values.
 144 func Sign(x float64) float64 {
 145     if x > 0 {
 146         return +1
 147     }
 148     if x < 0 {
 149         return -1
 150     }
 151     if x == 0 {
 152         return 0
 153     }
 154     return math.NaN()
 155 }
 156 
 157 // IsInteger checks if a floating-point value is already rounded to an integer
 158 // value.
 159 func IsInteger(x float64) bool {
 160     _, frac := math.Modf(x)
 161     return frac == 0
 162 }
 163 
 164 func Square(x float64) float64 {
 165     return x * x
 166 }
 167 
 168 func Cube(x float64) float64 {
 169     return x * x * x
 170 }
 171 
 172 func RoundTo(x float64, decimals float64) float64 {
 173     k := math.Pow10(int(decimals))
 174     return math.Round(k*x) / k
 175 }
 176 
 177 func RelDiff(x, y float64) float64 {
 178     return (x - y) / y
 179 }
 180 
 181 // Bool booleanizes a number: 0 stays 0, and anything else becomes 1.
 182 func Bool(x float64) float64 {
 183     if x == 0 {
 184         return 0
 185     }
 186     return 1
 187 }
 188 
 189 // DeNaN replaces NaN with the alternative value given: regular values are
 190 // returned as given.
 191 func DeNaN(x float64, instead float64) float64 {
 192     if !math.IsNaN(x) {
 193         return x
 194     }
 195     return instead
 196 }
 197 
 198 // DeInf replaces either infinity with the alternative value given: regular
 199 // values are returned as given.
 200 func DeInf(x float64, instead float64) float64 {
 201     if !math.IsInf(x, 0) {
 202         return x
 203     }
 204     return instead
 205 }
 206 
 207 // Revalue replaces NaN and either infinity with the alternative value given:
 208 // regular values are returned as given.
 209 func Revalue(x float64, instead float64) float64 {
 210     if !math.IsNaN(x) && !math.IsInf(x, 0) {
 211         return x
 212     }
 213     return instead
 214 }
 215 
 216 // Linear evaluates a linear polynomial, the first arg being the main input,
 217 // followed by the polynomial coefficients in decreasing-power order.
 218 func Linear(x float64, a, b float64) float64 {
 219     return a*x + b
 220 }
 221 
 222 // Quadratic evaluates a 2nd-degree polynomial, the first arg being the main
 223 // input, followed by the polynomial coefficients in decreasing-power order.
 224 func Quadratic(x float64, a, b, c float64) float64 {
 225     return (a*x+b)*x + c
 226 }
 227 
 228 // Cubic evaluates a cubic polynomial, the first arg being the main input,
 229 // followed by the polynomial coefficients in decreasing-power order.
 230 func Cubic(x float64, a, b, c, d float64) float64 {
 231     return ((a*x+b)*x+c)*x + d
 232 }
 233 
 234 func LinearFMA(x float64, a, b float64) float64 {
 235     return math.FMA(x, a, b)
 236 }
 237 
 238 func QuadraticFMA(x float64, a, b, c float64) float64 {
 239     lin := math.FMA(x, a, b)
 240     return math.FMA(lin, x, c)
 241 }
 242 
 243 func CubicFMA(x float64, a, b, c, d float64) float64 {
 244     lin := math.FMA(x, a, b)
 245     quad := math.FMA(lin, x, c)
 246     return math.FMA(quad, x, d)
 247 }
 248 
 249 // Radians converts angular degrees into angular radians: 180 degrees are pi
 250 // pi radians.
 251 func Radians(deg float64) float64 {
 252     const k = math.Pi / 180
 253     return k * deg
 254 }
 255 
 256 // Degrees converts angular radians into angular degrees: pi radians are 180
 257 // degrees.
 258 func Degrees(rad float64) float64 {
 259     const k = 180 / math.Pi
 260     return k * rad
 261 }
 262 
 263 // Fract calculates the non-integer/fractional part of a number.
 264 func Fract(x float64) float64 {
 265     return x - math.Floor(x)
 266 }
 267 
 268 // Mix interpolates 2 numbers using a third number, used as an interpolation
 269 // coefficient. This parameter naturally falls in the range [0, 1], but doesn't
 270 // have to be: when given outside that range, the parameter can extrapolate in
 271 // either direction instead.
 272 func Mix(x, y float64, k float64) float64 {
 273     return (1-k)*(y-x) + x
 274 }
 275 
 276 // Step implements a step function with a parametric threshold.
 277 func Step(edge, x float64) float64 {
 278     if x < edge {
 279         return 0
 280     }
 281     return 1
 282 }
 283 
 284 // SmoothStep is like the `smoothstep` func found in GLSL, using a cubic
 285 // interpolator in the transition region.
 286 func SmoothStep(edge0, edge1, x float64) float64 {
 287     if x <= edge0 {
 288         return 0
 289     }
 290     if x >= edge1 {
 291         return 1
 292     }
 293 
 294     // use the cubic hermite interpolator 3x^2 - 2x^3 in the transition band
 295     return x * x * (3 - 2*x)
 296 }
 297 
 298 // Logistic approximates the math func of the same name.
 299 func Logistic(x float64) float64 {
 300     return 1 / (1 + math.Exp(-x))
 301 }
 302 
 303 // Sinc approximates the math func of the same name.
 304 func Sinc(x float64) float64 {
 305     if x != 0 {
 306         return math.Sin(x) / x
 307     }
 308     return 1
 309 }
 310 
 311 // Sum adds all the numbers in an array.
 312 func Sum(v ...float64) float64 {
 313     s := 0.0
 314     for _, f := range v {
 315         s += f
 316     }
 317     return s
 318 }
 319 
 320 // Product multiplies all the numbers in an array.
 321 func Product(v ...float64) float64 {
 322     p := 1.0
 323     for _, f := range v {
 324         p *= f
 325     }
 326     return p
 327 }
 328 
 329 // Length calculates the Euclidean length of the vector given.
 330 func Length(v ...float64) float64 {
 331     ss := 0.0
 332     for _, f := range v {
 333         ss += f * f
 334     }
 335     return math.Sqrt(ss)
 336 }
 337 
 338 // Dot calculates the dot product of 2 vectors
 339 func Dot(x []float64, y []float64) float64 {
 340     l := len(x)
 341     if len(y) < l {
 342         l = len(y)
 343     }
 344 
 345     dot := 0.0
 346     for i := 0; i < l; i++ {
 347         dot += x[i] * y[i]
 348     }
 349     return dot
 350 }
 351 
 352 // Min finds the minimum value from the numbers given.
 353 func Min(v ...float64) float64 {
 354     min := +math.Inf(+1)
 355     for _, f := range v {
 356         min = math.Min(min, f)
 357     }
 358     return min
 359 }
 360 
 361 // Max finds the maximum value from the numbers given.
 362 func Max(v ...float64) float64 {
 363     max := +math.Inf(-1)
 364     for _, f := range v {
 365         max = math.Max(max, f)
 366     }
 367     return max
 368 }
 369 
 370 // Hypot calculates the Euclidean n-dimensional hypothenuse from the numbers
 371 // given: all numbers can be lengths, or simply positional coordinates.
 372 func Hypot(v ...float64) float64 {
 373     sumsq := 0.0
 374     for _, f := range v {
 375         sumsq += f * f
 376     }
 377     return math.Sqrt(sumsq)
 378 }
 379 
 380 // Polyval evaluates a polynomial using Horner's algorithm. The array has all
 381 // the coefficients in textbook order, from the highest power down to the final
 382 // constant.
 383 func Polyval(x float64, v ...float64) float64 {
 384     if len(v) == 0 {
 385         return 0
 386     }
 387 
 388     x0 := x
 389     x = 1.0
 390     y := 0.0
 391     for i := len(v) - 1; i >= 0; i-- {
 392         y += v[i] * x
 393         x *= x0
 394     }
 395     return y
 396 }
 397 
 398 // LnGamma is a 1-input 1-output version of math.Lgamma from the stdlib.
 399 func LnGamma(x float64) float64 {
 400     y, s := math.Lgamma(x)
 401     if s < 0 {
 402         return math.NaN()
 403     }
 404     return y
 405 }
 406 
 407 // LnBeta calculates the natural-logarithm of the beta function.
 408 func LnBeta(x float64, y float64) float64 {
 409     return LnGamma(x) + LnGamma(y) - LnGamma(x+y)
 410 }
 411 
 412 // Beta calculates the beta function.
 413 func Beta(x float64, y float64) float64 {
 414     return math.Exp(LnBeta(x, y))
 415 }
 416 
 417 // Factorial calculates the product of all integers in [1, n]
 418 func Factorial(n int) int64 {
 419     return int64(math.Round(math.Gamma(float64(n + 1))))
 420 }
 421 
 422 // IsPrime checks whether an integer is bigger than 1 and can only be fully
 423 // divided by 1 and itself, which is the definition of a prime number.
 424 func IsPrime(n int64) bool {
 425     // prime numbers start at 2
 426     if n < 2 {
 427         return false
 428     }
 429 
 430     // 2 is the only even prime
 431     if n%2 == 0 {
 432         return n == 2
 433     }
 434 
 435     // no divisor can be more than the square root of the target number:
 436     // this limit makes the loop an O(sqrt(n)) one, instead of O(n); this
 437     // is a major algorithmic speedup both in theory and in practice
 438     max := int64(math.Floor(math.Sqrt(float64(n))))
 439 
 440     // the only possible full-divisors are odd integers 3..sqrt(n),
 441     // since reaching this point guarantees n is odd and n > 2
 442     for d := int64(3); d <= max; d += 2 {
 443         if n%d == 0 {
 444             return false
 445         }
 446     }
 447     return true
 448 }
 449 
 450 // LCM finds the least common-multiple of 2 positive integers; when one or
 451 // both inputs aren't positive, this func returns 0.
 452 func LCM(x, y int64) int64 {
 453     if gcd := GCD(x, y); gcd > 0 {
 454         return x * y / gcd
 455     }
 456     return 0
 457 }
 458 
 459 // GCD finds the greatest common-divisor of 2 positive integers; when one or
 460 // both inputs aren't positive, this func returns 0.
 461 func GCD(x, y int64) int64 {
 462     if x < 1 || y < 1 {
 463         return 0
 464     }
 465 
 466     // the loop below requires a >= b
 467     a, b := x, y
 468     if a < b {
 469         a, b = y, x
 470     }
 471 
 472     for b > 0 {
 473         a, b = b, a%b
 474     }
 475     return a
 476 }
 477 
 478 // Perm counts the number of all possible permutations from n objects when
 479 // picking k times. When one or both inputs aren't positive, the result is 0.
 480 func Perm(n, k int) int64 {
 481     if n < k || n < 0 || k < 0 {
 482         return 0
 483     }
 484 
 485     perm := int64(1)
 486     for i := n - k + 1; i <= n; i++ {
 487         perm *= int64(i)
 488     }
 489     return perm
 490 }
 491 
 492 // Choose counts the number of all possible combinations from n objects when
 493 // picking k times. When one or both inputs aren't positive, the result is 0.
 494 func Choose(n, k int) int64 {
 495     if n < k || n < 0 || k < 0 {
 496         return 0
 497     }
 498 
 499     // the log trick isn't always more accurate when there's no overflow:
 500     // for those cases calculate using the textbook definition
 501     f := math.Round(float64(Perm(n, k) / Factorial(k)))
 502     if !math.IsInf(f, 0) {
 503         return int64(f)
 504     }
 505 
 506     // calculate using the log-factorial of n, k, and n - k
 507     a, _ := math.Lgamma(float64(n + 1))
 508     b, _ := math.Lgamma(float64(k + 1))
 509     c, _ := math.Lgamma(float64(n - k + 1))
 510     return int64(math.Round(math.Exp(a - b - c)))
 511 }
 512 
 513 // BinomialMass calculates the probability mass of the binomial random process
 514 // given. When the probability given isn't between 0 and 1, the result is NaN.
 515 func BinomialMass(x, n int, p float64) float64 {
 516     // invalid probability input
 517     if p < 0 || p > 1 {
 518         return math.NaN()
 519     }
 520     // events outside the support are impossible by definition
 521     if x < 0 || x > n {
 522         return 0
 523     }
 524 
 525     q := 1 - p
 526     m := n - x
 527     ncx := float64(Choose(n, x))
 528     return ncx * math.Pow(p, float64(x)) * math.Pow(q, float64(m))
 529 }
 530 
 531 // CumulativeBinomialDensity calculates cumulative probabilities/masses up to
 532 // the value given for the binomial random process given. When the probability
 533 // given isn't between 0 and 1, the result is NaN.
 534 func CumulativeBinomialDensity(x, n int, p float64) float64 {
 535     // invalid probability input
 536     if p < 0 || p > 1 {
 537         return math.NaN()
 538     }
 539     if x < 0 {
 540         return 0
 541     }
 542     if x >= n {
 543         return 1
 544     }
 545 
 546     p0 := p
 547     q0 := 1 - p0
 548     q := math.Pow(q0, float64(n))
 549 
 550     pbinom := 0.0
 551     np1 := float64(n + 1)
 552     for k := 0; k < x; k++ {
 553         a, _ := math.Lgamma(np1)
 554         b, _ := math.Lgamma(float64(k + 1))
 555         c, _ := math.Lgamma(float64(n - k + 1))
 556         // count all possible combinations for this event
 557         ncomb := math.Round(math.Exp(a - b - c))
 558         pbinom += ncomb * p * q
 559         p *= p0
 560         q /= q0
 561     }
 562     return pbinom
 563 }
 564 
 565 // NormalDensity calculates the density at a point along the normal distribution
 566 // given.
 567 func NormalDensity(x float64, mu, sigma float64) float64 {
 568     z := (x - mu) / sigma
 569     return math.Sqrt(0.5/sigma) * math.Exp(-(z*z)/sigma)
 570 }
 571 
 572 // CumulativeNormalDensity calculates the probability of a normal variate of
 573 // being up to the value given.
 574 func CumulativeNormalDensity(x float64, mu, sigma float64) float64 {
 575     z := (x - mu) / sigma
 576     return 0.5 + 0.5*math.Erf(z/math.Sqrt2)
 577 }
 578 
 579 // Epanechnikov is a commonly-used kernel function.
 580 func Epanechnikov(x float64) float64 {
 581     if math.Abs(x) > 1 {
 582         // func is 0 ouside -1..+1
 583         return 0
 584     }
 585     return 0.75 * (1 - x*x)
 586 }
 587 
 588 // Gauss is the commonly-used Gaussian kernel function.
 589 func Gauss(x float64) float64 {
 590     return math.Exp(-(x * x))
 591 }
 592 
 593 // Tricube is a commonly-used kernel function.
 594 func Tricube(x float64) float64 {
 595     a := math.Abs(x)
 596     if a > 1 {
 597         // func is 0 ouside -1..+1
 598         return 0
 599     }
 600 
 601     b := a * a * a
 602     c := 1 - b
 603     return 70.0 / 81.0 * c * c * c
 604 }
 605 
 606 // SolveQuad finds the solutions of a 2nd-degree polynomial, using a formula
 607 // which is more accurate than the textbook one.
 608 func SolveQuad(a, b, c float64) (x1 float64, x2 float64) {
 609     div := 2 * c
 610     disc := math.Sqrt(b*b - 4*a*c)
 611     x1 = div / (-b - disc)
 612     x2 = div / (-b + disc)
 613     return x1, x2
 614 }
     File: ./mathplus/functions_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29     "testing"
  30 )
  31 
  32 func TestRounding(t *testing.T) {
  33     decadeTests := []struct {
  34         Number   float64
  35         Expected float64
  36     }{
  37         {0, 0},
  38         {-1, -10},
  39         {-0.75, -10},
  40         {-0.725, -10},
  41         {123.532123143, 120},
  42         {1932.532123143, 1930},
  43         {2023.4, 2020},
  44     }
  45 
  46     for _, tc := range decadeTests {
  47         y := Decade(tc.Number)
  48         if y != tc.Expected {
  49             const fs = `decade(%f): expected %f, got %f`
  50             t.Fatalf(fs, tc.Number, tc.Expected, y)
  51         }
  52     }
  53 
  54     centuryTests := []struct {
  55         Number   float64
  56         Expected float64
  57     }{
  58         {0, 0},
  59         {-1, -100},
  60         {-0.75, -100},
  61         {-0.725, -100},
  62         {123.532123143, 100},
  63         {1932.532123143, 1900},
  64         {2023.4, 2000},
  65     }
  66 
  67     for _, tc := range centuryTests {
  68         y := Century(tc.Number)
  69         if y != tc.Expected {
  70             const fs = `century(%f): expected %f, got %f`
  71             t.Fatalf(fs, tc.Number, tc.Expected, y)
  72         }
  73     }
  74 
  75     roundingTests := []struct {
  76         Number   float64
  77         Expected [6]float64
  78     }{
  79         {0, [6]float64{0, 0, 0, 0, 0, 0}},
  80         {-1, [6]float64{-1, -1, -1, -1, -1, -1}},
  81         {-0.75, [6]float64{-0.8, -0.75, -0.75, -0.75, -0.75, -0.75}},
  82         {-0.725, [6]float64{-0.7, -0.73, -0.725, -0.725, -0.725, -0.725}},
  83         {2023.4, [6]float64{2023.4, 2023.4, 2023.4, 2023.4, 2023.4, 2023.4}},
  84         {
  85             123.532123143,
  86             [6]float64{123.5, 123.53, 123.532, 123.5321, 123.53212, 123.532123},
  87         },
  88         {
  89             1932.532123143,
  90             [6]float64{1932.5, 1932.53, 1932.532, 1932.5321, 1932.53212, 1932.532123},
  91         },
  92     }
  93 
  94     for _, tc := range roundingTests {
  95         x := tc.Number
  96         y := []float64{
  97             Round1(x), Round2(x), Round3(x), Round4(x), Round5(x), Round6(x),
  98         }
  99 
 100         for i, f := range y {
 101             exp := tc.Expected[i]
 102             if math.Abs(exp-f) > 1e-12 {
 103                 const fs = `r%d(%f): expected %f, got %f`
 104                 t.Fatalf(fs, i+1, tc.Number, exp, f)
 105             }
 106         }
 107     }
 108 }
 109 
 110 func TestScale(t *testing.T) {
 111     scaleTests := []struct {
 112         Input    float64
 113         InMin    float64
 114         InMax    float64
 115         OutMin   float64
 116         OutMax   float64
 117         Expected float64
 118     }{
 119         {-2, -5, 4, 0, 1, 1.0 / 3},
 120         {0.1, 0, 0.5, -3, 5, -1.4},
 121     }
 122 
 123     for _, tc := range scaleTests {
 124         in := tc.Input
 125         exp := tc.Expected
 126         got := Scale(in, tc.InMin, tc.InMax, tc.OutMin, tc.OutMax)
 127         if got != exp {
 128             const fs = `Scale(%f, %f, %f, %f, %f): expected %f, got %f`
 129             t.Fatalf(fs, in, tc.InMin, tc.InMax, tc.OutMin, tc.OutMax, exp, got)
 130         }
 131     }
 132 }
 133 
 134 func TestIsPrime(t *testing.T) {
 135     tests := []struct {
 136         Input    int64
 137         Expected bool
 138     }{
 139         {-3, false},
 140         {0, false},
 141         {1, false},
 142         {4, false},
 143         {9, false},
 144         {21, false},
 145 
 146         {2, true},
 147         {3, true},
 148         {5, true},
 149         {19, true},
 150         // 15,485,863 is the millionth prime
 151         {15_485_863, true},
 152     }
 153 
 154     for _, tc := range tests {
 155         if v := IsPrime(tc.Input); v != tc.Expected {
 156             const fs = `isprime(%d) wrongly returned %v`
 157             t.Fatalf(fs, tc.Input, v)
 158         }
 159     }
 160 }
 161 
 162 func TestHorner(t *testing.T) {
 163     tests := []struct {
 164         X        float64
 165         C        []float64
 166         Expected float64
 167     }{
 168         {2, []float64{1, 2, 3}, 11},
 169         {3, []float64{3, 5, -1}, 41},
 170     }
 171 
 172     for _, tc := range tests {
 173         got := Polyval(tc.X, tc.C...)
 174         if got != tc.Expected {
 175             const fs = `horner(%f, %#v) gave %f, instead of %f`
 176             t.Fatalf(fs, tc.X, tc.C, got, tc.Expected)
 177             return
 178         }
 179     }
 180 }
 181 
 182 func TestGCD(t *testing.T) {
 183     tests := []struct {
 184         X        int64
 185         Y        int64
 186         Expected int64
 187     }{
 188         {0, 0, 0},
 189         {-1, 10, 0},
 190         {1, -10, 0},
 191         {1, 1, 1},
 192         {1, 7, 1},
 193         {3 * 12, 12, 12},
 194         {1280, 1920, 640},
 195     }
 196 
 197     for _, tc := range tests {
 198         got := GCD(tc.X, tc.Y)
 199         if got != tc.Expected {
 200             const fs = `gcd(%d, %d) gave %d, instead of %d`
 201             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 202             return
 203         }
 204     }
 205 }
 206 
 207 func TestPerm(t *testing.T) {
 208     tests := []struct {
 209         X        int
 210         Y        int
 211         Expected int64
 212     }{
 213         {10, 4, 5_040},
 214         {5, 0, 1},
 215         {5, 5, 120},
 216     }
 217 
 218     for _, tc := range tests {
 219         got := Perm(tc.X, tc.Y)
 220         if got != tc.Expected {
 221             const fs = `perm(%d, %d) gave %d, instead of %d`
 222             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 223             return
 224         }
 225     }
 226 }
 227 
 228 func TestChoose(t *testing.T) {
 229     tests := []struct {
 230         X        int
 231         Y        int
 232         Expected int64
 233     }{
 234         {10, 4, 210},
 235         {10, 0, 1},
 236         {10, 10, 1},
 237     }
 238 
 239     for _, tc := range tests {
 240         got := Choose(tc.X, tc.Y)
 241         if got != tc.Expected {
 242             const fs = `comb(%d, %d) gave %d, instead of %d`
 243             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 244             return
 245         }
 246     }
 247 }
     File: ./mathplus/integers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import "math"
  28 
  29 const (
  30     MinInt16 = -(1 << 15)
  31     MinInt32 = -(1 << 31)
  32     MinInt64 = -(1 << 63)
  33 
  34     MaxInt16 = 1<<15 - 1
  35     MaxInt32 = 1<<31 - 1
  36     MaxInt64 = 1<<63 - 1
  37 
  38     MaxUint16 = 1<<16 - 1
  39     MaxUint32 = 1<<32 - 1
  40     MaxUint64 = 1<<64 - 1
  41 )
  42 
  43 func CountIntegerDigits(n int64) int {
  44     if n < 0 {
  45         n = -n
  46     }
  47 
  48     // 0 doesn't have a log10
  49     if n == 0 {
  50         return 1
  51     }
  52     // add 1 to the floored logarithm, since digits are always full and
  53     // with ceiling-like behavior, to use an analogy for this formula
  54     return int(math.Floor(math.Log10(float64(n)))) + 1
  55 }
  56 
  57 func LoopThousandsGroups(n int64, fn func(i, n int)) {
  58     // 0 doesn't have a log10
  59     if n == 0 {
  60         fn(0, 0)
  61         return
  62     }
  63 
  64     sign := +1
  65     if n < 0 {
  66         n = -n
  67         sign = -1
  68     }
  69 
  70     intLog1000 := int(math.Log10(float64(n)) / 3)
  71     remBase := int64(math.Pow10(3 * intLog1000))
  72 
  73     for i := 0; remBase > 0; i++ {
  74         group := (1000 * n) / remBase / 1000
  75         fn(i, sign*int(group))
  76         // if original number was negative, ensure only first
  77         // group gives a negative input to the callback
  78         sign = +1
  79 
  80         n %= remBase
  81         remBase /= 1000
  82     }
  83 }
  84 
  85 var pow10 = []int64{
  86     1,
  87     10,
  88     100,
  89     1000,
  90     10000,
  91     100000,
  92     1000000,
  93     10000000,
  94     100000000,
  95     1000000000, // last entry for int32
  96     10000000000,
  97     100000000000,
  98     1000000000000,
  99     10000000000000,
 100     100000000000000,
 101     1000000000000000,
 102     10000000000000000,
 103     100000000000000000,
 104     1000000000000000000,
 105     // 10000000000000000000,
 106 }
 107 
 108 var pow2 = []int64{
 109     1,
 110     2,
 111     4,
 112     8,
 113     16,
 114     32,
 115     64,
 116     128,
 117     256,
 118     512,
 119     1024,
 120     2048,
 121     4096,
 122     8192,
 123     16384,
 124     32768,
 125     65536,
 126     131072,
 127     262144,
 128     524288,
 129     1048576,
 130     2097152,
 131     4194304,
 132     8388608,
 133     16777216,
 134     33554432,
 135     67108864,
 136     134217728,
 137     268435456,
 138     536870912,
 139     1073741824,
 140     2147483648, // last entry for int32
 141     4294967296,
 142     8589934592,
 143     17179869184,
 144     34359738368,
 145     68719476736,
 146     137438953472,
 147     274877906944,
 148     549755813888,
 149     1099511627776,
 150     2199023255552,
 151     4398046511104,
 152     8796093022208,
 153     17592186044416,
 154     35184372088832,
 155     70368744177664,
 156     140737488355328,
 157     281474976710656,
 158     562949953421312,
 159     1125899906842624,
 160     2251799813685248,
 161     4503599627370496,
 162     9007199254740992,
 163     18014398509481984,
 164     36028797018963968,
 165     72057594037927936,
 166     144115188075855872,
 167     288230376151711744,
 168     576460752303423488,
 169     1152921504606846976,
 170     2305843009213693952,
 171     4611686018427387904,
 172     // 9223372036854775808,
 173 }
 174 
 175 // Log2Int gives you the floor of the base-2 logarithm, or negative results
 176 // for invalid inputs. Values less than 1 aren't supported, since they don't
 177 // have logarithms of any valid base, let alone base-2 logarithms.
 178 func Log2Int(n int64) (log2 int, ok bool) {
 179     if n < 1 {
 180         return -1, false
 181     }
 182 
 183     v := uint64(n)
 184     mask := uint64(1 << 63) // 2**63
 185     for i := int64(63); i > 0; i-- {
 186         if v&mask != 0 {
 187             return int(i), true
 188         }
 189         mask >>= 1
 190     }
 191     return 0, true
 192 }
 193 
 194 // Log10Int gives you the floor of the base-10 logarithm, or negative results
 195 // for invalid inputs. Values less than 1 aren't supported, since they don't
 196 // have logarithms of any valid base, let alone base-10 logarithms.
 197 func Log10Int(n int64) (log10 int, ok bool) {
 198     if n < 1 {
 199         return -1, false
 200     }
 201 
 202     for i := int64(0); i < 19; i++ {
 203         n /= 10
 204         if n == 0 {
 205             return int(i), true
 206         }
 207     }
 208     return 0, true
 209 }
 210 
 211 // Pow10Int gives you the integers powers of 10 as far as int64 allows: negative
 212 // inputs aren't supported, since negative exponents don't give integer results.
 213 func Pow10Int(n int) (power10 int64, ok bool) {
 214     if 0 <= n && n < len(pow10) {
 215         return pow10[n], true
 216     }
 217     return -1, false
 218 }
 219 
 220 // Pow2Int gives you the integers powers of 2 as far as int64 allows: negative
 221 // inputs aren't supported, since negative exponents don't give integer results.
 222 func Pow2Int(n int) (power2 int64, ok bool) {
 223     if 0 <= n && n < len(pow2) {
 224         return pow2[n], true
 225     }
 226     return -1, false
 227 }
     File: ./mathplus/integers_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import "testing"
  28 
  29 func TestCountIntegerDigits(t *testing.T) {
  30     tests := map[int64]int{
  31         0:          1,
  32         -32:        2,
  33         999:        3,
  34         3_490:      4,
  35         12_332:     5,
  36         999_999:    6,
  37         1_000_000:  7,
  38         1_000_001:  7,
  39         12_345_678: 8,
  40     }
  41 
  42     for input, expected := range tests {
  43         if n := CountIntegerDigits(input); n != expected {
  44             const fs = `integer digits in %d: got %d instead of %d`
  45             t.Errorf(fs, input, n, expected)
  46         }
  47     }
  48 }
  49 
  50 func TestLoopThousandsGroups(t *testing.T) {
  51     tests := map[int64][]int{
  52         0:         []int{0},
  53         -32:       []int{-32}, // negatives not supported yet
  54         999:       []int{999},
  55         1_670:     []int{1, 670},
  56         3_490:     []int{3, 490},
  57         12_332:    []int{12, 332},
  58         999_999:   []int{999, 999},
  59         1_000_000: []int{1, 0, 0},
  60         1_000_001: []int{1, 0, 1},
  61         1_234_567: []int{1, 234, 567},
  62     }
  63 
  64     for input, expected := range tests {
  65         count := 0
  66         LoopThousandsGroups(input, func(i, n int) {
  67             // t.Log(tc.Input, i, n)
  68             if n != expected[i] {
  69                 const fs = `group %d in %d: got %d instead of %d`
  70                 t.Errorf(fs, i, input, n, expected[i])
  71             }
  72             count++
  73         })
  74 
  75         if count != len(expected) {
  76             const fs = `thousands-groups from %d: got %d instead of %d`
  77             t.Errorf(fs, input, count, len(expected))
  78         }
  79     }
  80 }
  81 
  82 func TestLog2Int(t *testing.T) {
  83     tests := map[int64]int{
  84         -3:         -1,
  85         1:          0,
  86         2:          1,
  87         3:          1,
  88         4:          2,
  89         1024:       10,
  90         1_025:      10,
  91         2*1024 - 1: 10,
  92     }
  93 
  94     for value, expected := range tests {
  95         got, ok := Log2Int(value)
  96         if got != expected || (ok && value < 1) {
  97             const fs = `log2int(%d) = %d, but got %d instead`
  98             t.Fatalf(fs, value, expected, got)
  99         }
 100     }
 101 }
 102 
 103 func TestLog10Int(t *testing.T) {
 104     tests := map[int64]int{
 105         -3:        -1,
 106         1:         0,
 107         10:        1,
 108         100:       2,
 109         101:       2,
 110         199:       2,
 111         1_000_000: 6,
 112     }
 113 
 114     for value, expected := range tests {
 115         got, ok := Log10Int(value)
 116         if got != expected || (ok && value < 1) {
 117             const fs = `log10int(%d) = %d, but got %d instead`
 118             t.Fatalf(fs, value, expected, got)
 119         }
 120     }
 121 }
     File: ./mathplus/numbers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 const (
  32     // the maximum integer a float64 can represent exactly; since float64
  33     // uses a sign bit, instead of a 2-complement mantissa, -maxflint is
  34     // the minimum integer
  35     maxflint = 2 << 52
  36 )
  37 
  38 // StringWidth counts how many runes it takes to represent the value given as a
  39 // string both as a plain no-commas string and as a string with digit-grouping
  40 // commas, if needed. Each group is 3 digits. Showing numbers with commas makes
  41 // long numbers easier to read.
  42 func StringWidth(f float64, decimals int) (plain, nice int) {
  43     if math.IsNaN(f) || math.IsInf(f, 0) {
  44         return 0, 0
  45     }
  46 
  47     // avoid wrong results, since decimals will be added at the end
  48     if decimals < 0 {
  49         decimals = 0
  50     }
  51 
  52     extras := 0 // count non-digits, such as negatives and dots
  53     if f < 0 {
  54         f = -f   // fix value for uintWidth
  55         extras++ // count the leading negative sign
  56     }
  57     if decimals > 0 {
  58         extras++ // count the decimal dot
  59     }
  60 
  61     // at this point, f >= 0 for sure
  62     plain, nice = uintWidth(f)
  63     plain += extras + decimals
  64     nice += extras + decimals
  65     return plain, nice
  66 }
  67 
  68 // uintWidth can't handle negatives correctly, as its name suggests
  69 func uintWidth(f float64) (plain, nice int) {
  70     // only 1 digit and 0 commas for 0 <= x < 10
  71     if f < 10 {
  72         return 1, 1
  73     }
  74 
  75     mag := math.Log10(math.Floor(f)) // order of magnitude
  76     digits := int(mag) + 1           // integer digits
  77     commas := int(mag) / 3           // commas separating digits in groups of 3
  78     return digits, digits + commas
  79 }
  80 
  81 // Equals allows easily comparing numbers, including NaN values, which otherwise
  82 // never equal anything they're compared to, including themselves.
  83 func Equals(x, y float64) bool {
  84     if x == y {
  85         return true
  86     }
  87     return math.IsNaN(x) && math.IsNaN(y)
  88 }
  89 
  90 // ApproxEqual allows approximate comparisons, as well as checking if both values
  91 // are NaN, which otherwise never equal themselves when compared directly. The
  92 // last parameter must be positive for approx. comparisons, or zero for exact
  93 // comparisons.
  94 func ApproxEqual(x, y float64, maxdiff float64) bool {
  95     if math.Abs(x-y) <= maxdiff {
  96         return true
  97     }
  98     return math.IsNaN(x) && math.IsNaN(y)
  99 }
 100 
 101 // GuessDecimalsCount tries to guess the number of decimal digits of the number
 102 // given.
 103 func GuessDecimalsCount(x float64, max int) int {
 104     if x < 0 {
 105         x = -x
 106     }
 107 
 108     // only up to 16, because log10(2**-53) ~= -15.9546
 109     if !(0 <= max && max <= 16) {
 110         max = 16
 111     }
 112 
 113     const tol = 5e-13 // 1e-11, 1e-12, 5e-13
 114     for digits := 0; digits <= max; digits++ {
 115         _, frac := math.Modf(x)
 116         frac = math.Abs(frac)
 117         // when it's time to stop, the absolute value of the fraction part is
 118         // either extremely close to 0 or extremely close to 1
 119         if frac < tol || (1-frac) < tol {
 120             return digits
 121         }
 122         x *= 10
 123     }
 124     return max
 125 }
 126 
 127 // Default returns the first non-NaN value among those given: failing that,
 128 // the result will be NaN.
 129 func Default(args ...float64) float64 {
 130     for _, x := range args {
 131         if !math.IsNaN(x) {
 132             return x
 133         }
 134     }
 135     return math.NaN()
 136 }
 137 
 138 // TrimSlice ignores all leading/trailing NaN values from the slice given: its
 139 // main use-case is after sorting via sort.Float64s, since all NaN values are
 140 // moved in place to the start/end of the now-sorted slice.
 141 //
 142 // # Example
 143 //
 144 // sort.Float64(values)
 145 // res := mathplus.TrimSlice(values)
 146 func TrimSlice(x []float64) []float64 {
 147     // ignore all leading NaNs
 148     for len(x) > 0 && math.IsNaN(x[0]) {
 149         x = x[1:]
 150     }
 151     // ignore all trailing NaNs
 152     for len(x) > 0 && math.IsNaN(x[len(x)-1]) {
 153         x = x[:len(x)-1]
 154     }
 155     return x
 156 }
 157 
 158 // Linspace works like the Matlab function of the same name, except it takes a
 159 // callback, generalizing its behavior.
 160 func Linspace(a, incr, b float64, f func(x float64)) {
 161     if incr <= 0 {
 162         return
 163     }
 164 
 165     if a < b {
 166         forwardLinspace(a, incr, b, f)
 167     } else if a > b {
 168         backwardLinspace(a, incr, b, f)
 169     } else {
 170         f(a)
 171     }
 172 }
 173 
 174 func forwardLinspace(a, incr, b float64, f func(x float64)) {
 175     for i := 0; true; i++ {
 176         x := float64(i)*incr + a
 177         if x <= b {
 178             f(x)
 179         } else {
 180             return
 181         }
 182     }
 183 }
 184 
 185 func backwardLinspace(a, incr, b float64, f func(x float64)) {
 186     for i := 0; true; i++ {
 187         x := b - float64(i)*incr
 188         if x >= a {
 189             f(x)
 190         } else {
 191             return
 192         }
 193     }
 194 }
 195 
 196 // Seq is a special case of Linspace, where the increment is +/-1.
 197 func Seq(a, b float64, f func(x float64)) {
 198     Linspace(a, 1, b, f)
 199 }
 200 
 201 // Increment increments the number given by adding 1, when it's in the int-safe
 202 // range of float64s; when outside that range, the smallest available delta is
 203 // used instead.
 204 func Increment(x float64) float64 {
 205     if math.IsNaN(x) || math.IsInf(x, 0) {
 206         return x
 207     }
 208     if incr := x + 1; incr != x {
 209         return incr
 210     }
 211     return math.Float64frombits(math.Float64bits(x) + 1)
 212 }
 213 
 214 // Decrement decrements the number given by subtracting 1, when it's in the
 215 // int-safe range of float64s: when outside that range, the smallest available
 216 // delta is used instead.
 217 func Decrement(x float64) float64 {
 218     if math.IsNaN(x) || math.IsInf(x, 0) {
 219         return x
 220     }
 221     if decr := x - 1; decr != x {
 222         return decr
 223     }
 224     // subtracting 1 from the value's uint64 counterpart doesn't work for values
 225     // less than the minimum exact-integer: uint64s use 2-complement arithmetic
 226     return -math.Float64frombits(math.Float64bits(-x) + 1)
 227 }
 228 
 229 func Deapproximate(x float64) (min float64, max float64) {
 230     if math.IsNaN(x) || math.IsInf(x, 0) {
 231         return x, x
 232     }
 233 
 234     if x == 0 {
 235         return -0.5, +0.5
 236     }
 237 
 238     if math.Remainder(x, 1) == 0 {
 239         if math.Remainder(x, 10) != 0 {
 240             return x - 0.5, x + 0.5
 241         }
 242 
 243         sign := Sign(x)
 244         abs := math.Abs(x)
 245         delta := float64(maxExactPow10(int(abs))) / 2
 246         return sign*abs - delta, sign*abs + delta
 247     }
 248 
 249     // return surrounding integers when given non-integers
 250     return math.Floor(x), math.Ceil(x)
 251 }
 252 
 253 func maxExactPow10(x int) int {
 254     pow10 := 1
 255     for {
 256         if x%pow10 != 0 {
 257             return pow10 / 10
 258         }
 259         pow10 *= 10
 260     }
 261 }
     File: ./mathplus/numbers_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29     "strconv"
  30     "testing"
  31 )
  32 
  33 func TestNumberWidth(t *testing.T) {
  34     tests := []struct {
  35         Input       float64
  36         Decimals    int
  37         PlainWidth  int
  38         PrettyWidth int
  39     }{
  40         {0, 0, len(`0`), len(`0`)},
  41         {0.923123, 0, len(`0`), len(`0`)},
  42         {0.923123, 2, len(`0.92`), len(`0.92`)},
  43         {-3, 0, len(`-3`), len(`-3`)},
  44         {-300, 0, len(`-300`), len(`-300`)},
  45         {+2_300, 0, len(`2300`), len(`2,300`)},
  46         {-1_000_000, 0, len(`-1000000`), len(`-1,000,000`)},
  47         {+1_000_000, 0, len(`1000000`), len(`1,000,000`)},
  48         {+1_610_058.23423, 4, len(`1610058.2342`), len(`1_610_058.2342`)},
  49         {-317_289, 4, len(`-317289.0000`), len(`-317_289.0000`)},
  50         {5_457_013.0000, 4, len(`5457013.0000`), len(`5_457_013.0000`)},
  51         {-118.406800000000004, 15, 20, 20},
  52     }
  53 
  54     for _, tc := range tests {
  55         pl, pr := StringWidth(tc.Input, tc.Decimals)
  56         if pl != tc.PlainWidth {
  57             const fs = `PlainWidth(%f, %d): expected %d, got %d`
  58             t.Fatalf(fs, tc.Input, tc.Decimals, tc.PlainWidth, pl)
  59         }
  60         if pr != tc.PrettyWidth {
  61             const fs = `PrettyWidth(%f, %d): expected %d, got %d`
  62             t.Fatalf(fs, tc.Input, tc.Decimals, tc.PrettyWidth, pr)
  63         }
  64     }
  65 }
  66 
  67 func TestNumberWidthInternal(t *testing.T) {
  68     tests := []struct {
  69         Number    float64
  70         Decimals  int
  71         IntDigits int
  72         Commas    int
  73     }{
  74         {0, 0, 1, 0},
  75         {-3, 0, 1, 0},
  76         {-300, 0, 3, 0},
  77         {+2_300, 0, 4, 1},
  78         {-1_000_000, 0, 7, 2},
  79         {+1_000_000, 0, 7, 2},
  80         {+1_610_058.23423, 4, 7, 2},
  81         {-317_289, 4, 6, 1},
  82         {5_457_013.0000, 4, 7, 2},
  83     }
  84 
  85     for _, tc := range tests {
  86         // remember that uintWidth can't handle negatives
  87         digits, pretty := uintWidth(math.Abs(tc.Number))
  88 
  89         commas := pretty - digits
  90         if digits != tc.IntDigits || commas != tc.Commas {
  91             const fs = `%f: expected %d and %d, but got %d and %d instead`
  92             t.Fatalf(fs, tc.Number, tc.IntDigits, tc.Commas, digits, commas)
  93         }
  94     }
  95 }
  96 
  97 func TestGuessDecimalsCount(t *testing.T) {
  98     tests := []struct {
  99         Input    float64
 100         Expected int
 101     }{
 102         {-1, 0},
 103         {-1.3, 1},
 104         {-1.1, 1},
 105         {10.103, 3},
 106         {3.9500, 2},
 107         {1.789000, 3},
 108         {5.0409999000000000, 7},
 109     }
 110 
 111     for _, tc := range tests {
 112         got := GuessDecimalsCount(tc.Input, -1)
 113         if got != tc.Expected {
 114             const fs = `guess(%f, %d) gave %d, but should have been %d`
 115             t.Fatalf(fs, tc.Input, -1, got, tc.Expected)
 116         }
 117     }
 118 
 119     var v = []float64{
 120         1.773, 1.789, 1.773, 1.779, 1.817, 1.797, 1.780, 1.737,
 121         1.723, 1.725, 1.733, 1.742, 1.746, 1.728, 1.726, 1.701,
 122         1.690, 1.675, 1.680, 1.672, 1.685, 1.679, 1.674, 1.653,
 123         1.668, 1.669, 1.680, 1.693, 1.713, 1.712, 1.733, 1.744,
 124         1.739, 1.708, 1.695, 1.691, 1.715, 1.733, 1.730, 1.722,
 125         1.737, 1.749, 1.774, 1.773, 1.789, 1.804, 1.795, 1.752,
 126         1.770, 1.736, 1.717, 1.690, 1.696, 1.703, 1.679, 1.673,
 127         1.694, 1.710, 1.711, 1.739, 1.725, 1.748, 1.764, 1.781,
 128         1.766, 1.741, 1.739, 1.750, 1.752, 1.731, 1.717, 1.699,
 129         1.689, 1.701, 1.713, 1.715, 1.713, 1.780, 1.795, 1.822,
 130         1.832, 1.835, 1.846, 1.857, 1.869, 1.847, 1.831, 1.874,
 131         1.928, 1.900, 1.925, 1.888, 1.842, 1.836, 1.825, 1.804,
 132         1.745, 1.753, 1.779, 1.797, 1.812, 1.820, 1.829, 1.818,
 133         1.799, 1.795, 1.795, 1.792, 1.792, 1.760, 1.741, 1.721,
 134         1.718, 1.734, 1.751, 1.776, 1.768, 1.774, 1.790, 1.813,
 135         1.840, 1.861, 1.853, 1.834, 1.848, 1.859, 1.856, 1.839,
 136         1.826, 1.833, 1.850, 1.861, 1.874, 1.905, 1.931, 1.951,
 137         1.971, 1.985, 1.997, 2.023, 2.047, 2.080, 2.103, 2.139,
 138         2.155, 2.151, 2.142, 2.147, 2.147, 2.165, 2.171, 2.176,
 139         2.174, 2.190, 2.192, 2.184, 2.170, 2.155, 2.155, 2.140,
 140         2.129, 2.124, 2.128, 2.146, 2.135, 2.151, 2.141, 2.134,
 141         2.086, 2.059, 2.014, 1.980, 1.969, 1.972, 1.953, 1.927,
 142         1.915, 1.901, 1.905, 1.941, 1.951, 1.940, 1.949, 1.959,
 143         1.985, 1.995, 2.001, 2.002, 2.017, 2.018, 2.006, 1.997,
 144         1.994, 1.987, 1.978, 1.977, 1.940, 1.928, 1.909, 1.813,
 145         1.736, 1.710, 1.712, 1.726, 1.740, 1.747, 1.748, 1.745,
 146         1.728, 1.714, 1.656, 1.649, 1.642, 1.630, 1.612, 1.588,
 147         1.583, 1.570, 1.568, 1.561, 1.537, 1.542, 1.548, 1.538,
 148         1.520, 1.521, 1.513, 1.494, 1.469, 1.458, 1.449, 1.441,
 149         1.434, 1.428, 1.431, 1.440, 1.447, 1.457, 1.456, 1.457,
 150         1.450, 1.451, 1.439, 1.423, 1.374, 1.100, 1.147, 1.134,
 151         1.121, 1.119, 1.115,
 152     }
 153 
 154     for i, x := range v {
 155         guess := gdc(t, x)
 156         // guess := GuessDecimalsCount(v, -1)
 157         if guess > 3 {
 158             _, diff := math.Modf(1000 * x)
 159             t.Logf("len: %d\n", len(v))
 160             const fs = `(item %2d) %f doesn't have %d decimals; diff: %.20f`
 161             t.Fatalf(fs, i+1, x, guess, diff)
 162         }
 163     }
 164 
 165     v = []float64{
 166         6000.00, 2300.00, 2000.00, 1700.00, 1500.00, 1500.00, 1400.00, 1300.00,
 167         900.00, 800.00, 800.00, 700.00, 600.00, 550.00, 550.00, 400.00,
 168         320.39, 300.00, 200.00, 198.56, 155.94, 155.73, 98.15, 80.00,
 169         80.00, 74.50, 70.00, 70.00, 70.00, 70.00, 70.00, 65.00,
 170         60.00, 55.45, 49.00, 35.00, 35.00, 25.00, 25.00, 25.00,
 171         20.00, 15.00, 15.00, 12.00, 10.00, 10.00, 10.00, 10.00,
 172         7.00, 6.00, 6.00, 5.00, 3.00, 2.00, 1.00,
 173     }
 174     for i, x := range v {
 175         guess := gdc(t, x)
 176         // guess := GuessDecimalsCount(v, -1)
 177         if guess > 2 {
 178             _, diff := math.Modf(100 * x)
 179             t.Logf("len: %d\n", len(v))
 180             const fs = `(item %2d) %f doesn't have %d decimals; diff: %.20f`
 181             t.Fatalf(fs, i+1, x, guess, diff)
 182         }
 183     }
 184 }
 185 
 186 // gdc logs each step of the loop inside GuessDecimalsCount
 187 func gdc(t *testing.T, x float64) int {
 188     const max = 16
 189     const tol = 5e-11 // 5e-13
 190     for digits := 0; digits <= max; digits++ {
 191         _, frac := math.Modf(x)
 192         const fs = "x: %f\tdigits: %d\tdiff: %.20f\tcdiff: %.20f\n"
 193         t.Logf(fs, x, digits, frac, 1-frac)
 194         ar := math.Abs(frac)
 195         if ar < tol || (1-ar) < tol {
 196             return digits
 197         }
 198         x *= 10
 199     }
 200     return max
 201 }
 202 
 203 func TestIncrement(t *testing.T) {
 204     tests := []struct {
 205         Input    float64
 206         Expected float64
 207     }{
 208         {math.NaN(), math.NaN()},
 209         {math.Inf(-1), math.Inf(-1)},
 210         {math.Inf(+1), math.Inf(+1)},
 211 
 212         {-maxflint - 2, -maxflint},
 213         {-maxflint, -maxflint + 1},
 214         {-1, 0},
 215         {0, 1},
 216         {10.25, 11.25},
 217         {maxflint - 1, maxflint},
 218         {maxflint, maxflint + 2},
 219     }
 220 
 221     for _, tc := range tests {
 222         name := strconv.FormatFloat(tc.Input, 'f', 2, 64)
 223         t.Run(name, func(t *testing.T) {
 224             got := Increment(tc.Input)
 225             if !Equals(got, tc.Expected) {
 226                 const fs = `got %v, instead of %v`
 227                 t.Fatalf(fs, got, tc.Expected)
 228             }
 229         })
 230     }
 231 }
 232 
 233 func TestDecrement(t *testing.T) {
 234     tests := []struct {
 235         Input    float64
 236         Expected float64
 237     }{
 238         {math.NaN(), math.NaN()},
 239         {math.Inf(-1), math.Inf(-1)},
 240         {math.Inf(+1), math.Inf(+1)},
 241 
 242         {-maxflint, -maxflint - 2},
 243         {-maxflint + 1, -maxflint},
 244         {-1, -2},
 245         {0, -1},
 246         {11.25, 10.25},
 247         {maxflint, maxflint - 1},
 248         {maxflint + 2, maxflint},
 249     }
 250 
 251     for _, tc := range tests {
 252         name := strconv.FormatFloat(tc.Input, 'f', 2, 64)
 253         t.Run(name, func(t *testing.T) {
 254             got := Decrement(tc.Input)
 255             if !Equals(got, tc.Expected) {
 256                 const fs = `got %v, instead of %v`
 257                 t.Fatalf(fs, got, tc.Expected)
 258             }
 259         })
 260     }
 261 }
 262 
 263 func TestDeapproximate(t *testing.T) {
 264     tests := []struct {
 265         Input       float64
 266         ExpectedMin float64
 267         ExpectedMax float64
 268     }{
 269         {0, -0.5, +0.5},
 270         {1, 0.5, 1.5},
 271         {10, 5, 15},
 272         {100, 50, 150},
 273         {101, 100.5, 101.5},
 274         {102, 101.5, 102.5},
 275         {120, 115, 125},
 276     }
 277 
 278     for _, tc := range tests {
 279         name := strconv.FormatFloat(tc.Input, 'f', 6, 64)
 280         t.Run(name, func(t *testing.T) {
 281             min, max := Deapproximate(tc.Input)
 282             if !Equals(min, tc.ExpectedMin) || !Equals(max, tc.ExpectedMax) {
 283                 const fs = `got %f and %f, instead of %f and %f`
 284                 t.Fatalf(fs, min, max, tc.ExpectedMin, tc.ExpectedMax)
 285             }
 286         })
 287     }
 288 }
 289 
 290 func TestMaxExactPow10(t *testing.T) {
 291     tests := []struct {
 292         Input    int
 293         Expected int
 294     }{
 295         // {0, 1},
 296         {1, 1},
 297         {4, 1},
 298         {10, 10},
 299         {100, 100},
 300         {101, 1},
 301         {102, 1},
 302         {120, 10},
 303     }
 304 
 305     for _, tc := range tests {
 306         name := strconv.Itoa(tc.Input)
 307         t.Run(name, func(t *testing.T) {
 308             got := maxExactPow10(int(tc.Input))
 309             if got != int(tc.Expected) {
 310                 const fs = `got %d, instead of %d`
 311                 t.Fatalf(fs, got, tc.Expected)
 312             }
 313         })
 314     }
 315 }
     File: ./mathplus/statistics.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 // Quantile calculates a sorted array's quantile, parameterized by a number in
  32 // [0, 1]; the sorted array must be in increasing order and can't contain any
  33 // NaN value.
  34 func Quantile(x []float64, q float64) float64 {
  35     l := len(x)
  36     // quantiles aren't defined for empty arrays/samples
  37     if l == 0 {
  38         return math.NaN()
  39     }
  40 
  41     // calculate indices of surrounding values, which match when index isn't
  42     // fractional
  43     mid := float64(l-1) * q
  44     low := math.Floor(mid)
  45     high := math.Ceil(mid)
  46 
  47     // for fractional indices, interpolate their 2 surrounding values
  48     a := x[int(low)]
  49     b := x[int(high)]
  50     return a + (mid-low)*(b-a)
  51 }
  52 
  53 // welford has almost everything needed to implement Welford's running-stats
  54 // algorithm for the arithmetic mean and the standard deviation: the only
  55 // thing missing is the count of values so far, which you must provide every
  56 // time you call its methods.
  57 type welford struct {
  58     Mean          float64
  59     meanOfSquares float64
  60 }
  61 
  62 // Update advances Welford's algorithm, using the value and item-count given.
  63 func (w *welford) Update(x float64, n int) {
  64     d1 := x - w.Mean
  65     w.Mean += d1 / float64(n)
  66     d2 := x - w.Mean
  67     w.meanOfSquares += d1 * d2
  68 }
  69 
  70 // SD calculates the current standard-deviation. The first parameter is the
  71 // bias-correction to use: 0 gives you the `population` SD, 1 gives you the
  72 // `sample` SD; 1.5 is very-rarely used and only in special circumstances.
  73 func (w welford) SD(bias float64, n int) float64 {
  74     denom := float64(n) - bias
  75     return math.Sqrt(w.meanOfSquares / denom)
  76 }
  77 
  78 // RMS calculates the current root-mean-square statistic.
  79 func (w welford) RMS() float64 {
  80     return math.Sqrt(w.meanOfSquares)
  81 }
  82 
  83 // NumberSummary has/updates numeric constant-space running stats.
  84 type NumberSummary struct {
  85     // struct welford has the Mean public field
  86     welford
  87 
  88     // Count is how many values there are so far, including NaNs
  89     Count int
  90 
  91     // NaN counts NaN values so far
  92     NaN int
  93 
  94     // Integers counts all integers so far
  95     Integers int
  96 
  97     // Negatives counts all negative number so far
  98     Negatives int
  99 
 100     // Zeros counts all zeros so far
 101     Zeros int
 102 
 103     // Positives counts all positive numbers so far
 104     Positives int
 105 
 106     // Min is the least number so far, ignoring NaNs
 107     Min float64
 108 
 109     // Max is the highest number so far, ignoring NaNs
 110     Max float64
 111 
 112     // Sum is the sum of all numbers so far, ignoring NaNs
 113     Sum float64
 114 
 115     // sumOfLogs is used to calculate the geometric mean
 116     sumOfLogs float64
 117 }
 118 
 119 // Update does exactly what it says.
 120 func (ns *NumberSummary) Update(f float64) {
 121     if ns.Count == 0 {
 122         ns.Min = math.Inf(+1)
 123         ns.Max = math.Inf(-1)
 124     }
 125 
 126     ns.Count++
 127     if math.IsNaN(f) {
 128         ns.NaN++
 129         return
 130     }
 131 
 132     if _, r := math.Modf(f); r == 0 {
 133         ns.Integers++
 134     }
 135 
 136     if f > 0 {
 137         ns.Positives++
 138     } else if f == 0 {
 139         ns.Zeros++
 140     } else if f < 0 {
 141         ns.Negatives++
 142     }
 143 
 144     ns.Sum += f
 145     ns.sumOfLogs += math.Log(f)
 146     ns.Min = math.Min(ns.Min, f)
 147     ns.Max = math.Max(ns.Max, f)
 148     ns.welford.Update(f, ns.Valid())
 149 }
 150 
 151 // Valid finds how many numbers are valid so far
 152 func (ns NumberSummary) Valid() int {
 153     return ns.Count - ns.NaN
 154 }
 155 
 156 // Invalid finds how many numbers are invalid so far
 157 func (ns NumberSummary) Invalid() int {
 158     return ns.NaN
 159 }
 160 
 161 // Geomean calculates the current geometric mean
 162 func (ns NumberSummary) Geomean() float64 {
 163     if ns.Negatives > 0 || ns.Zeros > 0 {
 164         return math.NaN()
 165     }
 166     return math.Exp(ns.sumOfLogs / float64(ns.Valid()))
 167 }
 168 
 169 // SD calculates the current standard-deviation. The only parameter is the
 170 // bias-correction to use:
 171 //
 172 //  0 means calculate the current population standard-deviation
 173 //  1 means calculate the current sample standard-deviation
 174 //  1.5 is very-rarely used and only in special circumstances
 175 func (ns NumberSummary) SD(bias float64) float64 {
 176     return ns.welford.SD(bias, ns.Valid())
 177 }
 178 
 179 // CommonQuantiles groups all the most commonly-used quantiles in practice.
 180 //
 181 // Funcs AppendAllDeciles and AppendAllPercentiles give you other sets of
 182 // commonly-used ranking stats.
 183 type CommonQuantiles struct {
 184     Min float64
 185     P01 float64 // 1st percentile
 186     P05 float64 // 5th percentile
 187     P10 float64 // 10th percentile, also the 1st decile
 188     P25 float64 // 1st quartile, also the 25th percentile
 189     P50 float64 // 2nd quartile, also the 50th percentile
 190     P75 float64 // 3rd quartile, also the 75th percentile
 191     P90 float64
 192     P95 float64
 193     P99 float64
 194     Max float64
 195 }
 196 
 197 // NewCommonQuantiles is a convenience constructor for struct CommonQuantiles.
 198 func NewCommonQuantiles(x []float64) CommonQuantiles {
 199     return CommonQuantiles{
 200         Min: Quantile(x, 0.00),
 201         P01: Quantile(x, 0.01),
 202         P05: Quantile(x, 0.05),
 203         P10: Quantile(x, 0.10),
 204         P25: Quantile(x, 0.25),
 205         P50: Quantile(x, 0.50),
 206         P75: Quantile(x, 0.75),
 207         P90: Quantile(x, 0.90),
 208         P95: Quantile(x, 0.95),
 209         P99: Quantile(x, 0.99),
 210         Max: Quantile(x, 1.00),
 211     }
 212 }
 213 
 214 // AppendAllDeciles appends 11 items to the slice given, which ultimately
 215 // lets you reuse slices for multiple such calculations, thus avoiding extra
 216 // allocations.
 217 //
 218 // The 1st item returned is the minimum, and the last one is the maximum.
 219 func AppendAllDeciles(dest []float64, x []float64) []float64 {
 220     for i := 0; i <= 10; i++ {
 221         dest = append(dest, Quantile(x, float64(i)/10))
 222     }
 223     return dest
 224 }
 225 
 226 // AppendAllPercentiles appends 101 items to the slice given, which ultimately
 227 // lets you reuse slices for multiple such calculations, thus avoiding extra
 228 // allocations.
 229 //
 230 // The 1st item returned is the minimum, while the last one is the maximum.
 231 func AppendAllPercentiles(dest []float64, x []float64) []float64 {
 232     for i := 0; i <= 100; i++ {
 233         dest = append(dest, Quantile(x, float64(i)/100))
 234     }
 235     return dest
 236 }
     File: ./mathplus/statistics_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29     "testing"
  30 )
  31 
  32 type NumericTestResult struct {
  33     Count     int
  34     Valid     int
  35     Integers  int
  36     Negatives int
  37     Zeros     int
  38     Positives int
  39 
  40     Min     float64
  41     Max     float64
  42     Sum     float64
  43     Mean    float64
  44     Geomean float64
  45     SD      float64
  46     RMS     float64
  47 }
  48 
  49 func (tr NumericTestResult) Match(ns NumberSummary) bool {
  50     return true &&
  51         tr.Count == ns.Count &&
  52         tr.Valid == ns.Valid() &&
  53         tr.Integers == ns.Integers &&
  54         tr.Negatives == ns.Negatives &&
  55         tr.Zeros == ns.Zeros &&
  56         tr.Positives == ns.Positives &&
  57         equals(tr.Min, ns.Min) &&
  58         equals(tr.Max, ns.Max) &&
  59         equals(tr.Sum, ns.Sum) &&
  60         equals(tr.Mean, ns.Mean) &&
  61         equals(tr.Geomean, ns.Geomean()) &&
  62         equals(tr.SD, ns.SD(0)) &&
  63         equals(tr.RMS, ns.RMS()) &&
  64         true
  65 }
  66 
  67 func (tr NumericTestResult) Test(t *testing.T, ns NumberSummary) {
  68     var checks = []struct {
  69         X       float64
  70         Y       float64
  71         Message string
  72     }{
  73         {float64(tr.Count), float64(ns.Count), `count`},
  74         {float64(tr.Valid), float64(ns.Valid()), `valid`},
  75         {float64(tr.Integers), float64(ns.Integers), `integers`},
  76         {float64(tr.Negatives), float64(ns.Negatives), `negatives`},
  77         {float64(tr.Zeros), float64(ns.Zeros), `zeros`},
  78         {float64(tr.Positives), float64(ns.Positives), `positives`},
  79         {float64(tr.Min), float64(ns.Min), `min`},
  80         {float64(tr.Max), float64(ns.Max), `max`},
  81         {float64(tr.Sum), float64(ns.Sum), `sum`},
  82         {float64(tr.Mean), float64(ns.Mean), `mean`},
  83         {float64(tr.Geomean), float64(ns.Geomean()), `geomean`},
  84         {float64(tr.SD), float64(ns.SD(0)), `sd`},
  85         {float64(tr.RMS), float64(ns.RMS()), `rms`},
  86     }
  87 
  88     const fs = "field %q failed: %f and %f differ\nexpected %#v\ngot      %#v"
  89     for _, tc := range checks {
  90         if !equals(tc.X, tc.Y) {
  91             t.Fatalf(fs, tc.Message, tc.X, tc.Y, tr, ns)
  92             return
  93         }
  94     }
  95 }
  96 
  97 var numericTests = []struct {
  98     Description string
  99     Input       []float64
 100     Expected    NumericTestResult
 101 }{
 102     {
 103         Description: `no values`,
 104         Input:       []float64{},
 105         Expected: NumericTestResult{
 106             Count:    0,
 107             Valid:    0,
 108             Integers: 0,
 109             // Min:      math.Inf(+1),
 110             // Max:      math.Inf(-1),
 111             Min:     0.0,
 112             Max:     0.0,
 113             Sum:     0.0,
 114             Mean:    0.0,
 115             Geomean: math.NaN(),
 116             SD:      math.NaN(),
 117             RMS:     0.0,
 118         },
 119     },
 120     {
 121         Description: `just a value`,
 122         Input:       []float64{-3.5},
 123         Expected: NumericTestResult{
 124             Count:     1,
 125             Valid:     1,
 126             Integers:  0,
 127             Negatives: 1,
 128             Zeros:     0,
 129             Positives: 0,
 130             Min:       -3.5,
 131             Max:       -3.5,
 132             Sum:       -3.5,
 133             Mean:      -3.5,
 134             Geomean:   math.NaN(),
 135             SD:        0.0,
 136             RMS:       0.0,
 137         },
 138     },
 139     {
 140         Description: `integers 1..10`,
 141         Input:       []float64{math.NaN(), 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
 142         Expected: NumericTestResult{
 143             Count:     11,
 144             Valid:     10,
 145             Integers:  10,
 146             Negatives: 0,
 147             Zeros:     0,
 148             Positives: 10,
 149             Min:       1,
 150             Max:       10,
 151             Sum:       55,
 152             Mean:      5.5,
 153             Geomean:   4.528728688116766,
 154             RMS:       9.082951062292475,
 155             SD:        2.8722813232690143,
 156         },
 157     },
 158     {
 159         Description: `nothing valid`,
 160         Input: []float64{
 161             math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
 162         },
 163         Expected: NumericTestResult{
 164             Count:    5,
 165             Valid:    0,
 166             Integers: 0,
 167             Min:      math.Inf(+1),
 168             Max:      math.Inf(-1),
 169             Sum:      0.0,
 170             Mean:     0.0,
 171             Geomean:  math.NaN(),
 172             SD:       math.NaN(),
 173             RMS:      0.0,
 174         },
 175     },
 176 }
 177 
 178 func TestNumberSummary(t *testing.T) {
 179     for _, tc := range numericTests {
 180         var s NumberSummary
 181         for _, x := range tc.Input {
 182             s.Update(x)
 183         }
 184         tc.Expected.Test(t, s)
 185     }
 186 }
 187 
 188 func equals(x, y float64) bool {
 189     if x == y {
 190         return true
 191     }
 192     return math.IsNaN(x) && math.IsNaN(y)
 193 }
     File: ./mediainfo/aiff.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "errors"
  29     "io"
  30     "math"
  31 )
  32 
  33 // http://paulbourke.net/dataformats/audio/
  34 
  35 var errTruncatedAIFF = errors.New("unexpected end of AIFF data")
  36 
  37 func aiffDuration(r io.Reader) (seconds float64, err error) {
  38     data, err := io.ReadAll(r)
  39     if err != nil {
  40         return 0, err
  41     }
  42 
  43     // all these are read when in the COMM block
  44     numChan := 0
  45     sampleSize := 0
  46     sampleRate := 0
  47 
  48     for size := 8; len(data) >= size; data = data[size:] {
  49         if len(data) < 8 {
  50             return seconds, errTruncatedAIFF
  51         }
  52         size = int(bytes2uint(data[4:8])) + 8
  53         if len(data) < size {
  54             return seconds, errTruncatedAIFF
  55         }
  56 
  57         switch id := string(data[:4]); id {
  58         case "FORM":
  59             if len(data) < 12 || !match4(data[8:12], 'A', 'I', 'F', 'F') {
  60                 return math.NaN(), errTruncatedAIFF
  61             }
  62             size = 12
  63 
  64         case "COMM":
  65             if len(data) < 25 {
  66                 return math.NaN(), errTruncatedAIFF
  67             }
  68             numChan = int(bytes2uint(data[8:10]))
  69             sampleSize = int(bytes2uint(data[14:16])) / 8
  70             sampleRate = int(float80(data[16:26]))
  71 
  72         case "SSND":
  73             if len(data) < 12 {
  74                 return math.NaN(), errTruncatedAIFF
  75             }
  76             offset := int(bytes2uint(data[8:12]))
  77             n := (size - offset - 4) / (sampleSize * numChan)
  78             seconds += float64(n) / float64(sampleRate)
  79         }
  80     }
  81 
  82     return seconds, nil
  83 }
     File: ./mediainfo/au.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "errors"
  30     "io"
  31     "math"
  32 )
  33 
  34 var (
  35     errInvalidAuData         = errors.New("invalid AU data")
  36     errUnsupportedAuEncoding = errors.New("unsupported AU data encoding")
  37 )
  38 
  39 type auHeader struct {
  40     Magic      uint32 // ".snd" if data are valid
  41     Offset     uint32
  42     Size       uint32
  43     Encoding   uint32
  44     SampleRate uint32
  45     Channels   uint32
  46 }
  47 
  48 func auDuration(r io.Reader, n int) (seconds float64, err error) {
  49     var header auHeader
  50     err = binary.Read(r, binary.BigEndian, &header)
  51     if err != nil {
  52         return 0, err
  53     }
  54 
  55     // check if first 4 bytes are ".snd" in ascii
  56     if header.Magic != 0x2e736e64 {
  57         return math.NaN(), errInvalidAuData
  58     }
  59 
  60     // find how many bytes each sample takes
  61     itemSize := 0
  62     switch header.Encoding {
  63     case 2:
  64         itemSize = 1
  65     case 3:
  66         itemSize = 2
  67     case 4:
  68         itemSize = 3
  69     case 5, 6:
  70         itemSize = 4
  71     case 7:
  72         itemSize = 8
  73     default:
  74         return math.NaN(), errUnsupportedAuEncoding
  75     }
  76 
  77     rate := header.SampleRate * header.Channels * uint32(itemSize)
  78     // if header has an unknown data size, calculate it from the file size
  79     if header.Size == 0xffffffff {
  80         // the au file header is 24 bytes
  81         return float64(n-int(header.Offset)-24) / float64(rate), nil
  82     }
  83     return float64(header.Size) / float64(rate), nil
  84 }
     File: ./mediainfo/avi.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "bytes"
  29     "encoding/binary"
  30     "io"
  31     "math"
  32 )
  33 
  34 // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/Aviriff/ns-aviriff-avimainheader
  35 type aviMainHeader struct {
  36     Type                [4]byte // "avih"
  37     Size                uint32  // structure size minus 8
  38     MicroSecPerFrame    uint32
  39     MaxBytesPerSec      uint32
  40     PaddingGranularity  uint32
  41     Flags               uint32
  42     TotalFrames         uint32
  43     InitialFrames       uint32
  44     Streams             uint32
  45     SuggestedBufferSize uint32
  46     Width               uint32
  47     Height              uint32
  48     Reserved            [4]uint32
  49 }
  50 
  51 // The stream header chunk ('strh') consists of an AVISTREAMHEADER structure
  52 // Note: there seem to be 4 more bytes in between "strh" and the start of the struct
  53 // https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
  54 // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/avifmt/ns-avifmt-avistreamheader
  55 type aviStreamHeader struct {
  56     Type          [4]byte // either "vids" or "auds"
  57     Handler       [4]byte
  58     Flags         uint32
  59     Priority      uint16
  60     Language      uint16
  61     InitialFrames uint32
  62 
  63     Scale  uint32
  64     Rate   uint32
  65     Start  uint32
  66     Length uint32
  67 
  68     SugBufferSize uint32 // suggested buffer size
  69     Quality       uint32
  70     SampleSize    uint32
  71 
  72     // `frame info` data are supposed to follow, whatever those are
  73 }
  74 
  75 func aviDuration(r io.Reader) (seconds float64, err error) {
  76     buf := make([]byte, 2048)
  77     n, err := r.Read(buf)
  78     if err != io.EOF && err != nil {
  79         return math.NaN(), err
  80     }
  81 
  82     sec := 0.0
  83     buf = buf[:n]
  84     for {
  85         i := bytes.Index(buf, []byte{'s', 't', 'r', 'h'})
  86         if i < 0 {
  87             break
  88         }
  89         i += 8
  90         buf = buf[i:]
  91 
  92         dur, err := aviStreamDuration(buf)
  93         if err != nil {
  94             break
  95         }
  96         if math.IsNaN(dur) {
  97             continue
  98         }
  99         sec = math.Max(sec, dur)
 100     }
 101 
 102     if sec == 0 && n > 0 {
 103         return math.NaN(), nil
 104     }
 105     return sec, nil
 106 }
 107 
 108 func aviStreamDuration(data []byte) (seconds float64, err error) {
 109     var sh aviStreamHeader
 110     r := bytes.NewReader(data)
 111     err = binary.Read(r, binary.LittleEndian, &sh)
 112     if err != nil {
 113         return math.NaN(), err
 114     }
 115     return float64(sh.Length) * float64(sh.Scale) / float64(sh.Rate), nil
 116 }
 117 
 118 func aviResolution(r io.Reader) (int, int, int, error) {
 119     buf := make([]byte, 2048)
 120     n, err := r.Read(buf)
 121     if err != io.EOF && err != nil {
 122         return -1, -1, -1, err
 123     }
 124 
 125     buf = buf[:n]
 126     i := bytes.Index(buf, []byte{'a', 'v', 'i', 'h'})
 127     if i < 0 {
 128         return -1, -1, -1, nil
 129     }
 130 
 131     var mh aviMainHeader
 132     r = bytes.NewReader(buf[i:])
 133     err = binary.Read(r, binary.LittleEndian, &mh)
 134     if err != nil {
 135         return -1, -1, -1, err
 136     }
 137 
 138     return int(mh.Width), int(mh.Height), -1, nil
 139 }
     File: ./mediainfo/bmp.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "errors"
  30     "io"
  31 )
  32 
  33 var errUnsupportedBMPFormat = errors.New("unsupported BMP format")
  34 
  35 type bmpHeader struct {
  36     Type             [2]byte
  37     Size             uint32
  38     Reserved         uint32
  39     PixelArrayOffset uint32
  40     InfoHeaderSize   uint32
  41     Width            int32
  42     Height           int32
  43     ColorPlanes      uint16
  44     BitsPerPixel     uint16
  45 }
  46 
  47 func bmpResolution(r io.Reader) (int, int, int, error) {
  48     var header bmpHeader
  49     err := binary.Read(r, binary.LittleEndian, &header)
  50     if err != nil {
  51         return 0, 0, 0, err
  52     }
  53     // only windows bitmaps are supported
  54     if header.Type[0] != 'B' || header.Type[1] != 'M' {
  55         return 0, 0, 0, errUnsupportedBMPFormat
  56     }
  57     return int(header.Width), int(header.Height), int(header.BitsPerPixel), nil
  58 }
     File: ./mediainfo/doc.go
   1 /*
   2 # mediainfo
   3 
   4 Package to extract all sorts of information from media (pics, audio, video)
   5 files/data.
   6 
   7 Right now it can find picture resolution for
   8   - GIF
   9   - PNG
  10   - WEBP (only some of its variants)
  11 
  12 and the play length in seconds for many common audio/video files, such as
  13   - AAC
  14   - AIFF
  15   - AVI
  16   - FLAC
  17   - MP3
  18   - MP4
  19   - WAVE
  20 
  21 Notably missing in the list of supported formats is JPEG.
  22 */
  23 
  24 package mediainfo
     File: ./mediainfo/flac.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "errors"
  30     "io"
  31     "math"
  32 )
  33 
  34 // https://gist.github.com/lukasklein/8c474782ed66c7115e10904fecbed86a
  35 
  36 var (
  37     errShortFlacInfoStream   = errors.New("not enough bytes in the stream-info block")
  38     errInvalidFlacSampleRate = errors.New("invalid sample rate")
  39     errNoFlacMarker          = errors.New("data not marked with \"fLaC\"")
  40 )
  41 
  42 func flacDuration(r io.Reader) (seconds float64, err error) {
  43     // check if file starts with a flac marker
  44     var flac [4]byte
  45     err = binary.Read(r, binary.BigEndian, &flac)
  46     if err == io.EOF {
  47         return 0, nil
  48     }
  49     if err != nil {
  50         return 0, err
  51     }
  52     if flac[0] != 'f' || flac[1] != 'L' || flac[2] != 'a' || flac[3] != 'C' {
  53         return 0, errNoFlacMarker
  54     }
  55 
  56     for {
  57         // read block-marker and block-size packed in 4 bytes
  58         var meta [4]byte
  59         err := binary.Read(r, binary.BigEndian, &meta)
  60         if err == io.EOF {
  61             return 0, nil
  62         }
  63         if err != nil {
  64             return 0, err
  65         }
  66 
  67         blockType := meta[0] & 0x7f
  68         size := bytes2uint(meta[1:4])
  69         // block-type 0 means it's a stream-info block
  70         if blockType == 0 {
  71             info := make([]byte, size)
  72             err := binary.Read(r, binary.BigEndian, &info)
  73             if err == io.EOF {
  74                 return 0, nil
  75             }
  76             if err != nil {
  77                 return 0, err
  78             }
  79 
  80             // https://xiph.org/flac/format.html#metadata_block_streaminfo
  81             if len(info) < 18 {
  82                 return math.NaN(), errShortFlacInfoStream
  83             }
  84             sr := bytes2uint(info[10:13]) >> 4 // lowest bits are metadata unrelated to sample rate
  85             n := bytes2uint([]byte{info[13] & 0x0f, info[14], info[15], info[16], info[17]})
  86             if sr == 0 {
  87                 return math.NaN(), errInvalidFlacSampleRate
  88             }
  89             return float64(n) / float64(sr), nil
  90         }
  91     }
  92 }
     File: ./mediainfo/gif.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "errors"
  30     "io"
  31 )
  32 
  33 var errInvalidGIFSignature = errors.New("invalid GIF signature")
  34 
  35 // http://www33146ue.sakura.ne.jp/staff/iz/formats/gif.html
  36 // global color table length is determined by the bits of its related info field
  37 // when top bit is 0 it's 0, else it's 2 to the (lowest 3 bits + 1)
  38 type gifHeader struct {
  39     Signature            [6]byte // "GIF87a" or "GIF89a"
  40     LogicalScreenWidth   uint16
  41     LogicalScreenHeight  uint16
  42     GlobalColorTableInfo byte
  43     BackgroundColorIndex byte
  44     PixelAspectRatio     byte
  45     // GlobalColorTable: variable-length RGB array
  46 }
  47 
  48 func gifResolution(r io.ReadSeeker) (int, int, int, error) {
  49     var header gifHeader
  50     err := binary.Read(r, binary.LittleEndian, &header)
  51     if err != nil {
  52         return 0, 0, 0, err
  53     }
  54 
  55     if !gifSignatureIsValid(header.Signature) {
  56         return 0, 0, 0, errInvalidGIFSignature
  57     }
  58     return int(header.LogicalScreenWidth), int(header.LogicalScreenHeight), 8, nil
  59 }
  60 
  61 func gifSignatureIsValid(s [6]byte) bool {
  62     // valid GIF data must start either with "GIF87a" or "GIF89a"
  63     if s[0] != 'G' || s[1] != 'I' || s[2] != 'F' {
  64         return false
  65     }
  66     if s[3] != '8' || (s[4] != '7' && s[4] != '9') || s[5] != 'a' {
  67         return false
  68     }
  69     return true
  70 }
     File: ./mediainfo/heic.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "bytes"
  29     "io"
  30 )
  31 
  32 func heicResolution(r io.Reader) (int, int, int, error) {
  33     var buf [4 * 1024]byte
  34     n, err := r.Read(buf[:])
  35     data := buf[:n]
  36 
  37     // seek the 1st `ispe` marker
  38     i := bytes.Index(data, []byte("ispe"))
  39     if i < 0 {
  40         return -1, -1, -1, ErrResolutionNotFound
  41     }
  42 
  43     // seek the 2nd `ispe` marker
  44     data = data[i+len("ispe"):]
  45     i = bytes.Index(data, []byte("ispe"))
  46     if i < 0 {
  47         return -1, -1, -1, ErrResolutionNotFound
  48     }
  49 
  50     data = data[i:]
  51     if len(data) < 16 {
  52         return -1, -1, -1, ErrResolutionNotFound
  53     }
  54 
  55     // width starts 8 bytes after the 2nd `ispe` marker and is big-endian
  56     data = data[8:]
  57     width := 16_777_216 * int(data[0])
  58     width += 65_536 * int(data[1])
  59     width += 256 * int(data[2])
  60     width += int(data[3])
  61 
  62     // height starts 4 bytes after the width and is big-endian
  63     data = data[4:]
  64     height := 16_777_216 * int(data[0])
  65     height += 65_536 * int(data[1])
  66     height += 256 * int(data[2])
  67     height += int(data[3])
  68 
  69     // bits-per-pixel are unknown, unless `pixi` metadata are found next
  70     bpp := -1
  71 
  72     // seek the `pixi` marker after the `ispe` markers
  73     if i := bytes.Index(data, []byte("pixi")); i >= 0 {
  74         if data := data[i+len("pixi"):]; len(data) >= 8 {
  75             // color-depth info starts 4 bytes after the `pixi` marker
  76             data = data[4:]
  77 
  78             // get the number of color channels/components
  79             n := data[0]
  80             data = data[1:]
  81 
  82             // add the bits-per-pixel count for each color channel/component
  83             bpp = 0
  84             for i := 0; i < int(n) && len(data) > 0; i++ {
  85                 bpp += int(data[0])
  86                 data = data[1:]
  87             }
  88         }
  89     }
  90 
  91     if err == io.EOF {
  92         err = nil
  93     }
  94     return width, height, bpp, err
  95 }
     File: ./mediainfo/ico.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "io"
  30 )
  31 
  32 // https://en.wikipedia.org/wiki/ICO_(file_format)
  33 
  34 const (
  35     IconDirTypeIcon   = 1
  36     IconDirTypeCursor = 2
  37 )
  38 
  39 type IconDir struct {
  40     Reserved uint16 // must be 0
  41     Type     uint16 // 1 means icon, 2 means cursor
  42     NumPics  uint16 // how many differently-resoluted pics are available
  43 }
  44 
  45 type IconDirEntry struct {
  46     Width  uint8 // image width in pixels: 0 means 256
  47     Height uint8 // image height in pixels: 0 means 256
  48 
  49     ColorCount uint8
  50     Reserved   uint8
  51 
  52     Extra1 uint16 // color palettes for icons, horizontal hotspot position for cursors
  53     Extra2 uint16 // bits-per-pixel for icons, vertical hotspot position for cursors
  54 
  55     Size   uint32 // how many bytes image data use
  56     Offset uint32 // where image bytes start from the beginning of the stream
  57 }
  58 
  59 func icoResolution(r io.ReadSeeker) (int, int, int, error) {
  60     var header IconDir
  61     err := binary.Read(r, binary.LittleEndian, &header)
  62     if err != nil {
  63         return 0, 0, 0, err
  64     }
  65 
  66     width := 0
  67     height := 0
  68     maxbpp := 8
  69     for i := 0; i < int(header.NumPics); i++ {
  70         var e IconDirEntry
  71         err = binary.Read(r, binary.LittleEndian, &e)
  72         if err == io.EOF {
  73             return width, height, maxbpp, nil
  74         }
  75         if err != nil {
  76             return 0, 0, 0, err
  77         }
  78 
  79         w := int(e.Width)
  80         // 0 width means 256
  81         if w == 0 {
  82             w = 256
  83         }
  84         h := int(e.Height)
  85         // 0 height means 256
  86         if h == 0 {
  87             h = 256
  88         }
  89         bpp := 8
  90         if e.ColorCount == 0 {
  91             bpp = int(e.Extra2)
  92         }
  93 
  94         // keep track of max width, height, and bpp
  95         if w > width {
  96             width = w
  97         }
  98         if h > height {
  99             height = h
 100         }
 101         if bpp > maxbpp {
 102             maxbpp = bpp
 103         }
 104     }
 105 
 106     return width, height, maxbpp, nil
 107 }
     File: ./mediainfo/id3v2.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "bytes"
  29     "errors"
  30     "io"
  31 )
  32 
  33 // calcSizeID3v2 finds the ID3v2 size in bytes from the ID3v2 header given; if
  34 // the slice given isn't a valid/complete ID3v2 header, the result is 0
  35 func calcSizeID3v2(b []byte) int {
  36     if len(b) >= 10 && bytes.HasPrefix(b, []byte{'I', 'D', '3'}) {
  37         n := 0
  38         // each byte has top bit 0, and the other 7 bits as payload: the 4
  39         // bytes thus result in a 28-bit value
  40         n += int(b[6]) * 128 * 128 * 128
  41         n += int(b[7]) * 128 * 128
  42         n += int(b[8]) * 128
  43         n += int(b[9])
  44         return n
  45     }
  46     return 0
  47 }
  48 
  49 func CopyThumbnailMP3(w io.Writer, r io.Reader) (mimetype string, err error) {
  50     const bufsize = 128 * 1024
  51     var buf [bufsize]byte
  52 
  53     for {
  54         n, err := r.Read(buf[:])
  55         data := buf[:n]
  56 
  57         if i := bytes.Index(data, []byte{'A', 'P', 'I', 'C'}); i >= 0 {
  58             return handleAPIC(w, r, data[i+len("APIC"):])
  59         }
  60 
  61         if err == io.EOF {
  62             return mimetype, errors.New("no thumbnail found")
  63         }
  64         if err != nil {
  65             return mimetype, err
  66         }
  67     }
  68 }
  69 
  70 func handleAPIC(w io.Writer, r io.Reader, data []byte) (mimetype string, err error) {
  71     const bufsize = 128 * 1024
  72     var buf [bufsize]byte
  73 
  74     if len(data) < 4 {
  75         const msg = "failed to detect thumbnail-payload size"
  76         return "", errors.New(msg)
  77     }
  78 
  79     size := 0
  80     // section-size seems stored as 4 little-endian bytes
  81     size += int(data[3]) * 128 * 128 * 128
  82     size += int(data[2]) * 128 * 128
  83     size += int(data[1]) * 128
  84     size += int(data[0])
  85 
  86     i, j := findThumbnailMIME(data)
  87     if i < 0 {
  88         const msg = "failed to sync to start of thumbnail data"
  89         return mimetype, errors.New(msg)
  90     }
  91 
  92     mimetype = string(data[i:j])
  93     data = data[j:]
  94     size -= j
  95     if len(data) < 2 {
  96         n, _ := r.Read(buf[:])
  97         data = buf[:n]
  98     }
  99     data = data[2:]
 100     size -= 2
 101     if i := bytes.IndexByte(data, 0); i >= 0 {
 102         data = data[i+1:]
 103         size -= i + 1
 104     } else {
 105         const msg = "failed to sync to comment before thumbnail"
 106         return mimetype, errors.New(msg)
 107     }
 108 
 109     start := bytes.NewReader(data)
 110     rest := io.LimitReader(r, int64(size-len(data)))
 111     _, err = io.Copy(w, io.MultiReader(start, rest))
 112     return mimetype, err
 113 }
 114 
 115 func findThumbnailMIME(data []byte) (start int, stop int) {
 116     if i := bytes.Index(data, []byte("image/")); i >= 0 {
 117         if j := bytes.IndexByte(data[i:], 0); j >= 0 {
 118             return i, i + j
 119         }
 120     }
 121     return -1, -1
 122 }
     File: ./mediainfo/jpeg.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "bytes"
  29     "encoding/binary"
  30     "io"
  31 )
  32 
  33 // https://www.media.mit.edu/pia/Research/deepview/exif.html
  34 
  35 // https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
  36 
  37 func jpegResolution(r io.Reader) (int, int, int, error) {
  38     var data [1024 * 8]byte
  39     n, err := r.Read(data[:])
  40     if err != nil {
  41         return 0, 0, 0, err
  42     }
  43     buf := data[:n]
  44 
  45     if bytes.Contains(buf, []byte{'J', 'F', 'I', 'F'}) {
  46         return jpegResolutionJFIF(buf, binary.BigEndian)
  47     }
  48     if bytes.Contains(buf, []byte{'E', 'x', 'i', 'f', 0, 0, 'I', 'I'}) {
  49         return jpegResolutionEXIF(buf, binary.LittleEndian)
  50     }
  51     if bytes.Contains(buf, []byte{'E', 'x', 'i', 'f', 0, 0, 'M', 'M'}) {
  52         return jpegResolutionEXIF(buf, binary.BigEndian)
  53     }
  54     return jpegResolutionJFIF(buf, binary.BigEndian)
  55 }
  56 
  57 func jpegResolutionEXIF(data []byte, order binary.ByteOrder) (int, int, int, error) {
  58     var imageWidth [2]byte = [2]byte{0xa0, 0x02}
  59     var imageHeight [2]byte = [2]byte{0xa0, 0x03}
  60     if order == binary.LittleEndian {
  61         imageWidth[0], imageWidth[1] = imageWidth[1], imageWidth[0]
  62         imageHeight[0], imageHeight[1] = imageHeight[1], imageHeight[0]
  63     }
  64 
  65     var w, h int
  66     for sub := data; len(sub) > 0; {
  67         wt, ht, rest := jpegWidthHeightEXIF(sub, order, imageWidth, imageHeight)
  68         if wt > w {
  69             w = wt
  70         }
  71         if ht > h {
  72             h = ht
  73         }
  74         sub = rest
  75     }
  76 
  77     // if resolution not found in EXIF metadata, try as a JFIF: sometimes it works
  78     if w == 0 && h == 0 {
  79         // return jpegResolutionJFIF(buf, order)
  80         return jpegResolutionJFIF(data, binary.BigEndian)
  81     }
  82     return int(w), int(h), -1, nil
  83 }
  84 
  85 func jpegWidthHeightEXIF(data []byte, order binary.ByteOrder, imageWidth, imageHeight [2]byte) (int, int, []byte) {
  86     var w, h uint16
  87     const markerLen = len(imageWidth)
  88 
  89     if i := bytes.Index(data, imageWidth[:]); i >= 0 && i+markerLen+2 < len(data) {
  90         data = data[i+markerLen:]
  91         offset := order.Uint16(data)
  92         i = 2 + int(offset)
  93         if i+2 < len(data) {
  94             data = data[i:]
  95             w = order.Uint16(data)
  96         }
  97     } else {
  98         return -1, -1, nil
  99     }
 100 
 101     if i := bytes.Index(data, imageHeight[:]); i >= 0 && i+markerLen+2 < len(data) {
 102         data = data[i+markerLen:]
 103         offset := order.Uint16(data)
 104         i = 2 + int(offset)
 105         if i+2 < len(data) {
 106             data = data[i:]
 107             h = order.Uint16(data)
 108         }
 109     } else {
 110         return int(w), -1, nil
 111     }
 112 
 113     return int(w), int(h), data
 114 }
 115 
 116 func jpegResolutionJFIF(buf []byte, order binary.ByteOrder) (int, int, int, error) {
 117     var i int
 118     // start of frame baseline-DCT
 119     i = bytes.Index(buf, []byte{0xff, 0xc0, 0x00, 0x11, 0x08})
 120     if i >= 0 && i+5+2*2 < len(buf) {
 121         h := order.Uint16(buf[i+5:])
 122         w := order.Uint16(buf[i+7:])
 123         return int(w), int(h), -1, nil
 124     }
 125     // start of frame progressive-DCT
 126     i = bytes.Index(buf, []byte{0xff, 0xc2, 0x00, 0x11, 0x08})
 127     if i >= 0 && i+5+2*2 < len(buf) {
 128         h := order.Uint16(buf[i+5:])
 129         w := order.Uint16(buf[i+7:])
 130         return int(w), int(h), -1, nil
 131     }
 132     return 0, 0, 0, ErrResolutionNotFound
 133 }
     File: ./mediainfo/mediainfo.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "errors"
  29     "io"
  30     "math"
  31     "os"
  32     "path/filepath"
  33     "strings"
  34 )
  35 
  36 var (
  37     ErrUnsupportedFormat = errors.New("not a supported media format")
  38     ErrNotPlayableMedia  = errors.New("not a playable media format")
  39     ErrNotPicture        = errors.New("not a supported picture format")
  40 
  41     ErrDurationNotFound   = errors.New("duration not found")
  42     ErrResolutionNotFound = errors.New("resolution not found")
  43 
  44     errNotSeeker = errors.New("a reader which could also seek was needed")
  45 )
  46 
  47 // Duration returns the time duration of the stream given, assuming if it's audio/video
  48 func Duration(r io.Reader, n int, typeHint string) (seconds float64, err error) {
  49     switch normalizeTypeHint(typeHint) {
  50     case "aiff", "aif", "snd", "AIFF", "AIF", "SND":
  51         return aiffDuration(r)
  52     case "au", "AU":
  53         return auDuration(r, n)
  54     case "flac", "x-flac", "FLAC", "X-FLAC": // the flac MIME-type is audio/x-flac
  55         return flacDuration(r)
  56     case "mp3", "MP3", "mpeg", "MPEG": // the mp3 MIME-type is audio/mpeg
  57         rs, ok := r.(io.ReadSeeker)
  58         if !ok {
  59             return math.NaN(), errNotSeeker
  60         }
  61         return mp3Duration(rs)
  62     case
  63         "aac", "m4a", "m4b", "mp4", "m4v", "mov", "3gp",
  64         "AAC", "M4A", "M4B", "MP4", "M4V", "MOV", "3GP":
  65         rs, ok := r.(io.ReadSeeker)
  66         if !ok {
  67             return math.NaN(), errNotSeeker
  68         }
  69         return mpeg4Duration(rs)
  70     case "wav", "x-wav", "WAV", "X-WAV": // the wav MIME-type is audio/x-wav
  71         return waveDuration(r)
  72     case "avi", "AVI":
  73         return aviDuration(r)
  74     case "webm", "WEBM":
  75         return webmDuration(r)
  76     case "aifc", "wma", "AIFC", "WMA":
  77         return math.NaN(), ErrUnsupportedFormat
  78     case "mkv", "MKV":
  79         // only works if it's a WEBM from youtube
  80         return webmDuration(r)
  81     case "wmv", "divx", "mpg", "ogg", "opus":
  82         return math.NaN(), ErrUnsupportedFormat
  83     case "WMV", "DIVX", "MPG", "OGG", "OPUS":
  84         return math.NaN(), ErrUnsupportedFormat
  85     default:
  86         return math.NaN(), ErrNotPlayableMedia
  87     }
  88 }
  89 
  90 // FileDuration returns the time duration of the file given, assuming if it's audio/video,
  91 // otherwise it's NaN: the reading position must be at the beginning before calling this function
  92 func FileDuration(f *os.File) (seconds float64, err error) {
  93     n := 0
  94     info, err := f.Stat()
  95     if err == nil {
  96         n = int(info.Size())
  97     }
  98     return Duration(f, n, f.Name())
  99 }
 100 
 101 // Resolution returns the size of the file given, assuming it's a picture
 102 func Resolution(r io.ReadSeeker, typeHint string) (w int, h int, bitDepth int, err error) {
 103     switch normalizeTypeHint(typeHint) {
 104     case "mp4", "MP4":
 105         return mpeg4Resolution(r)
 106     case "avi", "AVI":
 107         return aviResolution(r)
 108     case "png", "PNG":
 109         return pngResolution(r)
 110     case "jpeg", "jpg", "JPEG", "JPG":
 111         return jpegResolution(r)
 112     case "heic", "HEIC":
 113         return heicResolution(r)
 114     case "gif", "GIF":
 115         return gifResolution(r)
 116     case "bmp", "BMP":
 117         return bmpResolution(r)
 118     case "webp", "WEBP":
 119         return webpResolution(r)
 120     case "svg", "SVG":
 121         return svgResolution(r)
 122     case "psd", "PSD":
 123         return psdResolution(r)
 124     case "tiff", "TIFF", "tif", "TIF":
 125         return tiffResolution(r)
 126     case "ico", "cur", "ICO", "CUR":
 127         return icoResolution(r)
 128     case "tga", "jp2", "TGA", "JP2":
 129         return 0, 0, 0, ErrUnsupportedFormat
 130     default:
 131         return 0, 0, 0, ErrNotPicture
 132     }
 133 }
 134 
 135 func normalizeTypeHint(s string) string {
 136     // use the file extension if given a filename, ignoring leading dots
 137     if ext := filepath.Ext(s); ext != "" {
 138         return strings.TrimPrefix(ext, ".")
 139     }
 140 
 141     // trim major MIME types
 142     if i := strings.LastIndex(s, "/"); i >= 0 {
 143         s = s[i+1:]
 144     }
 145     // trim charset type, which is preceded by a semicolon in MIME types
 146     if i := strings.LastIndex(s, ";"); i >= 0 {
 147         s = s[:i]
 148     }
 149     return s
 150 }
     File: ./mediainfo/mp3.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 /*
  28 The MIT License (MIT)
  29 
  30 Copyright (c) 2026 pacman64
  31 
  32 Permission is hereby granted, free of charge, to any person obtaining a copy of
  33 this software and associated documentation files (the "Software"), to deal
  34 in the Software without restriction, including without limitation the rights to
  35 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  36 of the Software, and to permit persons to whom the Software is furnished to do
  37 so, subject to the following conditions:
  38 
  39 The above copyright notice and this permission notice shall be included in all
  40 copies or substantial portions of the Software.
  41 
  42 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  43 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  44 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  45 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  46 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  47 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  48 SOFTWARE.
  49 */
  50 
  51 import (
  52     "errors"
  53     "io"
  54     "math"
  55 )
  56 
  57 // http://www.mp3-tech.org/programmer/frame_header.html
  58 
  59 /*
  60 aaaaaaaaaaa bb cc  d eeee ff  g  h ii jj k l mm
  61 11111111111 00 00  0 0000 00  0  0 00 00 0 0 00     frame-sync mask
  62 00000000000 11 00  0 0000 00  0  0 00 00 0 0 00     audio version mask
  63 00000000000 00 11  0 0000 00  0  0 00 00 0 0 00     audio layer mask
  64 00000000000 00 00  1 0000 00  0  0 00 00 0 0 00     CRC check mask
  65 00000000000 00 00  0 1111 00  0  0 00 00 0 0 00     bit-rate index mask
  66 00000000000 00 00  0 0000 11  0  0 00 00 0 0 00     sample-rate index mask
  67 00000000000 00 00  0 0000 00  1  0 00 00 0 0 00     frame-padding check mask
  68 
  69 aaaaaaaa aaabbccd eeeeffgh iijjklmm
  70 */
  71 
  72 // these errors are for the rarely-used reserved options in frame headers
  73 var (
  74     ErrMP3ReservedLayer   = errors.New("MP3 data use reserved format layer")
  75     ErrMP3ReservedVersion = errors.New("MP3 data use reserved format versions")
  76 )
  77 
  78 var mp3BitRates = []int{
  79     0, 0, 0, 0, 0, 0, // free bit-rates
  80     32000, 32000, 32000, 32000, 8000, 8000,
  81     64000, 48000, 40000, 48000, 16000, 16000,
  82     96000, 56000, 48000, 56000, 24000, 24000,
  83     128000, 64000, 56000, 64000, 32000, 32000,
  84     160000, 80000, 64000, 80000, 40000, 40000,
  85     192000, 96000, 80000, 96000, 48000, 48000,
  86     224000, 112000, 96000, 112000, 56000, 56000,
  87     256000, 128000, 112000, 128000, 64000, 64000,
  88     288000, 160000, 128000, 144000, 80000, 80000,
  89     320000, 192000, 160000, 160000, 96000, 96000,
  90     352000, 224000, 192000, 176000, 112000, 112000,
  91     384000, 256000, 224000, 192000, 128000, 128000,
  92     416000, 320000, 256000, 224000, 144000, 144000,
  93     448000, 384000, 320000, 256000, 160000, 160000,
  94     0, 0, 0, 0, 0, 0, // reserved (invalid) space for bit-rates
  95 }
  96 
  97 var mp3SampleRates = []int{
  98     44100, 22050, 11025,
  99     48000, 24000, 12000,
 100     32000, 16000, 8000,
 101     0, 0, 0,
 102 }
 103 
 104 // https://stackoverflow.com/questions/6220660/calculating-the-length-of-mp3-frames-in-milliseconds
 105 var mp3SamplesPerFrame = []int{
 106     384, 1152, 1152, // MPEG 1
 107     384, 1152, 576, // MPEG 2
 108     384, 1152, 576, // MPEG 2.5
 109 }
 110 
 111 // mp3Duration tries to find the duration in seconds of the MP3 stream given
 112 func mp3Duration(r io.ReadSeeker) (seconds float64, err error) {
 113     // buffers larger than 32kb don't seem to speed things up further
 114     var b [32 * 1024]byte
 115 
 116     // how many leading bytes to skip/ignore from current chunk
 117     skip := 0
 118 
 119     for i := 0; true; i++ {
 120         n, err := r.Read(b[:])
 121         if n <= 0 {
 122             return seconds, nil
 123         }
 124         if err != nil && err != io.EOF {
 125             return seconds, err
 126         }
 127 
 128         // only check for ID3v2 metadata on the first chunk read
 129         if i == 0 && n >= 10 {
 130             skip = calcSizeID3v2(b[:n])
 131         }
 132 
 133         // done, if there aren't enough data for a frame intro
 134         if n < 3 {
 135             return seconds, nil
 136         }
 137 
 138         // check whether whole chunk needs skipping, as unlikely as that is
 139         if n < skip {
 140             skip -= n
 141             continue
 142         }
 143 
 144         dt, skipnext, err := mp3SliceDuration(b[skip:n])
 145         if err != nil {
 146             return seconds, err
 147         }
 148 
 149         skip = skipnext
 150         seconds += dt
 151     }
 152 
 153     return seconds, nil
 154 }
 155 
 156 // mp3SliceDuration handles the slice logic for func mp3Duration
 157 func mp3SliceDuration(b []byte) (sec float64, skip int, err error) {
 158     // when there aren't enough data for a frame, the duration is 0 seconds
 159     if len(b) < 3 {
 160         return 0, 0, nil
 161     }
 162 
 163     // upper-limit for index is 2 less than length, since there's a 2-byte
 164     // look-ahead in loop
 165     for i := 0; i < len(b)-2; i++ {
 166         // frames start with their first 11 bits all on
 167         syn := b[i]
 168         if syn != 255 {
 169             // not all the first 8 bits are on: not a frame-sync
 170             continue
 171         }
 172         h1 := b[i+1]
 173         if h1 < 224 {
 174             // not all the 3 extra bits are on: not a frame-sync
 175             continue
 176         }
 177         h2 := b[i+2]
 178 
 179         // check the audio layer number using the 3rd-last and 2nd-last bits
 180         layer := 0
 181         switch h1 & 0b00000110 {
 182         case 0:
 183             // return t, ErrMP3ReservedLayer
 184             continue
 185         case 2:
 186             layer = 3
 187         case 4:
 188             layer = 2
 189         case 6:
 190             layer = 1
 191         }
 192 
 193         // check the MPEG version using the 5th-last and 4th-last bits:
 194         // version 3 means MPEG 2.5
 195         version := 0
 196         switch h1 & 0b00011000 {
 197         case 0: // MPEG 2.5
 198             version = 3
 199         case 8: // reserved (invalid) // 0b00001000
 200             // return t, ErrMP3ReservedVersion
 201             // ignore frames with a reserved-value version, instead of
 202             // giving an error
 203             continue
 204         case 16: // MPEG 2 // 0b00010000
 205             version = 2
 206         case 24: // MPEG 1 // 0b00011000
 207             version = 1
 208         }
 209 
 210         // check for frame padding using the 2nd-last bit
 211         padding := 0
 212         if h2&0b00000010 != 0 {
 213             padding = 1
 214         }
 215 
 216         bitRateRow := int(h2 >> 4)
 217         if bitRateRow == 0 || bitRateRow == 15 {
 218             continue
 219         }
 220 
 221         sampleRateRow := int(h2 & 0b00001100 >> 2)
 222         if sampleRateRow == 3 {
 223             continue
 224         }
 225 
 226         bitRate := 0
 227         switch version {
 228         case 1:
 229             bitRate = mp3BitRates[6*bitRateRow+layer-1]
 230         case 2, 3:
 231             bitRate = mp3BitRates[6*bitRateRow+2*layer-1]
 232         }
 233         sampleRate := mp3SampleRates[3*sampleRateRow+version-1]
 234 
 235         // update time duration value
 236         spf := mp3SamplesPerFrame[3*(version-1)+layer-1]
 237         sec += float64(spf) / float64(sampleRate)
 238 
 239         // calculate how many bytes to jump forward
 240         //
 241         // http://www.mp3-converter.com/mp3codec/frames.htm
 242         // the formula suggested there seems wrong
 243         //   frame_size = 144 * bit_rate / (sample_rate + padding)
 244         // and should instead be
 245         //   frame_size = floor(144 * bit_rate / sample_rate) + padding
 246         n := int(math.Floor(float64(144*bitRate)/float64(sampleRate))) + padding
 247 
 248         // handle skipping inside current slice
 249         if i+n < len(b)-3 {
 250             // jump ahead by n - 1 instead of n, since the loop already adds 1
 251             i += n - 1
 252             continue
 253         }
 254 
 255         // handle skipping beyond current slice
 256         skip = n - (len(b) - i)
 257         if skip < 0 {
 258             skip = 0
 259         }
 260         return sec, skip, nil
 261     }
 262 
 263     return sec, 0, nil
 264 }
     File: ./mediainfo/mp3_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "io"
  29     "math"
  30     "os"
  31     "testing"
  32 )
  33 
  34 func TestDurationMP3(t *testing.T) {
  35     fname := `testdata/test.mp3`
  36     f, err := os.Open(fname)
  37     if os.IsNotExist(err) {
  38         t.Skipf(`file %s not available: skipping test`, fname)
  39         return
  40     }
  41     if err != nil {
  42         t.Error(err.Error())
  43         return
  44     }
  45     defer f.Close()
  46 
  47     d, err := Duration(f, 0, ".mp3")
  48     if err != nil {
  49         t.Error(err.Error())
  50         return
  51     }
  52 
  53     const exp = 10.187755
  54     if math.Abs(d-exp) > 1e-6 {
  55         const fs = "expected duration of %f seconds, but got %f instead"
  56         t.Fatalf(fs, exp, d)
  57     }
  58 }
  59 
  60 // BenchmarkDurationMP3 mainly tests how the buffer-size used in func
  61 // mp3Duration affects performance
  62 func BenchmarkDurationMP3(b *testing.B) {
  63     fname := `testdata/test.mp3`
  64     f, err := os.Open(fname)
  65     if os.IsNotExist(err) {
  66         b.Skipf(`file %s not available: skipping benchmark`, fname)
  67         return
  68     }
  69     if err != nil {
  70         b.Error(err.Error())
  71         return
  72     }
  73     defer f.Close()
  74 
  75     b.Run("mp3-duration", func(b *testing.B) {
  76         f.Seek(0, io.SeekStart)
  77         b.ResetTimer()
  78         mp3Duration(f)
  79     })
  80 }
     File: ./mediainfo/mpeg4.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "bytes"
  29     "encoding/binary"
  30     "io"
  31     "math"
  32 )
  33 
  34 // For a general description of the mpeg4 container format see
  35 // http://www.cimarronsystems.com/wp-content/uploads/2017/04/Elements-of-the-H.264-VideoAAC-Audio-MP4-Movie-v2_0.pdf
  36 // especially pages 4 and 5 describing the "moov" chunk
  37 
  38 // Details of the 9-item matrix are in section "Matrices" (page 199) from the official Quicktime Format spec
  39 // https://developer.apple.com/standards/qtff-2001.pdf
  40 
  41 type mpeg4ChunkHeader struct {
  42     Size uint32
  43     Type [4]byte
  44 }
  45 
  46 type mpeg4MOOVChunkInfo struct {
  47     // what follows is the info section for the first track
  48     mpeg4ChunkHeader
  49 
  50     Version byte
  51     Flags   [3]byte
  52 
  53     CreationTime     uint32 // seconds since the start of 1904
  54     ModificationTime uint32 // seconds since the start of 1904
  55     TimeScale        uint32 // number of time units per seconds
  56     Duration         uint32 // total play length in time units
  57 
  58     PreferredRate   uint32
  59     PreferredVolume uint16
  60     Reserved        [10]byte // should all be 0s
  61 
  62     // more fields which I don't care about
  63 }
  64 
  65 // this one comes right after a chunk header of type "trak"
  66 type mpeg4TrackHeaderInfo struct {
  67     mpeg4ChunkHeader // type is "tkhd"
  68 
  69     Version byte
  70     Flags   [3]byte
  71 
  72     CreationTime     uint32
  73     ModificationTime uint32
  74     TrackID          uint32
  75     Reserved         uint32
  76     Duration         uint32
  77 
  78     ReservedZeros    [8]byte // should all be 0s
  79     Layer            uint16
  80     AlternativeGroup uint16
  81     Matrix           [9]uint32
  82 
  83     TrackWidth  uint32 // divide by 65,536 for video width
  84     TrackHeight uint32 // divide by 65,536 for video height
  85 }
  86 
  87 func mpeg4Duration(r io.ReadSeeker) (float64, error) {
  88     var h mpeg4ChunkHeader
  89     for {
  90         err := binary.Read(r, binary.BigEndian, &h)
  91         if err == io.EOF {
  92             return math.NaN(), nil
  93         }
  94         if err != nil {
  95             return math.NaN(), err
  96         }
  97 
  98         if h.Type[0] == 'm' && h.Type[1] == 'o' && h.Type[2] == 'o' && h.Type[3] == 'v' {
  99             var info mpeg4MOOVChunkInfo
 100             err := binary.Read(r, binary.BigEndian, &info)
 101             return float64(info.Duration) / float64(info.TimeScale), err
 102         }
 103         r.Seek(int64(h.Size)-8, io.SeekCurrent)
 104     }
 105 }
 106 
 107 func mpeg4Resolution(r io.ReadSeeker) (int, int, int, error) {
 108     buf := make([]byte, 2048)
 109     n, err := r.Read(buf)
 110     if err != nil {
 111         return -1, -1, -1, err
 112     }
 113 
 114     buf = buf[:n]
 115     i := bytes.Index(buf, []byte{'t', 'r', 'a', 'k'})
 116     if i < 0 {
 117         return -1, -1, -1, nil
 118     }
 119 
 120     i += 8 // skip over the "trak" chunk header
 121     r = bytes.NewReader(buf[i:])
 122     var info mpeg4TrackHeaderInfo
 123     err = binary.Read(r, binary.BigEndian, &info)
 124     return int(info.TrackWidth / 65_536), int(info.TrackHeight / 65_536), -1, err
 125 }
     File: ./mediainfo/png.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "errors"
  30     "io"
  31 )
  32 
  33 var (
  34     errInvalidPNGSignature = errors.New("invalid PNG signature")
  35     errInvalidPNGColorType = errors.New("invalid PNG color type")
  36 )
  37 
  38 type pngHeader struct {
  39     Signature uint64
  40     Image     struct {
  41         ChunkLength       uint32
  42         ChunkType         [4]byte
  43         Width             int32
  44         Height            int32
  45         BitDepth          byte
  46         ColorType         byte
  47         CompressionMethod byte
  48         FilterMethod      byte
  49         InterlaceMethod   byte
  50     }
  51 }
  52 
  53 func pngResolution(r io.Reader) (int, int, int, error) {
  54     var header pngHeader
  55     err := binary.Read(r, binary.BigEndian, &header)
  56     if err != nil {
  57         return 0, 0, 0, err
  58     }
  59     if header.Signature != 9894494448401390090 {
  60         return 0, 0, 0, errInvalidPNGSignature
  61     }
  62 
  63     var bpp int
  64     switch header.Image.ColorType {
  65     case 0: // grayscale
  66         bpp = int(1 * header.Image.BitDepth)
  67     case 2: // truecolor
  68         bpp = int(3 * header.Image.BitDepth)
  69     case 3: // indexed
  70         bpp = int(1 * header.Image.BitDepth)
  71     case 4: // grayscale + alpha
  72         bpp = int(2 * header.Image.BitDepth)
  73     case 6: // truecolor + alpha
  74         bpp = int(4 * header.Image.BitDepth)
  75     default:
  76         return 0, 0, 0, errInvalidPNGColorType
  77     }
  78     return int(header.Image.Width), int(header.Image.Height), bpp, nil
  79 }
     File: ./mediainfo/psd.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "errors"
  30     "io"
  31 )
  32 
  33 // https://docs.fileformat.com/image/psd/
  34 
  35 type psdHeader struct {
  36     Signature   [4]byte // "8BPS"
  37     Version     uint16  // always 1
  38     Reserved    [6]byte // all zero bits
  39     NumChannels uint16  // range allowed is 1..56
  40     Height      int32   // range allowed is 1..30000
  41     Width       int32   // range allowed is 1..30000
  42     Depth       uint16  // bits per channel
  43     ColorMode   uint16
  44 }
  45 
  46 var errUnsupportedPSDFormat = errors.New("data doesn't start with PSD file signature")
  47 
  48 func psdResolution(r io.Reader) (int, int, int, error) {
  49     var h psdHeader
  50     err := binary.Read(r, binary.BigEndian, &h)
  51     if err != nil {
  52         return 0, 0, 0, err
  53     }
  54     s := h.Signature
  55     if s[0] != '8' || s[1] != 'B' || s[2] != 'P' || s[3] != 'S' {
  56         return 0, 0, 0, errUnsupportedPSDFormat
  57     }
  58     return int(h.Width), int(h.Height), int(h.NumChannels * h.Depth), nil
  59 }
     File: ./mediainfo/read.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 func bytes2uint(s []byte) uint {
  32     total := uint(0)
  33     for _, b := range s {
  34         total <<= 8
  35         total += uint(b)
  36     }
  37     return total
  38 }
  39 
  40 // https://www.onicos.com/staff/iz/formats/ieee.c
  41 
  42 func float80(s []byte) float64 {
  43     f := 0.0
  44     expon := (int(s[0]&0x7F) << 8) | int(s[1]&0xFF)
  45     hiMant := (int(s[2]&0xFF) << 24) | (int(s[3]&0xFF) << 16) | (int(s[4]&0xFF) << 8) | (int(s[5] & 0xFF))
  46     loMant := (int(s[6]&0xFF) << 24) | (int(s[7]&0xFF) << 16) | (int(s[8]&0xFF) << 8) | (int(s[9] & 0xFF))
  47     sign := 1.0
  48     if s[0]&0x80 != 0 {
  49         sign = -1.0
  50     }
  51 
  52     if expon == 0 && hiMant == 0 && loMant == 0 {
  53         // floating-point can have value -0
  54         return sign * 0
  55     }
  56 
  57     // detect Infinity or NaN
  58     if expon == 0x7FFF {
  59         f = math.Inf(1)
  60     } else {
  61         expon -= 16383
  62         f = math.Ldexp(float64(hiMant), expon-31) + math.Ldexp(float64(loMant), expon-31-32)
  63     }
  64 
  65     return sign * f
  66 }
  67 
  68 func match4(buf []byte, a, b, c, d byte) bool {
  69     return len(buf) >= 4 && buf[0] == a && buf[1] == b && buf[2] == c && buf[3] == d
  70 }
     File: ./mediainfo/svg.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "bytes"
  29     "io"
  30     "strconv"
  31 )
  32 
  33 func svgResolution(r io.Reader) (int, int, int, error) {
  34     var data [1024]byte
  35     n, err := r.Read(data[:])
  36     if err != nil {
  37         return -1, -1, -1, err
  38     }
  39 
  40     w := -1
  41     h := -1
  42     buf := data[:n]
  43 
  44     if i := bytes.Index(buf, []byte("width=\"")); i >= 0 {
  45         sub := buf[i+len("width=\""):]
  46         if i = bytes.IndexRune(sub, '"'); i >= 0 {
  47             if n, err := strconv.Atoi(string(sub[:i])); err == nil {
  48                 w = n
  49             }
  50         }
  51     }
  52 
  53     if i := bytes.Index(buf, []byte("height=\"")); i >= 0 {
  54         sub := buf[i+len("height=\""):]
  55         if i = bytes.IndexRune(sub, '"'); i >= 0 {
  56             if n, err := strconv.Atoi(string(sub[:i])); err == nil {
  57                 h = n
  58             }
  59         }
  60     }
  61 
  62     return w, h, -1, nil
  63 }
     File: ./mediainfo/tiff.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "io"
  29 )
  30 
  31 func tiffResolution(r io.Reader) (int, int, int, error) {
  32     return jpegResolution(r)
  33 }
     File: ./mediainfo/wav.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "errors"
  30     "io"
  31     "math"
  32     "os"
  33 )
  34 
  35 var errFileNeeded = errors.New(
  36     "a file was needed, since declared audio-data size exceeds 4GB",
  37 )
  38 
  39 type riffHeader struct {
  40     Format [4]byte
  41     Size   uint32
  42     Type   [4]byte // "WAVE"
  43     Chunk  [4]byte // "fmt "
  44 }
  45 
  46 // http://www.topherlee.com/software/pcm-tut-wavformat.html
  47 type riffWave32Info struct {
  48     FormatLength   uint32
  49     Format         uint16
  50     Channels       uint16
  51     SampleRate     uint32
  52     BytesPerSecond uint32
  53 
  54     HardToName    uint16
  55     BitsPerSample uint16
  56 
  57     Data       [4]byte // "data"
  58     DataLength uint32
  59 }
  60 
  61 // https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf pages 8 and 9
  62 // type rf64WaveInfo struct {
  63 //  Neg1 uint32  // the 32-bite size field should be -1 in rf64 format
  64 //  Wave [4]byte // "WAVE"
  65 //  DS64 [4]byte // "ds64"
  66 
  67 //  _size       uint32
  68 //  RIFFSize    uint64
  69 //  DataSize    uint64
  70 //  SampleCount uint64
  71 //  TableLength uint32
  72 //  Table       uint32
  73 
  74 //  // variable-length area should follow here
  75 
  76 //  // riffWave32Info
  77 // }
  78 
  79 type waveFormat int
  80 
  81 const (
  82     riffWaveFormat    = waveFormat(1)
  83     rf64WaveFormat    = waveFormat(2)
  84     unknownWaveFormat = waveFormat(3)
  85 )
  86 
  87 func detectWaveType(h riffHeader) waveFormat {
  88     t := h.Type
  89     if t[0] != 'W' || t[1] != 'A' || t[2] != 'V' || t[3] != 'E' {
  90         return unknownWaveFormat
  91     }
  92 
  93     c := h.Chunk
  94     if c[0] != 'f' || c[1] != 'm' || c[2] != 't' || c[3] != ' ' {
  95         return unknownWaveFormat
  96     }
  97 
  98     f := h.Format
  99     if f[0] == 'R' && f[1] == 'I' && f[2] == 'F' && f[3] == 'F' {
 100         return riffWaveFormat
 101     }
 102     if f[0] == 'B' && f[1] == 'W' && f[2] == '6' && f[3] == '4' {
 103         // return rf64WaveFormat
 104         return riffWaveFormat
 105     }
 106     return unknownWaveFormat
 107 }
 108 
 109 var errInvalidWave = errors.New("invalid wave audio format")
 110 
 111 func waveDuration(r io.Reader) (seconds float64, err error) {
 112     var h riffHeader
 113     err = binary.Read(r, binary.LittleEndian, &h)
 114     if err != nil {
 115         return math.NaN(), err
 116     }
 117 
 118     switch detectWaveType(h) {
 119     case riffWaveFormat:
 120         return riffWave32Duration(r, h.Size)
 121     case rf64WaveFormat:
 122         return rf64WaveDuration(r)
 123     default:
 124         return math.NaN(), errInvalidWave
 125     }
 126 }
 127 
 128 func riffWave32Duration(r io.Reader, size uint32) (seconds float64, err error) {
 129     if size != ^uint32(0) {
 130         return _riffWave32Duration(r, uint64(size))
 131     }
 132 
 133     // handle case where size is >= 4GB, using a file
 134     f, ok := r.(*os.File)
 135     if !ok {
 136         return math.NaN(), errFileNeeded
 137     }
 138     st, err := f.Stat()
 139     if err != nil {
 140         return math.NaN(), err
 141     }
 142     return _riffWave32Duration(r, uint64(st.Size()))
 143 }
 144 
 145 func _riffWave32Duration(r io.Reader, size uint64) (seconds float64, err error) {
 146     var h riffWave32Info
 147     err = binary.Read(r, binary.LittleEndian, &h)
 148     if err != nil {
 149         return math.NaN(), err
 150     }
 151     dlen := uint64(h.DataLength)
 152     n := math.Max(float64(size-dlen), float64(h.DataLength))
 153     seconds = n / float64(h.BytesPerSecond)
 154     return seconds, nil
 155 }
 156 
 157 func rf64WaveDuration(r io.Reader) (seconds float64, err error) {
 158     _ = r
 159     return math.NaN(), errInvalidWave
 160 }
     File: ./mediainfo/webm.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "bytes"
  29     "io"
  30     "math"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 // youtube, which is the dominant source/standard for webm files, gives string-format durations
  36 func webmDuration(r io.Reader) (seconds float64, err error) {
  37     buf := make([]byte, 2048) // 2 kb is more than enough to get all DURATIOND fields
  38     n, err := r.Read(buf)
  39     if err != io.EOF && err != nil {
  40         return math.NaN(), err
  41     }
  42 
  43     sec := 0.0
  44     buf = buf[:n]
  45     // since there are 2 DURATIOND fields near the start of youtube webm files,
  46     // keep the highest of these to get a more accurate result
  47     for {
  48         i := bytes.Index(buf, []byte{'D', 'U', 'R', 'A', 'T', 'I', 'O', 'N', 'D'})
  49         if i < 0 {
  50             break
  51         }
  52 
  53         start := i + len("DURATIOND")
  54         buf = buf[start:]
  55         dur := parseWEBMDurationJunk(buf)
  56         sec = math.Max(sec, dur)
  57     }
  58 
  59     if sec == 0 && n > 0 {
  60         return math.NaN(), nil
  61     }
  62     return sec, nil
  63 }
  64 
  65 func parseWEBMDurationJunk(data []byte) (seconds float64) {
  66     // get starting index of duration string
  67     start := 0
  68     for i, b := range data {
  69         if '0' <= b && b <= '9' {
  70             start = i
  71             break
  72         }
  73     }
  74 
  75     numpieces := 0
  76     dotsAvailable := 1
  77     // get limit index of duration string
  78     stop := 0
  79     for i, b := range data[start:] {
  80         if b == ':' {
  81             numpieces++
  82             continue
  83         }
  84         if '0' <= b && b <= '9' {
  85             continue
  86         }
  87         if b == '.' && dotsAvailable > 0 {
  88             dotsAvailable--
  89             continue
  90         }
  91         stop = start + i
  92         break
  93     }
  94 
  95     // don't even bother splitting if there aren't any `:`s to separate time fields
  96     if numpieces == 0 {
  97         return math.NaN()
  98     }
  99 
 100     sec := 0.0
 101     value := 1.0
 102     pieces := strings.Split(string(data[start:stop]), ":")
 103     // no need to worry about fields beyond hours, since youtube limits duration to 12 hours
 104     // https://support.google.com/youtube/answer/71673?co=GENIE.Platform%3DDesktop&hl=en
 105     for i := len(pieces) - 1; i >= 0; i-- {
 106         f, err := strconv.ParseFloat(pieces[i], 64)
 107         if err != nil {
 108             return math.NaN()
 109         }
 110         sec += value * f
 111         value *= 60
 112     }
 113     return sec
 114 }
     File: ./mediainfo/webp.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mediainfo
  26 
  27 import (
  28     "encoding/binary"
  29     "errors"
  30     "io"
  31 )
  32 
  33 // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
  34 type webpHeader struct {
  35     Signature     [4]byte // "RIFF"
  36     BlockLength   uint32
  37     ContainerName [4]byte // "WEBP"
  38     ChunkTag      [4]byte // "VP8L", or "VP8X", or "VP8 "
  39     Extra         [8]byte // dunno what to call this field
  40     Data          [20]byte
  41 
  42     // Info          [4]byte // StreamLength  uint32
  43     // HeaderEnd     byte    // 0x2f
  44 }
  45 
  46 var errInvalidWEBPFormat = errors.New("invalid WEBP format")
  47 
  48 func webpResolution(r io.Reader) (int, int, int, error) {
  49     var header webpHeader
  50     err := binary.Read(r, binary.LittleEndian, &header)
  51     if err != nil {
  52         return 0, 0, 0, err
  53     }
  54     if !webpHeaderIsValid(header) {
  55         return 0, 0, 0, errInvalidWEBPFormat
  56     }
  57 
  58     // https://github.com/golang/image/blob/master/webp/decode.go
  59     switch header.ChunkTag[3] {
  60     case 'L':
  61         return -1, -1, -1, ErrUnsupportedFormat
  62 
  63     case 'X':
  64         b := header.Data
  65         width := int(uint32(b[0])|uint32(b[1])<<8|uint32(b[2])<<16) + 1
  66         height := int(uint32(b[3])|uint32(b[4])<<8|uint32(b[5])<<16) + 1
  67         bpp := -1
  68         return width, height, bpp, nil
  69 
  70     case ' ':
  71         return -1, -1, -1, ErrUnsupportedFormat
  72 
  73     default:
  74         // webpHeaderIsValid should have prevented reaching this point
  75         return -1, -1, -1, errInvalidWEBPFormat
  76     }
  77 }
  78 
  79 func webpHeaderIsValid(h webpHeader) bool {
  80     s := h.Signature
  81     if s[0] != 'R' || s[1] != 'I' || s[2] != 'F' || s[3] != 'F' {
  82         return false
  83     }
  84     n := h.ContainerName
  85     if n[0] != 'W' || n[1] != 'E' || n[2] != 'B' || n[3] != 'P' {
  86         return false
  87     }
  88     t := h.ChunkTag
  89     if t[0] != 'V' || t[1] != 'P' || t[2] != '8' || (t[3] != 'L' && t[3] != 'X' && t[3] != ' ') {
  90         return false
  91     }
  92     return true
  93 }
     File: ./mimetype/mimetype.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mimetype
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "io/fs"
  32     "os"
  33     "path/filepath"
  34 
  35     "../filetypes"
  36 )
  37 
  38 const info = `
  39 mimetype [options...] [filenames...]
  40 
  41 Try to auto-detect MIME-TYPEs for all the files given. If no files are given,
  42 the standard input is used by default.
  43 
  44 The options are, available both in single and double-dash versions
  45 
  46     -h, -help              show this help message
  47 `
  48 
  49 func Main() {
  50     liveLines := true
  51     args := os.Args[1:]
  52 
  53     for len(args) > 0 {
  54         switch args[0] {
  55         case `-b`, `--b`, `-buffered`, `--buffered`:
  56             liveLines = false
  57             args = args[1:]
  58             continue
  59 
  60         case `-h`, `--h`, `-help`, `--help`:
  61             os.Stdout.WriteString(info[1:])
  62             return
  63         }
  64 
  65         break
  66     }
  67 
  68     if len(args) > 0 && args[0] == `--` {
  69         args = args[1:]
  70     }
  71 
  72     if liveLines {
  73         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  74             liveLines = false
  75         }
  76     }
  77 
  78     paths, ok := findAllFiles(args)
  79     if !ok {
  80         os.Exit(1)
  81         return
  82     }
  83 
  84     if err := run(paths, liveLines); err != nil && err != io.EOF {
  85         showError(err)
  86         os.Exit(1)
  87         return
  88     }
  89 }
  90 
  91 func showError(err error) {
  92     os.Stderr.WriteString(`mimetype: `)
  93     os.Stderr.WriteString(err.Error())
  94     os.Stderr.WriteString("\n")
  95 }
  96 
  97 // findAllFiles can be given a mix of file/folder paths, finding all files
  98 // recursively in folders, avoiding duplicates
  99 func findAllFiles(paths []string) (files []string, success bool) {
 100     walk := filepath.WalkDir
 101     got := make(map[string]struct{})
 102     success = true
 103 
 104     for _, path := range paths {
 105         if _, ok := got[path]; ok {
 106             continue
 107         }
 108         got[path] = struct{}{}
 109 
 110         // a dash means standard input
 111         if path == `-` {
 112             files = append(files, path)
 113             continue
 114         }
 115 
 116         info, err := os.Stat(path)
 117         if os.IsNotExist(err) {
 118             // on windows, file-not-found messages may mention `CreateFile`,
 119             // even when trying to open files in read-only mode
 120             err = errors.New(`can't find file/folder named ` + path)
 121         }
 122 
 123         if err != nil {
 124             showError(err)
 125             success = false
 126             continue
 127         }
 128 
 129         if !info.IsDir() {
 130             files = append(files, path)
 131             continue
 132         }
 133 
 134         err = walk(path, func(path string, info fs.DirEntry, err error) error {
 135             path, err = filepath.Abs(path)
 136             if err != nil {
 137                 showError(err)
 138                 success = false
 139                 return err
 140             }
 141 
 142             if _, ok := got[path]; ok {
 143                 if info.IsDir() {
 144                     return fs.SkipDir
 145                 }
 146                 return nil
 147             }
 148             got[path] = struct{}{}
 149 
 150             if err != nil {
 151                 showError(err)
 152                 success = false
 153                 return err
 154             }
 155 
 156             if info.IsDir() {
 157                 return nil
 158             }
 159 
 160             files = append(files, path)
 161             return nil
 162         })
 163 
 164         if err != nil {
 165             showError(err)
 166             success = false
 167         }
 168     }
 169 
 170     return files, success
 171 }
 172 
 173 func run(paths []string, liveLines bool) error {
 174     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 175     defer bw.Flush()
 176 
 177     if _, err := bw.WriteString("name\tmime\n"); err != nil {
 178         return io.EOF
 179     }
 180     if liveLines && bw.Flush() != nil {
 181         return io.EOF
 182     }
 183 
 184     for _, path := range paths {
 185         if err := handleFile(bw, path, liveLines); err != nil {
 186             return err
 187         }
 188     }
 189     if len(paths) == 0 {
 190         return mimeType(bw, os.Stdin, `-`, liveLines)
 191     }
 192     return nil
 193 }
 194 
 195 func handleFile(w *bufio.Writer, path string, liveLines bool) error {
 196     f, err := os.Open(path)
 197     if err != nil {
 198         return err
 199     }
 200     defer f.Close()
 201     return mimeType(w, f, path, liveLines)
 202 }
 203 
 204 func mimeType(w *bufio.Writer, r io.Reader, path string, live bool) error {
 205     var buf [64]byte
 206     n, err := r.Read(buf[:])
 207     if n < 1 && err != nil {
 208         return err
 209     }
 210 
 211     if n == 0 {
 212         const pre = `can't autodetect MIME-type for empty file named `
 213         return errors.New(pre + path)
 214     }
 215 
 216     mime, ok := filetypes.DetectMIME(buf[:n])
 217     if !ok {
 218         const pre = `can't autodetect MIME-type for file named `
 219         return errors.New(pre + path)
 220     }
 221 
 222     w.WriteString(path)
 223     w.WriteByte('\t')
 224     w.WriteString(mime)
 225     if w.WriteByte('\n') != nil {
 226         return io.EOF
 227     }
 228 
 229     if live && w.Flush() != nil {
 230         return io.EOF
 231     }
 232     return nil
 233 }
     File: ./mit-license.txt
   1 The MIT License (MIT)
   2 
   3 Copyright (c) 2026 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: ./n/n.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package n
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strconv"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 n [options...] [start...] [filenames...]
  38 
  39 Number lines starting from the (optional) line-count given, or starting to
  40 count from 1 by default. Line counts and line contents are separated by a
  41 tab.
  42 
  43 The options are, available both in single and double-dash versions
  44 
  45     -h, -help              show this help message
  46 `
  47 
  48 type config struct {
  49     n         int
  50     liveLines bool
  51 }
  52 
  53 func Main() {
  54     var cfg config
  55     cfg.n = 1
  56     cfg.liveLines = true
  57     args := os.Args[1:]
  58 
  59     for len(args) > 0 {
  60         switch args[0] {
  61         case `-b`, `--b`, `-buffered`, `--buffered`:
  62             cfg.liveLines = false
  63             args = args[1:]
  64             continue
  65 
  66         case `-h`, `--h`, `-help`, `--help`:
  67             os.Stdout.WriteString(info[1:])
  68             return
  69         }
  70 
  71         break
  72     }
  73 
  74     if len(args) > 0 {
  75         s := strings.Replace(args[0], `_`, ``, -1)
  76         if v, err := strconv.ParseInt(s, 10, 64); err == nil {
  77             cfg.n = int(v)
  78             args = args[1:]
  79         }
  80     }
  81 
  82     if len(args) > 0 && args[0] == `--` {
  83         args = args[1:]
  84     }
  85 
  86     if cfg.liveLines {
  87         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  88             cfg.liveLines = false
  89         }
  90     }
  91 
  92     if err := run(args, &cfg); err != nil && err != io.EOF {
  93         os.Stderr.WriteString(`n: `)
  94         os.Stderr.WriteString(err.Error())
  95         os.Stderr.WriteString("\n")
  96         os.Exit(1)
  97         return
  98     }
  99 }
 100 
 101 func run(paths []string, cfg *config) error {
 102     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 103     defer bw.Flush()
 104 
 105     for _, p := range paths {
 106         if err := handleFile(bw, p, cfg); err != nil {
 107             return err
 108         }
 109     }
 110 
 111     if len(paths) == 0 {
 112         return handleReader(bw, os.Stdin, cfg)
 113     }
 114 
 115     return nil
 116 }
 117 
 118 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 119     f, err := os.Open(path)
 120     if err != nil {
 121         // on windows, file-not-found error messages may mention `CreateFile`,
 122         // even when trying to open files in read-only mode
 123         return errors.New(`can't open file named ` + path)
 124     }
 125     defer f.Close()
 126     return handleReader(w, f, cfg)
 127 }
 128 
 129 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 130     var buf [24]byte
 131     const gb = 1024 * 1024 * 1024
 132     sc := bufio.NewScanner(r)
 133     sc.Buffer(nil, 8*gb)
 134 
 135     for i := 0; sc.Scan(); i++ {
 136         s := sc.Text()
 137         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 138             s = s[3:]
 139         }
 140 
 141         w.Write(strconv.AppendInt(buf[:0], int64(cfg.n), 10))
 142         w.WriteByte('\t')
 143         w.WriteString(s)
 144         cfg.n++
 145 
 146         if w.WriteByte('\n') != nil {
 147             return io.EOF
 148         }
 149 
 150         if cfg.liveLines && w.Flush() != nil {
 151             return io.EOF
 152         }
 153     }
 154 
 155     return sc.Err()
 156 }
     File: ./ncol/ncol.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ncol
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strconv"
  33     "strings"
  34     "unicode/utf8"
  35 )
  36 
  37 const info = `
  38 ncol [options...] [filenames...]
  39 
  40 
  41 Nice COLumns realigns and styles data tables using ANSI color sequences. In
  42 particular, all auto-detected numbers are styled so they're easier to read
  43 at a glance. Input tables can be either lines of space-separated values or
  44 tab-separated values, and are auto-detected using the first non-empty line.
  45 
  46 When not given filepaths to read data from, this tool reads from standard
  47 input by default.
  48 
  49 The options are, available both in single and double-dash versions
  50 
  51     -h, -help              show this help message
  52     -m, -max-columns       use the row with the most items for the item-count
  53     -no-sums, -unsummed    avoid showing a final row with column sums
  54     -no-tiles, -untiled    avoid showing color-coded tiles
  55     -s, -simple            avoid showing color-coded tiles and sums
  56 `
  57 
  58 const columnGap = 2
  59 
  60 // altDigitStyle is used to make 4+ digit-runs easier to read
  61 const altDigitStyle = "\x1b[38;2;168;168;168m"
  62 
  63 func Main() {
  64     sums := true
  65     tiles := true
  66     maxCols := false
  67     args := os.Args[1:]
  68 
  69     for len(args) > 0 {
  70         switch args[0] {
  71         case `-h`, `--h`, `-help`, `--help`:
  72             os.Stdout.WriteString(info[1:])
  73             return
  74 
  75         case
  76             `-m`, `--m`,
  77             `-maxcols`, `--maxcols`,
  78             `-max-columns`, `--max-columns`:
  79             maxCols = true
  80             args = args[1:]
  81             continue
  82 
  83         case
  84             `-no-sums`, `--no-sums`, `-no-totals`, `--no-totals`,
  85             `-unsummed`, `--unsummed`, `-untotaled`, `--untotaled`,
  86             `-untotalled`, `--untotalled`:
  87             sums = false
  88             args = args[1:]
  89             continue
  90 
  91         case `-no-tiles`, `--no-tiles`, `-untiled`, `--untiled`:
  92             tiles = false
  93             args = args[1:]
  94             continue
  95 
  96         case `-s`, `--s`, `-simple`, `--simple`:
  97             sums = false
  98             tiles = false
  99             args = args[1:]
 100             continue
 101         }
 102 
 103         break
 104     }
 105 
 106     if len(args) > 0 && args[0] == `--` {
 107         args = args[1:]
 108     }
 109 
 110     var res table
 111     res.MaxColumns = maxCols
 112     res.ShowTiles = tiles
 113     res.ShowSums = sums
 114 
 115     if err := run(args, &res); err != nil {
 116         os.Stderr.WriteString(`ncol: `)
 117         os.Stderr.WriteString(err.Error())
 118         os.Stderr.WriteString("\n")
 119         os.Exit(1)
 120         return
 121     }
 122 }
 123 
 124 // table has all summary info gathered from the data, along with the row
 125 // themselves, stored as lines/strings
 126 type table struct {
 127     Columns int
 128 
 129     Rows []string
 130 
 131     MaxWidth []int
 132 
 133     MaxDotDecimals []int
 134 
 135     Numeric []int
 136 
 137     Sums []float64
 138 
 139     LoopItems func(line string, items int, t *table, f itemFunc) int
 140 
 141     sb strings.Builder
 142 
 143     MaxColumns bool
 144 
 145     ShowTiles bool
 146 
 147     ShowSums bool
 148 }
 149 
 150 type itemFunc func(i int, s string, t *table)
 151 
 152 func run(paths []string, res *table) error {
 153     for _, p := range paths {
 154         if err := handleFile(res, p); err != nil {
 155             return err
 156         }
 157     }
 158 
 159     if len(paths) == 0 {
 160         if err := handleReader(res, os.Stdin); err != nil {
 161             return err
 162         }
 163     }
 164 
 165     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 166     defer bw.Flush()
 167     realign(bw, res)
 168     return nil
 169 }
 170 
 171 func handleFile(res *table, path string) error {
 172     f, err := os.Open(path)
 173     if err != nil {
 174         // on windows, file-not-found error messages may mention `CreateFile`,
 175         // even when trying to open files in read-only mode
 176         return errors.New(`can't open file named ` + path)
 177     }
 178     defer f.Close()
 179     return handleReader(res, f)
 180 }
 181 
 182 func handleReader(t *table, r io.Reader) error {
 183     const gb = 1024 * 1024 * 1024
 184     sc := bufio.NewScanner(r)
 185     sc.Buffer(nil, 8*gb)
 186 
 187     const maxInt = int(^uint(0) >> 1)
 188     maxCols := maxInt
 189 
 190     for i := 0; sc.Scan(); i++ {
 191         s := sc.Text()
 192         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 193             s = s[3:]
 194         }
 195 
 196         if len(s) == 0 {
 197             continue
 198         }
 199 
 200         t.Rows = append(t.Rows, s)
 201 
 202         if t.Columns == 0 {
 203             if t.LoopItems == nil {
 204                 if strings.IndexByte(s, '\t') >= 0 {
 205                     t.LoopItems = loopItemsTSV
 206                 } else {
 207                     t.LoopItems = loopItemsSSV
 208                 }
 209             }
 210 
 211             if !t.MaxColumns {
 212                 t.Columns = t.LoopItems(s, maxCols, t, doNothing)
 213                 maxCols = t.Columns
 214             }
 215         }
 216 
 217         t.LoopItems(s, maxCols, t, updateItem)
 218     }
 219 
 220     return sc.Err()
 221 }
 222 
 223 // doNothing is given to LoopItems to count items, while doing nothing else
 224 func doNothing(i int, s string, t *table) {}
 225 
 226 func updateItem(i int, s string, t *table) {
 227     // ensure column-info-slices have enough room
 228     if i >= len(t.MaxWidth) {
 229         // update column-count if in max-columns mode
 230         if t.MaxColumns {
 231             t.Columns = i + 1
 232         }
 233         t.MaxWidth = append(t.MaxWidth, 0)
 234         t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
 235         t.Numeric = append(t.Numeric, 0)
 236         t.Sums = append(t.Sums, 0)
 237     }
 238 
 239     // keep track of widest rune-counts for each column
 240     w := countWidth(s)
 241     if t.MaxWidth[i] < w {
 242         t.MaxWidth[i] = w
 243     }
 244 
 245     // update stats for numeric items
 246     if isNumeric(s, &(t.sb)) {
 247         dd := countDotDecimals(s)
 248         if t.MaxDotDecimals[i] < dd {
 249             t.MaxDotDecimals[i] = dd
 250         }
 251 
 252         t.Numeric[i]++
 253         f, _ := strconv.ParseFloat(t.sb.String(), 64)
 254         t.Sums[i] += f
 255     }
 256 }
 257 
 258 // loopItemsSSV loops over a line's items, allocation-free style; when given
 259 // empty strings, the callback func is never called
 260 func loopItemsSSV(s string, max int, t *table, f itemFunc) int {
 261     i := 0
 262     s = trimTrailingSpaces(s)
 263 
 264     for {
 265         s = trimLeadingSpaces(s)
 266         if len(s) == 0 {
 267             return i
 268         }
 269 
 270         if i+1 == max {
 271             f(i, s, t)
 272             return i + 1
 273         }
 274 
 275         j := strings.IndexByte(s, ' ')
 276         if j < 0 {
 277             f(i, s, t)
 278             return i + 1
 279         }
 280 
 281         f(i, s[:j], t)
 282         s = s[j+1:]
 283         i++
 284     }
 285 }
 286 
 287 func trimLeadingSpaces(s string) string {
 288     for len(s) > 0 && s[0] == ' ' {
 289         s = s[1:]
 290     }
 291     return s
 292 }
 293 
 294 func trimTrailingSpaces(s string) string {
 295     for len(s) > 0 && s[len(s)-1] == ' ' {
 296         s = s[:len(s)-1]
 297     }
 298     return s
 299 }
 300 
 301 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 302 // when given empty strings, the callback func is never called
 303 func loopItemsTSV(s string, max int, t *table, f itemFunc) int {
 304     if len(s) == 0 {
 305         return 0
 306     }
 307 
 308     i := 0
 309 
 310     for {
 311         if i+1 == max {
 312             f(i, s, t)
 313             return i + 1
 314         }
 315 
 316         j := strings.IndexByte(s, '\t')
 317         if j < 0 {
 318             f(i, s, t)
 319             return i + 1
 320         }
 321 
 322         f(i, s[:j], t)
 323         s = s[j+1:]
 324         i++
 325     }
 326 }
 327 
 328 func skipLeadingEscapeSequences(s string) string {
 329     for len(s) >= 2 {
 330         if s[0] != '\x1b' {
 331             return s
 332         }
 333 
 334         switch s[1] {
 335         case '[':
 336             s = skipSingleLeadingANSI(s[2:])
 337 
 338         case ']':
 339             if len(s) < 3 || s[2] != '8' {
 340                 return s
 341             }
 342             s = skipSingleLeadingOSC(s[3:])
 343 
 344         default:
 345             return s
 346         }
 347     }
 348 
 349     return s
 350 }
 351 
 352 func skipSingleLeadingANSI(s string) string {
 353     for len(s) > 0 {
 354         upper := s[0] &^ 32
 355         s = s[1:]
 356         if 'A' <= upper && upper <= 'Z' {
 357             break
 358         }
 359     }
 360 
 361     return s
 362 }
 363 
 364 func skipSingleLeadingOSC(s string) string {
 365     var prev byte
 366 
 367     for len(s) > 0 {
 368         b := s[0]
 369         s = s[1:]
 370         if prev == '\x1b' && b == '\\' {
 371             break
 372         }
 373         prev = b
 374     }
 375 
 376     return s
 377 }
 378 
 379 // isNumeric checks if a string is valid/useable as a number
 380 func isNumeric(s string, sb *strings.Builder) bool {
 381     if len(s) == 0 {
 382         return false
 383     }
 384 
 385     sb.Reset()
 386 
 387     s = skipLeadingEscapeSequences(s)
 388     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 389         sb.WriteByte(s[0])
 390         s = s[1:]
 391     }
 392 
 393     s = skipLeadingEscapeSequences(s)
 394     if len(s) == 0 {
 395         return false
 396     }
 397     if b := s[0]; b == '.' {
 398         sb.WriteByte(b)
 399         return isDigits(s[1:], sb)
 400     }
 401 
 402     digits := 0
 403 
 404     for {
 405         s = skipLeadingEscapeSequences(s)
 406         if len(s) == 0 {
 407             break
 408         }
 409 
 410         b := s[0]
 411         sb.WriteByte(b)
 412 
 413         if b == '.' {
 414             return isDigits(s[1:], sb)
 415         }
 416 
 417         if !('0' <= b && b <= '9') {
 418             return false
 419         }
 420 
 421         digits++
 422         s = s[1:]
 423     }
 424 
 425     s = skipLeadingEscapeSequences(s)
 426     return len(s) == 0 && digits > 0
 427 }
 428 
 429 func isDigits(s string, sb *strings.Builder) bool {
 430     if len(s) == 0 {
 431         return false
 432     }
 433 
 434     digits := 0
 435 
 436     for {
 437         s = skipLeadingEscapeSequences(s)
 438         if len(s) == 0 {
 439             break
 440         }
 441 
 442         if b := s[0]; '0' <= b && b <= '9' {
 443             sb.WriteByte(b)
 444             s = s[1:]
 445             digits++
 446         } else {
 447             return false
 448         }
 449     }
 450 
 451     s = skipLeadingEscapeSequences(s)
 452     return len(s) == 0 && digits > 0
 453 }
 454 
 455 // countDecimals counts decimal digits from the string given, assuming it
 456 // represents a valid/useable float64, when parsed
 457 func countDecimals(s string) int {
 458     dot := strings.IndexByte(s, '.')
 459     if dot < 0 {
 460         return 0
 461     }
 462 
 463     decs := 0
 464     s = s[dot+1:]
 465 
 466     for len(s) > 0 {
 467         s = skipLeadingEscapeSequences(s)
 468         if len(s) == 0 {
 469             break
 470         }
 471         if '0' <= s[0] && s[0] <= '9' {
 472             decs++
 473         }
 474         s = s[1:]
 475     }
 476 
 477     return decs
 478 }
 479 
 480 // countDotDecimals is like func countDecimals, but this one also includes
 481 // the dot, when any decimals are present, else the count stays at 0
 482 func countDotDecimals(s string) int {
 483     decs := countDecimals(s)
 484     if decs > 0 {
 485         return decs + 1
 486     }
 487     return decs
 488 }
 489 
 490 func countWidth(s string) int {
 491     width := 0
 492 
 493     for len(s) > 0 {
 494         i, j := indexEscapeSequence(s)
 495         if i < 0 {
 496             break
 497         }
 498         if j < 0 {
 499             j = len(s)
 500         }
 501 
 502         width += utf8.RuneCountInString(s[:i])
 503         s = s[j:]
 504     }
 505 
 506     // count trailing/all runes in strings which don't end with ANSI-sequences
 507     width += utf8.RuneCountInString(s)
 508     return width
 509 }
 510 
 511 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 512 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 513 // indices which can be independently negative when either the start/end of
 514 // a sequence isn't found; given their fairly-common use, even the hyperlink
 515 // ESC]8 sequences are supported
 516 func indexEscapeSequence(s string) (int, int) {
 517     var prev byte
 518 
 519     for i := range s {
 520         b := s[i]
 521 
 522         if prev == '\x1b' && b == '[' {
 523             j := indexLetter(s[i+1:])
 524             if j < 0 {
 525                 return i, -1
 526             }
 527             return i - 1, i + 1 + j + 1
 528         }
 529 
 530         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 531             j := indexPair(s[i+1:], '\x1b', '\\')
 532             if j < 0 {
 533                 return i, -1
 534             }
 535             return i - 1, i + 1 + j + 2
 536         }
 537 
 538         prev = b
 539     }
 540 
 541     return -1, -1
 542 }
 543 
 544 func indexLetter(s string) int {
 545     for i, b := range s {
 546         upper := b &^ 32
 547         if 'A' <= upper && upper <= 'Z' {
 548             return i
 549         }
 550     }
 551 
 552     return -1
 553 }
 554 
 555 func indexPair(s string, x byte, y byte) int {
 556     var prev byte
 557 
 558     for i := range s {
 559         b := s[i]
 560         if prev == x && b == y && i > 0 {
 561             return i
 562         }
 563         prev = b
 564     }
 565 
 566     return -1
 567 }
 568 
 569 func realign(w *bufio.Writer, t *table) {
 570     // make sums row first, as final alignments are usually affected by these
 571     var sums []string
 572     if t.ShowSums {
 573         sums = make([]string, 0, t.Columns)
 574 
 575         for i := 0; i < t.Columns; i++ {
 576             if t.Numeric[i] == 0 {
 577                 sums = append(sums, `-`)
 578                 if t.MaxWidth[i] < 1 {
 579                     t.MaxWidth[i] = 1
 580                 }
 581                 continue
 582             }
 583 
 584             decs := t.MaxDotDecimals[i]
 585             if decs > 0 {
 586                 decs--
 587             }
 588 
 589             var buf [64]byte
 590             s := strconv.AppendFloat(buf[:0], t.Sums[i], 'f', decs, 64)
 591             sums = append(sums, string(s))
 592             if t.MaxWidth[i] < len(s) {
 593                 t.MaxWidth[i] = len(s)
 594             }
 595         }
 596     }
 597 
 598     // due keeps track of how many spaces are due, when separating realigned
 599     // items from their immediate predecessor on the same row; this counter
 600     // is also used to right-pad numbers with decimals, as such items can be
 601     // padded with spaces from either side
 602     due := 0
 603 
 604     showItem := func(i int, s string, t *table) {
 605         if i > 0 {
 606             due += columnGap
 607         }
 608 
 609         if isNumeric(s, &(t.sb)) {
 610             dd := countDotDecimals(s)
 611             rpad := t.MaxDotDecimals[i] - dd
 612             width := countWidth(s)
 613             lpad := t.MaxWidth[i] - (width + rpad) + due
 614             writeSpaces(w, lpad)
 615             f, _ := strconv.ParseFloat(t.sb.String(), 64)
 616             writeNumericItem(w, s, numericStyle(f))
 617             due = rpad
 618             return
 619         }
 620 
 621         writeSpaces(w, due)
 622         w.WriteString(s)
 623         due = t.MaxWidth[i] - countWidth(s)
 624     }
 625 
 626     writeTile := func(i int, s string, t *table) {
 627         // make empty items stand out
 628         if len(s) == 0 {
 629             w.WriteString("\x1b[0m○")
 630             return
 631         }
 632 
 633         if isNumeric(s, &(t.sb)) {
 634             f, _ := strconv.ParseFloat(t.sb.String(), 64)
 635             w.WriteString(numericStyle(f))
 636             w.WriteString("■")
 637             return
 638         }
 639 
 640         // make padded items stand out: these items have spaces at either end
 641         if s[0] == ' ' || s[len(s)-1] == ' ' {
 642             w.WriteString("\x1b[38;2;196;160;0m■")
 643             return
 644         }
 645 
 646         w.WriteString("\x1b[38;2;128;128;128m■")
 647     }
 648 
 649     // show realigned rows
 650 
 651     for _, line := range t.Rows {
 652         due = 0
 653 
 654         if t.ShowTiles {
 655             end := t.LoopItems(line, t.Columns, t, writeTile)
 656             if end < len(t.MaxWidth)-1 {
 657                 w.WriteString("\x1b[0m")
 658             }
 659             // make rows with missing trailing items stand out
 660             for i := end; i < len(t.MaxWidth); i++ {
 661                 w.WriteString("×")
 662             }
 663             w.WriteString("\x1b[0m")
 664             due += columnGap
 665         }
 666 
 667         t.LoopItems(line, t.Columns, t, showItem)
 668         if w.WriteByte('\n') != nil {
 669             return
 670         }
 671     }
 672 
 673     if t.Columns > 0 && t.ShowSums {
 674         realignSums(w, t, sums)
 675     }
 676 }
 677 
 678 func realignSums(w *bufio.Writer, t *table, sums []string) {
 679     due := 0
 680     if t.ShowTiles {
 681         due += t.Columns + columnGap
 682     }
 683 
 684     for i, s := range sums {
 685         if i > 0 {
 686             due += columnGap
 687         }
 688 
 689         if t.Numeric[i] == 0 {
 690             writeSpaces(w, due)
 691             w.WriteString(s)
 692             due = t.MaxWidth[i] - countWidth(s)
 693             continue
 694         }
 695 
 696         lpad := t.MaxWidth[i] - len(s) + due
 697         writeSpaces(w, lpad)
 698         writeNumericItem(w, s, numericStyle(t.Sums[i]))
 699         due = 0
 700     }
 701 
 702     w.WriteByte('\n')
 703 }
 704 
 705 // writeSpaces does what it says, minimizing calls to write-like funcs
 706 func writeSpaces(w *bufio.Writer, n int) {
 707     const spaces = `                                `
 708     if n < 1 {
 709         return
 710     }
 711 
 712     for n >= len(spaces) {
 713         w.WriteString(spaces)
 714         n -= len(spaces)
 715     }
 716     w.WriteString(spaces[:n])
 717 }
 718 
 719 func writeRowTiles(w *bufio.Writer, s string, t *table, writeTile itemFunc) {
 720     end := t.LoopItems(s, t.Columns, t, writeTile)
 721 
 722     if end < len(t.MaxWidth)-1 {
 723         w.WriteString("\x1b[0m")
 724     }
 725     for i := end + 1; i < len(t.MaxWidth); i++ {
 726         w.WriteString("×")
 727     }
 728     w.WriteString("\x1b[0m")
 729 }
 730 
 731 func numericStyle(f float64) string {
 732     if f > 0 {
 733         if float64(int64(f)) == f {
 734             return "\x1b[38;2;0;135;0m"
 735         }
 736         return "\x1b[38;2;0;155;95m"
 737     }
 738     if f < 0 {
 739         if float64(int64(f)) == f {
 740             return "\x1b[38;2;204;0;0m"
 741         }
 742         return "\x1b[38;2;215;95;95m"
 743     }
 744     if f == 0 {
 745         return "\x1b[38;2;0;95;215m"
 746     }
 747     return "\x1b[38;2;128;128;128m"
 748 }
 749 
 750 func writeNumericItem(w *bufio.Writer, s string, startStyle string) {
 751     w.WriteString(startStyle)
 752     if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
 753         w.WriteByte(s[0])
 754         s = s[1:]
 755     }
 756 
 757     dot := strings.IndexByte(s, '.')
 758     if dot < 0 {
 759         restyleDigits(w, s, altDigitStyle)
 760         w.WriteString("\x1b[0m")
 761         return
 762     }
 763 
 764     if len(s[:dot]) > 3 {
 765         restyleDigits(w, s[:dot], altDigitStyle)
 766         w.WriteString("\x1b[0m")
 767         w.WriteString(startStyle)
 768         w.WriteByte('.')
 769     } else {
 770         w.WriteString(s[:dot])
 771         w.WriteByte('.')
 772     }
 773 
 774     rest := s[dot+1:]
 775     restyleDigits(w, rest, altDigitStyle)
 776     if len(rest) < 4 {
 777         w.WriteString("\x1b[0m")
 778     }
 779 }
 780 
 781 // restyleDigits renders a run of digits as alternating styled/unstyled runs
 782 // of 3 digits, which greatly improves readability, and is the only purpose
 783 // of this app; string is assumed to be all decimal digits
 784 func restyleDigits(w *bufio.Writer, digits string, altStyle string) {
 785     if len(digits) < 4 {
 786         // digit sequence is short, so emit it as is
 787         w.WriteString(digits)
 788         return
 789     }
 790 
 791     // separate leading 0..2 digits which don't align with the 3-digit groups
 792     i := len(digits) % 3
 793     // emit leading digits unstyled, if there are any
 794     w.WriteString(digits[:i])
 795     // the rest is guaranteed to have a length which is a multiple of 3
 796     digits = digits[i:]
 797 
 798     // start by styling, unless there were no leading digits
 799     style := i != 0
 800 
 801     for len(digits) > 0 {
 802         if style {
 803             w.WriteString(altStyle)
 804             w.WriteString(digits[:3])
 805             w.WriteString("\x1b[0m")
 806         } else {
 807             w.WriteString(digits[:3])
 808         }
 809 
 810         // advance to the next triple: the start of this func is supposed
 811         // to guarantee this step always works
 812         digits = digits[3:]
 813 
 814         // alternate between styled and unstyled 3-digit groups
 815         style = !style
 816     }
 817 }
     File: ./ncol/ncol_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ncol
  26 
  27 import "testing"
  28 
  29 func TestCountWidth(t *testing.T) {
  30     tests := map[string]struct {
  31         input    string
  32         expected int
  33     }{
  34         `empty`:         {``, 0},
  35         `empty ANSI`:    {"\x1b[38;5;0;0;0m\x1b[0m", 0},
  36         `simple plain`:  {`abc def`, 7},
  37         `unicode plain`: {`abc●def`, 7},
  38         `simple ANSI`:   {"abc \x1b[7mde\x1b[0mf", 7},
  39         `unicode ANSI`:  {"abc●\x1b[7mde\x1b[0mf", 7},
  40     }
  41 
  42     for name, tc := range tests {
  43         t.Run(name, func(t *testing.T) {
  44             got := countWidth(tc.input)
  45             if got != tc.expected {
  46                 const fs = "expected width %d, got %d instead"
  47                 t.Errorf(fs, tc.expected, got)
  48             }
  49         })
  50     }
  51 }
     File: ./ngron/data.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ngron
  26 
  27 // escapedStringBytes helps funcs like handleString and writeString treat all
  28 // string bytes quickly and correctly, using their officially-supported JSON
  29 // escape sequences
  30 //
  31 // https://www.rfc-editor.org/rfc/rfc8259#section-7
  32 var escapedStringBytes = [128][]byte{
  33     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
  34     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
  35     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
  36     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
  37     {'\\', 'b'}, {'\\', 't'},
  38     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
  39     {'\\', 'f'}, {'\\', 'r'},
  40     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
  41     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
  42     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
  43     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
  44     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
  45     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
  46     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
  47     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
  48     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
  49     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
  50     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
  51     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
  52     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
  53     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
  54     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
  55     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
  56     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
  57     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
  58     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
  59     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
  60     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
  61 }
     File: ./ngron/json2.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ngron
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "strconv"
  33 )
  34 
  35 // writeSpaces does what it says, minimizing calls to write-like funcs
  36 func writeSpaces(w *bufio.Writer, n int) {
  37     const spaces = `                                `
  38     if n < 1 {
  39         return
  40     }
  41 
  42     for n >= len(spaces) {
  43         w.WriteString(spaces)
  44         n -= len(spaces)
  45     }
  46     w.WriteString(spaces[:n])
  47 }
  48 
  49 // json2 does it all, given a reader and a writer
  50 func json2(w *bufio.Writer, data any) error {
  51     if err := writeValue(w, data, 0, 0); err != nil {
  52         return err
  53     }
  54     // don't forget ending the last line for the last value
  55     w.WriteByte('\n')
  56 
  57     return nil
  58 }
  59 
  60 // writeValue handles recursion for func json2
  61 func writeValue(w *bufio.Writer, data any, pre, level int) error {
  62     switch data := data.(type) {
  63     case nil:
  64         writeSpaces(w, 2*pre)
  65         w.WriteString(`null`)
  66         return nil
  67 
  68     case bool:
  69         writeSpaces(w, 2*pre)
  70         if data {
  71             w.WriteString(`true`)
  72         } else {
  73             w.WriteString(`false`)
  74         }
  75         return nil
  76 
  77     case int:
  78         writeSpaces(w, 2*pre)
  79         writeInt(w, data)
  80         return nil
  81 
  82     case float64:
  83         writeSpaces(w, 2*pre)
  84         writeFloat(w, data)
  85         return nil
  86 
  87     case json.Number:
  88         writeSpaces(w, 2*pre)
  89         w.WriteString(data.String())
  90         return nil
  91 
  92     case string:
  93         return writeString(w, data, pre)
  94 
  95     case []any:
  96         return writeArray(w, data, pre, level)
  97 
  98     case object:
  99         return writeObject(w, data, pre, level)
 100 
 101     default:
 102         // return fmt.Errorf(`unsupported type %T`, t)
 103         return errors.New(`unsupported value-type for JSON`)
 104     }
 105 }
 106 
 107 // writeArray handles arrays for func writeValue
 108 func writeArray(w *bufio.Writer, data []any, pre, level int) error {
 109     for i, v := range data {
 110         if i == 0 {
 111             writeSpaces(w, 2*pre)
 112             w.WriteByte('[')
 113             w.WriteByte('\n')
 114         } else {
 115             w.WriteByte(',')
 116             w.WriteByte('\n')
 117             if w.Flush() != nil {
 118                 // a write error may be the consequence of stdout being closed,
 119                 // perhaps by another app along a pipe
 120                 return io.EOF
 121             }
 122         }
 123 
 124         if err := writeValue(w, v, level+1, level+1); err != nil {
 125             return err
 126         }
 127     }
 128 
 129     if len(data) == 0 {
 130         writeSpaces(w, 2*pre)
 131         w.WriteByte('[')
 132         w.WriteByte(']')
 133     } else {
 134         w.WriteByte('\n')
 135         writeSpaces(w, 2*level)
 136         w.WriteByte(']')
 137     }
 138     return nil
 139 }
 140 
 141 func writeFloat(w *bufio.Writer, data float64) error {
 142     var buf [32]byte
 143     w.Write(strconv.AppendFloat(buf[:0], data, 'f', -1, 64))
 144     return nil
 145 }
 146 
 147 func writeInt(w *bufio.Writer, data int) error {
 148     var buf [32]byte
 149     w.Write(strconv.AppendInt(buf[:0], int64(data), 64))
 150     return nil
 151 }
 152 
 153 // writeObject handles objects for func writeValue
 154 func writeObject(w *bufio.Writer, data object, pre, level int) error {
 155     for i, k := range data.Keys {
 156         if i == 0 {
 157             writeSpaces(w, 2*pre)
 158             w.WriteByte('{')
 159             w.WriteByte('\n')
 160         } else {
 161             w.WriteByte(',')
 162             w.WriteByte('\n')
 163         }
 164 
 165         if err := writeString(w, k, level+1); err != nil {
 166             return err
 167         }
 168 
 169         w.WriteString(": ")
 170 
 171         if err := writeValue(w, data.Map[k], 0, level+1); err != nil {
 172             return err
 173         }
 174     }
 175 
 176     if len(data.Keys) == 0 {
 177         writeSpaces(w, 2*pre)
 178         w.WriteByte('{')
 179         w.WriteByte('}')
 180     } else {
 181         w.WriteByte('\n')
 182         writeSpaces(w, 2*level)
 183         w.WriteByte('}')
 184     }
 185     return nil
 186 }
 187 
 188 // writeString handles strings for func writeValue, and keys for func
 189 // writeObject
 190 func writeString(w *bufio.Writer, s string, level int) error {
 191     writeSpaces(w, 2*level)
 192 
 193     for _, r := range s {
 194         if r < ' ' || r == '"' || r == '\\' {
 195             w.WriteByte('"')
 196             for _, r := range s {
 197                 if int(r) < len(escapedStringBytes) {
 198                     w.Write(escapedStringBytes[r])
 199                 } else {
 200                     w.WriteRune(r)
 201                 }
 202             }
 203             w.WriteByte('"')
 204             return nil
 205         }
 206     }
 207 
 208     w.WriteByte('"')
 209     w.WriteString(s)
 210     w.WriteByte('"')
 211     return nil
 212 }
     File: ./ngron/ngron.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ngron
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34 )
  35 
  36 const info = `
  37 ngron [options...] [filepath...]
  38 
  39 
  40 Nice GRON converts JSON data into 'grep'-friendly lines, similar to what
  41 tool 'gron' (GRep jsON; https://github.com/tomnomnom/gron) does. This tool
  42 uses nicer ANSI styles than the original, hence its name.
  43 
  44 Unlike the original 'gron', there's no sort-mode: object keys are always
  45 remembered in the original input order. The original 'gron' also supports
  46 reading from URIs, but this tool doesn't.
  47 
  48 When not given a named source (filepath) to read from, JSON data are read
  49 from the standard input.
  50 
  51 Options, where leading double-dashes are also allowed:
  52 
  53     -c, -color         color, enables ANSI-styled output-mode
  54     -m, -monochrome    monochrome (default), enables unstyled output-mode
  55     -h, -help          show this help message
  56     -u, -ungron        turn GRON-style lines into valid JSON
  57 `
  58 
  59 type emitConfig struct {
  60     path    func(w *bufio.Writer, path []any) error
  61     null    func(w *bufio.Writer) error
  62     boolean func(w *bufio.Writer, b bool) error
  63     number  func(w *bufio.Writer, n json.Number) error
  64     key     func(w *bufio.Writer, k string) error
  65     text    func(w *bufio.Writer, s string) error
  66 
  67     arrayDecl  string
  68     objectDecl string
  69 }
  70 
  71 var monochrome = emitConfig{
  72     path:    monoPath,
  73     null:    monoNull,
  74     boolean: monoBool,
  75     number:  monoNumber,
  76     key:     monoString,
  77     text:    monoString,
  78 
  79     arrayDecl:  `[]`,
  80     objectDecl: `{}`,
  81 }
  82 
  83 var styled = emitConfig{
  84     path:    styledPath,
  85     null:    styledNull,
  86     boolean: styledBool,
  87     number:  styledNumber,
  88     key:     styledString,
  89     text:    styledString,
  90 
  91     arrayDecl:  "\x1b[38;2;168;168;168m[]\x1b[0m",
  92     objectDecl: "\x1b[38;2;168;168;168m{}\x1b[0m",
  93 }
  94 
  95 var config = monochrome
  96 
  97 func Main() {
  98     args := os.Args[1:]
  99     run := gron
 100 
 101     for len(args) > 0 {
 102         switch args[0] {
 103         case `-c`, `--c`, `-color`, `--color`, `-colorize`, `--colorize`:
 104             config = styled
 105             args = args[1:]
 106             continue
 107 
 108         case `-h`, `--h`, `-help`, `--help`:
 109             os.Stdout.WriteString(info[1:])
 110             return
 111 
 112         case `-m`, `--m`, `-monochrome`, `--monochrome`:
 113             config = monochrome
 114             args = args[1:]
 115             continue
 116 
 117         case `-u`, `--u`, `-ungron`, `--ungron`:
 118             run = ungron
 119             args = args[1:]
 120             continue
 121         }
 122 
 123         break
 124     }
 125 
 126     if len(args) > 0 && args[0] == `--` {
 127         args = args[1:]
 128     }
 129 
 130     if len(args) > 2 {
 131         os.Stderr.WriteString("multiple inputs not allowed\n")
 132         os.Exit(1)
 133         return
 134     }
 135 
 136     // figure out whether input should come from a named file or from stdin
 137     path := `-`
 138     if len(args) > 0 {
 139         path = args[0]
 140     }
 141 
 142     err := handleInput(os.Stdout, path, run)
 143     if err != nil && err != io.EOF {
 144         os.Stderr.WriteString(`ngron: `)
 145         os.Stderr.WriteString(err.Error())
 146         os.Stderr.WriteString("\n")
 147         os.Exit(1)
 148         return
 149     }
 150 }
 151 
 152 type handlerFunc func(*bufio.Writer, *json.Decoder, json.Token, []any) error
 153 type runnerFunc func(w *bufio.Writer, r io.Reader) error
 154 
 155 // handleInput simplifies control-flow for func main
 156 func handleInput(w io.Writer, path string, run runnerFunc) error {
 157     if path == `-` {
 158         bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 159         defer bw.Flush()
 160         return run(bw, os.Stdin)
 161     }
 162 
 163     f, err := os.Open(path)
 164     if err != nil {
 165         // on windows, file-not-found error messages may mention `CreateFile`,
 166         // even when trying to open files in read-only mode
 167         return errors.New(`can't open file named ` + path)
 168     }
 169     defer f.Close()
 170 
 171     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 172     defer bw.Flush()
 173     return run(bw, f)
 174 }
 175 
 176 // gron does it all, given a reader and a writer
 177 func gron(w *bufio.Writer, r io.Reader) error {
 178     dec := json.NewDecoder(r)
 179     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 180     // even if JSON parsers aren't required to guarantee such input-fidelity
 181     // for numbers
 182     dec.UseNumber()
 183 
 184     t, err := dec.Token()
 185     if err == io.EOF {
 186         return errors.New(`input has no JSON values`)
 187     }
 188 
 189     if err = handleToken(w, dec, t, make([]any, 0, 50)); err != nil {
 190         return err
 191     }
 192 
 193     _, err = dec.Token()
 194     if err == io.EOF {
 195         // input is over, so it's a success
 196         return nil
 197     }
 198 
 199     if err == nil {
 200         // a successful `read` is a failure, as it means there are
 201         // trailing JSON tokens
 202         return errors.New(`unexpected trailing data`)
 203     }
 204 
 205     // any other error, perhaps some invalid-JSON-syntax-type error
 206     return err
 207 }
 208 
 209 // handleToken handles recursion for func run
 210 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, path []any) error {
 211     switch t := t.(type) {
 212     case json.Delim:
 213         switch t {
 214         case json.Delim('['):
 215             return handleArray(w, dec, path)
 216         case json.Delim('{'):
 217             return handleObject(w, dec, path)
 218         default:
 219             return errors.New(`unsupported JSON syntax ` + string(t))
 220         }
 221 
 222     case nil:
 223         config.path(w, path)
 224         config.null(w)
 225         return endLine(w)
 226 
 227     case bool:
 228         config.path(w, path)
 229         config.boolean(w, t)
 230         return endLine(w)
 231 
 232     case json.Number:
 233         config.path(w, path)
 234         config.number(w, t)
 235         return endLine(w)
 236 
 237     case string:
 238         config.path(w, path)
 239         config.text(w, t)
 240         return endLine(w)
 241 
 242     default:
 243         // return fmt.Errorf(`unsupported token type %T`, t)
 244         return errors.New(`invalid JSON token`)
 245     }
 246 }
 247 
 248 // handleArray handles arrays for func handleToken
 249 func handleArray(w *bufio.Writer, dec *json.Decoder, path []any) error {
 250     config.path(w, path)
 251     w.WriteString(config.arrayDecl)
 252     if err := endLine(w); err != nil {
 253         return err
 254     }
 255 
 256     path = append(path, 0)
 257     last := len(path) - 1
 258 
 259     for i := 0; true; i++ {
 260         path[last] = i
 261 
 262         t, err := dec.Token()
 263         if err == io.EOF {
 264             return errors.New(`end of JSON before array was closed`)
 265         }
 266         if err != nil {
 267             return err
 268         }
 269 
 270         if t == json.Delim(']') {
 271             return nil
 272         }
 273 
 274         err = handleToken(w, dec, t, path)
 275         if err != nil {
 276             return err
 277         }
 278     }
 279 
 280     // make the compiler happy
 281     return nil
 282 }
 283 
 284 // handleObject handles objects for func handleToken
 285 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
 286     config.path(w, path)
 287     w.WriteString(config.objectDecl)
 288     if err := endLine(w); err != nil {
 289         return err
 290     }
 291 
 292     path = append(path, ``)
 293     last := len(path) - 1
 294 
 295     for i := 0; true; i++ {
 296         t, err := dec.Token()
 297         if err == io.EOF {
 298             return errors.New(`end of JSON before object was closed`)
 299         }
 300         if err != nil {
 301             return err
 302         }
 303 
 304         if t == json.Delim('}') {
 305             return nil
 306         }
 307 
 308         k, ok := t.(string)
 309         if !ok {
 310             return errors.New(`expected a string for a key-value pair`)
 311         }
 312 
 313         path[last] = k
 314         if err != nil {
 315             return err
 316         }
 317 
 318         t, err = dec.Token()
 319         if err == io.EOF {
 320             return errors.New(`expected a value for a key-value pair`)
 321         }
 322 
 323         err = handleToken(w, dec, t, path)
 324         if err != nil {
 325             return err
 326         }
 327     }
 328 
 329     // make the compiler happy
 330     return nil
 331 }
 332 
 333 func monoPath(w *bufio.Writer, path []any) error {
 334     var buf [24]byte
 335 
 336     w.WriteString(`json`)
 337 
 338     for _, v := range path {
 339         switch v := v.(type) {
 340         case int:
 341             w.WriteByte('[')
 342             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 343             w.WriteByte(']')
 344 
 345         case string:
 346             if !needsEscaping(v) {
 347                 w.WriteByte('.')
 348                 w.WriteString(v)
 349                 continue
 350             }
 351             w.WriteByte('[')
 352             monoString(w, v)
 353             w.WriteByte(']')
 354         }
 355     }
 356 
 357     w.WriteString(` = `)
 358     return nil
 359 }
 360 
 361 func monoNull(w *bufio.Writer) error {
 362     w.WriteString(`null`)
 363     return nil
 364 }
 365 
 366 func monoBool(w *bufio.Writer, b bool) error {
 367     if b {
 368         w.WriteString(`true`)
 369     } else {
 370         w.WriteString(`false`)
 371     }
 372     return nil
 373 }
 374 
 375 func monoNumber(w *bufio.Writer, n json.Number) error {
 376     w.WriteString(n.String())
 377     return nil
 378 }
 379 
 380 func monoString(w *bufio.Writer, s string) error {
 381     for _, r := range s {
 382         if r > 127 {
 383             b, err := json.Marshal(s)
 384             if err != nil {
 385                 return err
 386             }
 387             w.Write(b)
 388             return nil
 389         }
 390 
 391         if r < ' ' || r == '"' || r == '\\' {
 392             w.WriteByte('"')
 393             for i := range s {
 394                 w.Write(escapedStringBytes[s[i]])
 395             }
 396             w.WriteByte('"')
 397             return nil
 398         }
 399     }
 400 
 401     w.WriteByte('"')
 402     w.WriteString(s)
 403     w.WriteByte('"')
 404     return nil
 405 }
 406 
 407 func styledPath(w *bufio.Writer, path []any) error {
 408     var buf [24]byte
 409 
 410     w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
 411 
 412     for _, v := range path {
 413         switch v := v.(type) {
 414         case int:
 415             w.WriteString("\x1b[38;2;168;168;168m[")
 416             w.WriteString("\x1b[38;2;0;135;95m")
 417             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 418             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 419 
 420         case string:
 421             if !needsEscaping(v) {
 422                 w.WriteString("\x1b[38;2;168;168;168m.")
 423                 w.WriteString("\x1b[38;2;135;95;255m")
 424                 w.WriteString(v)
 425                 w.WriteString("\x1b[0m")
 426                 continue
 427             }
 428 
 429             w.WriteString("\x1b[38;2;168;168;168m[")
 430             styledString(w, v)
 431             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 432         }
 433     }
 434 
 435     w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
 436     return nil
 437 }
 438 
 439 func styledNull(w *bufio.Writer) error {
 440     w.WriteString("\x1b[38;2;168;168;168m")
 441     w.WriteString(`null`)
 442     w.WriteString("\x1b[0m")
 443     return nil
 444 }
 445 
 446 func styledBool(w *bufio.Writer, b bool) error {
 447     if b {
 448         w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
 449     } else {
 450         w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
 451     }
 452     return nil
 453 }
 454 
 455 func styledNumber(w *bufio.Writer, n json.Number) error {
 456     w.WriteString("\x1b[38;2;0;135;95m")
 457     w.WriteString(n.String())
 458     w.WriteString("\x1b[0m")
 459     return nil
 460 }
 461 
 462 func styledString(w *bufio.Writer, s string) error {
 463     for _, r := range s {
 464         if r > 127 {
 465             w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 466             b, err := json.Marshal(s)
 467             if err != nil {
 468                 return err
 469             }
 470             w.Write(b[1 : len(b)-1])
 471             w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 472             return nil
 473         }
 474 
 475         if r < ' ' || r == '"' || r == '\\' {
 476             w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 477             for i := range s {
 478                 w.Write(escapedStringBytes[s[i]])
 479             }
 480             w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 481             return nil
 482         }
 483     }
 484 
 485     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 486     w.WriteString(s)
 487     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 488     return nil
 489 }
 490 
 491 func needsEscaping(s string) bool {
 492     if len(s) == 0 {
 493         return true
 494     }
 495 
 496     for _, r := range s {
 497         if r < ' ' || r > '~' {
 498             return true
 499         }
 500 
 501         switch r {
 502         case '"', '\'', '\\':
 503             return true
 504         }
 505     }
 506 
 507     return false
 508 }
 509 
 510 func endLine(w *bufio.Writer) error {
 511     w.WriteByte(';')
 512     if err := w.WriteByte('\n'); err == nil {
 513         return nil
 514     }
 515     return io.EOF
 516 }
     File: ./ngron/ungron.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ngron
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "fmt"
  32     "io"
  33     "math"
  34     "strconv"
  35     "strings"
  36 )
  37 
  38 var errBadFloatJSON = errors.New(`JSON numbers can't be NaNs or Infinities`)
  39 
  40 // object is a map which also remembers the order of its keys
  41 type object struct {
  42     Keys []string
  43     Map  map[string]any
  44 }
  45 
  46 func ungron(w *bufio.Writer, r io.Reader) error {
  47     data, err := parseGRON(r)
  48     if err != nil {
  49         return err
  50     }
  51     return json2(w, data)
  52 }
  53 
  54 func parseFloatJSON(s string) (f float64, err error) {
  55     f, err = strconv.ParseFloat(s, 64)
  56     if math.IsInf(f, 0) || math.IsNaN(f) {
  57         return math.NaN(), errBadFloatJSON
  58     }
  59     return f, err
  60 }
  61 
  62 func parseGRON(r io.Reader) (data any, err error) {
  63     const gb = 1024 * 1024 * 1024
  64     sc := bufio.NewScanner(r)
  65     sc.Buffer(nil, 8*gb)
  66 
  67     for sc.Scan() {
  68         if err := ungronUpdate(&data, sc.Text()); err != nil {
  69             err = fmt.Errorf(`%s: %w`, sc.Text(), err)
  70             return data, err
  71         }
  72     }
  73     return data, sc.Err()
  74 }
  75 
  76 func ungronUpdate(data *any, line string) error {
  77     // ensure lines starts with `json`
  78     rest, ok := strings.CutPrefix(line, `json`)
  79     if !ok {
  80         return fmt.Errorf(`valid gron lines always start with 'json'`)
  81     }
  82 
  83     line = rest
  84     line = trimLeadingSpaces(line)
  85     line = trimTrailingSpaces(line)
  86 
  87     // ignore trailing semicolon
  88     if len(line) > 0 && line[len(line)-1] == ';' {
  89         line = line[:len(line)-1]
  90     }
  91 
  92     // ignore empty(ish) lines
  93     if len(line) == 0 {
  94         return nil
  95     }
  96 
  97     return ungronUpdateRec(data, line)
  98 }
  99 
 100 func ungronUpdateRec(data *any, line string) error {
 101     line = trimLeadingSpaces(line)
 102     if len(line) == 0 {
 103         return errors.New(`invalid gron line is missing value-assignment`)
 104     }
 105 
 106     switch line[0] {
 107     case '=':
 108         return ungronAssign(data, line[1:])
 109 
 110     case '.':
 111         return ungronIndexIdent(data, line[1:])
 112 
 113     case '[':
 114         return ungronIndex(data, line[1:])
 115 
 116     default:
 117         return fmt.Errorf(`unexpected syntax %c`, line[0])
 118     }
 119 }
 120 
 121 func ungronAssign(data *any, line string) error {
 122     line = trimLeadingSpaces(line)
 123 
 124     if line == `[]` {
 125         *data = []any{}
 126         return nil
 127     }
 128     if line == `{}` {
 129         *data = object{Map: make(map[string]any)}
 130         return nil
 131     }
 132 
 133     if _, err := parseFloatJSON(line); err == nil {
 134         *data = json.Number(line)
 135         return nil
 136     }
 137 
 138     var res any
 139     if err := json.Unmarshal([]byte(line), &res); err != nil {
 140         return err
 141     }
 142     *data = res
 143     return nil
 144 }
 145 
 146 func ungronIndexIdent(data *any, line string) error {
 147     line = trimLeadingSpaces(line)
 148 
 149     if k, ok, rest := scanIdentifier(line); ok {
 150         line = trimLeadingSpaces(rest)
 151         kv, ok := (*data).(object)
 152         if !ok {
 153             return errors.New(`expected an object`)
 154         }
 155 
 156         v, ok := kv.Map[k]
 157         if !ok {
 158             kv.Keys = append(kv.Keys, k)
 159             v = nil
 160         }
 161 
 162         var res any = v
 163         if err := ungronUpdateRec(&res, line); err != nil {
 164             return err
 165         }
 166         kv.Map[k] = res
 167         *data = kv
 168         return nil
 169     }
 170 
 171     return errors.New(`missing identifier`)
 172 }
 173 
 174 func ungronIndex(data *any, line string) error {
 175     line = trimLeadingSpaces(line)
 176 
 177     if i, ok, rest := scanArrayIndex(line); ok {
 178         if i < 0 {
 179             return errors.New(`negative array indices not supported`)
 180         }
 181 
 182         line = trimLeadingSpaces(rest)
 183         if len(line) == 0 || line[0] != ']' {
 184             return errors.New(`missing ']' syntax`)
 185         }
 186         line = line[1:]
 187         line = trimLeadingSpaces(line)
 188 
 189         v, ok := (*data).([]any)
 190         if !ok {
 191             const msg = `expected an array given the numeric index`
 192             return errors.New(msg)
 193         }
 194 
 195         for len(v) <= i {
 196             v = append(v, nil)
 197         }
 198         *data = v
 199 
 200         return ungronUpdateRec(&v[i], line)
 201     }
 202 
 203     if k, ok, rest := scanQuotedKey(line); ok {
 204         var key string
 205         // json.Unmarshal seems faster than strings.IndexByte and slicing
 206         // the quotes out, when no backslashes are found in the quoted key
 207         if err := json.Unmarshal([]byte(k), &key); err != nil {
 208             return err
 209         }
 210 
 211         line = trimLeadingSpaces(rest)
 212         if len(line) == 0 || line[0] != ']' {
 213             return errors.New(`missing ']' syntax`)
 214         }
 215         line = line[1:]
 216         line = trimLeadingSpaces(line)
 217 
 218         kv, ok := (*data).(object)
 219         if !ok {
 220             return errors.New(`expected an object`)
 221         }
 222 
 223         if v, ok := kv.Map[key]; ok {
 224             return ungronUpdateRec(&v, line)
 225         }
 226 
 227         var v any
 228         if err := ungronUpdateRec(&v, line); err != nil {
 229             return err
 230         }
 231         kv.Map[key] = v
 232         return nil
 233     }
 234 
 235     return errors.New(`missing key/index`)
 236 }
 237 
 238 func scanArrayIndex(s string) (n int, ok bool, rest string) {
 239     end := -1
 240     for i, b := range s {
 241         if '0' <= b && b <= '9' {
 242             continue
 243         }
 244         end = i
 245         break
 246     }
 247 
 248     if end < 1 {
 249         return 0, false, s
 250     }
 251 
 252     if n, err := strconv.Atoi(s[:end]); err == nil {
 253         return n, true, s[end:]
 254     }
 255     return 0, false, s
 256 }
 257 
 258 func scanIdentifier(s string) (id string, ok bool, rest string) {
 259     end := -1
 260     for i, b := range s {
 261         if i > 0 && '0' <= b && b <= '9' {
 262             continue
 263         }
 264         if b == '_' {
 265             continue
 266         }
 267         if 'A' <= b && b <= 'Z' {
 268             continue
 269         }
 270         if 'a' <= b && b <= 'z' {
 271             continue
 272         }
 273 
 274         end = i
 275         break
 276     }
 277 
 278     if end < 1 {
 279         return ``, false, s
 280     }
 281     return s[:end], true, s[end:]
 282 }
 283 
 284 func scanQuotedKey(s string) (k string, ok bool, rest string) {
 285     end := -1
 286     esc := false
 287 
 288     for i, b := range s {
 289         if esc {
 290             esc = false
 291             continue
 292         }
 293         if b == '\\' {
 294             esc = true
 295         }
 296         if b != '"' || i == 0 {
 297             continue
 298         }
 299         end = i + 1
 300         break
 301     }
 302 
 303     if end < 0 {
 304         return ``, false, s
 305     }
 306     return s[:end], true, s[end:]
 307 }
 308 
 309 func trimLeadingSpaces(s string) string {
 310     for len(s) > 0 && s[0] == ' ' {
 311         s = s[1:]
 312     }
 313     return s
 314 }
 315 
 316 func trimTrailingSpaces(s string) string {
 317     for len(s) > 0 && s[len(s)-1] == ' ' {
 318         s = s[:len(s)-1]
 319     }
 320     return s
 321 }
     File: ./ngron/ungron_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ngron
  26 
  27 import (
  28     "encoding/json"
  29     "reflect"
  30     "strconv"
  31     "strings"
  32     "testing"
  33 )
  34 
  35 func TestLines(t *testing.T) {
  36     var tests = []struct {
  37         lines    []string
  38         expected any
  39     }{
  40         {[]string{`json = null;`}, nil},
  41         {[]string{`json = false;`}, false},
  42         {[]string{`json = true;`}, true},
  43         {[]string{`json = 123;`}, 123},
  44         {[]string{`json = 123.25;`}, 123.25},
  45         {[]string{`json = "abc";`}, `abc`},
  46         {[]string{`json = [];`}, []any{}},
  47         {[]string{`json = {};`}, object{}},
  48 
  49         {
  50             []string{
  51                 `json = [];`,
  52                 `json[0] = 0;`,
  53                 `json[1] = 1;`,
  54                 `json[2] = 2;`,
  55                 `json[3] = 3;`,
  56                 `json[4] = 4;`,
  57             },
  58             []any{0, 1, 2, 3, 4},
  59         },
  60 
  61         {
  62             []string{
  63                 `json = {};`,
  64                 `json.abc = 123;`,
  65                 `json.def = 456;`,
  66             },
  67             object{
  68                 Keys: []string{`abc`, `def`},
  69                 Map: map[string]any{
  70                     `abc`: 123,
  71                     `def`: 456,
  72                 },
  73             },
  74         },
  75     }
  76 
  77     for _, tc := range tests {
  78         src := strings.Join(tc.lines, "\n")
  79 
  80         t.Run(src, func(t *testing.T) {
  81             data, err := parseGRON(strings.NewReader(src))
  82             if err != nil {
  83                 t.Fatal(err.Error())
  84                 return
  85             }
  86 
  87             if !same(data, tc.expected) {
  88                 exp, _ := json.Marshal(tc.expected)
  89                 got, _ := json.Marshal(data)
  90                 const f = `expected %s, but got %s instead`
  91                 t.Fatalf(f, string(exp), string(got))
  92                 return
  93             }
  94         })
  95     }
  96 }
  97 
  98 func same(x, y any) bool {
  99     if x == nil || y == nil {
 100         return x == nil && y == nil
 101     }
 102 
 103     if _, ok := x.(json.Number); ok {
 104         if n, ok := y.(int); ok {
 105             y = json.Number(strconv.FormatInt(int64(n), 10))
 106         }
 107         if n, ok := y.(float64); ok {
 108             y = json.Number(strconv.FormatFloat(n, 'f', -1, 64))
 109         }
 110     }
 111 
 112     if n, ok := x.(int); ok {
 113         x = float64(n)
 114     }
 115     if n, ok := y.(int); ok {
 116         y = float64(n)
 117     }
 118 
 119     if reflect.TypeOf(x).String() != reflect.TypeOf(y).String() {
 120         return false
 121     }
 122 
 123     switch x := x.(type) {
 124     case nil:
 125         return true
 126 
 127     case bool:
 128         y := y.(bool)
 129         return x == y
 130 
 131     case int:
 132         y := y.(int)
 133         return x == y
 134 
 135     case float64:
 136         y := y.(float64)
 137         return x == y
 138 
 139     case json.Number:
 140         y := y.(json.Number)
 141         return x == y
 142 
 143     case string:
 144         y := y.(string)
 145         return x == y
 146 
 147     case []any:
 148         y := y.([]any)
 149         if len(x) != len(y) {
 150             return false
 151         }
 152         for i := range x {
 153             if !same(x[i], y[i]) {
 154                 return false
 155             }
 156         }
 157         return true
 158 
 159     case object:
 160         y := y.(object)
 161         if len(x.Keys) != len(y.Keys) {
 162             return false
 163         }
 164         for i := range x.Keys {
 165             if x.Keys[i] != y.Keys[i] {
 166                 return false
 167             }
 168             if !same(x.Map[x.Keys[i]], y.Map[y.Keys[i]]) {
 169                 return false
 170             }
 171         }
 172         return true
 173 
 174     default:
 175         return false
 176     }
 177 }
     File: ./nhex/ansi.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "strconv"
  31 )
  32 
  33 // styledHexBytes is a super-fast direct byte-to-result lookup table, and was
  34 // autogenerated by running the command
  35 //
  36 // seq 0 255 | ./hex-styles.awk
  37 var styledHexBytes = [256]string{
  38     "\x1b[38;5;111m00 ", "\x1b[38;5;246m01 ",
  39     "\x1b[38;5;246m02 ", "\x1b[38;5;246m03 ",
  40     "\x1b[38;5;246m04 ", "\x1b[38;5;246m05 ",
  41     "\x1b[38;5;246m06 ", "\x1b[38;5;246m07 ",
  42     "\x1b[38;5;246m08 ", "\x1b[38;5;246m09 ",
  43     "\x1b[38;5;246m0a ", "\x1b[38;5;246m0b ",
  44     "\x1b[38;5;246m0c ", "\x1b[38;5;246m0d ",
  45     "\x1b[38;5;246m0e ", "\x1b[38;5;246m0f ",
  46     "\x1b[38;5;246m10 ", "\x1b[38;5;246m11 ",
  47     "\x1b[38;5;246m12 ", "\x1b[38;5;246m13 ",
  48     "\x1b[38;5;246m14 ", "\x1b[38;5;246m15 ",
  49     "\x1b[38;5;246m16 ", "\x1b[38;5;246m17 ",
  50     "\x1b[38;5;246m18 ", "\x1b[38;5;246m19 ",
  51     "\x1b[38;5;246m1a ", "\x1b[38;5;246m1b ",
  52     "\x1b[38;5;246m1c ", "\x1b[38;5;246m1d ",
  53     "\x1b[38;5;246m1e ", "\x1b[38;5;246m1f ",
  54     "\x1b[38;5;72m20\x1b[38;5;239m ", "\x1b[38;5;72m21\x1b[38;5;239m!",
  55     "\x1b[38;5;72m22\x1b[38;5;239m\"", "\x1b[38;5;72m23\x1b[38;5;239m#",
  56     "\x1b[38;5;72m24\x1b[38;5;239m$", "\x1b[38;5;72m25\x1b[38;5;239m%",
  57     "\x1b[38;5;72m26\x1b[38;5;239m&", "\x1b[38;5;72m27\x1b[38;5;239m'",
  58     "\x1b[38;5;72m28\x1b[38;5;239m(", "\x1b[38;5;72m29\x1b[38;5;239m)",
  59     "\x1b[38;5;72m2a\x1b[38;5;239m*", "\x1b[38;5;72m2b\x1b[38;5;239m+",
  60     "\x1b[38;5;72m2c\x1b[38;5;239m,", "\x1b[38;5;72m2d\x1b[38;5;239m-",
  61     "\x1b[38;5;72m2e\x1b[38;5;239m.", "\x1b[38;5;72m2f\x1b[38;5;239m/",
  62     "\x1b[38;5;72m30\x1b[38;5;239m0", "\x1b[38;5;72m31\x1b[38;5;239m1",
  63     "\x1b[38;5;72m32\x1b[38;5;239m2", "\x1b[38;5;72m33\x1b[38;5;239m3",
  64     "\x1b[38;5;72m34\x1b[38;5;239m4", "\x1b[38;5;72m35\x1b[38;5;239m5",
  65     "\x1b[38;5;72m36\x1b[38;5;239m6", "\x1b[38;5;72m37\x1b[38;5;239m7",
  66     "\x1b[38;5;72m38\x1b[38;5;239m8", "\x1b[38;5;72m39\x1b[38;5;239m9",
  67     "\x1b[38;5;72m3a\x1b[38;5;239m:", "\x1b[38;5;72m3b\x1b[38;5;239m;",
  68     "\x1b[38;5;72m3c\x1b[38;5;239m<", "\x1b[38;5;72m3d\x1b[38;5;239m=",
  69     "\x1b[38;5;72m3e\x1b[38;5;239m>", "\x1b[38;5;72m3f\x1b[38;5;239m?",
  70     "\x1b[38;5;72m40\x1b[38;5;239m@", "\x1b[38;5;72m41\x1b[38;5;239mA",
  71     "\x1b[38;5;72m42\x1b[38;5;239mB", "\x1b[38;5;72m43\x1b[38;5;239mC",
  72     "\x1b[38;5;72m44\x1b[38;5;239mD", "\x1b[38;5;72m45\x1b[38;5;239mE",
  73     "\x1b[38;5;72m46\x1b[38;5;239mF", "\x1b[38;5;72m47\x1b[38;5;239mG",
  74     "\x1b[38;5;72m48\x1b[38;5;239mH", "\x1b[38;5;72m49\x1b[38;5;239mI",
  75     "\x1b[38;5;72m4a\x1b[38;5;239mJ", "\x1b[38;5;72m4b\x1b[38;5;239mK",
  76     "\x1b[38;5;72m4c\x1b[38;5;239mL", "\x1b[38;5;72m4d\x1b[38;5;239mM",
  77     "\x1b[38;5;72m4e\x1b[38;5;239mN", "\x1b[38;5;72m4f\x1b[38;5;239mO",
  78     "\x1b[38;5;72m50\x1b[38;5;239mP", "\x1b[38;5;72m51\x1b[38;5;239mQ",
  79     "\x1b[38;5;72m52\x1b[38;5;239mR", "\x1b[38;5;72m53\x1b[38;5;239mS",
  80     "\x1b[38;5;72m54\x1b[38;5;239mT", "\x1b[38;5;72m55\x1b[38;5;239mU",
  81     "\x1b[38;5;72m56\x1b[38;5;239mV", "\x1b[38;5;72m57\x1b[38;5;239mW",
  82     "\x1b[38;5;72m58\x1b[38;5;239mX", "\x1b[38;5;72m59\x1b[38;5;239mY",
  83     "\x1b[38;5;72m5a\x1b[38;5;239mZ", "\x1b[38;5;72m5b\x1b[38;5;239m[",
  84     "\x1b[38;5;72m5c\x1b[38;5;239m\\", "\x1b[38;5;72m5d\x1b[38;5;239m]",
  85     "\x1b[38;5;72m5e\x1b[38;5;239m^", "\x1b[38;5;72m5f\x1b[38;5;239m_",
  86     "\x1b[38;5;72m60\x1b[38;5;239m`", "\x1b[38;5;72m61\x1b[38;5;239ma",
  87     "\x1b[38;5;72m62\x1b[38;5;239mb", "\x1b[38;5;72m63\x1b[38;5;239mc",
  88     "\x1b[38;5;72m64\x1b[38;5;239md", "\x1b[38;5;72m65\x1b[38;5;239me",
  89     "\x1b[38;5;72m66\x1b[38;5;239mf", "\x1b[38;5;72m67\x1b[38;5;239mg",
  90     "\x1b[38;5;72m68\x1b[38;5;239mh", "\x1b[38;5;72m69\x1b[38;5;239mi",
  91     "\x1b[38;5;72m6a\x1b[38;5;239mj", "\x1b[38;5;72m6b\x1b[38;5;239mk",
  92     "\x1b[38;5;72m6c\x1b[38;5;239ml", "\x1b[38;5;72m6d\x1b[38;5;239mm",
  93     "\x1b[38;5;72m6e\x1b[38;5;239mn", "\x1b[38;5;72m6f\x1b[38;5;239mo",
  94     "\x1b[38;5;72m70\x1b[38;5;239mp", "\x1b[38;5;72m71\x1b[38;5;239mq",
  95     "\x1b[38;5;72m72\x1b[38;5;239mr", "\x1b[38;5;72m73\x1b[38;5;239ms",
  96     "\x1b[38;5;72m74\x1b[38;5;239mt", "\x1b[38;5;72m75\x1b[38;5;239mu",
  97     "\x1b[38;5;72m76\x1b[38;5;239mv", "\x1b[38;5;72m77\x1b[38;5;239mw",
  98     "\x1b[38;5;72m78\x1b[38;5;239mx", "\x1b[38;5;72m79\x1b[38;5;239my",
  99     "\x1b[38;5;72m7a\x1b[38;5;239mz", "\x1b[38;5;72m7b\x1b[38;5;239m{",
 100     "\x1b[38;5;72m7c\x1b[38;5;239m|", "\x1b[38;5;72m7d\x1b[38;5;239m}",
 101     "\x1b[38;5;72m7e\x1b[38;5;239m~", "\x1b[38;5;246m7f ",
 102     "\x1b[38;5;246m80 ", "\x1b[38;5;246m81 ",
 103     "\x1b[38;5;246m82 ", "\x1b[38;5;246m83 ",
 104     "\x1b[38;5;246m84 ", "\x1b[38;5;246m85 ",
 105     "\x1b[38;5;246m86 ", "\x1b[38;5;246m87 ",
 106     "\x1b[38;5;246m88 ", "\x1b[38;5;246m89 ",
 107     "\x1b[38;5;246m8a ", "\x1b[38;5;246m8b ",
 108     "\x1b[38;5;246m8c ", "\x1b[38;5;246m8d ",
 109     "\x1b[38;5;246m8e ", "\x1b[38;5;246m8f ",
 110     "\x1b[38;5;246m90 ", "\x1b[38;5;246m91 ",
 111     "\x1b[38;5;246m92 ", "\x1b[38;5;246m93 ",
 112     "\x1b[38;5;246m94 ", "\x1b[38;5;246m95 ",
 113     "\x1b[38;5;246m96 ", "\x1b[38;5;246m97 ",
 114     "\x1b[38;5;246m98 ", "\x1b[38;5;246m99 ",
 115     "\x1b[38;5;246m9a ", "\x1b[38;5;246m9b ",
 116     "\x1b[38;5;246m9c ", "\x1b[38;5;246m9d ",
 117     "\x1b[38;5;246m9e ", "\x1b[38;5;246m9f ",
 118     "\x1b[38;5;246ma0 ", "\x1b[38;5;246ma1 ",
 119     "\x1b[38;5;246ma2 ", "\x1b[38;5;246ma3 ",
 120     "\x1b[38;5;246ma4 ", "\x1b[38;5;246ma5 ",
 121     "\x1b[38;5;246ma6 ", "\x1b[38;5;246ma7 ",
 122     "\x1b[38;5;246ma8 ", "\x1b[38;5;246ma9 ",
 123     "\x1b[38;5;246maa ", "\x1b[38;5;246mab ",
 124     "\x1b[38;5;246mac ", "\x1b[38;5;246mad ",
 125     "\x1b[38;5;246mae ", "\x1b[38;5;246maf ",
 126     "\x1b[38;5;246mb0 ", "\x1b[38;5;246mb1 ",
 127     "\x1b[38;5;246mb2 ", "\x1b[38;5;246mb3 ",
 128     "\x1b[38;5;246mb4 ", "\x1b[38;5;246mb5 ",
 129     "\x1b[38;5;246mb6 ", "\x1b[38;5;246mb7 ",
 130     "\x1b[38;5;246mb8 ", "\x1b[38;5;246mb9 ",
 131     "\x1b[38;5;246mba ", "\x1b[38;5;246mbb ",
 132     "\x1b[38;5;246mbc ", "\x1b[38;5;246mbd ",
 133     "\x1b[38;5;246mbe ", "\x1b[38;5;246mbf ",
 134     "\x1b[38;5;246mc0 ", "\x1b[38;5;246mc1 ",
 135     "\x1b[38;5;246mc2 ", "\x1b[38;5;246mc3 ",
 136     "\x1b[38;5;246mc4 ", "\x1b[38;5;246mc5 ",
 137     "\x1b[38;5;246mc6 ", "\x1b[38;5;246mc7 ",
 138     "\x1b[38;5;246mc8 ", "\x1b[38;5;246mc9 ",
 139     "\x1b[38;5;246mca ", "\x1b[38;5;246mcb ",
 140     "\x1b[38;5;246mcc ", "\x1b[38;5;246mcd ",
 141     "\x1b[38;5;246mce ", "\x1b[38;5;246mcf ",
 142     "\x1b[38;5;246md0 ", "\x1b[38;5;246md1 ",
 143     "\x1b[38;5;246md2 ", "\x1b[38;5;246md3 ",
 144     "\x1b[38;5;246md4 ", "\x1b[38;5;246md5 ",
 145     "\x1b[38;5;246md6 ", "\x1b[38;5;246md7 ",
 146     "\x1b[38;5;246md8 ", "\x1b[38;5;246md9 ",
 147     "\x1b[38;5;246mda ", "\x1b[38;5;246mdb ",
 148     "\x1b[38;5;246mdc ", "\x1b[38;5;246mdd ",
 149     "\x1b[38;5;246mde ", "\x1b[38;5;246mdf ",
 150     "\x1b[38;5;246me0 ", "\x1b[38;5;246me1 ",
 151     "\x1b[38;5;246me2 ", "\x1b[38;5;246me3 ",
 152     "\x1b[38;5;246me4 ", "\x1b[38;5;246me5 ",
 153     "\x1b[38;5;246me6 ", "\x1b[38;5;246me7 ",
 154     "\x1b[38;5;246me8 ", "\x1b[38;5;246me9 ",
 155     "\x1b[38;5;246mea ", "\x1b[38;5;246meb ",
 156     "\x1b[38;5;246mec ", "\x1b[38;5;246med ",
 157     "\x1b[38;5;246mee ", "\x1b[38;5;246mef ",
 158     "\x1b[38;5;246mf0 ", "\x1b[38;5;246mf1 ",
 159     "\x1b[38;5;246mf2 ", "\x1b[38;5;246mf3 ",
 160     "\x1b[38;5;246mf4 ", "\x1b[38;5;246mf5 ",
 161     "\x1b[38;5;246mf6 ", "\x1b[38;5;246mf7 ",
 162     "\x1b[38;5;246mf8 ", "\x1b[38;5;246mf9 ",
 163     "\x1b[38;5;246mfa ", "\x1b[38;5;246mfb ",
 164     "\x1b[38;5;246mfc ", "\x1b[38;5;246mfd ",
 165     "\x1b[38;5;246mfe ", "\x1b[38;5;209mff ",
 166 }
 167 
 168 // hexSymbols is a direct lookup table combining 2 hex digits with either a
 169 // space or a displayable ASCII symbol matching the byte's own ASCII value;
 170 // this table was autogenerated by running the command
 171 //
 172 // seq 0 255 | ./hex-symbols.awk
 173 var hexSymbols = [256]string{
 174     `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
 175     `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
 176     `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
 177     `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
 178     `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
 179     `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
 180     `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
 181     `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
 182     `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
 183     `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
 184     `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
 185     `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
 186     "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
 187     `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
 188     `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
 189     `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
 190     `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
 191     `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
 192     `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
 193     `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
 194     `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
 195     `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
 196     `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
 197     `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
 198     `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
 199     `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
 200     `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
 201     `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
 202     `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
 203     `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
 204     `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
 205     `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
 206 }
 207 
 208 const (
 209     unknownStyle = 0
 210     zeroStyle    = 1
 211     otherStyle   = 2
 212     asciiStyle   = 3
 213     allOnStyle   = 4
 214 )
 215 
 216 // byteStyles turns bytes into one of several distinct visual types, which
 217 // allows quickly telling when ANSI styles codes are repetitive and when
 218 // they're actually needed
 219 var byteStyles = [256]int{
 220     zeroStyle, otherStyle, otherStyle, otherStyle,
 221     otherStyle, otherStyle, otherStyle, otherStyle,
 222     otherStyle, otherStyle, otherStyle, otherStyle,
 223     otherStyle, otherStyle, otherStyle, otherStyle,
 224     otherStyle, otherStyle, otherStyle, otherStyle,
 225     otherStyle, otherStyle, otherStyle, otherStyle,
 226     otherStyle, otherStyle, otherStyle, otherStyle,
 227     otherStyle, otherStyle, otherStyle, otherStyle,
 228     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 229     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 230     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 231     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 232     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 233     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 234     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 235     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 236     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 237     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 238     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 239     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 240     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 241     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 242     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 243     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 244     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 245     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 246     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 247     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 248     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 249     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 250     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 251     asciiStyle, asciiStyle, asciiStyle, otherStyle,
 252     otherStyle, otherStyle, otherStyle, otherStyle,
 253     otherStyle, otherStyle, otherStyle, otherStyle,
 254     otherStyle, otherStyle, otherStyle, otherStyle,
 255     otherStyle, otherStyle, otherStyle, otherStyle,
 256     otherStyle, otherStyle, otherStyle, otherStyle,
 257     otherStyle, otherStyle, otherStyle, otherStyle,
 258     otherStyle, otherStyle, otherStyle, otherStyle,
 259     otherStyle, otherStyle, otherStyle, otherStyle,
 260     otherStyle, otherStyle, otherStyle, otherStyle,
 261     otherStyle, otherStyle, otherStyle, otherStyle,
 262     otherStyle, otherStyle, otherStyle, otherStyle,
 263     otherStyle, otherStyle, otherStyle, otherStyle,
 264     otherStyle, otherStyle, otherStyle, otherStyle,
 265     otherStyle, otherStyle, otherStyle, otherStyle,
 266     otherStyle, otherStyle, otherStyle, otherStyle,
 267     otherStyle, otherStyle, otherStyle, otherStyle,
 268     otherStyle, otherStyle, otherStyle, otherStyle,
 269     otherStyle, otherStyle, otherStyle, otherStyle,
 270     otherStyle, otherStyle, otherStyle, otherStyle,
 271     otherStyle, otherStyle, otherStyle, otherStyle,
 272     otherStyle, otherStyle, otherStyle, otherStyle,
 273     otherStyle, otherStyle, otherStyle, otherStyle,
 274     otherStyle, otherStyle, otherStyle, otherStyle,
 275     otherStyle, otherStyle, otherStyle, otherStyle,
 276     otherStyle, otherStyle, otherStyle, otherStyle,
 277     otherStyle, otherStyle, otherStyle, otherStyle,
 278     otherStyle, otherStyle, otherStyle, otherStyle,
 279     otherStyle, otherStyle, otherStyle, otherStyle,
 280     otherStyle, otherStyle, otherStyle, otherStyle,
 281     otherStyle, otherStyle, otherStyle, otherStyle,
 282     otherStyle, otherStyle, otherStyle, otherStyle,
 283     otherStyle, otherStyle, otherStyle, allOnStyle,
 284 }
 285 
 286 // writeMetaANSI shows metadata right before the ANSI-styled hex byte-view
 287 func writeMetaANSI(w *bufio.Writer, fname string, fsize int, cfg config) {
 288     if cfg.Title != "" {
 289         fmt.Fprintf(w, "\x1b[4m%s\x1b[0m\n", cfg.Title)
 290         w.WriteString("\n")
 291     }
 292 
 293     if fsize < 0 {
 294         fmt.Fprintf(w, "• %s\n", fname)
 295     } else {
 296         const fs = "• %s  \x1b[38;5;248m(%s bytes)\x1b[0m\n"
 297         fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
 298     }
 299 
 300     if cfg.Skip > 0 {
 301         const fs = "   \x1b[38;5;5mskipping first %s bytes\x1b[0m\n"
 302         fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
 303     }
 304     if cfg.MaxBytes > 0 {
 305         const fs = "   \x1b[38;5;5mshowing only up to %s bytes\x1b[0m\n"
 306         fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
 307     }
 308     w.WriteString("\n")
 309 }
 310 
 311 // writeBufferANSI shows the hex byte-view using ANSI colors/styles
 312 func writeBufferANSI(rc rendererConfig, first, second []byte) error {
 313     // show a ruler every few lines to make eye-scanning easier
 314     if rc.chunks%5 == 0 && rc.chunks > 0 {
 315         writeRulerANSI(rc)
 316     }
 317 
 318     return writeLineANSI(rc, first, second)
 319 }
 320 
 321 // writeRulerANSI emits an indented ANSI-styled line showing spaced-out dots,
 322 // so as to help eye-scan items on nearby output lines
 323 func writeRulerANSI(rc rendererConfig) {
 324     w := rc.out
 325     if len(rc.ruler) == 0 {
 326         w.WriteByte('\n')
 327         return
 328     }
 329 
 330     w.WriteString("\x1b[38;5;248m")
 331     indent := int(rc.offsetWidth) + len(padding)
 332     writeSpaces(w, indent)
 333     w.Write(rc.ruler)
 334     w.WriteString("\x1b[0m\n")
 335 }
 336 
 337 func writeLineANSI(rc rendererConfig, first, second []byte) error {
 338     w := rc.out
 339 
 340     // start each line with the byte-offset for the 1st item shown on it
 341     if rc.showOffsets {
 342         writeStyledCounter(w, int(rc.offsetWidth), rc.offset)
 343         w.WriteString(padding + "\x1b[48;5;254m")
 344     } else {
 345         w.WriteString(padding)
 346     }
 347 
 348     prevStyle := unknownStyle
 349     for _, b := range first {
 350         // using the slow/generic fmt.Fprintf is a performance bottleneck,
 351         // since it's called for each input byte
 352         // w.WriteString(styledHexBytes[b])
 353 
 354         // this more complicated way of emitting output avoids repeating
 355         // ANSI styles when dealing with bytes which aren't displayable
 356         // ASCII symbols, thus emitting fewer bytes when dealing with
 357         // general binary datasets; it makes no difference for plain-text
 358         // ASCII input
 359         style := byteStyles[b]
 360         if style != prevStyle {
 361             w.WriteString(styledHexBytes[b])
 362             if style == asciiStyle {
 363                 // styling displayable ASCII symbols uses multiple different
 364                 // styles each time it happens, always forcing ANSI-style
 365                 // updates
 366                 style = unknownStyle
 367             }
 368         } else {
 369             w.WriteString(hexSymbols[b])
 370         }
 371         prevStyle = style
 372     }
 373 
 374     w.WriteString("\x1b[0m")
 375     if rc.showASCII {
 376         writePlainASCII(w, first, second, int(rc.perLine))
 377     }
 378 
 379     return w.WriteByte('\n')
 380 }
 381 
 382 func writeStyledCounter(w *bufio.Writer, width int, n uint) {
 383     var buf [32]byte
 384     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 385 
 386     // left-pad the final result with leading spaces
 387     writeSpaces(w, width-len(str))
 388 
 389     var style bool
 390     // emit leading part with 1 or 2 digits unstyled, ensuring the
 391     // rest or the rendered number's string is a multiple of 3 long
 392     if rem := len(str) % 3; rem != 0 {
 393         w.Write(str[:rem])
 394         str = str[rem:]
 395         // next digit-group needs some styling
 396         style = true
 397     } else {
 398         style = false
 399     }
 400 
 401     // alternate between styled/unstyled 3-digit groups
 402     for len(str) > 0 {
 403         if !style {
 404             w.Write(str[:3])
 405         } else {
 406             w.WriteString("\x1b[38;5;248m")
 407             w.Write(str[:3])
 408             w.WriteString("\x1b[0m")
 409         }
 410 
 411         style = !style
 412         str = str[3:]
 413     }
 414 }
     File: ./nhex/config.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "flag"
  31     "fmt"
  32 )
  33 
  34 const (
  35     usageMaxBytes   = `limit input up to n bytes; negative to disable`
  36     usagePerLine    = `how many bytes to show on each line`
  37     usageSkip       = `how many leading bytes to skip/ignore`
  38     usageTitle      = `use this to show a title/description`
  39     usageTo         = `the output format to use (plain or ansi)`
  40     usagePlain      = `show plain-text output, as opposed to ansi-styled output`
  41     usageShowOffset = `start lines with the offset of the 1st byte shown on each`
  42     usageShowASCII  = `repeat all ASCII strings on the side, so they're searcheable`
  43 )
  44 
  45 const defaultOffsetCounterWidth = 8
  46 
  47 const (
  48     plainOutput = `plain`
  49     ansiOutput  = `ansi`
  50 )
  51 
  52 // config is the parsed cmd-line options given to the app
  53 type config struct {
  54     // MaxBytes limits how many bytes are shown; a negative value means no limit
  55     MaxBytes int
  56 
  57     // PerLine is how many bytes are shown per output line
  58     PerLine int
  59 
  60     // Skip is how many leading bytes to skip/ignore
  61     Skip int
  62 
  63     // OffsetCounterWidth is the max string-width; not exposed as a cmd-line option
  64     OffsetCounterWidth uint
  65 
  66     // Title is an optional title preceding the output proper
  67     Title string
  68 
  69     // To is the output format
  70     To string
  71 
  72     // Filenames is the list of input filenames
  73     Filenames []string
  74 
  75     // Ruler is a prerendered ruler to emit every few output lines
  76     Ruler []byte
  77 
  78     // ShowOffsets starts lines with the offset of the 1st byte shown on each
  79     ShowOffsets bool
  80 
  81     // ShowASCII shows a side-panel with searcheable ASCII-runs
  82     ShowASCII bool
  83 }
  84 
  85 // parseFlags is the constructor for type config
  86 func parseFlags(usage string) config {
  87     flag.Usage = func() {
  88         fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
  89         flag.PrintDefaults()
  90     }
  91 
  92     cfg := config{
  93         MaxBytes:           -1,
  94         PerLine:            16,
  95         OffsetCounterWidth: 0,
  96         To:                 ansiOutput,
  97         ShowOffsets:        true,
  98         ShowASCII:          true,
  99     }
 100 
 101     plain := false
 102     flag.IntVar(&cfg.MaxBytes, `max`, cfg.MaxBytes, usageMaxBytes)
 103     flag.IntVar(&cfg.PerLine, `width`, cfg.PerLine, usagePerLine)
 104     flag.IntVar(&cfg.Skip, `skip`, cfg.Skip, usageSkip)
 105     flag.StringVar(&cfg.Title, `title`, cfg.Title, usageTitle)
 106     flag.StringVar(&cfg.To, `to`, cfg.To, usageTo)
 107     flag.BoolVar(&cfg.ShowOffsets, `n`, cfg.ShowOffsets, usageShowOffset)
 108     flag.BoolVar(&cfg.ShowASCII, `ascii`, cfg.ShowASCII, usageShowASCII)
 109     flag.BoolVar(&plain, `p`, plain, "alias for option `plain`")
 110     flag.BoolVar(&plain, `plain`, plain, usagePlain)
 111     flag.Parse()
 112 
 113     if plain {
 114         cfg.To = plainOutput
 115     }
 116 
 117     // normalize values for option -to
 118     switch cfg.To {
 119     case `text`, `plaintext`, `plain-text`:
 120         cfg.To = plainOutput
 121     }
 122 
 123     cfg.Ruler = makeRuler(cfg.PerLine)
 124     cfg.Filenames = flag.Args()
 125     return cfg
 126 }
 127 
 128 // makeRuler prerenders a ruler-line, used to make the output lines breathe
 129 func makeRuler(numitems int) []byte {
 130     if n := numitems / 4; n > 0 {
 131         var pat = []byte(`           ·`)
 132         return bytes.Repeat(pat, n)
 133     }
 134     return nil
 135 }
 136 
 137 // rendererConfig groups several arguments given to any of the rendering funcs
 138 type rendererConfig struct {
 139     // out is writer to send all output to
 140     out *bufio.Writer
 141 
 142     // offset is the byte-offset of the first byte shown on the current output
 143     // line: if shown at all, it's shown at the start the line
 144     offset uint
 145 
 146     // chunks is the 0-based counter for byte-chunks/lines shown so far, which
 147     // indirectly keeps track of when it's time to show a `breather` line
 148     chunks uint
 149 
 150     // ruler is the `ruler` content to show on `breather` lines
 151     ruler []byte
 152 
 153     // perLine is how many hex-encoded bytes are shown per line
 154     perLine uint
 155 
 156     // offsetWidth is the max string-width for the byte-offsets shown at the
 157     // start of output lines, and determines those values' left-padding
 158     offsetWidth uint
 159 
 160     // showOffsets determines whether byte-offsets are shown at all
 161     showOffsets bool
 162 
 163     // showASCII determines whether the ASCII-panels are shown at all
 164     showASCII bool
 165 }
     File: ./nhex/hex-styles.awk
   1 #!/usr/bin/awk -f
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the "Software"), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # all 0 bits
  27 $0 == 0 {
  28     print "\"\\x1b[38;5;111m00 \","
  29     next
  30 }
  31 
  32 # ascii symbol which need backslashing
  33 $0 == 34 || $0 == 92 {
  34     printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m\\%c\",\n", $0 + 0, $0
  35     next
  36 }
  37 
  38 # all other ascii symbol
  39 32 <= $0 && $0 <= 126 {
  40     printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m%c\",\n", $0 + 0, $0
  41     next
  42 }
  43 
  44 # all 1 bits
  45 $0 == 255 {
  46     print "\"\\x1b[38;5;209mff \","
  47     next
  48 }
  49 
  50 # all other bytes
  51 1 {
  52     printf "\"\\x1b[38;5;246m%02x \",\n", $0 + 0
  53     next
  54 }
     File: ./nhex/hex-symbols.awk
   1 #!/usr/bin/awk -f
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the "Software"), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # ascii symbol which need backslashing
  27 $0 == 34 || $0 == 92 {
  28     printf "\"%02x\\%c\",\n", $0 + 0, $0
  29     next
  30 }
  31 
  32 # all other ascii symbol
  33 32 <= $0 && $0 <= 126 {
  34     printf "\"%02x%c\",\n", $0 + 0, $0
  35     next
  36 }
  37 
  38 # all other bytes
  39 1 {
  40     printf "\"%02x \",\n", $0 + 0
  41     next
  42 }
     File: ./nhex/info.txt
   1 nhex [options...] [filenames...]
   2 
   3 
   4 Nice HEXadecimal is a simple hexadecimal viewer to easily inspect bytes
   5 from files/data.
   6 
   7 Each line shows the starting offset for the bytes shown, 16 of the bytes
   8 themselves in base-16 notation, and any ASCII codes when the byte values
   9 are in the typical ASCII range. The offsets shown are base-10.
  10 
  11 The base-16 codes are color-coded, with most bytes shown in gray, while
  12 all-1 and all-0 bytes are shown in orange and blue respectively.
  13 
  14 All-0 bytes are the commonest kind in most binary file types and, along
  15 with all-1 bytes are also a special case worth noticing when exploring
  16 binary data, so it makes sense for them to stand out right away.
     File: ./nhex/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "io"
  31     "math"
  32     "os"
  33 
  34     _ "embed"
  35 )
  36 
  37 //go:embed info.txt
  38 var usage string
  39 
  40 func Main() {
  41     if err := run(parseFlags(usage)); err != nil {
  42         os.Stderr.WriteString(`nhex: `)
  43         os.Stderr.WriteString(err.Error())
  44         os.Stderr.WriteString("\n")
  45         os.Exit(1)
  46         return
  47     }
  48 }
  49 
  50 func run(cfg config) error {
  51     // f, _ := os.Create(`nh.prof`)
  52     // defer f.Close()
  53     // pprof.StartCPUProfile(f)
  54     // defer pprof.StopCPUProfile()
  55 
  56     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  57     defer w.Flush()
  58 
  59     // with no filenames given, handle stdin and quit
  60     if len(cfg.Filenames) == 0 {
  61         return handle(w, os.Stdin, `<stdin>`, -1, cfg)
  62     }
  63 
  64     // show all files given
  65     for i, fname := range cfg.Filenames {
  66         if i > 0 {
  67             w.WriteString("\n")
  68             w.WriteString("\n")
  69         }
  70 
  71         err := handleFile(w, fname, cfg)
  72         if err != nil {
  73             return err
  74         }
  75     }
  76 
  77     return nil
  78 }
  79 
  80 // handleFile is like handleReader, except it also shows file-related info
  81 func handleFile(w *bufio.Writer, fname string, cfg config) error {
  82     f, err := os.Open(fname)
  83     if err != nil {
  84         return err
  85     }
  86     defer f.Close()
  87 
  88     stat, err := f.Stat()
  89     if err != nil {
  90         return handle(w, f, fname, -1, cfg)
  91     }
  92 
  93     fsize := int(stat.Size())
  94     return handle(w, f, fname, fsize, cfg)
  95 }
  96 
  97 // handle shows some messages related to the input and the cmd-line options
  98 // used, and then follows them by the hexadecimal byte-view
  99 func handle(w *bufio.Writer, r io.Reader, name string, size int, cfg config) error {
 100     skip(r, cfg.Skip)
 101     if cfg.MaxBytes > 0 {
 102         r = io.LimitReader(r, int64(cfg.MaxBytes))
 103     }
 104 
 105     // finish config setup based on the filesize, if a valid one was given
 106     if cfg.OffsetCounterWidth < 1 {
 107         if size < 1 {
 108             cfg.OffsetCounterWidth = defaultOffsetCounterWidth
 109         } else {
 110             w := math.Log10(float64(size))
 111             w = math.Max(math.Ceil(w), 1)
 112             cfg.OffsetCounterWidth = uint(w)
 113         }
 114     }
 115 
 116     switch cfg.To {
 117     case plainOutput:
 118         writeMetaPlain(w, name, size, cfg)
 119         // when done, emit a new line in case only part of the last line is
 120         // shown, which means no newline was emitted for it
 121         defer w.WriteString("\n")
 122         return render(w, r, cfg, writeBufferPlain)
 123 
 124     case ansiOutput:
 125         writeMetaANSI(w, name, size, cfg)
 126         // when done, emit a new line in case only part of the last line is
 127         // shown, which means no newline was emitted for it
 128         defer w.WriteString("\x1b[0m\n")
 129         return render(w, r, cfg, writeBufferANSI)
 130 
 131     default:
 132         const fs = `unsupported output format %q`
 133         return fmt.Errorf(fs, cfg.To)
 134     }
 135 }
 136 
 137 // skip ignores n bytes from the reader given
 138 func skip(r io.Reader, n int) {
 139     if n < 1 {
 140         return
 141     }
 142 
 143     // use func Seek for input files, except for stdin, which you can't seek
 144     if f, ok := r.(*os.File); ok && r != os.Stdin {
 145         f.Seek(int64(n), io.SeekCurrent)
 146         return
 147     }
 148     io.CopyN(io.Discard, r, int64(n))
 149 }
 150 
 151 // renderer is the type for the hex-view render funcs
 152 type renderer func(rc rendererConfig, first, second []byte) error
 153 
 154 // render reads all input and shows the hexadecimal byte-view for the input
 155 // data via the rendering callback given
 156 func render(w *bufio.Writer, r io.Reader, cfg config, fn renderer) error {
 157     if cfg.PerLine < 1 {
 158         cfg.PerLine = 16
 159     }
 160 
 161     rc := rendererConfig{
 162         out:     w,
 163         offset:  uint(cfg.Skip),
 164         chunks:  0,
 165         perLine: uint(cfg.PerLine),
 166         ruler:   cfg.Ruler,
 167 
 168         offsetWidth: cfg.OffsetCounterWidth,
 169         showOffsets: cfg.ShowOffsets,
 170         showASCII:   cfg.ShowASCII,
 171     }
 172 
 173     // calling func Read directly can sometimes result in chunks shorter
 174     // than the max chunk-size, even when there are plenty of bytes yet
 175     // to read; to avoid that, use a buffered-reader to explicitly fill
 176     // a slice instead
 177     br := bufio.NewReader(r)
 178 
 179     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 180     cur := make([]byte, 0, cfg.PerLine)
 181     ahead := make([]byte, 0, cfg.PerLine)
 182 
 183     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 184     // so to speak
 185     cur, err := fillChunk(cur[:0], cfg.PerLine, br)
 186     if len(cur) == 0 {
 187         if err == io.EOF {
 188             err = nil
 189         }
 190         return err
 191     }
 192 
 193     for {
 194         ahead, err := fillChunk(ahead[:0], cfg.PerLine, br)
 195         if err != nil && err != io.EOF {
 196             return err
 197         }
 198 
 199         if len(ahead) == 0 {
 200             // done, maybe except for an extra line of output
 201             break
 202         }
 203 
 204         // show the byte-chunk on its own output line
 205         err = fn(rc, cur, ahead)
 206         if err != nil {
 207             // probably a pipe was closed
 208             return nil
 209         }
 210 
 211         rc.chunks++
 212         rc.offset += uint(len(cur))
 213         cur = cur[:copy(cur, ahead)]
 214     }
 215 
 216     // don't forget the last output line
 217     if len(cur) > 0 {
 218         return fn(rc, cur, nil)
 219     }
 220     return nil
 221 }
 222 
 223 // fillChunk tries to read the number of bytes given, appending them to the
 224 // byte-slice given; this func returns an EOF error only when no bytes are
 225 // read, which somewhat simplifies error-handling for the func caller
 226 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 227     // read buffered-bytes up to the max chunk-size
 228     for i := 0; i < n; i++ {
 229         b, err := br.ReadByte()
 230         if err == nil {
 231             chunk = append(chunk, b)
 232             continue
 233         }
 234 
 235         if err == io.EOF && i > 0 {
 236             return chunk, nil
 237         }
 238         return chunk, err
 239     }
 240 
 241     // got the full byte-count asked for
 242     return chunk, nil
 243 }
     File: ./nhex/numbers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "math"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
  36 // handles negatives, even though this app only uses it with non-negatives.
  37 func loopThousandsGroups(n int, fn func(i, n int)) {
  38     // 0 doesn't have a log10
  39     if n == 0 {
  40         fn(0, 0)
  41         return
  42     }
  43 
  44     sign := +1
  45     if n < 0 {
  46         n = -n
  47         sign = -1
  48     }
  49 
  50     intLog1000 := int(math.Log10(float64(n)) / 3)
  51     remBase := int(math.Pow10(3 * intLog1000))
  52 
  53     for i := 0; remBase > 0; i++ {
  54         group := (1000 * n) / remBase / 1000
  55         fn(i, sign*group)
  56         // if original number was negative, ensure only first
  57         // group gives a negative input to the callback
  58         sign = +1
  59 
  60         n %= remBase
  61         remBase /= 1000
  62     }
  63 }
  64 
  65 // sprintCommas turns the non-negative number given into a readable string,
  66 // where digits are grouped-separated by commas
  67 func sprintCommas(n int) string {
  68     var sb strings.Builder
  69     loopThousandsGroups(n, func(i, n int) {
  70         if i == 0 {
  71             var buf [4]byte
  72             sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
  73             return
  74         }
  75         sb.WriteByte(',')
  76         writePad0Sub1000Counter(&sb, uint(n))
  77     })
  78     return sb.String()
  79 }
  80 
  81 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
  82 func writePad0Sub1000Counter(w io.Writer, n uint) {
  83     // precondition is 0...999
  84     if n > 999 {
  85         w.Write([]byte(`???`))
  86         return
  87     }
  88 
  89     var buf [3]byte
  90     buf[0] = byte(n/100) + '0'
  91     n %= 100
  92     buf[1] = byte(n/10) + '0'
  93     buf[2] = byte(n%10) + '0'
  94     w.Write(buf[:])
  95 }
  96 
  97 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
  98 // matters because it's called for every byte of input which isn't
  99 // all 0s or all 1s
 100 func writeHex(w *bufio.Writer, b byte) {
 101     const hexDigits = `0123456789abcdef`
 102     w.WriteByte(hexDigits[b>>4])
 103     w.WriteByte(hexDigits[b&0x0f])
 104 }
     File: ./nhex/plain.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "strconv"
  31 )
  32 
  33 // padding is the padding/spacing emitted across each output line, except for
  34 // the breather/ruler lines
  35 const padding = `  `
  36 
  37 // writeMetaPlain shows metadata right before the plain-text hex byte-view
  38 func writeMetaPlain(w *bufio.Writer, fname string, fsize int, cfg config) {
  39     if cfg.Title != `` {
  40         w.WriteString(cfg.Title)
  41         w.WriteString("\n")
  42         w.WriteString("\n")
  43     }
  44 
  45     if fsize < 0 {
  46         fmt.Fprintf(w, "• %s\n", fname)
  47     } else {
  48         const fs = "• %s  (%s bytes)\n"
  49         fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
  50     }
  51 
  52     if cfg.Skip > 0 {
  53         const fs = "   skipping first %s bytes\n"
  54         fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
  55     }
  56     if cfg.MaxBytes > 0 {
  57         const fs = "   showing only up to %s bytes\n"
  58         fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
  59     }
  60     w.WriteString("\n")
  61 }
  62 
  63 // writeBufferPlain shows the hex byte-view withOUT using ANSI colors/styles
  64 func writeBufferPlain(rc rendererConfig, first, second []byte) error {
  65     // show a ruler every few lines to make eye-scanning easier
  66     if rc.chunks%5 == 0 && rc.chunks > 0 {
  67         rc.out.WriteByte('\n')
  68     }
  69 
  70     return writeLinePlain(rc, first, second)
  71 }
  72 
  73 func writeLinePlain(rc rendererConfig, first, second []byte) error {
  74     w := rc.out
  75 
  76     // start each line with the byte-offset for the 1st item shown on it
  77     if rc.showOffsets {
  78         writePlainCounter(w, int(rc.offsetWidth), rc.offset)
  79         w.WriteByte(' ')
  80     } else {
  81         w.WriteString(padding)
  82     }
  83 
  84     for _, b := range first {
  85         // fmt.Fprintf(w, ` %02x`, b)
  86         //
  87         // the commented part above was a performance bottleneck, since
  88         // the slow/generic fmt.Fprintf was called for each input byte
  89         w.WriteByte(' ')
  90         writeHex(w, b)
  91     }
  92 
  93     if rc.showASCII {
  94         writePlainASCII(w, first, second, int(rc.perLine))
  95     }
  96 
  97     return w.WriteByte('\n')
  98 }
  99 
 100 // writePlainCounter just emits a left-padded number
 101 func writePlainCounter(w *bufio.Writer, width int, n uint) {
 102     var buf [32]byte
 103     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 104     writeSpaces(w, width-len(str))
 105     w.Write(str)
 106 }
 107 
 108 // writeRulerPlain emits a breather line using a ruler-like pattern of spaces
 109 // and dots, to guide the eye across the main output lines
 110 // func writeRulerPlain(w *bufio.Writer, indent int, offset int, numitems int) {
 111 //  writeSpaces(w, indent)
 112 //  for i := 0; i < numitems-1; i++ {
 113 //      if (i+offset+1)%5 == 0 {
 114 //          w.WriteString(`   `)
 115 //      } else {
 116 //          w.WriteString(`  ·`)
 117 //      }
 118 //  }
 119 // }
 120 
 121 // writeSpaces bulk-emits the number of spaces given
 122 func writeSpaces(w *bufio.Writer, n int) {
 123     const spaces = `                                `
 124     for ; n > len(spaces); n -= len(spaces) {
 125         w.WriteString(spaces)
 126     }
 127     if n > 0 {
 128         w.WriteString(spaces[:n])
 129     }
 130 }
 131 
 132 // writePlainASCII emits the side-panel showing all ASCII runs for each line
 133 func writePlainASCII(w *bufio.Writer, first, second []byte, perline int) {
 134     // prev keeps track of the previous byte, so spaces are added
 135     // when bytes change from non-visible-ASCII to visible-ASCII
 136     prev := byte(0)
 137 
 138     spaces := 3*(perline-len(first)) + len(padding)
 139 
 140     for _, b := range first {
 141         if 32 < b && b < 127 {
 142             if !(32 < prev && prev < 127) {
 143                 writeSpaces(w, spaces)
 144                 spaces = 1
 145             }
 146             w.WriteByte(b)
 147         }
 148         prev = b
 149     }
 150 
 151     for _, b := range second {
 152         if 32 < b && b < 127 {
 153             if !(32 < prev && prev < 127) {
 154                 writeSpaces(w, spaces)
 155                 spaces = 1
 156             }
 157             w.WriteByte(b)
 158         }
 159         prev = b
 160     }
 161 }
     File: ./njson/njson.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package njson
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 njson [filepath...]
  37 
  38 Nice JSON shows JSON data as ANSI-styled indented lines, using 2 spaces for
  39 each indentation level.
  40 `
  41 
  42 // indent is how many spaces each indentation level uses
  43 const indent = 2
  44 
  45 const (
  46     // boolStyle is bluish, and very distinct from all other colors used
  47     boolStyle = "\x1b[38;2;95;175;215m"
  48 
  49     // keyStyle is magenta, and very distinct from normal strings
  50     keyStyle = "\x1b[38;2;135;95;255m"
  51 
  52     // nullStyle is a light-gray, just like syntax elements, but the word
  53     // `null` is wide enough to stand out from syntax items at a glance
  54     nullStyle = syntaxStyle
  55 
  56     // positiveNumberStyle is a nice green
  57     positiveNumberStyle = "\x1b[38;2;0;135;95m"
  58 
  59     // negativeNumberStyle is a nice red
  60     negativeNumberStyle = "\x1b[38;2;204;0;0m"
  61 
  62     // zeroNumberStyle is a nice blue
  63     zeroNumberStyle = "\x1b[38;2;0;95;215m"
  64 
  65     // stringStyle used to be bluish, but it's better to keep it plain,
  66     // which also minimizes how many different colors the output can show
  67     stringStyle = ""
  68 
  69     // syntaxStyle is a light-gray, not too light, not too dark
  70     syntaxStyle = "\x1b[38;2;168;168;168m"
  71 )
  72 
  73 func Main() {
  74     args := os.Args[1:]
  75 
  76     if len(args) > 0 {
  77         switch args[0] {
  78         case `-h`, `--h`, `-help`, `--help`:
  79             os.Stdout.WriteString(info[1:])
  80             return
  81         }
  82     }
  83 
  84     if len(args) > 0 && args[0] == `--` {
  85         args = args[1:]
  86     }
  87 
  88     if len(args) > 1 {
  89         showError(errors.New(`multiple inputs not allowed`))
  90         os.Exit(1)
  91         return
  92     }
  93 
  94     // figure out whether input should come from a named file or from stdin
  95     name := `-`
  96     if len(args) == 1 {
  97         name = args[0]
  98     }
  99 
 100     var err error
 101     if name == `-` {
 102         // handle lack of filepath arg, or `-` as the filepath
 103         err = niceJSON(os.Stdout, os.Stdin)
 104     } else {
 105         // handle being given a normal filepath
 106         err = handleFile(os.Stdout, os.Args[1])
 107     }
 108 
 109     if err != nil && err != io.EOF {
 110         showError(err)
 111         os.Exit(1)
 112         return
 113     }
 114 }
 115 
 116 // showError standardizes how errors look in this app
 117 func showError(err error) {
 118     os.Stderr.WriteString(`njson: `)
 119     os.Stderr.WriteString(err.Error())
 120     os.Stderr.WriteString("\n")
 121 }
 122 
 123 // writeSpaces does what it says, minimizing calls to write-like funcs
 124 func writeSpaces(w *bufio.Writer, n int) {
 125     const spaces = `                                `
 126     for n >= len(spaces) {
 127         w.WriteString(spaces)
 128         n -= len(spaces)
 129     }
 130     if n > 0 {
 131         w.WriteString(spaces[:n])
 132     }
 133 }
 134 
 135 func handleFile(w io.Writer, path string) error {
 136     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
 137     //  resp, err := http.Get(path)
 138     //  if err != nil {
 139     //      return err
 140     //  }
 141     //  defer resp.Body.Close()
 142     //  return niceJSON(w, resp.Body)
 143     // }
 144 
 145     f, err := os.Open(path)
 146     if err != nil {
 147         // on windows, file-not-found error messages may mention `CreateFile`,
 148         // even when trying to open files in read-only mode
 149         return errors.New(`can't open file named ` + path)
 150     }
 151     defer f.Close()
 152 
 153     return niceJSON(w, f)
 154 }
 155 
 156 func niceJSON(w io.Writer, r io.Reader) error {
 157     bw := bufio.NewWriterSize(w, 32*1024)
 158     defer bw.Flush()
 159 
 160     dec := json.NewDecoder(r)
 161     // using string-like json.Number values instead of float64 ones avoids
 162     // unneeded reformatting of numbers; reformatting parsed float64 values
 163     // can potentially even drop/change decimals, causing the output not to
 164     // match the input digits exactly, which is best to avoid
 165     dec.UseNumber()
 166 
 167     t, err := dec.Token()
 168     if err == io.EOF {
 169         return errors.New(`empty input isn't valid JSON`)
 170     }
 171     if err != nil {
 172         return err
 173     }
 174 
 175     if err := handleToken(bw, dec, t, 0, 0); err != nil {
 176         return err
 177     }
 178     // don't forget to end the last output line
 179     bw.WriteByte('\n')
 180 
 181     if _, err := dec.Token(); err != io.EOF {
 182         return errors.New(`unexpected trailing JSON data`)
 183     }
 184     return nil
 185 }
 186 
 187 func handleToken(w *bufio.Writer, d *json.Decoder, t json.Token, pre, level int) error {
 188     switch t := t.(type) {
 189     case json.Delim:
 190         switch t {
 191         case json.Delim('['):
 192             return handleArray(w, d, pre, level)
 193 
 194         case json.Delim('{'):
 195             return handleObject(w, d, pre, level)
 196 
 197         default:
 198             // return fmt.Errorf(`unsupported JSON delimiter %v`, t)
 199             return errors.New(`unsupported JSON delimiter`)
 200         }
 201 
 202     case nil:
 203         return handleNull(w, pre)
 204 
 205     case bool:
 206         return handleBoolean(w, t, pre)
 207 
 208     case string:
 209         return handleString(w, t, pre)
 210 
 211     case json.Number:
 212         return handleNumber(w, t, pre)
 213 
 214     default:
 215         // return fmt.Errorf(`unsupported token type %T`, t)
 216         return errors.New(`unsupported token type`)
 217     }
 218 }
 219 
 220 func handleArray(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 221     for i := 0; true; i++ {
 222         t, err := d.Token()
 223         if err == io.EOF {
 224             return errors.New(`end of JSON before array was closed`)
 225         }
 226         if err != nil {
 227             return err
 228         }
 229 
 230         if t == json.Delim(']') {
 231             if i == 0 {
 232                 writeSpaces(w, indent*pre)
 233                 w.WriteString(syntaxStyle + "[]\x1b[0m")
 234             } else {
 235                 w.WriteString("\n")
 236                 writeSpaces(w, indent*level)
 237                 w.WriteString(syntaxStyle + "]\x1b[0m")
 238             }
 239             return nil
 240         }
 241 
 242         if i == 0 {
 243             writeSpaces(w, indent*pre)
 244             w.WriteString(syntaxStyle + "[\x1b[0m\n")
 245         } else {
 246             // this is a good spot to check for early-quit opportunities
 247             w.WriteString(syntaxStyle + ",\x1b[0m\n")
 248         }
 249 
 250         if err := handleToken(w, d, t, level+1, level+1); err != nil {
 251             return err
 252         }
 253     }
 254 
 255     // make the compiler happy
 256     return nil
 257 }
 258 
 259 func handleBoolean(w *bufio.Writer, b bool, pre int) error {
 260     writeSpaces(w, indent*pre)
 261     if b {
 262         w.WriteString(boolStyle + "true\x1b[0m")
 263     } else {
 264         w.WriteString(boolStyle + "false\x1b[0m")
 265     }
 266     return nil
 267 }
 268 
 269 func handleKey(w *bufio.Writer, s string, pre int) error {
 270     writeSpaces(w, indent*pre)
 271     w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle)
 272     w.WriteString(s)
 273     w.WriteString(syntaxStyle + "\":\x1b[0m ")
 274     return nil
 275 }
 276 
 277 func handleNull(w *bufio.Writer, pre int) error {
 278     writeSpaces(w, indent*pre)
 279     w.WriteString(nullStyle + "null\x1b[0m")
 280     return nil
 281 }
 282 
 283 // func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 284 //  writeSpaces(w, indent*pre)
 285 //  w.WriteString(numberStyle)
 286 //  w.WriteString(n.String())
 287 //  w.WriteString("\x1b[0m")
 288 //  return nil
 289 // }
 290 
 291 func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 292     writeSpaces(w, indent*pre)
 293     f, _ := n.Float64()
 294     if f > 0 {
 295         w.WriteString(positiveNumberStyle)
 296     } else if f < 0 {
 297         w.WriteString(negativeNumberStyle)
 298     } else {
 299         w.WriteString(zeroNumberStyle)
 300     }
 301     w.WriteString(n.String())
 302     w.WriteString("\x1b[0m")
 303     return nil
 304 }
 305 
 306 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 307     for i := 0; true; i++ {
 308         t, err := d.Token()
 309         if err == io.EOF {
 310             return errors.New(`end of JSON before object was closed`)
 311         }
 312         if err != nil {
 313             return err
 314         }
 315 
 316         if t == json.Delim('}') {
 317             if i == 0 {
 318                 writeSpaces(w, indent*pre)
 319                 w.WriteString(syntaxStyle + "{}\x1b[0m")
 320             } else {
 321                 w.WriteString("\n")
 322                 writeSpaces(w, indent*level)
 323                 w.WriteString(syntaxStyle + "}\x1b[0m")
 324             }
 325             return nil
 326         }
 327 
 328         if i == 0 {
 329             writeSpaces(w, indent*pre)
 330             w.WriteString(syntaxStyle + "{\x1b[0m\n")
 331         } else {
 332             // this is a good spot to check for early-quit opportunities
 333             w.WriteString(syntaxStyle + ",\x1b[0m\n")
 334         }
 335 
 336         // the stdlib's JSON parser is supposed to complain about non-string
 337         // keys anyway, but make sure just in case
 338         k, ok := t.(string)
 339         if !ok {
 340             return errors.New(`expected key to be a string`)
 341         }
 342         if err := handleKey(w, k, level+1); err != nil {
 343             return err
 344         }
 345 
 346         // handle value
 347         t, err = d.Token()
 348         if err != nil {
 349             return err
 350         }
 351         if err := handleToken(w, d, t, 0, level+1); err != nil {
 352             return err
 353         }
 354     }
 355 
 356     // make the compiler happy
 357     return nil
 358 }
 359 
 360 func needsEscaping(s string) bool {
 361     for _, r := range s {
 362         switch r {
 363         case '"', '\\', '\t', '\r', '\n':
 364             return true
 365         }
 366     }
 367     return false
 368 }
 369 
 370 func handleString(w *bufio.Writer, s string, pre int) error {
 371     writeSpaces(w, indent*pre)
 372     w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle)
 373     if !needsEscaping(s) {
 374         w.WriteString(s)
 375     } else {
 376         escapeString(w, s)
 377     }
 378     w.WriteString(syntaxStyle + "\"\x1b[0m")
 379     return nil
 380 }
 381 
 382 func escapeString(w *bufio.Writer, s string) {
 383     for _, r := range s {
 384         switch r {
 385         case '"', '\\':
 386             w.WriteByte('\\')
 387             w.WriteRune(r)
 388         case '\t':
 389             w.WriteByte('\\')
 390             w.WriteByte('t')
 391         case '\r':
 392             w.WriteByte('\\')
 393             w.WriteByte('r')
 394         case '\n':
 395             w.WriteByte('\\')
 396             w.WriteByte('n')
 397         default:
 398             w.WriteRune(r)
 399         }
 400     }
 401 }
     File: ./nl/nl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nl
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 nl [options...] [files...]
  37 
  38 Number Lines from all the inputs. When not given any filepaths, the standard
  39 input is used instead. Only non-empty lines advance the line-counter; also,
  40 empty lines don't show the leading counter.
  41 
  42 Options
  43 
  44     -h, -help      show this help message
  45     -v [number]    change starting number to count lines with (default is 1)
  46 `
  47 
  48 func Main() {
  49     start := 1
  50 
  51     args := os.Args[1:]
  52     for len(args) > 0 {
  53         switch args[0] {
  54         case `-v`:
  55             args = args[1:]
  56             if len(args) == 0 {
  57                 os.Stderr.WriteString("missing starting number\n")
  58                 os.Exit(1)
  59                 return
  60             }
  61 
  62             s := strings.Replace(args[0], `_`, ``, -1)
  63             n, err := strconv.ParseInt(s, 10, 64)
  64             if err != nil {
  65                 os.Stderr.WriteString(`nl: invalid number: `)
  66                 os.Stderr.WriteString(err.Error())
  67                 os.Stderr.WriteString("\n")
  68                 os.Exit(1)
  69                 return
  70             }
  71 
  72             args = args[1:]
  73             start = int(n)
  74             continue
  75 
  76         case `--help`:
  77             os.Stderr.WriteString(info[1:])
  78             return
  79         }
  80 
  81         break
  82     }
  83 
  84     if len(args) > 0 && args[0] == `--` {
  85         args = args[1:]
  86     }
  87 
  88     if err := run(args, start); err != nil && err != io.EOF {
  89         os.Stderr.WriteString(`nl: `)
  90         os.Stderr.WriteString(err.Error())
  91         os.Stderr.WriteString("\n")
  92         os.Exit(1)
  93         return
  94     }
  95 }
  96 
  97 func run(paths []string, count int) error {
  98     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  99     defer w.Flush()
 100 
 101     for _, path := range paths {
 102         if err := handleFile(w, path, &count); err != nil {
 103             return err
 104         }
 105     }
 106 
 107     if len(paths) == 0 {
 108         if err := nl(w, os.Stdin, &count); err != nil {
 109             return err
 110         }
 111     }
 112     return nil
 113 }
 114 
 115 func handleFile(w *bufio.Writer, path string, count *int) error {
 116     f, err := os.Open(path)
 117     if err != nil {
 118         return err
 119     }
 120     defer f.Close()
 121     return nl(w, f, count)
 122 }
 123 
 124 func nl(w *bufio.Writer, r io.Reader, count *int) error {
 125     const gb = 1024 * 1024 * 1024
 126     sc := bufio.NewScanner(r)
 127     sc.Buffer(nil, 8*gb)
 128 
 129     const spaces = `      `
 130     var buf [24]byte
 131 
 132     for sc.Scan() {
 133         s := sc.Bytes()
 134 
 135         // only number non-empty lines, just like the standard `nl` command
 136         if len(s) > 0 {
 137             n := strconv.AppendInt(buf[:0], int64(*count), 10)
 138 
 139             // right-align the line-count
 140             if len(n) < len(spaces) {
 141                 w.WriteString(spaces[:len(spaces)-len(n)])
 142             }
 143             w.Write(n)
 144 
 145             // separate the line count and the line with a few spaces
 146             w.WriteString(spaces[:2])
 147         }
 148 
 149         w.Write(s)
 150         if w.WriteByte('\n') != nil {
 151             return io.EOF
 152         }
 153 
 154         if len(s) > 0 {
 155             *count++
 156         }
 157     }
 158 
 159     return sc.Err()
 160 }
     File: ./nn/nn.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nn
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 nn [options...] [file...]
  38 
  39 
  40 Nice Numbers is an app which renders the UTF-8 text it's given to make long
  41 numbers much easier to read. It does so by alternating 3-digit groups which
  42 are colored using ANSI-codes with plain/unstyled 3-digit groups.
  43 
  44 Unlike the common practice of inserting commas between 3-digit groups, this
  45 trick doesn't widen the original text, keeping alignments across lines the
  46 same.
  47 
  48 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  49 feeds.
  50 
  51 All (optional) leading options start with either single or double-dash,
  52 and most of them change the style/color used. Some of the options are,
  53 shown in their single-dash form:
  54 
  55     -h, -help    show this help message
  56 
  57     -b          use a blue color
  58     -blue       use a blue color
  59     -bold       bold-style digits
  60     -g          use a green color
  61     -gray       use a gray color (default)
  62     -green      use a green color
  63     -hi         use a highlighting/inverse style
  64     -m          use a magenta color
  65     -magenta    use a magenta color
  66     -o          use an orange color
  67     -orange     use an orange color
  68     -r          use a red color
  69     -red        use a red color
  70     -u          underline digits
  71     -underline  underline digits
  72 `
  73 
  74 type config struct {
  75     // style is the ANSI-style sequence to use verbatim
  76     style string
  77 
  78     // live is whether lines are flushed each time
  79     live bool
  80 }
  81 
  82 func Main() {
  83     var cfg config
  84     cfg.live = true
  85     args := os.Args[1:]
  86 
  87     for len(args) > 0 {
  88         switch args[0] {
  89         case `-b`, `--b`, `-buffered`, `--buffered`:
  90             cfg.live = false
  91             args = args[1:]
  92             continue
  93 
  94         case `-h`, `--h`, `-help`, `--help`:
  95             os.Stdout.WriteString(info[1:])
  96             return
  97         }
  98 
  99         break
 100     }
 101 
 102     options := true
 103     if len(args) > 0 && args[0] == `--` {
 104         options = false
 105         args = args[1:]
 106     }
 107 
 108     cfg.style, _ = lookupStyle(`gray`)
 109 
 110     // if the first argument is 1 or 2 dashes followed by a supported
 111     // style-name, change the style used
 112     if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
 113         name := args[0]
 114         name = strings.TrimPrefix(name, `-`)
 115         name = strings.TrimPrefix(name, `-`)
 116         args = args[1:]
 117 
 118         // check if the `dedashed` argument is a supported style-name
 119         if s, ok := lookupStyle(name); ok {
 120             cfg.style = s
 121         } else {
 122             os.Stderr.WriteString(`nn: invalid style name `)
 123             os.Stderr.WriteString(name)
 124             os.Stderr.WriteString("\n")
 125             os.Exit(1)
 126             return
 127         }
 128     }
 129 
 130     if cfg.live {
 131         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 132             cfg.live = false
 133         }
 134     }
 135 
 136     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 137         os.Stderr.WriteString(`nn: `)
 138         os.Stderr.WriteString(err.Error())
 139         os.Stderr.WriteString("\n")
 140         os.Exit(1)
 141         return
 142     }
 143 }
 144 
 145 func run(w io.Writer, args []string, cfg config) error {
 146     bw := bufio.NewWriterSize(w, 32*1024)
 147     defer bw.Flush()
 148 
 149     if len(args) == 0 {
 150         return restyle(bw, os.Stdin, cfg)
 151     }
 152 
 153     for _, name := range args {
 154         if err := handleFile(bw, name, cfg); err != nil {
 155             return err
 156         }
 157     }
 158     return nil
 159 }
 160 
 161 func handleFile(w *bufio.Writer, name string, cfg config) error {
 162     if name == `` || name == `-` {
 163         return restyle(w, os.Stdin, cfg)
 164     }
 165 
 166     f, err := os.Open(name)
 167     if err != nil {
 168         return errors.New(`can't read from file named "` + name + `"`)
 169     }
 170     defer f.Close()
 171 
 172     return restyle(w, f, cfg)
 173 }
 174 
 175 func restyle(w *bufio.Writer, r io.Reader, cfg config) error {
 176     const gb = 1024 * 1024 * 1024
 177     sc := bufio.NewScanner(r)
 178     sc.Buffer(nil, 8*gb)
 179 
 180     for i := 0; sc.Scan(); i++ {
 181         s := sc.Bytes()
 182         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 183             s = s[3:]
 184         }
 185 
 186         restyleLine(w, s, cfg.style)
 187         if w.WriteByte('\n') != nil {
 188             return io.EOF
 189         }
 190 
 191         if cfg.live && w.Flush() != nil {
 192             // a write error may be the consequence of stdout being closed,
 193             // perhaps by another app along a pipe
 194             return io.EOF
 195         }
 196     }
 197     return sc.Err()
 198 }
 199 
 200 func lookupStyle(name string) (style string, ok bool) {
 201     if alias, ok := styleAliases[name]; ok {
 202         name = alias
 203     }
 204 
 205     style, ok = styles[name]
 206     return style, ok
 207 }
 208 
 209 var styleAliases = map[string]string{
 210     `b`: `blue`,
 211     `g`: `green`,
 212     `m`: `magenta`,
 213     `o`: `orange`,
 214     `p`: `purple`,
 215     `r`: `red`,
 216     `u`: `underline`,
 217 
 218     `bolded`:      `bold`,
 219     `h`:           `inverse`,
 220     `hi`:          `inverse`,
 221     `highlight`:   `inverse`,
 222     `highlighted`: `inverse`,
 223     `hilite`:      `inverse`,
 224     `hilited`:     `inverse`,
 225     `inv`:         `inverse`,
 226     `invert`:      `inverse`,
 227     `inverted`:    `inverse`,
 228     `underlined`:  `underline`,
 229 
 230     `bb`: `blueback`,
 231     `bg`: `greenback`,
 232     `bm`: `magentaback`,
 233     `bo`: `orangeback`,
 234     `bp`: `purpleback`,
 235     `br`: `redback`,
 236 
 237     `gb`: `greenback`,
 238     `mb`: `magentaback`,
 239     `ob`: `orangeback`,
 240     `pb`: `purpleback`,
 241     `rb`: `redback`,
 242 
 243     `bblue`:    `blueback`,
 244     `bgray`:    `grayback`,
 245     `bgreen`:   `greenback`,
 246     `bmagenta`: `magentaback`,
 247     `borange`:  `orangeback`,
 248     `bpurple`:  `purpleback`,
 249     `bred`:     `redback`,
 250 
 251     `backblue`:    `blueback`,
 252     `backgray`:    `grayback`,
 253     `backgreen`:   `greenback`,
 254     `backmagenta`: `magentaback`,
 255     `backorange`:  `orangeback`,
 256     `backpurple`:  `purpleback`,
 257     `backred`:     `redback`,
 258 }
 259 
 260 // styles turns style-names into the ANSI-code sequences used for the
 261 // alternate groups of digits
 262 var styles = map[string]string{
 263     `blue`:      "\x1b[38;2;0;95;215m",
 264     `bold`:      "\x1b[1m",
 265     `gray`:      "\x1b[38;2;168;168;168m",
 266     `green`:     "\x1b[38;2;0;135;95m",
 267     `inverse`:   "\x1b[7m",
 268     `magenta`:   "\x1b[38;2;215;0;255m",
 269     `orange`:    "\x1b[38;2;215;95;0m",
 270     `plain`:     "\x1b[0m",
 271     `red`:       "\x1b[38;2;204;0;0m",
 272     `underline`: "\x1b[4m",
 273 
 274     // `blue`:      "\x1b[38;5;26m",
 275     // `bold`:      "\x1b[1m",
 276     // `gray`:      "\x1b[38;5;248m",
 277     // `green`:     "\x1b[38;5;29m",
 278     // `inverse`:   "\x1b[7m",
 279     // `magenta`:   "\x1b[38;5;99m",
 280     // `orange`:    "\x1b[38;5;166m",
 281     // `plain`:     "\x1b[0m",
 282     // `red`:       "\x1b[31m",
 283     // `underline`: "\x1b[4m",
 284 
 285     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 286     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 287     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 288     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 289     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 290     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 291     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 292 }
 293 
 294 // restyleLine renders the line given, using ANSI-styles to make any long
 295 // numbers in it more legible; this func doesn't emit a line-feed, which
 296 // is up to its caller
 297 func restyleLine(w *bufio.Writer, line []byte, style string) {
 298     for len(line) > 0 {
 299         i := indexDigit(line)
 300         if i < 0 {
 301             // no (more) digits to style for sure
 302             w.Write(line)
 303             return
 304         }
 305 
 306         // emit line before current digit-run
 307         w.Write(line[:i])
 308         // advance to the start of the current digit-run
 309         line = line[i:]
 310 
 311         // see where the digit-run ends
 312         j := indexNonDigit(line)
 313         if j < 0 {
 314             // the digit-run goes until the end
 315             restyleDigits(w, line, style)
 316             return
 317         }
 318 
 319         // emit styled digit-run
 320         restyleDigits(w, line[:j], style)
 321         // skip right past the end of the digit-run
 322         line = line[j:]
 323     }
 324 }
 325 
 326 // indexDigit finds the index of the first digit in a string, or -1 when the
 327 // string has no decimal digits
 328 func indexDigit(s []byte) int {
 329     for i := 0; i < len(s); i++ {
 330         switch s[i] {
 331         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 332             return i
 333         }
 334     }
 335 
 336     // empty slice, or a slice without any digits
 337     return -1
 338 }
 339 
 340 // indexNonDigit finds the index of the first non-digit in a string, or -1
 341 // when the string is all decimal digits
 342 func indexNonDigit(s []byte) int {
 343     for i := 0; i < len(s); i++ {
 344         switch s[i] {
 345         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 346             continue
 347         default:
 348             return i
 349         }
 350     }
 351 
 352     // empty slice, or a slice which only has digits
 353     return -1
 354 }
 355 
 356 // restyleDigits renders a run of digits as alternating styled/unstyled runs
 357 // of 3 digits, which greatly improves readability, and is the only purpose
 358 // of this app; string is assumed to be all decimal digits
 359 func restyleDigits(w *bufio.Writer, digits []byte, altStyle string) {
 360     if len(digits) < 4 {
 361         // digit sequence is short, so emit it as is
 362         w.Write(digits)
 363         return
 364     }
 365 
 366     // separate leading 0..2 digits which don't align with the 3-digit groups
 367     i := len(digits) % 3
 368     // emit leading digits unstyled, if there are any
 369     w.Write(digits[:i])
 370     // the rest is guaranteed to have a length which is a multiple of 3
 371     digits = digits[i:]
 372 
 373     // start by styling, unless there were no leading digits
 374     style := i != 0
 375 
 376     for len(digits) > 0 {
 377         if style {
 378             w.WriteString(altStyle)
 379             w.Write(digits[:3])
 380             w.Write([]byte{'\x1b', '[', '0', 'm'})
 381         } else {
 382             w.Write(digits[:3])
 383         }
 384 
 385         // advance to the next triple: the start of this func is supposed
 386         // to guarantee this step always works
 387         digits = digits[3:]
 388 
 389         // alternate between styled and unstyled 3-digit groups
 390         style = !style
 391     }
 392 }
     File: ./nn/nn_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nn
  26 
  27 import (
  28     "bufio"
  29     "strings"
  30     "testing"
  31 )
  32 
  33 func TestRestyleLine(t *testing.T) {
  34     var (
  35         r = "\x1b[0m"
  36         d = string(styles[`gray`])
  37     )
  38 
  39     tests := map[string]string{
  40         ``:                 ``,
  41         `abc`:              `abc`,
  42         `  abc 123456 `:    `  abc 123` + d + `456` + r + ` `,
  43         `  123456789 text`: `  123` + d + `456` + r + `789 text`,
  44         `0`:                `0`,
  45         `01`:               `01`,
  46         `012`:              `012`,
  47         `0123`:             `0` + d + `123` + r,
  48         `01234`:            `01` + d + `234` + r,
  49         `012345`:           `012` + d + `345` + r,
  50         `0123456`:          `0` + d + `123` + r + `456`,
  51         `01234567`:         `01` + d + `234` + r + `567`,
  52         `012345678`:        `012` + d + `345` + r + `678`,
  53         `0123456789`:       `0` + d + `123` + r + `456` + d + `789` + r,
  54         `01234567890`:      `01` + d + `234` + r + `567` + d + `890` + r,
  55         `012345678901`:     `012` + d + `345` + r + `678` + d + `901` + r,
  56 
  57         `0123456789012`: `0` + d + `123` + r + `456` + d + `789` + r + `012`,
  58         `00321`:         `00` + d + `321` + r,
  59         `123.456789`:    `123.` + `456` + d + `789` + r,
  60         `123456.123456`: `123` + d + `456` + r + `.` + `123` + d + `456` + r,
  61     }
  62 
  63     for input, expected := range tests {
  64         t.Run(input, func(t *testing.T) {
  65             var b strings.Builder
  66             w := bufio.NewWriter(&b)
  67             restyleLine(w, []byte(input), d)
  68             w.Flush()
  69 
  70             if got := b.String(); got != expected {
  71                 t.Fatalf(`expected %q, but got %q instead`, expected, got)
  72             }
  73         })
  74     }
  75 }
     File: ./now/info.txt
   1 now [options...] [timezones/places...]
   2 
   3 Show the current date and time for the places given, along with the local
   4 date/time.
   5 
   6 All (optional) leading options start with either single or double-dash:
   7 
   8     -h, -help                  show this help message
     File: ./now/lookup.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package now
  26 
  27 import (
  28     "strings"
  29     "time"
  30 )
  31 
  32 var aliases = map[string]string{
  33     // `istanbul`: `Asia/Istanbul`,
  34     `istanbul`: `Europe/Istanbul`,
  35 
  36     // `nicosia`: `Asia/Nicosia`,
  37     `nicosia`: `Europe/Nicosia`,
  38 
  39     `africa/abidjan`:                 `Africa/Abidjan`,
  40     `africa/accra`:                   `Africa/Accra`,
  41     `africa/addis ababa`:             `Africa/Addis_Ababa`,
  42     `africa/algiers`:                 `Africa/Algiers`,
  43     `africa/asmara`:                  `Africa/Asmara`,
  44     `africa/bamako`:                  `Africa/Bamako`,
  45     `africa/bangui`:                  `Africa/Bangui`,
  46     `africa/banjul`:                  `Africa/Banjul`,
  47     `africa/bissau`:                  `Africa/Bissau`,
  48     `africa/blantyre`:                `Africa/Blantyre`,
  49     `africa/brazzaville`:             `Africa/Brazzaville`,
  50     `africa/bujumbura`:               `Africa/Bujumbura`,
  51     `africa/cairo`:                   `Africa/Cairo`,
  52     `africa/casablanca`:              `Africa/Casablanca`,
  53     `africa/ceuta`:                   `Africa/Ceuta`,
  54     `africa/conakry`:                 `Africa/Conakry`,
  55     `africa/dakar`:                   `Africa/Dakar`,
  56     `africa/dar es salaam`:           `Africa/Dar_es_Salaam`,
  57     `africa/djibouti`:                `Africa/Djibouti`,
  58     `africa/douala`:                  `Africa/Douala`,
  59     `africa/el aaiun`:                `Africa/El_Aaiun`,
  60     `africa/freetown`:                `Africa/Freetown`,
  61     `africa/gaborone`:                `Africa/Gaborone`,
  62     `africa/harare`:                  `Africa/Harare`,
  63     `africa/johannesburg`:            `Africa/Johannesburg`,
  64     `africa/juba`:                    `Africa/Juba`,
  65     `africa/kampala`:                 `Africa/Kampala`,
  66     `africa/khartoum`:                `Africa/Khartoum`,
  67     `africa/kigali`:                  `Africa/Kigali`,
  68     `africa/kinshasa`:                `Africa/Kinshasa`,
  69     `africa/lagos`:                   `Africa/Lagos`,
  70     `africa/libreville`:              `Africa/Libreville`,
  71     `africa/lome`:                    `Africa/Lome`,
  72     `africa/luanda`:                  `Africa/Luanda`,
  73     `africa/lubumbashi`:              `Africa/Lubumbashi`,
  74     `africa/lusaka`:                  `Africa/Lusaka`,
  75     `africa/malabo`:                  `Africa/Malabo`,
  76     `africa/maputo`:                  `Africa/Maputo`,
  77     `africa/maseru`:                  `Africa/Maseru`,
  78     `africa/mbabane`:                 `Africa/Mbabane`,
  79     `africa/mogadishu`:               `Africa/Mogadishu`,
  80     `africa/monrovia`:                `Africa/Monrovia`,
  81     `africa/nairobi`:                 `Africa/Nairobi`,
  82     `africa/ndjamena`:                `Africa/Ndjamena`,
  83     `africa/niamey`:                  `Africa/Niamey`,
  84     `africa/nouakchott`:              `Africa/Nouakchott`,
  85     `africa/ouagadougou`:             `Africa/Ouagadougou`,
  86     `africa/porto-novo`:              `Africa/Porto-Novo`,
  87     `africa/sao tome`:                `Africa/Sao_Tome`,
  88     `africa/timbuktu`:                `Africa/Timbuktu`,
  89     `africa/tripoli`:                 `Africa/Tripoli`,
  90     `africa/tunis`:                   `Africa/Tunis`,
  91     `africa/windhoek`:                `Africa/Windhoek`,
  92     `america/adak`:                   `America/Adak`,
  93     `america/anchorage`:              `America/Anchorage`,
  94     `america/anguilla`:               `America/Anguilla`,
  95     `america/antigua`:                `America/Antigua`,
  96     `america/araguaina`:              `America/Araguaina`,
  97     `america/argentina/buenos aires`: `America/Argentina/Buenos_Aires`,
  98     `america/argentina/catamarca`:    `America/Argentina/Catamarca`,
  99     `america/argentina/cordoba`:      `America/Argentina/Cordoba`,
 100     `america/argentina/jujuy`:        `America/Argentina/Jujuy`,
 101     `america/argentina/la rioja`:     `America/Argentina/La_Rioja`,
 102     `america/argentina/mendoza`:      `America/Argentina/Mendoza`,
 103     `america/argentina/rio gallegos`: `America/Argentina/Rio_Gallegos`,
 104     `america/argentina/salta`:        `America/Argentina/Salta`,
 105     `america/argentina/san juan`:     `America/Argentina/San_Juan`,
 106     `america/argentina/san luis`:     `America/Argentina/San_Luis`,
 107     `america/argentina/tucuman`:      `America/Argentina/Tucuman`,
 108     `america/argentina/ushuaia`:      `America/Argentina/Ushuaia`,
 109     `america/aruba`:                  `America/Aruba`,
 110     `america/asuncion`:               `America/Asuncion`,
 111     `america/atikokan`:               `America/Atikokan`,
 112     `america/atka`:                   `America/Atka`,
 113     `america/bahia`:                  `America/Bahia`,
 114     `america/bahia banderas`:         `America/Bahia_Banderas`,
 115     `america/barbados`:               `America/Barbados`,
 116     `america/belem`:                  `America/Belem`,
 117     `america/belize`:                 `America/Belize`,
 118     `america/blanc-sablon`:           `America/Blanc-Sablon`,
 119     `america/boa vista`:              `America/Boa_Vista`,
 120     `america/bogota`:                 `America/Bogota`,
 121     `america/boise`:                  `America/Boise`,
 122     `america/cambridge bay`:          `America/Cambridge_Bay`,
 123     `america/campo grande`:           `America/Campo_Grande`,
 124     `america/cancun`:                 `America/Cancun`,
 125     `america/caracas`:                `America/Caracas`,
 126     `america/cayenne`:                `America/Cayenne`,
 127     `america/cayman`:                 `America/Cayman`,
 128     `america/chicago`:                `America/Chicago`,
 129     `america/chihuahua`:              `America/Chihuahua`,
 130     `america/ciudad juarez`:          `America/Ciudad_Juarez`,
 131     `america/coral harbour`:          `America/Coral_Harbour`,
 132     `america/costa rica`:             `America/Costa_Rica`,
 133     `america/coyhaique`:              `America/Coyhaique`,
 134     `america/creston`:                `America/Creston`,
 135     `america/cuiaba`:                 `America/Cuiaba`,
 136     `america/curacao`:                `America/Curacao`,
 137     `america/danmarkshavn`:           `America/Danmarkshavn`,
 138     `america/dawson`:                 `America/Dawson`,
 139     `america/dawson creek`:           `America/Dawson_Creek`,
 140     `america/denver`:                 `America/Denver`,
 141     `america/detroit`:                `America/Detroit`,
 142     `america/dominica`:               `America/Dominica`,
 143     `america/edmonton`:               `America/Edmonton`,
 144     `america/eirunepe`:               `America/Eirunepe`,
 145     `america/el salvador`:            `America/El_Salvador`,
 146     `america/ensenada`:               `America/Ensenada`,
 147     `america/fort nelson`:            `America/Fort_Nelson`,
 148     `america/fortaleza`:              `America/Fortaleza`,
 149     `america/glace bay`:              `America/Glace_Bay`,
 150     `america/goose bay`:              `America/Goose_Bay`,
 151     `america/grand turk`:             `America/Grand_Turk`,
 152     `america/grenada`:                `America/Grenada`,
 153     `america/guadeloupe`:             `America/Guadeloupe`,
 154     `america/guatemala`:              `America/Guatemala`,
 155     `america/guayaquil`:              `America/Guayaquil`,
 156     `america/guyana`:                 `America/Guyana`,
 157     `america/halifax`:                `America/Halifax`,
 158     `america/havana`:                 `America/Havana`,
 159     `america/hermosillo`:             `America/Hermosillo`,
 160     `america/indiana/indianapolis`:   `America/Indiana/Indianapolis`,
 161     `america/indiana/knox`:           `America/Indiana/Knox`,
 162     `america/indiana/marengo`:        `America/Indiana/Marengo`,
 163     `america/indiana/petersburg`:     `America/Indiana/Petersburg`,
 164     `america/indiana/tell city`:      `America/Indiana/Tell_City`,
 165     `america/indiana/vevay`:          `America/Indiana/Vevay`,
 166     `america/indiana/vincennes`:      `America/Indiana/Vincennes`,
 167     `america/indiana/winamac`:        `America/Indiana/Winamac`,
 168     `america/inuvik`:                 `America/Inuvik`,
 169     `america/iqaluit`:                `America/Iqaluit`,
 170     `america/jamaica`:                `America/Jamaica`,
 171     `america/juneau`:                 `America/Juneau`,
 172     `america/kentucky/louisville`:    `America/Kentucky/Louisville`,
 173     `america/kentucky/monticello`:    `America/Kentucky/Monticello`,
 174     `america/kralendijk`:             `America/Kralendijk`,
 175     `america/la paz`:                 `America/La_Paz`,
 176     `america/lima`:                   `America/Lima`,
 177     `america/los angeles`:            `America/Los_Angeles`,
 178     `america/lower princes`:          `America/Lower_Princes`,
 179     `america/maceio`:                 `America/Maceio`,
 180     `america/managua`:                `America/Managua`,
 181     `america/manaus`:                 `America/Manaus`,
 182     `america/marigot`:                `America/Marigot`,
 183     `america/martinique`:             `America/Martinique`,
 184     `america/matamoros`:              `America/Matamoros`,
 185     `america/mazatlan`:               `America/Mazatlan`,
 186     `america/menominee`:              `America/Menominee`,
 187     `america/merida`:                 `America/Merida`,
 188     `america/metlakatla`:             `America/Metlakatla`,
 189     `america/mexico city`:            `America/Mexico_City`,
 190     `america/miquelon`:               `America/Miquelon`,
 191     `america/moncton`:                `America/Moncton`,
 192     `america/monterrey`:              `America/Monterrey`,
 193     `america/montevideo`:             `America/Montevideo`,
 194     `america/montreal`:               `America/Montreal`,
 195     `america/montserrat`:             `America/Montserrat`,
 196     `america/nassau`:                 `America/Nassau`,
 197     `america/new york`:               `America/New_York`,
 198     `america/nipigon`:                `America/Nipigon`,
 199     `america/nome`:                   `America/Nome`,
 200     `america/noronha`:                `America/Noronha`,
 201     `america/north dakota/beulah`:    `America/North_Dakota/Beulah`,
 202     `america/north dakota/center`:    `America/North_Dakota/Center`,
 203     `america/north dakota/new salem`: `America/North_Dakota/New_Salem`,
 204     `america/nuuk`:                   `America/Nuuk`,
 205     `america/ojinaga`:                `America/Ojinaga`,
 206     `america/panama`:                 `America/Panama`,
 207     `america/pangnirtung`:            `America/Pangnirtung`,
 208     `america/paramaribo`:             `America/Paramaribo`,
 209     `america/phoenix`:                `America/Phoenix`,
 210     `america/port-au-prince`:         `America/Port-au-Prince`,
 211     `america/port of spain`:          `America/Port_of_Spain`,
 212     `america/porto acre`:             `America/Porto_Acre`,
 213     `america/porto velho`:            `America/Porto_Velho`,
 214     `america/puerto rico`:            `America/Puerto_Rico`,
 215     `america/punta arenas`:           `America/Punta_Arenas`,
 216     `america/rainy river`:            `America/Rainy_River`,
 217     `america/rankin inlet`:           `America/Rankin_Inlet`,
 218     `america/recife`:                 `America/Recife`,
 219     `america/regina`:                 `America/Regina`,
 220     `america/resolute`:               `America/Resolute`,
 221     `america/rio branco`:             `America/Rio_Branco`,
 222     `america/santa isabel`:           `America/Santa_Isabel`,
 223     `america/santarem`:               `America/Santarem`,
 224     `america/santiago`:               `America/Santiago`,
 225     `america/santo domingo`:          `America/Santo_Domingo`,
 226     `america/sao paulo`:              `America/Sao_Paulo`,
 227     `america/scoresbysund`:           `America/Scoresbysund`,
 228     `america/shiprock`:               `America/Shiprock`,
 229     `america/sitka`:                  `America/Sitka`,
 230     `america/st barthelemy`:          `America/St_Barthelemy`,
 231     `america/st johns`:               `America/St_Johns`,
 232     `america/st kitts`:               `America/St_Kitts`,
 233     `america/st lucia`:               `America/St_Lucia`,
 234     `america/st thomas`:              `America/St_Thomas`,
 235     `america/st vincent`:             `America/St_Vincent`,
 236     `america/swift current`:          `America/Swift_Current`,
 237     `america/tegucigalpa`:            `America/Tegucigalpa`,
 238     `america/thule`:                  `America/Thule`,
 239     `america/thunder bay`:            `America/Thunder_Bay`,
 240     `america/tijuana`:                `America/Tijuana`,
 241     `america/toronto`:                `America/Toronto`,
 242     `america/tortola`:                `America/Tortola`,
 243     `america/vancouver`:              `America/Vancouver`,
 244     `america/virgin`:                 `America/Virgin`,
 245     `america/whitehorse`:             `America/Whitehorse`,
 246     `america/winnipeg`:               `America/Winnipeg`,
 247     `america/yakutat`:                `America/Yakutat`,
 248     `america/yellowknife`:            `America/Yellowknife`,
 249     `antarctica/casey`:               `Antarctica/Casey`,
 250     `antarctica/davis`:               `Antarctica/Davis`,
 251     `antarctica/dumontdurville`:      `Antarctica/DumontDUrville`,
 252     `antarctica/macquarie`:           `Antarctica/Macquarie`,
 253     `antarctica/mawson`:              `Antarctica/Mawson`,
 254     `antarctica/mcmurdo`:             `Antarctica/McMurdo`,
 255     `antarctica/palmer`:              `Antarctica/Palmer`,
 256     `antarctica/rothera`:             `Antarctica/Rothera`,
 257     `antarctica/syowa`:               `Antarctica/Syowa`,
 258     `antarctica/troll`:               `Antarctica/Troll`,
 259     `antarctica/vostok`:              `Antarctica/Vostok`,
 260     `arctic/longyearbyen`:            `Arctic/Longyearbyen`,
 261     `asia/aden`:                      `Asia/Aden`,
 262     `asia/almaty`:                    `Asia/Almaty`,
 263     `asia/amman`:                     `Asia/Amman`,
 264     `asia/anadyr`:                    `Asia/Anadyr`,
 265     `asia/aqtau`:                     `Asia/Aqtau`,
 266     `asia/aqtobe`:                    `Asia/Aqtobe`,
 267     `asia/ashgabat`:                  `Asia/Ashgabat`,
 268     `asia/atyrau`:                    `Asia/Atyrau`,
 269     `asia/baghdad`:                   `Asia/Baghdad`,
 270     `asia/bahrain`:                   `Asia/Bahrain`,
 271     `asia/baku`:                      `Asia/Baku`,
 272     `asia/bangkok`:                   `Asia/Bangkok`,
 273     `asia/barnaul`:                   `Asia/Barnaul`,
 274     `asia/beirut`:                    `Asia/Beirut`,
 275     `asia/bishkek`:                   `Asia/Bishkek`,
 276     `asia/brunei`:                    `Asia/Brunei`,
 277     `asia/chita`:                     `Asia/Chita`,
 278     `asia/chongqing`:                 `Asia/Chongqing`,
 279     `asia/colombo`:                   `Asia/Colombo`,
 280     `asia/damascus`:                  `Asia/Damascus`,
 281     `asia/dhaka`:                     `Asia/Dhaka`,
 282     `asia/dili`:                      `Asia/Dili`,
 283     `asia/dubai`:                     `Asia/Dubai`,
 284     `asia/dushanbe`:                  `Asia/Dushanbe`,
 285     `asia/famagusta`:                 `Asia/Famagusta`,
 286     `asia/gaza`:                      `Asia/Gaza`,
 287     `asia/harbin`:                    `Asia/Harbin`,
 288     `asia/hebron`:                    `Asia/Hebron`,
 289     `asia/ho chi minh`:               `Asia/Ho_Chi_Minh`,
 290     `asia/hong kong`:                 `Asia/Hong_Kong`,
 291     `asia/hovd`:                      `Asia/Hovd`,
 292     `asia/irkutsk`:                   `Asia/Irkutsk`,
 293     `asia/istanbul`:                  `Asia/Istanbul`,
 294     `asia/jakarta`:                   `Asia/Jakarta`,
 295     `asia/jayapura`:                  `Asia/Jayapura`,
 296     `asia/jerusalem`:                 `Asia/Jerusalem`,
 297     `asia/kabul`:                     `Asia/Kabul`,
 298     `asia/kamchatka`:                 `Asia/Kamchatka`,
 299     `asia/karachi`:                   `Asia/Karachi`,
 300     `asia/kashgar`:                   `Asia/Kashgar`,
 301     `asia/kathmandu`:                 `Asia/Kathmandu`,
 302     `asia/khandyga`:                  `Asia/Khandyga`,
 303     `asia/kolkata`:                   `Asia/Kolkata`,
 304     `asia/krasnoyarsk`:               `Asia/Krasnoyarsk`,
 305     `asia/kuala lumpur`:              `Asia/Kuala_Lumpur`,
 306     `asia/kuching`:                   `Asia/Kuching`,
 307     `asia/kuwait`:                    `Asia/Kuwait`,
 308     `asia/macau`:                     `Asia/Macau`,
 309     `asia/magadan`:                   `Asia/Magadan`,
 310     `asia/makassar`:                  `Asia/Makassar`,
 311     `asia/manila`:                    `Asia/Manila`,
 312     `asia/muscat`:                    `Asia/Muscat`,
 313     `asia/nicosia`:                   `Asia/Nicosia`,
 314     `asia/novokuznetsk`:              `Asia/Novokuznetsk`,
 315     `asia/novosibirsk`:               `Asia/Novosibirsk`,
 316     `asia/omsk`:                      `Asia/Omsk`,
 317     `asia/oral`:                      `Asia/Oral`,
 318     `asia/phnom penh`:                `Asia/Phnom_Penh`,
 319     `asia/pontianak`:                 `Asia/Pontianak`,
 320     `asia/pyongyang`:                 `Asia/Pyongyang`,
 321     `asia/qatar`:                     `Asia/Qatar`,
 322     `asia/qostanay`:                  `Asia/Qostanay`,
 323     `asia/qyzylorda`:                 `Asia/Qyzylorda`,
 324     `asia/riyadh`:                    `Asia/Riyadh`,
 325     `asia/sakhalin`:                  `Asia/Sakhalin`,
 326     `asia/samarkand`:                 `Asia/Samarkand`,
 327     `asia/seoul`:                     `Asia/Seoul`,
 328     `asia/shanghai`:                  `Asia/Shanghai`,
 329     `asia/singapore`:                 `Asia/Singapore`,
 330     `asia/srednekolymsk`:             `Asia/Srednekolymsk`,
 331     `asia/taipei`:                    `Asia/Taipei`,
 332     `asia/tashkent`:                  `Asia/Tashkent`,
 333     `asia/tbilisi`:                   `Asia/Tbilisi`,
 334     `asia/tehran`:                    `Asia/Tehran`,
 335     `asia/tel aviv`:                  `Asia/Tel_Aviv`,
 336     `asia/thimphu`:                   `Asia/Thimphu`,
 337     `asia/tokyo`:                     `Asia/Tokyo`,
 338     `asia/tomsk`:                     `Asia/Tomsk`,
 339     `asia/ulaanbaatar`:               `Asia/Ulaanbaatar`,
 340     `asia/urumqi`:                    `Asia/Urumqi`,
 341     `asia/ust-nera`:                  `Asia/Ust-Nera`,
 342     `asia/vientiane`:                 `Asia/Vientiane`,
 343     `asia/vladivostok`:               `Asia/Vladivostok`,
 344     `asia/yakutsk`:                   `Asia/Yakutsk`,
 345     `asia/yangon`:                    `Asia/Yangon`,
 346     `asia/yekaterinburg`:             `Asia/Yekaterinburg`,
 347     `asia/yerevan`:                   `Asia/Yerevan`,
 348     `atlantic/azores`:                `Atlantic/Azores`,
 349     `atlantic/bermuda`:               `Atlantic/Bermuda`,
 350     `atlantic/canary`:                `Atlantic/Canary`,
 351     `atlantic/cape verde`:            `Atlantic/Cape_Verde`,
 352     `atlantic/faroe`:                 `Atlantic/Faroe`,
 353     `atlantic/jan mayen`:             `Atlantic/Jan_Mayen`,
 354     `atlantic/madeira`:               `Atlantic/Madeira`,
 355     `atlantic/reykjavik`:             `Atlantic/Reykjavik`,
 356     `atlantic/south georgia`:         `Atlantic/South_Georgia`,
 357     `atlantic/st helena`:             `Atlantic/St_Helena`,
 358     `atlantic/stanley`:               `Atlantic/Stanley`,
 359     `australia/adelaide`:             `Australia/Adelaide`,
 360     `australia/brisbane`:             `Australia/Brisbane`,
 361     `australia/broken hill`:          `Australia/Broken_Hill`,
 362     `australia/canberra`:             `Australia/Canberra`,
 363     `australia/currie`:               `Australia/Currie`,
 364     `australia/darwin`:               `Australia/Darwin`,
 365     `australia/eucla`:                `Australia/Eucla`,
 366     `australia/hobart`:               `Australia/Hobart`,
 367     `australia/lindeman`:             `Australia/Lindeman`,
 368     `australia/lord howe`:            `Australia/Lord_Howe`,
 369     `australia/melbourne`:            `Australia/Melbourne`,
 370     `australia/perth`:                `Australia/Perth`,
 371     `australia/sydney`:               `Australia/Sydney`,
 372     `australia/yancowinna`:           `Australia/Yancowinna`,
 373     `etc/greenwich`:                  `Etc/Greenwich`,
 374     `etc/uct`:                        `Etc/UCT`,
 375     `etc/utc`:                        `Etc/UTC`,
 376     `etc/universal`:                  `Etc/Universal`,
 377     `etc/zulu`:                       `Etc/Zulu`,
 378     `europe/amsterdam`:               `Europe/Amsterdam`,
 379     `europe/andorra`:                 `Europe/Andorra`,
 380     `europe/astrakhan`:               `Europe/Astrakhan`,
 381     `europe/athens`:                  `Europe/Athens`,
 382     `europe/belfast`:                 `Europe/Belfast`,
 383     `europe/belgrade`:                `Europe/Belgrade`,
 384     `europe/berlin`:                  `Europe/Berlin`,
 385     `europe/bratislava`:              `Europe/Bratislava`,
 386     `europe/brussels`:                `Europe/Brussels`,
 387     `europe/bucharest`:               `Europe/Bucharest`,
 388     `europe/budapest`:                `Europe/Budapest`,
 389     `europe/busingen`:                `Europe/Busingen`,
 390     `europe/chisinau`:                `Europe/Chisinau`,
 391     `europe/copenhagen`:              `Europe/Copenhagen`,
 392     `europe/dublin`:                  `Europe/Dublin`,
 393     `europe/gibraltar`:               `Europe/Gibraltar`,
 394     `europe/guernsey`:                `Europe/Guernsey`,
 395     `europe/helsinki`:                `Europe/Helsinki`,
 396     `europe/isle of man`:             `Europe/Isle_of_Man`,
 397     `europe/istanbul`:                `Europe/Istanbul`,
 398     `europe/jersey`:                  `Europe/Jersey`,
 399     `europe/kaliningrad`:             `Europe/Kaliningrad`,
 400     `europe/kirov`:                   `Europe/Kirov`,
 401     `europe/kyiv`:                    `Europe/Kyiv`,
 402     `europe/lisbon`:                  `Europe/Lisbon`,
 403     `europe/ljubljana`:               `Europe/Ljubljana`,
 404     `europe/london`:                  `Europe/London`,
 405     `europe/luxembourg`:              `Europe/Luxembourg`,
 406     `europe/madrid`:                  `Europe/Madrid`,
 407     `europe/malta`:                   `Europe/Malta`,
 408     `europe/mariehamn`:               `Europe/Mariehamn`,
 409     `europe/minsk`:                   `Europe/Minsk`,
 410     `europe/monaco`:                  `Europe/Monaco`,
 411     `europe/moscow`:                  `Europe/Moscow`,
 412     `europe/nicosia`:                 `Europe/Nicosia`,
 413     `europe/oslo`:                    `Europe/Oslo`,
 414     `europe/paris`:                   `Europe/Paris`,
 415     `europe/podgorica`:               `Europe/Podgorica`,
 416     `europe/prague`:                  `Europe/Prague`,
 417     `europe/riga`:                    `Europe/Riga`,
 418     `europe/rome`:                    `Europe/Rome`,
 419     `europe/samara`:                  `Europe/Samara`,
 420     `europe/san marino`:              `Europe/San_Marino`,
 421     `europe/sarajevo`:                `Europe/Sarajevo`,
 422     `europe/saratov`:                 `Europe/Saratov`,
 423     `europe/simferopol`:              `Europe/Simferopol`,
 424     `europe/skopje`:                  `Europe/Skopje`,
 425     `europe/sofia`:                   `Europe/Sofia`,
 426     `europe/stockholm`:               `Europe/Stockholm`,
 427     `europe/tallinn`:                 `Europe/Tallinn`,
 428     `europe/tirane`:                  `Europe/Tirane`,
 429     `europe/tiraspol`:                `Europe/Tiraspol`,
 430     `europe/ulyanovsk`:               `Europe/Ulyanovsk`,
 431     `europe/vaduz`:                   `Europe/Vaduz`,
 432     `europe/vatican`:                 `Europe/Vatican`,
 433     `europe/vienna`:                  `Europe/Vienna`,
 434     `europe/vilnius`:                 `Europe/Vilnius`,
 435     `europe/volgograd`:               `Europe/Volgograd`,
 436     `europe/warsaw`:                  `Europe/Warsaw`,
 437     `europe/zagreb`:                  `Europe/Zagreb`,
 438     `europe/zurich`:                  `Europe/Zurich`,
 439     `indian/antananarivo`:            `Indian/Antananarivo`,
 440     `indian/chagos`:                  `Indian/Chagos`,
 441     `indian/christmas`:               `Indian/Christmas`,
 442     `indian/cocos`:                   `Indian/Cocos`,
 443     `indian/comoro`:                  `Indian/Comoro`,
 444     `indian/kerguelen`:               `Indian/Kerguelen`,
 445     `indian/mahe`:                    `Indian/Mahe`,
 446     `indian/maldives`:                `Indian/Maldives`,
 447     `indian/mauritius`:               `Indian/Mauritius`,
 448     `indian/mayotte`:                 `Indian/Mayotte`,
 449     `indian/reunion`:                 `Indian/Reunion`,
 450     `pacific/apia`:                   `Pacific/Apia`,
 451     `pacific/auckland`:               `Pacific/Auckland`,
 452     `pacific/bougainville`:           `Pacific/Bougainville`,
 453     `pacific/chatham`:                `Pacific/Chatham`,
 454     `pacific/chuuk`:                  `Pacific/Chuuk`,
 455     `pacific/easter`:                 `Pacific/Easter`,
 456     `pacific/efate`:                  `Pacific/Efate`,
 457     `pacific/fakaofo`:                `Pacific/Fakaofo`,
 458     `pacific/fiji`:                   `Pacific/Fiji`,
 459     `pacific/funafuti`:               `Pacific/Funafuti`,
 460     `pacific/galapagos`:              `Pacific/Galapagos`,
 461     `pacific/gambier`:                `Pacific/Gambier`,
 462     `pacific/guadalcanal`:            `Pacific/Guadalcanal`,
 463     `pacific/guam`:                   `Pacific/Guam`,
 464     `pacific/honolulu`:               `Pacific/Honolulu`,
 465     `pacific/johnston`:               `Pacific/Johnston`,
 466     `pacific/kanton`:                 `Pacific/Kanton`,
 467     `pacific/kiritimati`:             `Pacific/Kiritimati`,
 468     `pacific/kosrae`:                 `Pacific/Kosrae`,
 469     `pacific/kwajalein`:              `Pacific/Kwajalein`,
 470     `pacific/majuro`:                 `Pacific/Majuro`,
 471     `pacific/marquesas`:              `Pacific/Marquesas`,
 472     `pacific/midway`:                 `Pacific/Midway`,
 473     `pacific/nauru`:                  `Pacific/Nauru`,
 474     `pacific/niue`:                   `Pacific/Niue`,
 475     `pacific/norfolk`:                `Pacific/Norfolk`,
 476     `pacific/noumea`:                 `Pacific/Noumea`,
 477     `pacific/pago pago`:              `Pacific/Pago_Pago`,
 478     `pacific/palau`:                  `Pacific/Palau`,
 479     `pacific/pitcairn`:               `Pacific/Pitcairn`,
 480     `pacific/pohnpei`:                `Pacific/Pohnpei`,
 481     `pacific/port moresby`:           `Pacific/Port_Moresby`,
 482     `pacific/rarotonga`:              `Pacific/Rarotonga`,
 483     `pacific/saipan`:                 `Pacific/Saipan`,
 484     `pacific/samoa`:                  `Pacific/Samoa`,
 485     `pacific/tahiti`:                 `Pacific/Tahiti`,
 486     `pacific/tarawa`:                 `Pacific/Tarawa`,
 487     `pacific/tongatapu`:              `Pacific/Tongatapu`,
 488     `pacific/wake`:                   `Pacific/Wake`,
 489     `pacific/wallis`:                 `Pacific/Wallis`,
 490     `pacific/yap`:                    `Pacific/Yap`,
 491     `abidjan`:                        `Africa/Abidjan`,
 492     `accra`:                          `Africa/Accra`,
 493     `addis ababa`:                    `Africa/Addis_Ababa`,
 494     `algiers`:                        `Africa/Algiers`,
 495     `asmara`:                         `Africa/Asmara`,
 496     `bamako`:                         `Africa/Bamako`,
 497     `bangui`:                         `Africa/Bangui`,
 498     `banjul`:                         `Africa/Banjul`,
 499     `bissau`:                         `Africa/Bissau`,
 500     `blantyre`:                       `Africa/Blantyre`,
 501     `brazzaville`:                    `Africa/Brazzaville`,
 502     `bujumbura`:                      `Africa/Bujumbura`,
 503     `cairo`:                          `Africa/Cairo`,
 504     `casablanca`:                     `Africa/Casablanca`,
 505     `ceuta`:                          `Africa/Ceuta`,
 506     `conakry`:                        `Africa/Conakry`,
 507     `dakar`:                          `Africa/Dakar`,
 508     `dar es salaam`:                  `Africa/Dar_es_Salaam`,
 509     `djibouti`:                       `Africa/Djibouti`,
 510     `douala`:                         `Africa/Douala`,
 511     `el aaiun`:                       `Africa/El_Aaiun`,
 512     `freetown`:                       `Africa/Freetown`,
 513     `gaborone`:                       `Africa/Gaborone`,
 514     `harare`:                         `Africa/Harare`,
 515     `johannesburg`:                   `Africa/Johannesburg`,
 516     `juba`:                           `Africa/Juba`,
 517     `kampala`:                        `Africa/Kampala`,
 518     `khartoum`:                       `Africa/Khartoum`,
 519     `kigali`:                         `Africa/Kigali`,
 520     `kinshasa`:                       `Africa/Kinshasa`,
 521     `lagos`:                          `Africa/Lagos`,
 522     `libreville`:                     `Africa/Libreville`,
 523     `lome`:                           `Africa/Lome`,
 524     `luanda`:                         `Africa/Luanda`,
 525     `lubumbashi`:                     `Africa/Lubumbashi`,
 526     `lusaka`:                         `Africa/Lusaka`,
 527     `malabo`:                         `Africa/Malabo`,
 528     `maputo`:                         `Africa/Maputo`,
 529     `maseru`:                         `Africa/Maseru`,
 530     `mbabane`:                        `Africa/Mbabane`,
 531     `mogadishu`:                      `Africa/Mogadishu`,
 532     `monrovia`:                       `Africa/Monrovia`,
 533     `nairobi`:                        `Africa/Nairobi`,
 534     `ndjamena`:                       `Africa/Ndjamena`,
 535     `niamey`:                         `Africa/Niamey`,
 536     `nouakchott`:                     `Africa/Nouakchott`,
 537     `ouagadougou`:                    `Africa/Ouagadougou`,
 538     `porto-novo`:                     `Africa/Porto-Novo`,
 539     `sao tome`:                       `Africa/Sao_Tome`,
 540     `timbuktu`:                       `Africa/Timbuktu`,
 541     `tripoli`:                        `Africa/Tripoli`,
 542     `tunis`:                          `Africa/Tunis`,
 543     `windhoek`:                       `Africa/Windhoek`,
 544     `adak`:                           `America/Adak`,
 545     `anchorage`:                      `America/Anchorage`,
 546     `anguilla`:                       `America/Anguilla`,
 547     `antigua`:                        `America/Antigua`,
 548     `araguaina`:                      `America/Araguaina`,
 549     `buenos aires`:                   `America/Argentina/Buenos_Aires`,
 550     `catamarca`:                      `America/Argentina/Catamarca`,
 551     `cordoba`:                        `America/Argentina/Cordoba`,
 552     `jujuy`:                          `America/Argentina/Jujuy`,
 553     `la rioja`:                       `America/Argentina/La_Rioja`,
 554     `mendoza`:                        `America/Argentina/Mendoza`,
 555     `rio gallegos`:                   `America/Argentina/Rio_Gallegos`,
 556     `salta`:                          `America/Argentina/Salta`,
 557     `san juan`:                       `America/Argentina/San_Juan`,
 558     `san luis`:                       `America/Argentina/San_Luis`,
 559     `tucuman`:                        `America/Argentina/Tucuman`,
 560     `ushuaia`:                        `America/Argentina/Ushuaia`,
 561     `aruba`:                          `America/Aruba`,
 562     `asuncion`:                       `America/Asuncion`,
 563     `atikokan`:                       `America/Atikokan`,
 564     `atka`:                           `America/Atka`,
 565     `bahia`:                          `America/Bahia`,
 566     `bahia banderas`:                 `America/Bahia_Banderas`,
 567     `barbados`:                       `America/Barbados`,
 568     `belem`:                          `America/Belem`,
 569     `belize`:                         `America/Belize`,
 570     `blanc-sablon`:                   `America/Blanc-Sablon`,
 571     `boa vista`:                      `America/Boa_Vista`,
 572     `bogota`:                         `America/Bogota`,
 573     `boise`:                          `America/Boise`,
 574     `cambridge bay`:                  `America/Cambridge_Bay`,
 575     `campo grande`:                   `America/Campo_Grande`,
 576     `cancun`:                         `America/Cancun`,
 577     `caracas`:                        `America/Caracas`,
 578     `cayenne`:                        `America/Cayenne`,
 579     `cayman`:                         `America/Cayman`,
 580     `chicago`:                        `America/Chicago`,
 581     `chihuahua`:                      `America/Chihuahua`,
 582     `ciudad juarez`:                  `America/Ciudad_Juarez`,
 583     `coral harbour`:                  `America/Coral_Harbour`,
 584     `costa rica`:                     `America/Costa_Rica`,
 585     `coyhaique`:                      `America/Coyhaique`,
 586     `creston`:                        `America/Creston`,
 587     `cuiaba`:                         `America/Cuiaba`,
 588     `curacao`:                        `America/Curacao`,
 589     `danmarkshavn`:                   `America/Danmarkshavn`,
 590     `dawson`:                         `America/Dawson`,
 591     `dawson creek`:                   `America/Dawson_Creek`,
 592     `denver`:                         `America/Denver`,
 593     `detroit`:                        `America/Detroit`,
 594     `dominica`:                       `America/Dominica`,
 595     `edmonton`:                       `America/Edmonton`,
 596     `eirunepe`:                       `America/Eirunepe`,
 597     `el salvador`:                    `America/El_Salvador`,
 598     `ensenada`:                       `America/Ensenada`,
 599     `fort nelson`:                    `America/Fort_Nelson`,
 600     `fortaleza`:                      `America/Fortaleza`,
 601     `glace bay`:                      `America/Glace_Bay`,
 602     `goose bay`:                      `America/Goose_Bay`,
 603     `grand turk`:                     `America/Grand_Turk`,
 604     `grenada`:                        `America/Grenada`,
 605     `guadeloupe`:                     `America/Guadeloupe`,
 606     `guatemala`:                      `America/Guatemala`,
 607     `guayaquil`:                      `America/Guayaquil`,
 608     `guyana`:                         `America/Guyana`,
 609     `halifax`:                        `America/Halifax`,
 610     `havana`:                         `America/Havana`,
 611     `hermosillo`:                     `America/Hermosillo`,
 612     `indianapolis`:                   `America/Indiana/Indianapolis`,
 613     `knox`:                           `America/Indiana/Knox`,
 614     `marengo`:                        `America/Indiana/Marengo`,
 615     `petersburg`:                     `America/Indiana/Petersburg`,
 616     `tell city`:                      `America/Indiana/Tell_City`,
 617     `vevay`:                          `America/Indiana/Vevay`,
 618     `vincennes`:                      `America/Indiana/Vincennes`,
 619     `winamac`:                        `America/Indiana/Winamac`,
 620     `inuvik`:                         `America/Inuvik`,
 621     `iqaluit`:                        `America/Iqaluit`,
 622     `jamaica`:                        `America/Jamaica`,
 623     `juneau`:                         `America/Juneau`,
 624     `louisville`:                     `America/Kentucky/Louisville`,
 625     `monticello`:                     `America/Kentucky/Monticello`,
 626     `kralendijk`:                     `America/Kralendijk`,
 627     `la paz`:                         `America/La_Paz`,
 628     `lima`:                           `America/Lima`,
 629     `los angeles`:                    `America/Los_Angeles`,
 630     `lower princes`:                  `America/Lower_Princes`,
 631     `maceio`:                         `America/Maceio`,
 632     `managua`:                        `America/Managua`,
 633     `manaus`:                         `America/Manaus`,
 634     `marigot`:                        `America/Marigot`,
 635     `martinique`:                     `America/Martinique`,
 636     `matamoros`:                      `America/Matamoros`,
 637     `mazatlan`:                       `America/Mazatlan`,
 638     `menominee`:                      `America/Menominee`,
 639     `merida`:                         `America/Merida`,
 640     `metlakatla`:                     `America/Metlakatla`,
 641     `mexico city`:                    `America/Mexico_City`,
 642     `miquelon`:                       `America/Miquelon`,
 643     `moncton`:                        `America/Moncton`,
 644     `monterrey`:                      `America/Monterrey`,
 645     `montevideo`:                     `America/Montevideo`,
 646     `montreal`:                       `America/Montreal`,
 647     `montserrat`:                     `America/Montserrat`,
 648     `nassau`:                         `America/Nassau`,
 649     `new york`:                       `America/New_York`,
 650     `nipigon`:                        `America/Nipigon`,
 651     `nome`:                           `America/Nome`,
 652     `noronha`:                        `America/Noronha`,
 653     `beulah`:                         `America/North_Dakota/Beulah`,
 654     `center`:                         `America/North_Dakota/Center`,
 655     `new salem`:                      `America/North_Dakota/New_Salem`,
 656     `nuuk`:                           `America/Nuuk`,
 657     `ojinaga`:                        `America/Ojinaga`,
 658     `panama`:                         `America/Panama`,
 659     `pangnirtung`:                    `America/Pangnirtung`,
 660     `paramaribo`:                     `America/Paramaribo`,
 661     `phoenix`:                        `America/Phoenix`,
 662     `port-au-prince`:                 `America/Port-au-Prince`,
 663     `port of spain`:                  `America/Port_of_Spain`,
 664     `porto acre`:                     `America/Porto_Acre`,
 665     `porto velho`:                    `America/Porto_Velho`,
 666     `puerto rico`:                    `America/Puerto_Rico`,
 667     `punta arenas`:                   `America/Punta_Arenas`,
 668     `rainy river`:                    `America/Rainy_River`,
 669     `rankin inlet`:                   `America/Rankin_Inlet`,
 670     `recife`:                         `America/Recife`,
 671     `regina`:                         `America/Regina`,
 672     `resolute`:                       `America/Resolute`,
 673     `rio branco`:                     `America/Rio_Branco`,
 674     `santa isabel`:                   `America/Santa_Isabel`,
 675     `santarem`:                       `America/Santarem`,
 676     `santiago`:                       `America/Santiago`,
 677     `santo domingo`:                  `America/Santo_Domingo`,
 678     `sao paulo`:                      `America/Sao_Paulo`,
 679     `scoresbysund`:                   `America/Scoresbysund`,
 680     `shiprock`:                       `America/Shiprock`,
 681     `sitka`:                          `America/Sitka`,
 682     `st barthelemy`:                  `America/St_Barthelemy`,
 683     `st johns`:                       `America/St_Johns`,
 684     `st kitts`:                       `America/St_Kitts`,
 685     `st lucia`:                       `America/St_Lucia`,
 686     `st thomas`:                      `America/St_Thomas`,
 687     `st vincent`:                     `America/St_Vincent`,
 688     `swift current`:                  `America/Swift_Current`,
 689     `tegucigalpa`:                    `America/Tegucigalpa`,
 690     `thule`:                          `America/Thule`,
 691     `thunder bay`:                    `America/Thunder_Bay`,
 692     `tijuana`:                        `America/Tijuana`,
 693     `toronto`:                        `America/Toronto`,
 694     `tortola`:                        `America/Tortola`,
 695     `vancouver`:                      `America/Vancouver`,
 696     `virgin`:                         `America/Virgin`,
 697     `whitehorse`:                     `America/Whitehorse`,
 698     `winnipeg`:                       `America/Winnipeg`,
 699     `yakutat`:                        `America/Yakutat`,
 700     `yellowknife`:                    `America/Yellowknife`,
 701     `casey`:                          `Antarctica/Casey`,
 702     `davis`:                          `Antarctica/Davis`,
 703     `dumontdurville`:                 `Antarctica/DumontDUrville`,
 704     `macquarie`:                      `Antarctica/Macquarie`,
 705     `mawson`:                         `Antarctica/Mawson`,
 706     `mcmurdo`:                        `Antarctica/McMurdo`,
 707     `palmer`:                         `Antarctica/Palmer`,
 708     `rothera`:                        `Antarctica/Rothera`,
 709     `syowa`:                          `Antarctica/Syowa`,
 710     `troll`:                          `Antarctica/Troll`,
 711     `vostok`:                         `Antarctica/Vostok`,
 712     `longyearbyen`:                   `Arctic/Longyearbyen`,
 713     `aden`:                           `Asia/Aden`,
 714     `almaty`:                         `Asia/Almaty`,
 715     `amman`:                          `Asia/Amman`,
 716     `anadyr`:                         `Asia/Anadyr`,
 717     `aqtau`:                          `Asia/Aqtau`,
 718     `aqtobe`:                         `Asia/Aqtobe`,
 719     `ashgabat`:                       `Asia/Ashgabat`,
 720     `atyrau`:                         `Asia/Atyrau`,
 721     `baghdad`:                        `Asia/Baghdad`,
 722     `bahrain`:                        `Asia/Bahrain`,
 723     `baku`:                           `Asia/Baku`,
 724     `bangkok`:                        `Asia/Bangkok`,
 725     `barnaul`:                        `Asia/Barnaul`,
 726     `beirut`:                         `Asia/Beirut`,
 727     `bishkek`:                        `Asia/Bishkek`,
 728     `brunei`:                         `Asia/Brunei`,
 729     `chita`:                          `Asia/Chita`,
 730     `chongqing`:                      `Asia/Chongqing`,
 731     `colombo`:                        `Asia/Colombo`,
 732     `damascus`:                       `Asia/Damascus`,
 733     `dhaka`:                          `Asia/Dhaka`,
 734     `dili`:                           `Asia/Dili`,
 735     `dubai`:                          `Asia/Dubai`,
 736     `dushanbe`:                       `Asia/Dushanbe`,
 737     `famagusta`:                      `Asia/Famagusta`,
 738     `gaza`:                           `Asia/Gaza`,
 739     `harbin`:                         `Asia/Harbin`,
 740     `hebron`:                         `Asia/Hebron`,
 741     `ho chi minh`:                    `Asia/Ho_Chi_Minh`,
 742     `hong kong`:                      `Asia/Hong_Kong`,
 743     `hovd`:                           `Asia/Hovd`,
 744     `irkutsk`:                        `Asia/Irkutsk`,
 745     `jakarta`:                        `Asia/Jakarta`,
 746     `jayapura`:                       `Asia/Jayapura`,
 747     `jerusalem`:                      `Asia/Jerusalem`,
 748     `kabul`:                          `Asia/Kabul`,
 749     `kamchatka`:                      `Asia/Kamchatka`,
 750     `karachi`:                        `Asia/Karachi`,
 751     `kashgar`:                        `Asia/Kashgar`,
 752     `kathmandu`:                      `Asia/Kathmandu`,
 753     `khandyga`:                       `Asia/Khandyga`,
 754     `kolkata`:                        `Asia/Kolkata`,
 755     `krasnoyarsk`:                    `Asia/Krasnoyarsk`,
 756     `kuala lumpur`:                   `Asia/Kuala_Lumpur`,
 757     `kuching`:                        `Asia/Kuching`,
 758     `kuwait`:                         `Asia/Kuwait`,
 759     `macau`:                          `Asia/Macau`,
 760     `magadan`:                        `Asia/Magadan`,
 761     `makassar`:                       `Asia/Makassar`,
 762     `manila`:                         `Asia/Manila`,
 763     `muscat`:                         `Asia/Muscat`,
 764     `novokuznetsk`:                   `Asia/Novokuznetsk`,
 765     `novosibirsk`:                    `Asia/Novosibirsk`,
 766     `omsk`:                           `Asia/Omsk`,
 767     `oral`:                           `Asia/Oral`,
 768     `phnom penh`:                     `Asia/Phnom_Penh`,
 769     `pontianak`:                      `Asia/Pontianak`,
 770     `pyongyang`:                      `Asia/Pyongyang`,
 771     `qatar`:                          `Asia/Qatar`,
 772     `qostanay`:                       `Asia/Qostanay`,
 773     `qyzylorda`:                      `Asia/Qyzylorda`,
 774     `riyadh`:                         `Asia/Riyadh`,
 775     `sakhalin`:                       `Asia/Sakhalin`,
 776     `samarkand`:                      `Asia/Samarkand`,
 777     `seoul`:                          `Asia/Seoul`,
 778     `shanghai`:                       `Asia/Shanghai`,
 779     `singapore`:                      `Asia/Singapore`,
 780     `srednekolymsk`:                  `Asia/Srednekolymsk`,
 781     `taipei`:                         `Asia/Taipei`,
 782     `tashkent`:                       `Asia/Tashkent`,
 783     `tbilisi`:                        `Asia/Tbilisi`,
 784     `tehran`:                         `Asia/Tehran`,
 785     `tel aviv`:                       `Asia/Tel_Aviv`,
 786     `thimphu`:                        `Asia/Thimphu`,
 787     `tokyo`:                          `Asia/Tokyo`,
 788     `tomsk`:                          `Asia/Tomsk`,
 789     `ulaanbaatar`:                    `Asia/Ulaanbaatar`,
 790     `urumqi`:                         `Asia/Urumqi`,
 791     `ust-nera`:                       `Asia/Ust-Nera`,
 792     `vientiane`:                      `Asia/Vientiane`,
 793     `vladivostok`:                    `Asia/Vladivostok`,
 794     `yakutsk`:                        `Asia/Yakutsk`,
 795     `yangon`:                         `Asia/Yangon`,
 796     `yekaterinburg`:                  `Asia/Yekaterinburg`,
 797     `yerevan`:                        `Asia/Yerevan`,
 798     `azores`:                         `Atlantic/Azores`,
 799     `bermuda`:                        `Atlantic/Bermuda`,
 800     `canary`:                         `Atlantic/Canary`,
 801     `cape verde`:                     `Atlantic/Cape_Verde`,
 802     `faroe`:                          `Atlantic/Faroe`,
 803     `jan mayen`:                      `Atlantic/Jan_Mayen`,
 804     `madeira`:                        `Atlantic/Madeira`,
 805     `reykjavik`:                      `Atlantic/Reykjavik`,
 806     `south georgia`:                  `Atlantic/South_Georgia`,
 807     `st helena`:                      `Atlantic/St_Helena`,
 808     `stanley`:                        `Atlantic/Stanley`,
 809     `adelaide`:                       `Australia/Adelaide`,
 810     `brisbane`:                       `Australia/Brisbane`,
 811     `broken hill`:                    `Australia/Broken_Hill`,
 812     `canberra`:                       `Australia/Canberra`,
 813     `currie`:                         `Australia/Currie`,
 814     `darwin`:                         `Australia/Darwin`,
 815     `eucla`:                          `Australia/Eucla`,
 816     `hobart`:                         `Australia/Hobart`,
 817     `lindeman`:                       `Australia/Lindeman`,
 818     `lord howe`:                      `Australia/Lord_Howe`,
 819     `melbourne`:                      `Australia/Melbourne`,
 820     `perth`:                          `Australia/Perth`,
 821     `sydney`:                         `Australia/Sydney`,
 822     `yancowinna`:                     `Australia/Yancowinna`,
 823     `greenwich`:                      `Etc/Greenwich`,
 824     `uct`:                            `Etc/UCT`,
 825     `utc`:                            `Etc/UTC`,
 826     `universal`:                      `Etc/Universal`,
 827     `zulu`:                           `Etc/Zulu`,
 828     `amsterdam`:                      `Europe/Amsterdam`,
 829     `andorra`:                        `Europe/Andorra`,
 830     `astrakhan`:                      `Europe/Astrakhan`,
 831     `athens`:                         `Europe/Athens`,
 832     `belfast`:                        `Europe/Belfast`,
 833     `belgrade`:                       `Europe/Belgrade`,
 834     `berlin`:                         `Europe/Berlin`,
 835     `bratislava`:                     `Europe/Bratislava`,
 836     `brussels`:                       `Europe/Brussels`,
 837     `bucharest`:                      `Europe/Bucharest`,
 838     `budapest`:                       `Europe/Budapest`,
 839     `busingen`:                       `Europe/Busingen`,
 840     `chisinau`:                       `Europe/Chisinau`,
 841     `copenhagen`:                     `Europe/Copenhagen`,
 842     `dublin`:                         `Europe/Dublin`,
 843     `gibraltar`:                      `Europe/Gibraltar`,
 844     `guernsey`:                       `Europe/Guernsey`,
 845     `helsinki`:                       `Europe/Helsinki`,
 846     `isle of man`:                    `Europe/Isle_of_Man`,
 847     `jersey`:                         `Europe/Jersey`,
 848     `kaliningrad`:                    `Europe/Kaliningrad`,
 849     `kirov`:                          `Europe/Kirov`,
 850     `kyiv`:                           `Europe/Kyiv`,
 851     `lisbon`:                         `Europe/Lisbon`,
 852     `ljubljana`:                      `Europe/Ljubljana`,
 853     `london`:                         `Europe/London`,
 854     `luxembourg`:                     `Europe/Luxembourg`,
 855     `madrid`:                         `Europe/Madrid`,
 856     `malta`:                          `Europe/Malta`,
 857     `mariehamn`:                      `Europe/Mariehamn`,
 858     `minsk`:                          `Europe/Minsk`,
 859     `monaco`:                         `Europe/Monaco`,
 860     `moscow`:                         `Europe/Moscow`,
 861     `oslo`:                           `Europe/Oslo`,
 862     `paris`:                          `Europe/Paris`,
 863     `podgorica`:                      `Europe/Podgorica`,
 864     `prague`:                         `Europe/Prague`,
 865     `riga`:                           `Europe/Riga`,
 866     `rome`:                           `Europe/Rome`,
 867     `samara`:                         `Europe/Samara`,
 868     `san marino`:                     `Europe/San_Marino`,
 869     `sarajevo`:                       `Europe/Sarajevo`,
 870     `saratov`:                        `Europe/Saratov`,
 871     `simferopol`:                     `Europe/Simferopol`,
 872     `skopje`:                         `Europe/Skopje`,
 873     `sofia`:                          `Europe/Sofia`,
 874     `stockholm`:                      `Europe/Stockholm`,
 875     `tallinn`:                        `Europe/Tallinn`,
 876     `tirane`:                         `Europe/Tirane`,
 877     `tiraspol`:                       `Europe/Tiraspol`,
 878     `ulyanovsk`:                      `Europe/Ulyanovsk`,
 879     `vaduz`:                          `Europe/Vaduz`,
 880     `vatican`:                        `Europe/Vatican`,
 881     `vienna`:                         `Europe/Vienna`,
 882     `vilnius`:                        `Europe/Vilnius`,
 883     `volgograd`:                      `Europe/Volgograd`,
 884     `warsaw`:                         `Europe/Warsaw`,
 885     `zagreb`:                         `Europe/Zagreb`,
 886     `zurich`:                         `Europe/Zurich`,
 887     `antananarivo`:                   `Indian/Antananarivo`,
 888     `chagos`:                         `Indian/Chagos`,
 889     `christmas`:                      `Indian/Christmas`,
 890     `cocos`:                          `Indian/Cocos`,
 891     `comoro`:                         `Indian/Comoro`,
 892     `kerguelen`:                      `Indian/Kerguelen`,
 893     `mahe`:                           `Indian/Mahe`,
 894     `maldives`:                       `Indian/Maldives`,
 895     `mauritius`:                      `Indian/Mauritius`,
 896     `mayotte`:                        `Indian/Mayotte`,
 897     `reunion`:                        `Indian/Reunion`,
 898     `apia`:                           `Pacific/Apia`,
 899     `auckland`:                       `Pacific/Auckland`,
 900     `bougainville`:                   `Pacific/Bougainville`,
 901     `chatham`:                        `Pacific/Chatham`,
 902     `chuuk`:                          `Pacific/Chuuk`,
 903     `easter`:                         `Pacific/Easter`,
 904     `efate`:                          `Pacific/Efate`,
 905     `fakaofo`:                        `Pacific/Fakaofo`,
 906     `fiji`:                           `Pacific/Fiji`,
 907     `funafuti`:                       `Pacific/Funafuti`,
 908     `galapagos`:                      `Pacific/Galapagos`,
 909     `gambier`:                        `Pacific/Gambier`,
 910     `guadalcanal`:                    `Pacific/Guadalcanal`,
 911     `guam`:                           `Pacific/Guam`,
 912     `honolulu`:                       `Pacific/Honolulu`,
 913     `johnston`:                       `Pacific/Johnston`,
 914     `kanton`:                         `Pacific/Kanton`,
 915     `kiritimati`:                     `Pacific/Kiritimati`,
 916     `kosrae`:                         `Pacific/Kosrae`,
 917     `kwajalein`:                      `Pacific/Kwajalein`,
 918     `majuro`:                         `Pacific/Majuro`,
 919     `marquesas`:                      `Pacific/Marquesas`,
 920     `midway`:                         `Pacific/Midway`,
 921     `nauru`:                          `Pacific/Nauru`,
 922     `niue`:                           `Pacific/Niue`,
 923     `norfolk`:                        `Pacific/Norfolk`,
 924     `noumea`:                         `Pacific/Noumea`,
 925     `pago pago`:                      `Pacific/Pago_Pago`,
 926     `palau`:                          `Pacific/Palau`,
 927     `pitcairn`:                       `Pacific/Pitcairn`,
 928     `pohnpei`:                        `Pacific/Pohnpei`,
 929     `port moresby`:                   `Pacific/Port_Moresby`,
 930     `rarotonga`:                      `Pacific/Rarotonga`,
 931     `saipan`:                         `Pacific/Saipan`,
 932     `samoa`:                          `Pacific/Samoa`,
 933     `tahiti`:                         `Pacific/Tahiti`,
 934     `tarawa`:                         `Pacific/Tarawa`,
 935     `tongatapu`:                      `Pacific/Tongatapu`,
 936     `wake`:                           `Pacific/Wake`,
 937     `wallis`:                         `Pacific/Wallis`,
 938     `yap`:                            `Pacific/Yap`,
 939 }
 940 
 941 // Lookup tries to find a timezone from the place/city name given
 942 func Lookup(place string) (*time.Location, error) {
 943     if loc, err := time.LoadLocation(place); err == nil {
 944         return loc, err
 945     }
 946 
 947     if s, ok := lookupAlias(place); ok {
 948         place = s
 949     }
 950 
 951     loc, err := time.LoadLocation(place)
 952     return loc, err
 953 }
 954 
 955 // LookupName tries to find a timezone name from the place/city name given
 956 func LookupName(place string) (string, bool) {
 957     if s, ok := lookupAlias(place); ok {
 958         return s, true
 959     }
 960 
 961     for _, s := range aliases {
 962         if strings.EqualFold(place, s) {
 963             return s, true
 964         }
 965     }
 966     return place, false
 967 }
 968 
 969 // lookupAlias tries to find a timezone alias from the place/city name given
 970 func lookupAlias(place string) (string, bool) {
 971     key := strings.ToLower(place)
 972     key = strings.ReplaceAll(key, `_`, ` `)
 973     key = strings.ReplaceAll(key, `-`, ` `)
 974 
 975     if s, ok := aliases[key]; ok {
 976         return s, true
 977     }
 978     return place, false
 979 }
     File: ./now/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package now
  26 
  27 import (
  28     "fmt"
  29     "io"
  30     "os"
  31     "time"
  32 
  33     _ "embed"
  34 )
  35 
  36 //go:embed info.txt
  37 var info string
  38 
  39 const timeFormat = `2006-01-02 15:04:05 Mon Jan 02`
  40 
  41 func Main() {
  42     args := os.Args[1:]
  43     if len(args) > 0 {
  44         switch args[0] {
  45         case `-h`, `--h`, `-help`, `--help`:
  46             os.Stdout.WriteString(info[1:])
  47             return
  48         }
  49     }
  50 
  51     if len(args) > 0 && args[0] == `--` {
  52         args = args[1:]
  53     }
  54 
  55     ok := true
  56     w := os.Stdout
  57 
  58     now := time.Now()
  59     showDateTime(w, now)
  60     place := now.Location().String()
  61     fmt.Fprintf(w, "  %s\n", place)
  62 
  63     for _, place := range args {
  64         loc, err := Lookup(place)
  65         if err != nil {
  66             fmt.Fprintln(os.Stderr, err.Error())
  67             ok = false
  68             continue
  69         }
  70 
  71         showDateTime(w, now.In(loc))
  72         fmt.Fprintf(w, "  %s\n", place)
  73     }
  74 
  75     if !ok {
  76         os.Exit(1)
  77         return
  78     }
  79 }
  80 
  81 func showDateTime(w io.Writer, t time.Time) {
  82     var buf [64]byte
  83     w.Write(t.AppendFormat(buf[:0], timeFormat))
  84 }
     File: ./nts/nts.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nts
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "os/exec"
  33     "strings"
  34     "time"
  35 )
  36 
  37 const info = `
  38 nts [options...] [command...] [arguments...]
  39 
  40 Nice TimeStamp emits each input line, starting it with an ANSI-style date/time
  41 string and a tab. When given a command, it runs the command, timestamping its
  42 resulting standard-output lines as they become available.
  43 
  44 When not given a command, it timestamps lines from its standard input, as they
  45 become available.
  46 
  47 All (optional) leading options start with either single or double-dash:
  48 
  49     -h, -help     show this help message
  50     -p, -plain    don't style timestamps with ANSI-codes
  51 `
  52 
  53 func Main() {
  54     style := "\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m"
  55     args := os.Args[1:]
  56 
  57     if len(args) > 0 {
  58         switch args[0] {
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62 
  63         case `-p`, `--p`, `-plain`, `--plain`:
  64             style = ``
  65             args = args[1:]
  66         }
  67     }
  68 
  69     options := true
  70     if len(args) > 0 && args[0] == `--` {
  71         options = false
  72         args = args[1:]
  73     }
  74 
  75     // if the first argument is 1 or 2 dashes followed by a supported
  76     // style-name, change the style used
  77     if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
  78         name := args[0]
  79         name = strings.TrimPrefix(name, `-`)
  80         name = strings.TrimPrefix(name, `-`)
  81         args = args[1:]
  82 
  83         // check if the `dedashed` argument is a supported style-name
  84         if s, ok := lookupStyle(name); ok {
  85             style = s
  86         } else {
  87             os.Stderr.WriteString(`nts: invalid style name `)
  88             os.Stderr.WriteString(name)
  89             os.Stderr.WriteString("\n")
  90             os.Exit(1)
  91             return
  92         }
  93     }
  94 
  95     err := run(os.Stdout, os.Stderr, os.Stdin, args, style)
  96     if ee, ok := err.(*exec.ExitError); ok {
  97         os.Exit(ee.ExitCode())
  98         return
  99     }
 100     if err != nil && err != io.EOF {
 101         os.Stderr.WriteString(`nts: `)
 102         os.Stderr.WriteString(err.Error())
 103         os.Stderr.WriteString("\n")
 104         os.Exit(1)
 105         return
 106     }
 107 }
 108 
 109 func run(w1, w2 io.Writer, r io.Reader, args []string, style string) error {
 110     bw1 := bufio.NewWriterSize(w1, 32*1024)
 111     defer bw1.Flush()
 112 
 113     if len(args) == 0 {
 114         return timestamp(bw1, r, style)
 115     }
 116 
 117     bw2 := bufio.NewWriterSize(w2, 32*1024)
 118     defer bw2.Flush()
 119 
 120     ts := timestamper{w: bw2, style: style}
 121     defer ts.Close()
 122 
 123     cmd := exec.Command(args[0], args[1:]...)
 124     cmd.Stdin = r
 125     cmd.Stdout = &ts
 126 
 127     if err := cmd.Start(); err != nil {
 128         return err
 129     }
 130 
 131     return cmd.Wait()
 132 }
 133 
 134 func timestamp(w *bufio.Writer, r io.Reader, style string) error {
 135     const gb = 1024 * 1024 * 1024
 136     sc := bufio.NewScanner(r)
 137     sc.Buffer(nil, 8*gb)
 138 
 139     for i := 0; sc.Scan(); i++ {
 140         s := sc.Bytes()
 141         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 142             s = s[3:]
 143         }
 144 
 145         writeTimestamp(w, time.Now(), style)
 146         w.WriteByte('\t')
 147         w.Write(s)
 148         w.WriteByte('\n')
 149 
 150         if w.Flush() != nil {
 151             // a write error may be the consequence of stdout being closed,
 152             // perhaps by another app along a pipe
 153             return io.EOF
 154         }
 155     }
 156 
 157     return sc.Err()
 158 }
 159 
 160 func writeTimestamp(w *bufio.Writer, when time.Time, style string) {
 161     var buf [64]byte
 162     timestamp := when.AppendFormat(buf[:0], `2006-01-02 15:04:05`)
 163 
 164     if len(style) == 0 {
 165         w.Write(timestamp)
 166         return
 167     }
 168 
 169     w.WriteString(style)
 170     w.Write(timestamp)
 171     w.WriteString("\x1b[0m")
 172 }
 173 
 174 func lookupStyle(name string) (style string, ok bool) {
 175     if alias, ok := styleAliases[name]; ok {
 176         name = alias
 177     }
 178 
 179     style, ok = styles[name]
 180     return style, ok
 181 }
 182 
 183 var styleAliases = map[string]string{
 184     `b`: `blue`,
 185     `g`: `green`,
 186     `m`: `magenta`,
 187     `o`: `orange`,
 188     `p`: `purple`,
 189     `r`: `red`,
 190     `u`: `underline`,
 191 
 192     `bolded`:      `bold`,
 193     `h`:           `inverse`,
 194     `hi`:          `inverse`,
 195     `highlight`:   `inverse`,
 196     `highlighted`: `inverse`,
 197     `hilite`:      `inverse`,
 198     `hilited`:     `inverse`,
 199     `inv`:         `inverse`,
 200     `invert`:      `inverse`,
 201     `inverted`:    `inverse`,
 202     `underlined`:  `underline`,
 203 
 204     `bb`: `blueback`,
 205     `bg`: `greenback`,
 206     `bm`: `magentaback`,
 207     `bo`: `orangeback`,
 208     `bp`: `purpleback`,
 209     `br`: `redback`,
 210 
 211     `gb`: `greenback`,
 212     `mb`: `magentaback`,
 213     `ob`: `orangeback`,
 214     `pb`: `purpleback`,
 215     `rb`: `redback`,
 216 
 217     `bblue`:    `blueback`,
 218     `bgray`:    `grayback`,
 219     `bgreen`:   `greenback`,
 220     `bmagenta`: `magentaback`,
 221     `borange`:  `orangeback`,
 222     `bpurple`:  `purpleback`,
 223     `bred`:     `redback`,
 224 
 225     `backblue`:    `blueback`,
 226     `backgray`:    `grayback`,
 227     `backgreen`:   `greenback`,
 228     `backmagenta`: `magentaback`,
 229     `backorange`:  `orangeback`,
 230     `backpurple`:  `purpleback`,
 231     `backred`:     `redback`,
 232 }
 233 
 234 // styles turns style-names into the ANSI-code sequences used for the
 235 // alternate groups of digits
 236 var styles = map[string]string{
 237     `blue`:      "\x1b[38;2;0;95;215m",
 238     `bold`:      "\x1b[1m",
 239     `gray`:      "\x1b[38;2;168;168;168m",
 240     `green`:     "\x1b[38;2;0;135;95m",
 241     `inverse`:   "\x1b[7m",
 242     `magenta`:   "\x1b[38;2;215;0;255m",
 243     `none`:      ``,
 244     `orange`:    "\x1b[38;2;215;95;0m",
 245     `plain`:     ``,
 246     `red`:       "\x1b[38;2;204;0;0m",
 247     `underline`: "\x1b[4m",
 248     `unstyled`:  ``,
 249 
 250     // `blue`:      "\x1b[38;5;26m",
 251     // `bold`:      "\x1b[1m",
 252     // `gray`:      "\x1b[38;5;248m",
 253     // `green`:     "\x1b[38;5;29m",
 254     // `inverse`:   "\x1b[7m",
 255     // `magenta`:   "\x1b[38;5;99m",
 256     // `orange`:    "\x1b[38;5;166m",
 257     // `plain`:     "\x1b[0m",
 258     // `red`:       "\x1b[31m",
 259     // `underline`: "\x1b[4m",
 260 
 261     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 262     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 263     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 264     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 265     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 266     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 267     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 268 }
 269 
 270 // timestamper is a writer which adds timestamps at the starts of lines; it
 271 // assumes that the chunks given to its func Write are sequential fragments
 272 // of text lines
 273 type timestamper struct {
 274     // w is the destination writer
 275     w *bufio.Writer
 276 
 277     // style is the ANSI-code to style timestamps with
 278     style string
 279 
 280     // last is to remember if the last byte was a line-feed
 281     last byte
 282 
 283     // anything is whether any bytes have been written yet: this both allows
 284     // emitting the first line's timestamp, and to reliably check if an extra
 285     // line-feed byte is needed at the end of the last line (in func Close)
 286     anything bool
 287 }
 288 
 289 // Write precedes/starts each line with a timestamp and a tab
 290 func (ts *timestamper) Write(p []byte) (n int, err error) {
 291     var when time.Time
 292     // flush tracks the presence of line-feeds, to flush the writer if so
 293     flush := false
 294     // timed avoids calling time.Now more than once
 295     timed := false
 296 
 297     if len(p) > 0 {
 298         ts.last = p[len(p)-1]
 299     }
 300 
 301     if !ts.anything && len(p) > 0 {
 302         when = time.Now()
 303         timed = true
 304 
 305         writeTimestamp(ts.w, when, ts.style)
 306         ts.w.WriteByte('\t')
 307         ts.anything = true
 308     }
 309 
 310     for len(p) > 0 {
 311         i := bytes.IndexByte(p, '\n')
 312         if i < 0 {
 313             m, err := ts.w.Write(p)
 314             n += m
 315             if err != nil {
 316                 return n, err
 317             }
 318             break
 319         }
 320 
 321         m, err := ts.w.Write(p[:i+1])
 322         n += m
 323         if err != nil {
 324             return n, err
 325         }
 326 
 327         flush = true
 328 
 329         // don't add a timestamp at the very end of a chunk, since the next
 330         // line will also start with its own timestamp, on the next Write
 331         if i == len(p)-1 {
 332             break
 333         }
 334 
 335         if !timed {
 336             when = time.Now()
 337             timed = true
 338         }
 339 
 340         writeTimestamp(ts.w, when, ts.style)
 341         ts.w.WriteByte('\t')
 342 
 343         p = p[i+1:]
 344     }
 345 
 346     if flush {
 347         err = ts.w.Flush()
 348     }
 349     return n, err
 350 }
 351 
 352 // Close ensures non-empty output streams always end with a line-feed
 353 func (ts *timestamper) Close() error {
 354     if !ts.anything || ts.last == '\n' {
 355         return nil
 356     }
 357     if err := ts.w.WriteByte('\n'); err != nil {
 358         return err
 359     }
 360     return ts.w.Flush()
 361 }
     File: ./null/null.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package null
  26 
  27 import (
  28     "io"
  29     "io/fs"
  30     "os"
  31     "os/exec"
  32 )
  33 
  34 const info = `
  35 nil [options...] [command...] [args...]
  36 
  37 Emit nothing to output and/or discard everything from input: this tool can
  38 be used at either end of a pipe, or as a command-runner which discards the
  39 command's standard output, but not its standard error.
  40 
  41 The options are, available both in single and double-dash versions
  42 
  43     -h, -help    show this help message
  44 `
  45 
  46 func Main() {
  47     args := os.Args[1:]
  48 
  49     if len(args) > 0 {
  50         switch args[0] {
  51         case `-h`, `--h`, `-help`, `--help`:
  52             os.Stdout.WriteString(info[1:])
  53             return
  54         }
  55     }
  56 
  57     if len(args) > 0 && args[0] == `--` {
  58         args = args[1:]
  59     }
  60 
  61     if len(args) > 0 {
  62         cmd := exec.Command(args[0], args[1:]...)
  63         cmd.Stdin = os.Stdin
  64         cmd.Stdout = io.Discard
  65         cmd.Stderr = os.Stderr
  66 
  67         if err := cmd.Start(); err != nil {
  68             os.Stderr.WriteString(`nil: `)
  69             os.Stderr.WriteString(err.Error())
  70             os.Stderr.WriteString("\n")
  71             os.Exit(1)
  72             return
  73         }
  74 
  75         err := cmd.Wait()
  76         if _, ok := err.(*exec.ExitError); ok {
  77             err = nil
  78         }
  79 
  80         if err != nil {
  81             os.Stderr.WriteString(`nil: `)
  82             os.Stderr.WriteString(err.Error())
  83             os.Stderr.WriteString("\n")
  84         }
  85 
  86         os.Exit(cmd.ProcessState.ExitCode())
  87         return
  88     }
  89 
  90     info, err := os.Stdin.Stat()
  91     if err != nil {
  92         os.Stderr.WriteString(`nil: `)
  93         os.Stderr.WriteString(err.Error())
  94         os.Stderr.WriteString("\n")
  95         os.Exit(1)
  96     }
  97 
  98     piped := int(info.Mode()&fs.ModeNamedPipe) != 0
  99     if !piped {
 100         return
 101     }
 102 
 103     var buf [32 * 1024]byte
 104     for {
 105         _, err := os.Stdin.Read(buf[:])
 106         if err != nil {
 107             break
 108         }
 109     }
 110 }
     File: ./pac/pac.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package pac
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "math"
  32     "math/rand"
  33     "os"
  34     "strconv"
  35     "strings"
  36     "time"
  37 )
  38 
  39 const info = `
  40 pac [options...] [args...]
  41 
  42 
  43 Postfix Array Calculator runs command-line arguments to calculate numeric
  44 results, also supporting arrays of numbers as its name indicates.
  45 
  46 All (optional) leading options start with either single or double-dash:
  47 
  48     -h, -help    show this help message
  49 
  50 Functions (and their shortcuts/aliases)
  51 
  52     [ ... ]               push multiple values as a single top array/value
  53     a, anchor             stick nearly-integers to their closest integer
  54     abs                   the absolute value
  55     b, bail               dump all arrays/values and quit right away
  56     bye, q, quit          quit right away
  57     c, clear, empty       clear the stack empty
  58     ceil, ceiling         round numbers upward
  59     cos                   the cosine
  60     cosh                  the hyperbolic cosine
  61     dump                  output each array/value on its own line
  62     dup                   duplicate the top/latest value
  63     dup2                  duplicate the 2 top/latest values
  64     e                     the number known as 'E'
  65     exp                   exponential function e^x
  66     f, flat, flatten      gather all values from all arrays into a single one
  67     floor                 round numbers downward
  68     fuzz                  undo number-rounding
  69     h, help               show this help message
  70     i, iota               push numbers 1..n
  71     l, len, length        the item-count in the top/latest array/value
  72     levels                the item-count of the stack overall
  73     ln, log               natural logarithm
  74     log10                 base-10 logarithm
  75     log2                  base-2 logarithm
  76     neg, negate           flip the sign
  77     over                  prepend top of the stack to beginning of the stack
  78     p, print              output the top/latest array/value on a single line
  79     pi                    the number known as 'PI'
  80     pop                   forget the top/latest value
  81     pow10                 10^x
  82     pow2                  2^x
  83     rand, random, rnd     emit n random numbers in range [0, 1)
  84     round                 round numbers
  85     s, swap, swap2        swap the 2 top/latest values
  86     sgn, sign             the sign function
  87     shift                 forget the oldest value still in the stack
  88     spread                flatten vertically, so stack has just 1-item arrays
  89     sin                   the sine
  90     sinh                  the hyperbolic sine
  91     tan                   the tangent
  92     tanh                  the hyperbolic tangent
  93     tau                   twice the number known as 'PI'
  94     top                   forget all values but the top/latest
  95     u, unit               push the number 1 onto the stack n times
  96 
  97 Examples
  98 
  99 # push numbers on the stack; only the last one will show as the result
 100 pac 4.5 -6.22 905_900
 101 
 102 # push numbers on the stack, then flatten everything, so all values show
 103 pac 4.5 -6.22 905_900 flat
 104 `
 105 
 106 // maxExactInt is the max integer value a float64 can represent exactly
 107 const maxExactInt float64 = 9007199254740992.0
 108 
 109 var (
 110     needAtLeast1 = errors.New(`need at least 1 number or array on the stack`)
 111     needAtLeast2 = errors.New(`need at least 2 numbers or arrays on the stack`)
 112 )
 113 
 114 func Main() {
 115     args := os.Args[1:]
 116 
 117     if len(args) > 0 {
 118         switch args[0] {
 119         case `-h`, `--h`, `-help`, `--help`:
 120             os.Stdout.WriteString(info[1:])
 121             return
 122         }
 123     }
 124 
 125     if len(args) > 0 && args[0] == `--` {
 126         args = args[1:]
 127     }
 128 
 129     if len(args) == 0 {
 130         os.Stderr.WriteString(info[1:])
 131         os.Exit(1)
 132         return
 133     }
 134 
 135     if err := pac(os.Stdout, args); err != nil && err != io.EOF {
 136         os.Stderr.WriteString(`pac: `)
 137         os.Stderr.WriteString(err.Error())
 138         os.Stderr.WriteString("\n")
 139         os.Exit(1)
 140         return
 141     }
 142 }
 143 
 144 type context struct {
 145     stack  [][]float64
 146     random *rand.Rand
 147 }
 148 
 149 func pac(w io.Writer, args []string) error {
 150     var ctx context
 151     ctx.stack = make([][]float64, 0, 64)
 152     reset(&ctx)
 153 
 154     if err := run(args, &ctx); err != nil {
 155         return err
 156     }
 157 
 158     // if len(ctx.stack) > 1 {
 159     //  os.Stderr.WriteString("multiple results left on the stack\n")
 160     // }
 161     if len(ctx.stack) == 0 {
 162         return errors.New(`no results left on the stack`)
 163     }
 164 
 165     show(w, &ctx)
 166     return nil
 167 }
 168 
 169 func show(w io.Writer, ctx *context) {
 170     if len(ctx.stack) == 0 {
 171         return
 172     }
 173 
 174     bw := bufio.NewWriterSize(w, 32*1024)
 175     defer bw.Flush()
 176 
 177     var buf [24]byte
 178     for i, f := range ctx.stack[len(ctx.stack)-1] {
 179         if i > 0 {
 180             if err := bw.WriteByte(' '); err != nil {
 181                 break
 182             }
 183         }
 184         bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
 185     }
 186     bw.WriteByte('\n')
 187 }
 188 
 189 func debug(w io.StringWriter, ctx *context) {
 190     for i, v := range ctx.stack {
 191         w.WriteString("\t")
 192         w.WriteString(strconv.Itoa(i))
 193         w.WriteString(":")
 194         for _, f := range v {
 195             w.WriteString(" ")
 196             w.WriteString(strconv.FormatFloat(f, 'f', -1, 64))
 197         }
 198 
 199         w.WriteString("\n")
 200     }
 201 
 202     w.WriteString("\n")
 203 }
 204 
 205 func run(args []string, ctx *context) error {
 206     for len(args) > 0 {
 207         s := args[0]
 208         args = args[1:]
 209 
 210         for len(s) > 0 {
 211             for len(s) > 0 && s[0] == ' ' {
 212                 s = s[1:]
 213             }
 214             if len(s) == 0 {
 215                 break
 216             }
 217 
 218             var cmd string
 219             if i := strings.IndexByte(s, ' '); i >= 0 {
 220                 cmd = s[:i]
 221             } else {
 222                 cmd = s
 223             }
 224             s = s[len(cmd):]
 225 
 226             if f, err := strconv.ParseFloat(cmd, 64); err == nil {
 227                 ctx.stack = append(ctx.stack, []float64{f})
 228                 continue
 229             }
 230 
 231             if cmd == `[` {
 232                 rest, err := appendArray(ctx, args)
 233                 if err != nil {
 234                     return err
 235                 }
 236                 args = rest
 237                 continue
 238             }
 239 
 240             if cmd == `.` {
 241                 debug(os.Stderr, ctx)
 242                 continue
 243             }
 244 
 245             if f, ok := funcs[cmd]; ok {
 246                 if err := call(ctx, f, cmd); err != nil {
 247                     return err
 248                 }
 249                 continue
 250             }
 251 
 252             return errors.New(`unknown command ` + cmd)
 253         }
 254     }
 255 
 256     return nil
 257 }
 258 
 259 func appendArray(ctx *context, args []string) ([]string, error) {
 260     end := -1
 261     for i, s := range args {
 262         if s == `]` {
 263             end = i
 264             break
 265         }
 266     }
 267 
 268     if end < 0 {
 269         return args, errors.New(`missing the ']' to close array`)
 270     }
 271 
 272     items := args[:end]
 273     rest := args[end+1:]
 274 
 275     var sub context
 276     err := run(items, &sub)
 277     flatten(&sub)
 278     if len(sub.stack) > 0 {
 279         ctx.stack = append(ctx.stack, sub.stack[len(sub.stack)-1])
 280     }
 281     return rest, err
 282 }
 283 
 284 func call(ctx *context, f any, name string) error {
 285     var err error
 286 
 287     switch f := f.(type) {
 288     case float64:
 289         ctx.stack = append(ctx.stack, []float64{f})
 290     case func(ctx *context) error:
 291         err = f(ctx)
 292     case func(x float64) float64:
 293         err = loop(ctx, f)
 294     case func(x, y float64) float64:
 295         err = parallel(ctx, f)
 296     case func(x, y float64) (float64, float64):
 297         err = parallel2(ctx, f)
 298     case func(ctx *context, n int) error:
 299         err = repeat(ctx, f)
 300     default:
 301         err = errors.New(`unsupported function type`)
 302     }
 303 
 304     if err == io.EOF {
 305         return io.EOF
 306     }
 307     if err != nil {
 308         return errors.New(name + `: ` + err.Error())
 309     }
 310     return nil
 311 }
 312 
 313 func cloneArray(x []float64) []float64 {
 314     clone := make([]float64, 0, len(x))
 315     for _, f := range x {
 316         clone = append(clone, f)
 317     }
 318     return clone
 319 }
 320 
 321 func debool(x bool) float64 {
 322     if x {
 323         return 1
 324     } else {
 325         return 0
 326     }
 327 }
 328 
 329 var funcs = map[string]any{
 330     `+`:  add,
 331     `-`:  sub,
 332     `*`:  mul,
 333     `/`:  div,
 334     `%`:  math.Mod,
 335     `^`:  math.Pow,
 336     `**`: math.Pow,
 337 
 338     `add`:     add,
 339     `sub`:     sub,
 340     `mul`:     mul,
 341     `div`:     div,
 342     `mod`:     math.Mod,
 343     `modulus`: math.Mod,
 344     `pow`:     math.Pow,
 345     `power`:   math.Pow,
 346 
 347     `m`: mul,
 348 
 349     `=`:  eql,
 350     `==`: eql,
 351     `!=`: neq,
 352     `<>`: neq,
 353     `<`:  lt,
 354     `<=`: lte,
 355     `>`:  gt,
 356     `>=`: gte,
 357 
 358     `eql`: eql,
 359     `neq`: neq,
 360     `lt`:  lt,
 361     `lte`: lte,
 362     `gt`:  gt,
 363     `gte`: gte,
 364 
 365     `a`:           anchor,
 366     `abs`:         math.Abs,
 367     `anchor`:      anchor,
 368     `ceil`:        math.Ceil,
 369     `ceiling`:     math.Ceil,
 370     `cos`:         math.Cos,
 371     `cosh`:        math.Cosh,
 372     `cosine`:      math.Cos,
 373     `div2`:        div2,
 374     `down`:        math.Floor,
 375     `exp`:         math.Exp,
 376     `floor`:       math.Floor,
 377     `hypot`:       math.Hypot,
 378     `hypotenuse`:  math.Hypot,
 379     `hypothenuse`: math.Hypot,
 380     `mid`:         mid,
 381     `middle`:      mid,
 382     `neg`:         negate,
 383     `negate`:      negate,
 384     `ln`:          math.Log,
 385     `log`:         math.Log,
 386     `log10`:       math.Log10,
 387     `log2`:        math.Log2,
 388     `pow10`:       pow10,
 389     `pow2`:        pow2,
 390     `round`:       math.Round,
 391     `sgn`:         sign,
 392     `sign`:        sign,
 393     `sin`:         math.Sin,
 394     `sine`:        math.Sin,
 395     `sinh`:        math.Sinh,
 396     `tan`:         math.Tan,
 397     `tangent`:     math.Tan,
 398     `tanh`:        math.Tanh,
 399     `up`:          math.Ceil,
 400 
 401     `b`:       bail,
 402     `bail`:    bail,
 403     `bye`:     bye,
 404     `c`:       empty,
 405     `clear`:   empty,
 406     `d`:       dup,
 407     `dump`:    dump,
 408     `dup`:     dup,
 409     `dup2`:    dup2,
 410     `e`:       math.E,
 411     `empty`:   empty,
 412     `f`:       flatten,
 413     `flat`:    flatten,
 414     `flatten`: flatten,
 415     `fuzz`:    fuzz,
 416     `h`:       help,
 417     `help`:    help,
 418     `i`:       iota,
 419     `inv`:     invert,
 420     `inverse`: invert,
 421     `invert`:  invert,
 422     `iota`:    iota,
 423     `l`:       toplen,
 424     `len`:     toplen,
 425     `length`:  toplen,
 426     `levels`:  levels,
 427     `over`:    over,
 428     `p`:       toprint,
 429     `pi`:      math.Pi,
 430     `print`:   toprint,
 431     `pop`:     pop,
 432     `q`:       bye,
 433     `quit`:    bye,
 434     `rand`:    random,
 435     `random`:  random,
 436     `rnd`:     random,
 437     `s`:       swap2,
 438     `shift`:   shift,
 439     `spread`:  spread,
 440     `swap`:    swap2,
 441     `swap2`:   swap2,
 442     `tau`:     2 * math.Pi,
 443     `top`:     top,
 444     `u`:       unit,
 445     `unit`:    unit,
 446 }
 447 
 448 func invert(x float64) float64 { return 1 / x }
 449 func negate(x float64) float64 { return -x }
 450 
 451 func add(x, y float64) float64 { return x + y }
 452 func sub(x, y float64) float64 { return x - y }
 453 func mid(x, y float64) float64 { return 0.5 * (x + y) }
 454 func mul(x, y float64) float64 { return x * y }
 455 func div(x, y float64) float64 { return x / y }
 456 func pow10(x float64) float64  { return math.Pow(10, x) }
 457 func pow2(x float64) float64   { return math.Pow(2, x) }
 458 
 459 func div2(x, y float64) (float64, float64) { return x / y, y / x }
 460 
 461 func eql(x, y float64) float64 { return debool(x == y) }
 462 func gt(x, y float64) float64  { return debool(x > y) }
 463 func gte(x, y float64) float64 { return debool(x >= y) }
 464 func lt(x, y float64) float64  { return debool(x < y) }
 465 func lte(x, y float64) float64 { return debool(x <= y) }
 466 func neq(x, y float64) float64 { return debool(x != y) }
 467 
 468 func anchor(x float64) float64 {
 469     const eps = 1e6
 470     if i, f := math.Modf(x); -eps <= f && f <= +eps {
 471         return float64(i)
 472     }
 473     return x
 474 }
 475 
 476 func sign(x float64) float64 {
 477     if x > 0 {
 478         return +1
 479     }
 480     if x < 0 {
 481         return -1
 482     }
 483     return x
 484 }
 485 
 486 func bail(ctx *context) error {
 487     dump(ctx)
 488     return bye(ctx)
 489 }
 490 
 491 func bye(ctx *context) error {
 492     return io.EOF
 493 }
 494 
 495 func dump(ctx *context) error {
 496     if len(ctx.stack) == 0 {
 497         return nil
 498     }
 499 
 500     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 501     defer bw.Flush()
 502 
 503     var buf [24]byte
 504     for _, v := range ctx.stack {
 505         for i, f := range v {
 506             if i > 0 {
 507                 if err := bw.WriteByte(' '); err != nil {
 508                     break
 509                 }
 510             }
 511             bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
 512         }
 513 
 514         if err := bw.WriteByte('\n'); err != nil {
 515             return io.EOF
 516         }
 517     }
 518 
 519     return nil
 520 }
 521 
 522 func dup(ctx *context) error {
 523     if len(ctx.stack) > 0 {
 524         x := ctx.stack[len(ctx.stack)-1]
 525         ctx.stack = append(ctx.stack, cloneArray(x))
 526     }
 527     return nil
 528 }
 529 
 530 func dup2(ctx *context) error {
 531     if len(ctx.stack) < 2 {
 532         return needAtLeast2
 533     }
 534 
 535     x := ctx.stack[len(ctx.stack)-2]
 536     y := ctx.stack[len(ctx.stack)-1]
 537     ctx.stack = append(ctx.stack, cloneArray(x))
 538     ctx.stack = append(ctx.stack, cloneArray(y))
 539     return nil
 540 }
 541 
 542 func empty(ctx *context) error {
 543     ctx.stack = ctx.stack[:0]
 544     return nil
 545 }
 546 
 547 func flatten(ctx *context) error {
 548     n := 0
 549     for _, v := range ctx.stack {
 550         n += len(v)
 551     }
 552 
 553     top := make([]float64, 0, n)
 554     for _, v := range ctx.stack {
 555         top = append(top, v...)
 556     }
 557     ctx.stack = append(ctx.stack[:0], top)
 558     return nil
 559 }
 560 
 561 // fuzz tries to undo a number-rounding, turning each number at the stack-top
 562 // into a 3-item slice: the original number itself, surrounded by its lowest
 563 // and highest values from the range it can be rounded from
 564 func fuzz(ctx *context) error {
 565     if len(ctx.stack) < 1 {
 566         return needAtLeast1
 567     }
 568 
 569     top := ctx.stack[len(ctx.stack)-1]
 570     if len(top) == 1 {
 571         f, err := fuzzValue(top[0])
 572         if err != nil {
 573             return err
 574         }
 575         ctx.stack[len(ctx.stack)-1] = f
 576     }
 577 
 578     ctx.stack = ctx.stack[:len(ctx.stack)-1]
 579     for _, f := range top {
 580         fuzzed, err := fuzzValue(f)
 581         if err != nil {
 582             return err
 583         }
 584         ctx.stack = append(ctx.stack, fuzzed)
 585     }
 586     return nil
 587 }
 588 
 589 // fuzzValue tries to undo a number's rounding, returning a slice with the
 590 // value itself, surrounded by its lowest and highest values from the range
 591 // it can be rounded from
 592 func fuzzValue(f float64) ([]float64, error) {
 593     if math.IsNaN(f) || math.IsInf(f, 0) {
 594         return []float64{f, f, f}, nil
 595     }
 596 
 597     // treat round numbers differently, by `fuzzing` with/around integers
 598     if math.Mod(f, 1.0) != 0.0 {
 599         lo := math.Floor(f)
 600         hi := math.Ceil(f)
 601         return []float64{lo, f, hi}, nil
 602     }
 603 
 604     // find the smallest power of 10 to fuzz with
 605     fuzzBy := 1.0
 606     for fuzzBy < maxExactInt/10 {
 607         next := 10 * fuzzBy
 608         if math.Mod(f, next) != 0.0 {
 609             break
 610         }
 611         fuzzBy = next
 612     }
 613 
 614     lo := fuzzBy*math.Trunc(f/fuzzBy) - fuzzBy/2
 615     hi := fuzzBy*math.Ceil(f/fuzzBy) + fuzzBy/2
 616     return []float64{lo, f, hi}, nil
 617 }
 618 
 619 func help(ctx *context) error {
 620     if _, err := os.Stdout.WriteString(info[1:]); err != nil {
 621         return io.EOF
 622     }
 623     return nil
 624 }
 625 
 626 func iota(ctx *context, n int) error {
 627     if n < 1 {
 628         return nil
 629     }
 630 
 631     top := make([]float64, n)
 632     for i := range top {
 633         top[i] = float64(i + 1)
 634     }
 635     ctx.stack = append(ctx.stack, top)
 636 
 637     return nil
 638 }
 639 
 640 func levels(ctx *context) error {
 641     l := float64(len(ctx.stack))
 642     ctx.stack = append(ctx.stack, []float64{l})
 643     return nil
 644 }
 645 
 646 func over(ctx *context) error {
 647     if len(ctx.stack) > 0 {
 648         top := ctx.stack[len(ctx.stack)-1]
 649         ctx.stack = append(ctx.stack, []float64{})
 650         copy(ctx.stack[1:], ctx.stack[:len(ctx.stack)-1])
 651         ctx.stack[0] = top
 652     }
 653     return nil
 654 }
 655 
 656 func pop(ctx *context) error {
 657     if len(ctx.stack) > 0 {
 658         ctx.stack = ctx.stack[:len(ctx.stack)-1]
 659     }
 660     return nil
 661 }
 662 
 663 func random(ctx *context, n int) error {
 664     if n < 1 {
 665         return nil
 666     }
 667 
 668     top := make([]float64, n)
 669     for i := range top {
 670         top[i] = ctx.random.Float64()
 671     }
 672     ctx.stack = append(ctx.stack, top)
 673 
 674     return nil
 675 }
 676 
 677 func reset(ctx *context) error {
 678     ctx.stack = ctx.stack[:0]
 679     ctx.random = rand.New(rand.NewSource(time.Now().UnixNano()))
 680     return nil
 681 }
 682 
 683 func shift(ctx *context) error {
 684     for i := 1; i < len(ctx.stack); i++ {
 685         ctx.stack[i-1] = ctx.stack[i]
 686     }
 687 
 688     if len(ctx.stack) > 0 {
 689         ctx.stack = ctx.stack[:len(ctx.stack)-1]
 690     }
 691 
 692     return nil
 693 }
 694 
 695 func spread(ctx *context) error {
 696     prev := ctx.stack
 697     next := ctx.stack[:0]
 698 
 699     for _, v := range prev {
 700         for _, f := range v {
 701             next = append(next, []float64{f})
 702         }
 703     }
 704 
 705     ctx.stack = next
 706     return nil
 707 }
 708 
 709 func toplen(ctx *context) error {
 710     if len(ctx.stack) < 1 {
 711         return needAtLeast1
 712     }
 713 
 714     top := ctx.stack[len(ctx.stack)-1]
 715     ctx.stack = append(ctx.stack, []float64{float64(len(top))})
 716     return nil
 717 }
 718 
 719 func swap2(ctx *context) error {
 720     if len(ctx.stack) < 2 {
 721         return needAtLeast2
 722     }
 723 
 724     x := ctx.stack[len(ctx.stack)-2]
 725     y := ctx.stack[len(ctx.stack)-1]
 726     ctx.stack[len(ctx.stack)-2] = y
 727     ctx.stack[len(ctx.stack)-1] = x
 728     return nil
 729 }
 730 
 731 func top(ctx *context) error {
 732     if len(ctx.stack) < 1 {
 733         return needAtLeast1
 734     }
 735 
 736     ctx.stack = append(ctx.stack[:0], ctx.stack[len(ctx.stack)-1])
 737     return nil
 738 }
 739 
 740 func toprint(ctx *context) error {
 741     show(os.Stdout, ctx)
 742     return nil
 743 }
 744 
 745 func unit(ctx *context, n int) error {
 746     if n < 1 {
 747         return nil
 748     }
 749 
 750     top := make([]float64, n)
 751     for i := range top {
 752         top[i] = 1
 753     }
 754     ctx.stack = append(ctx.stack, top)
 755 
 756     return nil
 757 }
 758 
 759 func loop(ctx *context, f func(x float64) float64) error {
 760     if len(ctx.stack) < 1 {
 761         return needAtLeast1
 762     }
 763 
 764     top := ctx.stack[len(ctx.stack)-1]
 765     for i, v := range top {
 766         top[i] = f(v)
 767     }
 768     return nil
 769 }
 770 
 771 func parallel(ctx *context, f func(x, y float64) float64) error {
 772     if len(ctx.stack) < 2 {
 773         return needAtLeast2
 774     }
 775 
 776     l := len(ctx.stack)
 777     xa := ctx.stack[l-2]
 778     ya := ctx.stack[l-1]
 779     ctx.stack = ctx.stack[:l-2]
 780 
 781     if len(xa) == 0 || len(ya) == 0 {
 782         return nil
 783     }
 784 
 785     var za []float64
 786     if len(xa) < len(ya) {
 787         za = ya
 788     } else {
 789         za = xa
 790     }
 791 
 792     for i := range za {
 793         za[i] = f(xa[i%len(xa)], ya[i%len(ya)])
 794     }
 795     ctx.stack = append(ctx.stack, za)
 796     return nil
 797 }
 798 
 799 func parallel2(ctx *context, f func(x, y float64) (float64, float64)) error {
 800     if len(ctx.stack) < 2 {
 801         return needAtLeast2
 802     }
 803 
 804     l := len(ctx.stack)
 805     xa := ctx.stack[l-2]
 806     ya := ctx.stack[l-1]
 807     ctx.stack = ctx.stack[:l-2]
 808 
 809     if len(xa) == 0 || len(ya) == 0 {
 810         return nil
 811     }
 812 
 813     var za []float64
 814     if len(xa) < len(ya) {
 815         za = ya
 816     } else {
 817         za = xa
 818     }
 819 
 820     r1 := make([]float64, len(za))
 821     r2 := make([]float64, len(za))
 822     for i := range r1 {
 823         x, y := f(xa[i%len(xa)], ya[i%len(ya)])
 824         r1[i] = x
 825         r2[i] = y
 826     }
 827     ctx.stack = append(ctx.stack, r1)
 828     ctx.stack = append(ctx.stack, r2)
 829     return nil
 830 }
 831 
 832 func repeat(ctx *context, f func(ctx *context, n int) error) error {
 833     if len(ctx.stack) < 1 {
 834         return needAtLeast1
 835     }
 836 
 837     counts := ctx.stack[len(ctx.stack)-1]
 838     ctx.stack = ctx.stack[:len(ctx.stack)-1]
 839 
 840     for _, n := range counts {
 841         if err := f(ctx, int(n)); err != nil {
 842             return err
 843         }
 844     }
 845 
 846     return nil
 847 }
     File: ./pac/pac_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package pac
  26 
  27 import (
  28     "strings"
  29     "testing"
  30 )
  31 
  32 func TestTypes(t *testing.T) {
  33     var ctx context
  34     reset(&ctx)
  35 
  36     for name, f := range funcs {
  37         t.Run(name, func(t *testing.T) {
  38             switch f.(type) {
  39             case
  40                 float64,
  41                 func(x float64) float64,
  42                 func(x, y float64) float64,
  43                 func(x, y float64) (float64, float64),
  44                 func(ctx *context) error,
  45                 func(ctx *context, n int) error:
  46                 return
  47 
  48             default:
  49                 t.Fatalf("%s: bad dispatch type %T", name, f)
  50             }
  51         })
  52     }
  53 }
  54 
  55 func TestResults(t *testing.T) {
  56     tests := map[string]string{
  57         `0.0`:    `0`,
  58         `2 iota`: `1 2`,
  59     }
  60 
  61     for arguments, expected := range tests {
  62         t.Run(arguments, func(t *testing.T) {
  63             var w strings.Builder
  64             if err := pac(&w, strings.Fields(arguments)); err != nil {
  65                 t.Fatal(err)
  66             }
  67 
  68             if got := strings.TrimSpace(w.String()); got != expected {
  69                 t.Fatalf("got %s, expected %s instead", got, expected)
  70             }
  71         })
  72     }
  73 }
     File: ./pad/pad.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package pad
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33     "unicode/utf8"
  34 )
  35 
  36 const info = `
  37 pad [options...] [filenames...]
  38 
  39 Pad lines with trailing spaces, so that all lines have the same number of
  40 symbols. ANSI style-codes are also kept as given.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help           show this help message
  45 `
  46 
  47 func Main() {
  48     args := os.Args[1:]
  49 
  50     if len(args) > 0 {
  51         switch args[0] {
  52         case `-h`, `--h`, `-help`, `--help`:
  53             os.Stdout.WriteString(info[1:])
  54             return
  55         }
  56     }
  57 
  58     if len(args) > 0 && args[0] == `--` {
  59         args = args[1:]
  60     }
  61 
  62     if err := run(args); err != nil {
  63         os.Stderr.WriteString(`pad: `)
  64         os.Stderr.WriteString(err.Error())
  65         os.Stderr.WriteString("\n")
  66         os.Exit(1)
  67         return
  68     }
  69 }
  70 
  71 type paddingData struct {
  72     lines []string
  73     max   int
  74 }
  75 
  76 func run(paths []string) error {
  77     var pd paddingData
  78 
  79     for _, p := range paths {
  80         if err := handleFile(&pd, p); err != nil {
  81             return err
  82         }
  83     }
  84 
  85     if len(paths) == 0 {
  86         if err := handleReader(&pd, os.Stdin); err != nil {
  87             return err
  88         }
  89     }
  90 
  91     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  92     defer bw.Flush()
  93 
  94     for _, line := range pd.lines {
  95         bw.WriteString(line)
  96         writeSpaces(bw, pd.max-countWidth(line))
  97         if bw.WriteByte('\n') != nil {
  98             break
  99         }
 100     }
 101     return nil
 102 }
 103 
 104 func handleFile(pd *paddingData, path string) error {
 105     f, err := os.Open(path)
 106     if err != nil {
 107         // on windows, file-not-found error messages may mention `CreateFile`,
 108         // even when trying to open files in read-only mode
 109         return errors.New(`can't open file named ` + path)
 110     }
 111     defer f.Close()
 112     return handleReader(pd, f)
 113 }
 114 
 115 func handleReader(pd *paddingData, r io.Reader) error {
 116     const gb = 1024 * 1024 * 1024
 117     sc := bufio.NewScanner(r)
 118     sc.Buffer(nil, 8*gb)
 119 
 120     for i := 0; sc.Scan(); i++ {
 121         line := sc.Text()
 122         if i == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
 123             line = line[3:]
 124         }
 125 
 126         n := countWidth(line)
 127         if pd.max < n {
 128             pd.max = n
 129         }
 130         pd.lines = append(pd.lines, line)
 131     }
 132 
 133     return sc.Err()
 134 }
 135 
 136 func countWidth(s string) int {
 137     width := 0
 138 
 139     for len(s) > 0 {
 140         i, j := indexEscapeSequence(s)
 141         if i < 0 {
 142             break
 143         }
 144         if j < 0 {
 145             j = len(s)
 146         }
 147 
 148         width += utf8.RuneCountInString(s[:i])
 149         s = s[j:]
 150     }
 151 
 152     // count trailing/all runes in strings which don't end with ANSI-sequences
 153     width += utf8.RuneCountInString(s)
 154     return width
 155 }
 156 
 157 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 158 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 159 // indices which can be independently negative when either the start/end of
 160 // a sequence isn't found; given their fairly-common use, even the hyperlink
 161 // ESC]8 sequences are supported
 162 func indexEscapeSequence(s string) (int, int) {
 163     var prev byte
 164 
 165     for i := range s {
 166         b := s[i]
 167 
 168         if prev == '\x1b' && b == '[' {
 169             j := indexLetter(s[i+1:])
 170             if j < 0 {
 171                 return i, -1
 172             }
 173             return i - 1, i + 1 + j + 1
 174         }
 175 
 176         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 177             j := indexPair(s[i+1:], '\x1b', '\\')
 178             if j < 0 {
 179                 return i, -1
 180             }
 181             return i - 1, i + 1 + j + 2
 182         }
 183 
 184         prev = b
 185     }
 186 
 187     return -1, -1
 188 }
 189 
 190 func indexLetter(s string) int {
 191     for i, b := range s {
 192         upper := b &^ 32
 193         if 'A' <= upper && upper <= 'Z' {
 194             return i
 195         }
 196     }
 197 
 198     return -1
 199 }
 200 
 201 func indexPair(s string, x byte, y byte) int {
 202     var prev byte
 203 
 204     for i := range s {
 205         b := s[i]
 206         if prev == x && b == y && i > 0 {
 207             return i
 208         }
 209         prev = b
 210     }
 211 
 212     return -1
 213 }
 214 
 215 // writeSpaces does what it says, minimizing calls to write-like funcs
 216 func writeSpaces(w *bufio.Writer, n int) {
 217     const spaces = `                                `
 218     if n < 1 {
 219         return
 220     }
 221 
 222     for n >= len(spaces) {
 223         w.WriteString(spaces)
 224         n -= len(spaces)
 225     }
 226     w.WriteString(spaces[:n])
 227 }
     File: ./paramatch/paramatch.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package paramatch
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 paramatch [options...] [regexes...]
  38 
  39 PARAgraph MATCH tries to match all paragraphs containing any of the regexes
  40 given.
  41 
  42 This tool also offers a special "flat" mode, which further tests the regexes
  43 against a flattened version of each current paragraph, where lines are joined
  44 via single spaces, instead of line-feeds.
  45 
  46 The options are, available both in single and double-dash versions
  47 
  48     -f, -flat    also check regexes with flat/space-joined paragraph lines
  49     -h, -help    show this help message
  50     -i, -ins     match regexes case-insensitively
  51 `
  52 
  53 type config struct {
  54     // patterns are the regexes to test against
  55     patterns []*regexp.Regexp
  56 
  57     // shown counts how many paragraphs have been/shown matched so far
  58     shown int
  59 
  60     // flatChance is whether to also test regexes against `flattened` lines
  61     flatChance bool
  62 
  63     // live is whether to flush output each time a paragraph is emitted
  64     live bool
  65 }
  66 
  67 func (cfg *config) dump(w *bufio.Writer, what []byte) error {
  68     if len(what) == 0 {
  69         return nil
  70     }
  71 
  72     if cfg.shown > 0 {
  73         w.WriteByte('\n')
  74     }
  75     cfg.shown++
  76 
  77     if _, err := w.Write(what); err != nil {
  78         return io.EOF
  79     }
  80 
  81     if cfg.live && w.Flush() != nil {
  82         return io.EOF
  83     }
  84     return nil
  85 }
  86 
  87 func (cfg config) match(line []byte) bool {
  88     for _, pat := range cfg.patterns {
  89         if pat.Match(line) {
  90             return true
  91         }
  92     }
  93     return false
  94 }
  95 
  96 func Main() {
  97     var cfg config
  98     cfg.live = true
  99     insensitive := false
 100     args := os.Args[1:]
 101 
 102     for len(args) > 0 {
 103         switch args[0] {
 104         case `-b`, `--b`, `-buffered`, `--buffered`:
 105             cfg.live = false
 106             args = args[1:]
 107             continue
 108 
 109         case `-f`, `--f`, `-flat`, `--flat`, `-flattened`, `--flattened`:
 110             cfg.flatChance = true
 111             args = args[1:]
 112             continue
 113 
 114         case `-h`, `--h`, `-help`, `--help`:
 115             os.Stdout.WriteString(info[1:])
 116             return
 117 
 118         case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
 119             insensitive = true
 120             args = args[1:]
 121             continue
 122         }
 123 
 124         if s := args[0]; s == `-` || s == `--` || !strings.HasPrefix(s, `-`) {
 125             break
 126         }
 127 
 128         for s := args[0][1:]; len(s) > 0; s = s[1:] {
 129             switch s[0] {
 130             case 'b':
 131                 cfg.live = false
 132                 continue
 133             case 'f':
 134                 cfg.flatChance = true
 135                 continue
 136             case 'h':
 137                 os.Stdout.WriteString(info[1:])
 138                 return
 139             case 'i':
 140                 insensitive = true
 141                 continue
 142             default:
 143                 os.Stderr.WriteString(`paramatch: unsupported option `)
 144                 os.Stderr.WriteString(args[0])
 145                 os.Stderr.WriteString("\n")
 146                 os.Exit(1)
 147             }
 148         }
 149 
 150         args = args[1:]
 151     }
 152 
 153     if len(args) > 0 && args[0] == `--` {
 154         args = args[1:]
 155     }
 156 
 157     if cfg.live {
 158         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 159             cfg.live = false
 160         }
 161     }
 162 
 163     if len(args) == 0 {
 164         os.Stderr.WriteString(info[1:])
 165         os.Exit(1)
 166         return
 167     }
 168 
 169     nerr := 0
 170 
 171     for _, s := range args {
 172         if insensitive && !strings.HasPrefix(s, `(?i)`) {
 173             s = `(?i)` + s
 174         }
 175 
 176         pat, err := regexp.Compile(s)
 177         if err != nil {
 178             os.Stderr.WriteString(`paramatch: `)
 179             os.Stderr.WriteString(err.Error())
 180             os.Stderr.WriteString("\n")
 181             nerr++
 182             continue
 183         }
 184         cfg.patterns = append(cfg.patterns, pat)
 185     }
 186 
 187     if nerr > 0 {
 188         os.Exit(1)
 189         return
 190     }
 191 
 192     if err := run(cfg); err != nil && err != io.EOF {
 193         os.Stderr.WriteString(`paramatch: `)
 194         os.Stderr.WriteString(err.Error())
 195         os.Stderr.WriteString("\n")
 196         os.Exit(1)
 197         return
 198     }
 199 }
 200 
 201 func run(cfg config) error {
 202     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 203     defer w.Flush()
 204 
 205     // par is the current paragraph's lines joined by line-feeds
 206     var par []byte
 207     // flat is the current paragraph's lines joined by single spaces
 208     var flat []byte
 209 
 210     const gb = 1024 * 1024 * 1024
 211     sc := bufio.NewScanner(os.Stdin)
 212     sc.Buffer(nil, 8*gb)
 213 
 214     didMatch := false
 215 
 216     for i := 0; sc.Scan(); i++ {
 217         line := sc.Bytes()
 218         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 219             line = line[3:]
 220         }
 221 
 222         if emptyish(line) {
 223             if !didMatch && cfg.flatChance && len(flat) > 0 {
 224                 didMatch = cfg.match(flat)
 225             }
 226 
 227             if !didMatch {
 228                 par = par[:0]
 229                 flat = flat[:0]
 230                 continue
 231             }
 232 
 233             if err := cfg.dump(w, par); err != nil {
 234                 return err
 235             }
 236 
 237             par = par[:0]
 238             flat = flat[:0]
 239             didMatch = false
 240             continue
 241         }
 242 
 243         // avoid checking already-matched paragraphs
 244         if !didMatch {
 245             if len(flat) > 0 {
 246                 flat = append(flat, ' ')
 247             }
 248             flat = append(flat, line...)
 249 
 250             didMatch = cfg.match(line)
 251             if didMatch {
 252                 flat = flat[:0]
 253             }
 254         }
 255 
 256         // remember all paragraph lines
 257         par = append(par, line...)
 258         par = append(par, '\n')
 259     }
 260 
 261     if err := sc.Err(); err != nil {
 262         return err
 263     }
 264 
 265     if !didMatch && cfg.flatChance && len(flat) > 0 {
 266         didMatch = cfg.match(flat)
 267     }
 268 
 269     if didMatch {
 270         if err := cfg.dump(w, par); err != nil {
 271             return err
 272         }
 273     }
 274 
 275     return nil
 276 }
 277 
 278 func emptyish(line []byte) bool {
 279     // trim leading spaces
 280     for len(line) > 0 && line[0] == ' ' {
 281         line = line[1:]
 282     }
 283 
 284     // trim trailing spaces
 285     for len(line) > 0 && line[len(line)-1] == ' ' {
 286         line = line[:len(line)-1]
 287     }
 288 
 289     return len(line) == 0
 290 }
     File: ./pbn/pbn.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package pbn
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 pbn [options...] [line numbers...]
  37 
  38 Pick (lines) By Number, using all the 1-based line-numbers given, or even
  39 negative ones, which count backward from the last line. Input lines always
  40 come from the standard input.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help           show this help message
  45 `
  46 
  47 func Main() {
  48     args := os.Args[1:]
  49     buffered := false
  50 
  51     if len(args) > 0 {
  52         switch args[0] {
  53         case `-b`, `--b`, `-buffered`, `--buffered`:
  54             buffered = true
  55             args = args[1:]
  56 
  57         case `-h`, `--h`, `-help`, `--help`:
  58             os.Stdout.WriteString(info[1:])
  59             return
  60         }
  61     }
  62 
  63     if len(args) > 0 && args[0] == `--` {
  64         args = args[1:]
  65     }
  66 
  67     which := make([]int, 0, len(args))
  68 
  69     for _, s := range args {
  70         n, err := strconv.ParseInt(s, 10, 64)
  71 
  72         if err != nil {
  73             os.Stderr.WriteString(`pbn: `)
  74             os.Stderr.WriteString(err.Error())
  75             os.Stderr.WriteString("\n")
  76             os.Exit(1)
  77             return
  78         }
  79 
  80         if n == 0 {
  81             os.Stderr.WriteString("indices can't be zero\n")
  82             os.Exit(1)
  83             return
  84         }
  85 
  86         i := n
  87         if n > 0 {
  88             i--
  89         }
  90 
  91         which = append(which, int(i))
  92     }
  93 
  94     liveLines := !buffered
  95     if !buffered {
  96         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  97             liveLines = false
  98         }
  99     }
 100 
 101     if err := run(which, liveLines); err != nil {
 102         os.Stderr.WriteString(`pbn: `)
 103         os.Stderr.WriteString(err.Error())
 104         os.Stderr.WriteString("\n")
 105         os.Exit(1)
 106         return
 107     }
 108 }
 109 
 110 func run(which []int, liveLines bool) error {
 111     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 112     defer bw.Flush()
 113 
 114     const gb = 1024 * 1024 * 1024
 115     sc := bufio.NewScanner(os.Stdin)
 116     sc.Buffer(nil, 8*gb)
 117 
 118     // see if lines even need to be stored
 119     prev := 0
 120     fullStreak := true
 121     for _, i := range which {
 122         if i < 1 || i < prev {
 123             fullStreak = false
 124             break
 125         }
 126         prev = i
 127     }
 128 
 129     var lines []string
 130 
 131     for i := 0; sc.Scan() && len(which) > 0; i++ {
 132         line := sc.Text()
 133         if i == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
 134             line = line[3:]
 135         }
 136 
 137         if !fullStreak {
 138             lines = append(lines, line)
 139         }
 140 
 141         for len(which) > 0 && i == which[0] {
 142             if writeLine(bw, line, liveLines) != nil {
 143                 return nil
 144             }
 145             which = which[1:]
 146         }
 147     }
 148 
 149     if err := sc.Err(); err != nil {
 150         return err
 151     }
 152 
 153     for _, i := range which {
 154         if i < 0 {
 155             i += len(lines)
 156         }
 157 
 158         if 0 <= i && i < len(lines) {
 159             if writeLine(bw, lines[i], liveLines) != nil {
 160                 return nil
 161             }
 162         }
 163     }
 164 
 165     return nil
 166 }
 167 
 168 func writeLine(w *bufio.Writer, s string, live bool) error {
 169     w.WriteString(s)
 170     if w.WriteByte('\n') != nil {
 171         return nil
 172     }
 173 
 174     if live && w.Flush() != nil {
 175         return io.EOF
 176     }
 177     return nil
 178 }
     File: ./pcol/pcol.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package pcol
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 pcol [column names...]
  37 
  38 
  39 Pick COLumns lets you select/reorder a subset of a table's columns, matching
  40 the column names given using the first line from the standard input. Input
  41 lines can be either space-separated or tab-separated; output lines are always
  42 TSV (Tab-Separated Values) ones, where trailing tabs are added if any values
  43 are missing.
  44 
  45 When a column name isn't matched exactly, a case-insensitive match is tried:
  46 if the latter also fails, number-matching is finally tried, before giving up
  47 on that column name. Column numbers start at 1, and can be negative to count
  48 backward from the last column.
  49 
  50 All (optional) leading options start with either single or double-dash:
  51 
  52     -h, -help    show this help message
  53 `
  54 
  55 func Main() {
  56     buffered := false
  57     args := os.Args[1:]
  58 
  59     if len(args) > 0 {
  60         switch args[0] {
  61         case `-b`, `--b`, `-buffered`, `--buffered`:
  62             buffered = true
  63             args = args[1:]
  64 
  65         case `-h`, `--h`, `-help`, `--help`:
  66             os.Stdout.WriteString(info[1:])
  67             return
  68         }
  69     }
  70 
  71     if len(args) > 0 && args[0] == `--` {
  72         args = args[1:]
  73     }
  74 
  75     if len(args) == 0 {
  76         os.Stderr.WriteString(info[1:])
  77         return
  78     }
  79 
  80     liveLines := !buffered
  81     if !buffered {
  82         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  83             liveLines = false
  84         }
  85     }
  86 
  87     if err := run(args, liveLines); err != nil && err != io.EOF {
  88         os.Stderr.WriteString(`pcol: `)
  89         os.Stderr.WriteString(err.Error())
  90         os.Stderr.WriteString("\n")
  91         os.Exit(1)
  92         return
  93     }
  94 }
  95 
  96 type itemFunc func(i int, s string) bool
  97 type handler func(s string, f itemFunc)
  98 
  99 func run(args []string, live bool) error {
 100     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 101     defer w.Flush()
 102 
 103     const gb = 1024 * 1024 * 1024
 104     sc := bufio.NewScanner(os.Stdin)
 105     sc.Buffer(nil, 8*gb)
 106 
 107     var which []int
 108     var fields []string
 109     var handle handler
 110 
 111     for i := 0; sc.Scan(); i++ {
 112         s := sc.Text()
 113         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 114             s = s[3:]
 115         }
 116 
 117         if i == 0 {
 118             picks, h, ok := match(s, args)
 119             if !ok {
 120                 return nil
 121             }
 122             which = picks
 123             handle = h
 124             fields = make([]string, 0, len(which))
 125         }
 126 
 127         fields = fields[:0]
 128         handle(s, func(i int, s string) bool {
 129             fields = append(fields, s)
 130             return true
 131         })
 132 
 133         got := 0
 134         for _, i := range which {
 135             if 0 <= i && i < len(fields) {
 136                 if got > 0 {
 137                     w.WriteByte('\t')
 138                 }
 139                 w.WriteString(fields[i])
 140                 got++
 141             }
 142         }
 143 
 144         if w.WriteByte('\n') != nil {
 145             return io.EOF
 146         }
 147 
 148         if live && w.Flush() != nil {
 149             return io.EOF
 150         }
 151     }
 152 
 153     return sc.Err()
 154 }
 155 
 156 func match(s string, args []string) (which []int, handle handler, ok bool) {
 157     if strings.IndexByte(s, '\t') >= 0 {
 158         handle = loopItemsTSV
 159     } else {
 160         handle = loopItemsSSV
 161     }
 162 
 163     // count columns, so negative indices can be fixed with it
 164     count := 0
 165     handle(s, func(i int, s string) bool {
 166         count++
 167         return true
 168     })
 169 
 170     for _, arg := range args {
 171         ok := false
 172 
 173         // try exact matches
 174         handle(s, func(i int, s string) bool {
 175             if s == arg {
 176                 ok = true
 177                 which = append(which, i)
 178                 return false
 179             }
 180             return true
 181         })
 182 
 183         if ok {
 184             continue
 185         }
 186 
 187         // try case-insensitive matches
 188         handle(s, func(i int, s string) bool {
 189             if strings.EqualFold(s, arg) {
 190                 ok = true
 191                 which = append(which, i)
 192                 return false
 193             }
 194             return true
 195         })
 196 
 197         if ok {
 198             continue
 199         }
 200 
 201         // try 1-based indices, even negative ones
 202         if n, err := strconv.Atoi(arg); err == nil {
 203             if n < 0 {
 204                 n += count
 205             } else if n > 0 {
 206                 n--
 207             }
 208 
 209             if 0 <= n && n < count {
 210                 which = append(which, n)
 211             }
 212         }
 213     }
 214 
 215     return which, handle, true
 216 }
 217 
 218 // loopItemsSSV loops over a line's items, allocation-free style; when given
 219 // empty strings, the callback func is never called
 220 func loopItemsSSV(s string, f itemFunc) {
 221     s = trimTrailingSpaces(s)
 222 
 223     for i := 0; true; i++ {
 224         s = trimLeadingSpaces(s)
 225         if len(s) == 0 {
 226             return
 227         }
 228 
 229         j := strings.IndexByte(s, ' ')
 230         if j < 0 {
 231             if !f(i, s) {
 232                 return
 233             }
 234             return
 235         }
 236 
 237         if !f(i, s[:j]) {
 238             return
 239         }
 240         s = s[j+1:]
 241     }
 242 }
 243 
 244 func trimLeadingSpaces(s string) string {
 245     for len(s) > 0 && s[0] == ' ' {
 246         s = s[1:]
 247     }
 248     return s
 249 }
 250 
 251 func trimTrailingSpaces(s string) string {
 252     for len(s) > 0 && s[len(s)-1] == ' ' {
 253         s = s[:len(s)-1]
 254     }
 255     return s
 256 }
 257 
 258 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 259 // when given empty strings, the callback func is never called
 260 func loopItemsTSV(s string, f itemFunc) {
 261     if len(s) == 0 {
 262         return
 263     }
 264 
 265     for i := 0; true; i++ {
 266         j := strings.IndexByte(s, '\t')
 267         if j < 0 {
 268             if !f(i, s) {
 269                 return
 270             }
 271             return
 272         }
 273 
 274         if !f(i, s[:j]) {
 275             return
 276         }
 277         s = s[j+1:]
 278     }
 279 }
     File: ./plain/plain.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package plain
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 plain [options...] [file...]
  37 
  38 
  39 Turn potentially ANSI-styled plain-text into actual plain-text.
  40 
  41 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  42 feeds.
  43 
  44 All (optional) leading options start with either single or double-dash:
  45 
  46     -h, -help    show this help message
  47 `
  48 
  49 func Main() {
  50     buffered := false
  51     args := os.Args[1:]
  52 
  53     if len(args) > 0 {
  54         switch args[0] {
  55         case `-b`, `--b`, `-buffered`, `--buffered`:
  56             buffered = true
  57             args = args[1:]
  58 
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62         }
  63     }
  64 
  65     if len(args) > 0 && args[0] == `--` {
  66         args = args[1:]
  67     }
  68 
  69     liveLines := !buffered
  70     if !buffered {
  71         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  72             liveLines = false
  73         }
  74     }
  75 
  76     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  77         os.Stderr.WriteString(`plain: `)
  78         os.Stderr.WriteString(err.Error())
  79         os.Stderr.WriteString("\n")
  80         os.Exit(1)
  81         return
  82     }
  83 }
  84 
  85 func run(w io.Writer, args []string, live bool) error {
  86     bw := bufio.NewWriterSize(w, 32*1024)
  87     defer bw.Flush()
  88 
  89     if len(args) == 0 {
  90         return plain(bw, os.Stdin, live)
  91     }
  92 
  93     for _, name := range args {
  94         if err := handleFile(bw, name, live); err != nil {
  95             return err
  96         }
  97     }
  98     return nil
  99 }
 100 
 101 func handleFile(w *bufio.Writer, name string, live bool) error {
 102     if name == `` || name == `-` {
 103         return plain(w, os.Stdin, live)
 104     }
 105 
 106     f, err := os.Open(name)
 107     if err != nil {
 108         return errors.New(`can't read from file named "` + name + `"`)
 109     }
 110     defer f.Close()
 111 
 112     return plain(w, f, live)
 113 }
 114 
 115 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 116 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 117 // indices which can be independently negative when either the start/end of
 118 // a sequence isn't found; given their fairly-common use, even the hyperlink
 119 // ESC]8 sequences are supported
 120 func indexEscapeSequence(s []byte) (int, int) {
 121     var prev byte
 122 
 123     for i, b := range s {
 124         if prev == '\x1b' && b == '[' {
 125             j := indexLetter(s[i+1:])
 126             if j < 0 {
 127                 return i, -1
 128             }
 129             return i - 1, i + 1 + j + 1
 130         }
 131 
 132         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 133             j := indexPair(s[i+1:], '\x1b', '\\')
 134             if j < 0 {
 135                 return i, -1
 136             }
 137             return i - 1, i + 1 + j + 2
 138         }
 139 
 140         prev = b
 141     }
 142 
 143     return -1, -1
 144 }
 145 
 146 func indexLetter(s []byte) int {
 147     for i, b := range s {
 148         upper := b &^ 32
 149         if 'A' <= upper && upper <= 'Z' {
 150             return i
 151         }
 152     }
 153 
 154     return -1
 155 }
 156 
 157 func indexPair(s []byte, x byte, y byte) int {
 158     var prev byte
 159 
 160     for i, b := range s {
 161         if prev == x && b == y && i > 0 {
 162             return i
 163         }
 164         prev = b
 165     }
 166 
 167     return -1
 168 }
 169 
 170 func plain(w *bufio.Writer, r io.Reader, live bool) error {
 171     const gb = 1024 * 1024 * 1024
 172     sc := bufio.NewScanner(r)
 173     sc.Buffer(nil, 8*gb)
 174 
 175     for i := 0; sc.Scan(); i++ {
 176         s := sc.Bytes()
 177         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 178             s = s[3:]
 179         }
 180 
 181         for line := s; len(line) > 0; {
 182             i, j := indexEscapeSequence(line)
 183             if i < 0 {
 184                 w.Write(line)
 185                 break
 186             }
 187             if j < 0 {
 188                 j = len(line)
 189             }
 190 
 191             if i > 0 {
 192                 w.Write(line[:i])
 193             }
 194 
 195             line = line[j:]
 196         }
 197 
 198         if w.WriteByte('\n') != nil {
 199             return io.EOF
 200         }
 201 
 202         if live && w.Flush() != nil {
 203             return io.EOF
 204         }
 205     }
 206 
 207     return sc.Err()
 208 }
     File: ./precho/precho.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package precho
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32 )
  33 
  34 const info = `
  35 precho [options...] [words...]
  36 
  37 PRecede ECHO emits a line with the words given as command-line arguments,
  38 then copies all bytes from the standard input into the standard output.
  39 This is a handy tool to add a missing header line with column names to
  40 SSV (space-separated values) table data.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help       show this help message
  45 `
  46 
  47 func Main() {
  48     buffered := false
  49     args := os.Args[1:]
  50 
  51     if len(args) > 0 {
  52         switch args[0] {
  53         case `-b`, `--b`, `-buffered`, `--buffered`:
  54             buffered = true
  55             args = args[1:]
  56 
  57         case `-h`, `--h`, `-help`, `--help`:
  58             os.Stdout.WriteString(info[1:])
  59             return
  60         }
  61     }
  62 
  63     if len(args) > 0 && args[0] == `--` {
  64         args = args[1:]
  65     }
  66 
  67     liveLines := !buffered
  68     if !buffered {
  69         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  70             liveLines = false
  71         }
  72     }
  73 
  74     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  75         os.Stderr.WriteString(`precho: `)
  76         os.Stderr.WriteString(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79         return
  80     }
  81 }
  82 
  83 func run(w io.Writer, args []string, live bool) error {
  84     bw := bufio.NewWriterSize(w, 32*1024)
  85     defer bw.Flush()
  86 
  87     for i, s := range args {
  88         if i > 0 {
  89             bw.WriteByte(' ')
  90         }
  91         bw.WriteString(s)
  92     }
  93 
  94     if len(args) > 0 && bw.WriteByte('\n') != nil {
  95         return io.EOF
  96     }
  97 
  98     if live && bw.Flush() != nil {
  99         return io.EOF
 100     }
 101 
 102     return catl(bw, os.Stdin, live)
 103 }
 104 
 105 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 106     const gb = 1024 * 1024 * 1024
 107     sc := bufio.NewScanner(r)
 108     sc.Buffer(nil, 8*gb)
 109 
 110     for i := 0; sc.Scan(); i++ {
 111         s := sc.Bytes()
 112         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 113             s = s[3:]
 114         }
 115 
 116         w.Write(s)
 117         if w.WriteByte('\n') != nil {
 118             return io.EOF
 119         }
 120 
 121         if live && w.Flush() != nil {
 122             return io.EOF
 123         }
 124     }
 125 
 126     return sc.Err()
 127 }
     File: ./pretsv/pretsv.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package pretsv
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32 )
  33 
  34 const info = `
  35 pretsv [options...] [header names...]
  36 
  37 PREcede TSV, emits a Tab-Separated-Values line using the arguments given,
  38 following it with lines from the standard input. This is a handy tool to
  39 add a missing header line with column names to TSV table data.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44 `
  45 
  46 func Main() {
  47     buffered := false
  48     args := os.Args[1:]
  49 
  50     if len(args) > 0 {
  51         switch args[0] {
  52         case `-b`, `--b`, `-buffered`, `--buffered`:
  53             buffered = true
  54             args = args[1:]
  55 
  56         case `-h`, `--h`, `-help`, `--help`:
  57             os.Stdout.WriteString(info[1:])
  58             return
  59         }
  60     }
  61 
  62     if len(args) > 0 && args[0] == `--` {
  63         args = args[1:]
  64     }
  65 
  66     liveLines := !buffered
  67     if !buffered {
  68         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  69             liveLines = false
  70         }
  71     }
  72 
  73     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  74         os.Stderr.WriteString(`pretsv: `)
  75         os.Stderr.WriteString(err.Error())
  76         os.Stderr.WriteString("\n")
  77         os.Exit(1)
  78         return
  79     }
  80 }
  81 
  82 func run(w io.Writer, args []string, live bool) error {
  83     bw := bufio.NewWriterSize(w, 32*1024)
  84     defer bw.Flush()
  85 
  86     for i, s := range args {
  87         if i > 0 {
  88             bw.WriteByte('\t')
  89         }
  90         bw.WriteString(s)
  91     }
  92 
  93     if len(args) > 0 && bw.WriteByte('\n') != nil {
  94         return io.EOF
  95     }
  96 
  97     if live && bw.Flush() != nil {
  98         return io.EOF
  99     }
 100 
 101     return catl(bw, os.Stdin, live)
 102 }
 103 
 104 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 105     const gb = 1024 * 1024 * 1024
 106     sc := bufio.NewScanner(r)
 107     sc.Buffer(nil, 8*gb)
 108 
 109     for i := 0; sc.Scan(); i++ {
 110         s := sc.Bytes()
 111         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 112             s = s[3:]
 113         }
 114 
 115         w.Write(s)
 116         if w.WriteByte('\n') != nil {
 117             return io.EOF
 118         }
 119 
 120         if live && w.Flush() != nil {
 121             return io.EOF
 122         }
 123     }
 124 
 125     return sc.Err()
 126 }
     File: ./primes/primes.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package primes
  26 
  27 import (
  28     "bufio"
  29     "math"
  30     "os"
  31     "strconv"
  32 )
  33 
  34 const info = `
  35 primes [options...] [count...]
  36 
  37 
  38 Show the first few prime numbers, starting from the lowest and showing one
  39 per line. When not given how many primes to find, the default is 1 million.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44 `
  45 
  46 func Main() {
  47     howMany := 1_000_000
  48     if len(os.Args) > 1 {
  49         switch os.Args[1] {
  50         case `-h`, `--h`, `-help`, `--help`:
  51             os.Stdout.WriteString(info[1:])
  52             return
  53         }
  54 
  55         n, err := strconv.Atoi(os.Args[1])
  56         if err != nil {
  57             os.Stderr.WriteString(`primes: `)
  58             os.Stderr.WriteString(err.Error())
  59             os.Stderr.WriteString("\n")
  60             os.Exit(1)
  61             return
  62         }
  63 
  64         if n < 0 {
  65             n = 0
  66         }
  67         howMany = n
  68     }
  69 
  70     primes(howMany)
  71 }
  72 
  73 func primes(left int) {
  74     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  75     defer bw.Flush()
  76 
  77     // 24 bytes are always enough for any 64-bit integer
  78     var buf [24]byte
  79 
  80     // 2 is the only even prime number
  81     if left > 0 {
  82         bw.WriteString("2\n")
  83         left--
  84     }
  85 
  86     for n := uint64(3); left > 0; n += 2 {
  87         if oddPrime(n) {
  88             bw.Write(strconv.AppendUint(buf[:0], n, 10))
  89             if err := bw.WriteByte('\n'); err != nil {
  90                 // assume errors come from closed stdout pipes
  91                 return
  92             }
  93             left--
  94         }
  95     }
  96 }
  97 
  98 // oddPrime assumes the number given to it is odd
  99 func oddPrime(n uint64) bool {
 100     max := uint64(math.Sqrt(float64(n)))
 101     for div := uint64(3); div <= max; div += 2 {
 102         if n%div == 0 {
 103             return false
 104         }
 105     }
 106     return true
 107 }
     File: ./ptsv/ptsv.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ptsv
  26 
  27 import (
  28     "bufio"
  29     "os"
  30     "strconv"
  31     "strings"
  32 )
  33 
  34 const info = `
  35 ptsv [options...] [columns/indices...]
  36 
  37 Pick Tab-Separated Values picks only the columns using the arguments given,
  38 in the order given. Arguments are matched to columns, first exactly, failing
  39 that case-insensitively, and failing even that, as 1-based indices, with
  40 negative numbers counting backward from the last column.
  41 
  42 Input is TSV (tab-separated values) lines from the standard input, where the
  43 first/header line has all the column names.
  44 
  45 The options are, available both in single and double-dash versions
  46 
  47     -h, -help    show this help message
  48 `
  49 
  50 func Main() {
  51     args := os.Args[1:]
  52 
  53     if len(args) > 0 {
  54         switch args[0] {
  55         case `-h`, `--h`, `-help`, `--help`:
  56             os.Stdout.WriteString(info[1:])
  57             return
  58         }
  59     }
  60 
  61     if len(args) > 0 && args[0] == `--` {
  62         args = args[1:]
  63     }
  64 
  65     // if len(args) == 0 {
  66     //  os.Stderr.WriteString(info[1:])
  67     //  os.Exit(1)
  68     //  return
  69     // }
  70 
  71     var items []string
  72     indices := make([]int, 0, len(args))
  73 
  74     sc := bufio.NewScanner(os.Stdin)
  75     sc.Buffer(nil, 8*1024*1024*1024)
  76 
  77     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  78     defer bw.Flush()
  79 
  80     for i := 0; sc.Scan(); i++ {
  81         line := sc.Text()
  82 
  83         if i == 0 {
  84             if strings.HasPrefix(line, "\xef\xbb\xbf") {
  85                 line = line[3:]
  86             }
  87 
  88             items = appendTSV(items[:0], line)
  89 
  90             for _, s := range args {
  91                 if j := findColumn(items, s); j >= 0 {
  92                     indices = append(indices, j)
  93                 }
  94             }
  95         } else {
  96             items = appendTSV(items[:0], line)
  97         }
  98 
  99         j := 0
 100 
 101         for _, k := range indices {
 102             if j > 0 {
 103                 bw.WriteByte('\t')
 104             }
 105             j++
 106             if k < len(items) {
 107                 bw.WriteString(items[k])
 108             }
 109         }
 110 
 111         if bw.WriteByte('\n') != nil {
 112             return
 113         }
 114     }
 115 }
 116 
 117 func appendTSV(to []string, line string) []string {
 118     for len(line) > 0 {
 119         i := strings.IndexByte(line, '\t')
 120         if i < 0 {
 121             return append(to, line)
 122         }
 123 
 124         to = append(to, line[:i])
 125         line = line[i+1:]
 126     }
 127 
 128     return to
 129 }
 130 
 131 // findColumn tries to find the 0-based column-index, given the first/header
 132 // row, and a name/integer-index
 133 func findColumn(items []string, name string) int {
 134     // try exact match
 135     for i, s := range items {
 136         if s == name {
 137             return i
 138         }
 139     }
 140 
 141     // try case-insensitive match
 142     for i, s := range items {
 143         if strings.EqualFold(s, name) {
 144             return i
 145         }
 146     }
 147 
 148     // try a non-zero numeric index
 149     i, err := strconv.Atoi(name)
 150     if err != nil || i == 0 {
 151         return -1
 152     }
 153 
 154     // make indices 0-based, and correct negative indices
 155     if i > 0 {
 156         i--
 157     } else if i < 0 {
 158         i += len(items)
 159     }
 160 
 161     // see if numeric index is in range
 162     if 0 <= i && i < len(items) {
 163         return i
 164     }
 165 
 166     return -1
 167 }
     File: ./realign/realign.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package realign
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33     "unicode/utf8"
  34 )
  35 
  36 const info = `
  37 realign [options...] [filenames...]
  38 
  39 Realign all detected columns, right-aligning any detected numbers in any
  40 column. ANSI style-codes are also kept as given.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help           show this help message
  45     -m, -max-columns    use the row with the most items for the item-count
  46 `
  47 
  48 func Main() {
  49     maxCols := false
  50     args := os.Args[1:]
  51 
  52     for len(args) > 0 {
  53         if args[0] == `--` {
  54             args = args[1:]
  55             break
  56         }
  57 
  58         switch args[0] {
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62 
  63         case
  64             `-m`, `--m`,
  65             `-maxcols`, `--maxcols`,
  66             `-max-columns`, `--max-columns`:
  67             maxCols = true
  68             args = args[1:]
  69             continue
  70         }
  71 
  72         break
  73     }
  74 
  75     if err := run(args, maxCols); err != nil {
  76         os.Stderr.WriteString(`realign: `)
  77         os.Stderr.WriteString(err.Error())
  78         os.Stderr.WriteString("\n")
  79         os.Exit(1)
  80         return
  81     }
  82 }
  83 
  84 // table has all summary info gathered from the data, along with the row
  85 // themselves, stored as lines/strings
  86 type table struct {
  87     Columns int
  88 
  89     Rows []string
  90 
  91     MaxWidth []int
  92 
  93     MaxIntWidth []int
  94 
  95     MaxDotDecimals []int
  96 
  97     LoopItems func(s string, max int, t *table, f itemFunc)
  98 
  99     MaxColumns bool
 100 }
 101 
 102 type itemFunc func(i int, s string, t *table)
 103 
 104 func run(paths []string, maxCols bool) error {
 105     var res table
 106     res.MaxColumns = maxCols
 107 
 108     for _, p := range paths {
 109         if err := handleFile(&res, p); err != nil {
 110             return err
 111         }
 112     }
 113 
 114     if len(paths) == 0 {
 115         if err := handleReader(&res, os.Stdin); err != nil {
 116             return err
 117         }
 118     }
 119 
 120     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 121     defer bw.Flush()
 122     realign(bw, res)
 123     return nil
 124 }
 125 
 126 func handleFile(res *table, path string) error {
 127     f, err := os.Open(path)
 128     if err != nil {
 129         // on windows, file-not-found error messages may mention `CreateFile`,
 130         // even when trying to open files in read-only mode
 131         return errors.New(`can't open file named ` + path)
 132     }
 133     defer f.Close()
 134     return handleReader(res, f)
 135 }
 136 
 137 func handleReader(t *table, r io.Reader) error {
 138     const gb = 1024 * 1024 * 1024
 139     sc := bufio.NewScanner(r)
 140     sc.Buffer(nil, 8*gb)
 141 
 142     const maxInt = int(^uint(0) >> 1)
 143     maxCols := maxInt
 144 
 145     for i := 0; sc.Scan(); i++ {
 146         s := sc.Text()
 147         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 148             s = s[3:]
 149         }
 150 
 151         if len(s) == 0 {
 152             if len(t.Rows) > 0 {
 153                 t.Rows = append(t.Rows, ``)
 154             }
 155             continue
 156         }
 157 
 158         t.Rows = append(t.Rows, s)
 159 
 160         if t.Columns == 0 {
 161             if t.LoopItems == nil {
 162                 if strings.IndexByte(s, '\t') >= 0 {
 163                     t.LoopItems = loopItemsTSV
 164                 } else {
 165                     t.LoopItems = loopItemsSSV
 166                 }
 167             }
 168 
 169             if !t.MaxColumns {
 170                 t.LoopItems(s, maxCols, t, updateColumnCount)
 171                 maxCols = t.Columns
 172             }
 173         }
 174 
 175         t.LoopItems(s, maxCols, t, updateItem)
 176     }
 177 
 178     return sc.Err()
 179 }
 180 
 181 func updateColumnCount(i int, s string, t *table) {
 182     t.Columns = i + 1
 183 }
 184 
 185 func updateItem(i int, s string, t *table) {
 186     // ensure column-info-slices have enough room
 187     if i >= len(t.MaxWidth) {
 188         // update column-count if in max-columns mode
 189         if t.MaxColumns {
 190             t.Columns = i + 1
 191         }
 192         t.MaxWidth = append(t.MaxWidth, 0)
 193         t.MaxIntWidth = append(t.MaxIntWidth, 0)
 194         t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
 195     }
 196 
 197     // keep track of widest rune-counts for each column
 198     w := countWidth(s)
 199     if t.MaxWidth[i] < w {
 200         t.MaxWidth[i] = w
 201     }
 202 
 203     // update stats for numeric items
 204     if isNumeric(s) {
 205         iw, dd := countNumericWidths(s)
 206         if t.MaxIntWidth[i] < iw {
 207             t.MaxIntWidth[i] = iw
 208         }
 209         if t.MaxDotDecimals[i] < dd {
 210             t.MaxDotDecimals[i] = dd
 211         }
 212     }
 213 }
 214 
 215 // loopItemsSSV loops over a line's items, allocation-free style; when given
 216 // empty strings, the callback func is never called
 217 func loopItemsSSV(s string, max int, t *table, f itemFunc) {
 218     s = trimTrailingSpaces(s)
 219 
 220     for i := 0; true; i++ {
 221         s = trimLeadingSpaces(s)
 222         if len(s) == 0 {
 223             return
 224         }
 225 
 226         if i+1 == max {
 227             f(i, s, t)
 228             return
 229         }
 230 
 231         j := strings.IndexByte(s, ' ')
 232         if j < 0 {
 233             f(i, s, t)
 234             return
 235         }
 236 
 237         f(i, s[:j], t)
 238         s = s[j+1:]
 239     }
 240 }
 241 
 242 func trimLeadingSpaces(s string) string {
 243     for len(s) > 0 && s[0] == ' ' {
 244         s = s[1:]
 245     }
 246     return s
 247 }
 248 
 249 func trimTrailingSpaces(s string) string {
 250     for len(s) > 0 && s[len(s)-1] == ' ' {
 251         s = s[:len(s)-1]
 252     }
 253     return s
 254 }
 255 
 256 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 257 // when given empty strings, the callback func is never called
 258 func loopItemsTSV(s string, max int, t *table, f itemFunc) {
 259     if len(s) == 0 {
 260         return
 261     }
 262 
 263     for i := 0; true; i++ {
 264         if i+1 == max {
 265             f(i, s, t)
 266             return
 267         }
 268 
 269         j := strings.IndexByte(s, '\t')
 270         if j < 0 {
 271             f(i, s, t)
 272             return
 273         }
 274 
 275         f(i, s[:j], t)
 276         s = s[j+1:]
 277     }
 278 }
 279 
 280 func skipLeadingEscapeSequences(s string) string {
 281     for len(s) >= 2 {
 282         if s[0] != '\x1b' {
 283             return s
 284         }
 285 
 286         switch s[1] {
 287         case '[':
 288             s = skipSingleLeadingANSI(s[2:])
 289 
 290         case ']':
 291             if len(s) < 3 || s[2] != '8' {
 292                 return s
 293             }
 294             s = skipSingleLeadingOSC(s[3:])
 295 
 296         default:
 297             return s
 298         }
 299     }
 300 
 301     return s
 302 }
 303 
 304 func skipSingleLeadingANSI(s string) string {
 305     for len(s) > 0 {
 306         upper := s[0] &^ 32
 307         s = s[1:]
 308         if 'A' <= upper && upper <= 'Z' {
 309             break
 310         }
 311     }
 312 
 313     return s
 314 }
 315 
 316 func skipSingleLeadingOSC(s string) string {
 317     var prev byte
 318 
 319     for len(s) > 0 {
 320         b := s[0]
 321         s = s[1:]
 322         if prev == '\x1b' && b == '\\' {
 323             break
 324         }
 325         prev = b
 326     }
 327 
 328     return s
 329 }
 330 
 331 // isNumeric checks if a string is valid/useable as a number
 332 func isNumeric(s string) bool {
 333     if len(s) == 0 {
 334         return false
 335     }
 336 
 337     s = skipLeadingEscapeSequences(s)
 338     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 339         s = s[1:]
 340     }
 341 
 342     s = skipLeadingEscapeSequences(s)
 343     if len(s) == 0 {
 344         return false
 345     }
 346     if s[0] == '.' {
 347         return isDigits(s[1:])
 348     }
 349 
 350     digits := 0
 351 
 352     for {
 353         s = skipLeadingEscapeSequences(s)
 354         if len(s) == 0 {
 355             break
 356         }
 357 
 358         if s[0] == '.' {
 359             return isDigits(s[1:])
 360         }
 361 
 362         if !('0' <= s[0] && s[0] <= '9') {
 363             return false
 364         }
 365 
 366         digits++
 367         s = s[1:]
 368     }
 369 
 370     s = skipLeadingEscapeSequences(s)
 371     return len(s) == 0 && digits > 0
 372 }
 373 
 374 func isDigits(s string) bool {
 375     if len(s) == 0 {
 376         return false
 377     }
 378 
 379     digits := 0
 380 
 381     for {
 382         s = skipLeadingEscapeSequences(s)
 383         if len(s) == 0 {
 384             break
 385         }
 386 
 387         if '0' <= s[0] && s[0] <= '9' {
 388             s = s[1:]
 389             digits++
 390         } else {
 391             return false
 392         }
 393     }
 394 
 395     s = skipLeadingEscapeSequences(s)
 396     return len(s) == 0 && digits > 0
 397 }
 398 
 399 // countDecimals counts decimal digits from the string given, assuming it
 400 // represents a valid/useable float64, when parsed
 401 func countDecimals(s string) int {
 402     dot := strings.IndexByte(s, '.')
 403     if dot < 0 {
 404         return 0
 405     }
 406 
 407     decs := 0
 408     s = s[dot+1:]
 409 
 410     for len(s) > 0 {
 411         s = skipLeadingEscapeSequences(s)
 412         if len(s) == 0 {
 413             break
 414         }
 415         if '0' <= s[0] && s[0] <= '9' {
 416             decs++
 417         }
 418         s = s[1:]
 419     }
 420 
 421     return decs
 422 }
 423 
 424 // countDotDecimals is like func countDecimals, but this one also includes
 425 // the dot, when any decimals are present, else the count stays at 0
 426 func countDotDecimals(s string) int {
 427     decs := countDecimals(s)
 428     if decs > 0 {
 429         return decs + 1
 430     }
 431     return decs
 432 }
 433 
 434 // countNumericWidths counts both the integer-width (including leading sign,
 435 // if found), and the dot-decimal trail from the string given, assuming it
 436 // represents a valid/useable float64, when parsed
 437 func countNumericWidths(s string) (intWidth int, dotDecimals int) {
 438     sign := 0
 439     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 440         sign++
 441         s = s[1:]
 442     }
 443 
 444     for len(s) > 0 {
 445         s = skipLeadingEscapeSequences(s)
 446         if len(s) == 0 {
 447             break
 448         }
 449         if '0' <= s[0] && s[0] <= '9' {
 450             intWidth++
 451         } else {
 452             break
 453         }
 454         s = s[1:]
 455     }
 456 
 457     intWidth += sign
 458 
 459     if len(s) == 0 || s[0] != '.' {
 460         return intWidth, dotDecimals
 461     }
 462 
 463     for len(s) > 0 {
 464         s = skipLeadingEscapeSequences(s)
 465         if len(s) == 0 {
 466             break
 467         }
 468         if '0' <= s[0] && s[0] <= '9' {
 469             dotDecimals++
 470         }
 471         s = s[1:]
 472     }
 473 
 474     if dotDecimals > 0 {
 475         dotDecimals++
 476     }
 477     return intWidth, dotDecimals
 478 }
 479 
 480 func countWidth(s string) int {
 481     width := 0
 482 
 483     for len(s) > 0 {
 484         i, j := indexEscapeSequence(s)
 485         if i < 0 {
 486             break
 487         }
 488         if j < 0 {
 489             j = len(s)
 490         }
 491 
 492         width += utf8.RuneCountInString(s[:i])
 493         s = s[j:]
 494     }
 495 
 496     // count trailing/all runes in strings which don't end with ANSI-sequences
 497     width += utf8.RuneCountInString(s)
 498     return width
 499 }
 500 
 501 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 502 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 503 // indices which can be independently negative when either the start/end of
 504 // a sequence isn't found; given their fairly-common use, even the hyperlink
 505 // ESC]8 sequences are supported
 506 func indexEscapeSequence(s string) (int, int) {
 507     var prev byte
 508 
 509     for i := range s {
 510         b := s[i]
 511 
 512         if prev == '\x1b' && b == '[' {
 513             j := indexLetter(s[i+1:])
 514             if j < 0 {
 515                 return i, -1
 516             }
 517             return i - 1, i + 1 + j + 1
 518         }
 519 
 520         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 521             j := indexPair(s[i+1:], '\x1b', '\\')
 522             if j < 0 {
 523                 return i, -1
 524             }
 525             return i - 1, i + 1 + j + 2
 526         }
 527 
 528         prev = b
 529     }
 530 
 531     return -1, -1
 532 }
 533 
 534 func indexLetter(s string) int {
 535     for i, b := range s {
 536         upper := b &^ 32
 537         if 'A' <= upper && upper <= 'Z' {
 538             return i
 539         }
 540     }
 541 
 542     return -1
 543 }
 544 
 545 func indexPair(s string, x byte, y byte) int {
 546     var prev byte
 547 
 548     for i := range s {
 549         b := s[i]
 550         if prev == x && b == y && i > 0 {
 551             return i
 552         }
 553         prev = b
 554     }
 555 
 556     return -1
 557 }
 558 
 559 func realign(w *bufio.Writer, t table) {
 560     // decimal numbers can pad beyond their max-width
 561     for i, _ := range t.MaxWidth {
 562         w := t.MaxIntWidth[i] + t.MaxDotDecimals[i]
 563         if t.MaxWidth[i] < w {
 564             t.MaxWidth[i] = w
 565         }
 566     }
 567 
 568     due := 0
 569     showItem := func(i int, s string, t *table) {
 570         if i > 0 {
 571             due += 2
 572         }
 573 
 574         if isNumeric(s) {
 575             dd := countDotDecimals(s)
 576             rpad := t.MaxDotDecimals[i] - dd
 577             width := countWidth(s)
 578             lpad := t.MaxWidth[i] - (width + rpad) + due
 579             writeSpaces(w, lpad)
 580             w.WriteString(s)
 581             due = rpad
 582             return
 583         }
 584 
 585         writeSpaces(w, due)
 586         w.WriteString(s)
 587         due = t.MaxWidth[i] - countWidth(s)
 588     }
 589 
 590     for _, line := range t.Rows {
 591         due = 0
 592         if len(line) > 0 {
 593             t.LoopItems(line, t.Columns, &t, showItem)
 594         }
 595         if w.WriteByte('\n') != nil {
 596             break
 597         }
 598     }
 599 }
 600 
 601 // writeSpaces does what it says, minimizing calls to write-like funcs
 602 func writeSpaces(w *bufio.Writer, n int) {
 603     const spaces = `                                `
 604     if n < 1 {
 605         return
 606     }
 607 
 608     for n >= len(spaces) {
 609         w.WriteString(spaces)
 610         n -= len(spaces)
 611     }
 612     w.WriteString(spaces[:n])
 613 }
     File: ./realign/realign_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package realign
  26 
  27 import "testing"
  28 
  29 func TestCountWidth(t *testing.T) {
  30     tests := map[string]struct {
  31         input    string
  32         expected int
  33     }{
  34         `empty`:         {``, 0},
  35         `empty ANSI`:    {"\x1b[38;5;0;0;0m\x1b[0m", 0},
  36         `simple plain`:  {`abc def`, 7},
  37         `unicode plain`: {`abc●def`, 7},
  38         `simple ANSI`:   {"abc \x1b[7mde\x1b[0mf", 7},
  39         `unicode ANSI`:  {"abc●\x1b[7mde\x1b[0mf", 7},
  40     }
  41 
  42     for name, tc := range tests {
  43         t.Run(name, func(t *testing.T) {
  44             got := countWidth(tc.input)
  45             if got != tc.expected {
  46                 const fs = "expected width %d, got %d instead"
  47                 t.Errorf(fs, tc.expected, got)
  48             }
  49         })
  50     }
  51 }
  52 
  53 func TestCountNumericWidths(t *testing.T) {
  54     tests := map[string]struct {
  55         intWidth    int
  56         dotDecimals int
  57     }{
  58         `123`:      {3, 0},
  59         `+123`:     {4, 0},
  60         `-123`:     {4, 0},
  61         `123.0`:    {3, 2},
  62         `+123.9`:   {4, 2},
  63         `-123.3`:   {4, 2},
  64         `123.01`:   {3, 3},
  65         `+123.93`:  {4, 3},
  66         `-123.344`: {4, 4},
  67     }
  68 
  69     for s, tc := range tests {
  70         t.Run(s, func(t *testing.T) {
  71             if !isNumeric(s) {
  72                 t.Errorf(`%s isn't numeric`, s)
  73                 return
  74             }
  75 
  76             iw, dd := countNumericWidths(s)
  77             if iw != tc.intWidth || dd != tc.dotDecimals {
  78                 const fs = `expected (%d, %d), got (%d, %d) instead`
  79                 t.Errorf(fs, tc.intWidth, tc.dotDecimals, iw, dd)
  80             }
  81         })
  82     }
  83 }
     File: ./reprose/reprose.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package reprose
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34     "strings"
  35     "unicode/utf8"
  36 )
  37 
  38 const info = `
  39 reprose [options...] [max width...] [files...]
  40 
  41 Reflow/trim lines of prose (text) to improve its legibility: this tool is
  42 especially useful when the text is pasted from web-pages being viewed in
  43 reader mode.
  44 
  45 This tool also ensures no lines across inputs are accidentally joined,
  46 since all lines it outputs end with line-feeds, even when the original
  47 files don't.
  48 
  49 The options are, available both in single and double-dash versions
  50 
  51     -h, -help    show this help message
  52 `
  53 
  54 type config struct {
  55     // pos is the current position/symbol-count for the current output line
  56     pos int
  57 
  58     // maxWidth is the maximum number of symbols per line, when doable
  59     maxWidth int
  60 
  61     // liveLines is whether to flush every output line
  62     liveLines bool
  63 }
  64 
  65 func Main() {
  66     var cfg config
  67     cfg.maxWidth = 80
  68     buffered := false
  69     args := os.Args[1:]
  70 
  71     for len(args) > 0 {
  72         switch args[0] {
  73         case `-b`, `--b`, `-buffered`, `--buffered`:
  74             buffered = true
  75             args = args[1:]
  76             continue
  77 
  78         case `-h`, `--h`, `-help`, `--help`:
  79             os.Stdout.WriteString(info[1:])
  80             return
  81         }
  82 
  83         break
  84     }
  85 
  86     if len(args) > 0 {
  87         s := strings.Replace(args[0], `_`, ``, -1)
  88         if v, err := strconv.ParseInt(s, 10, 64); err == nil {
  89             cfg.maxWidth = int(v)
  90             args = args[1:]
  91         }
  92     }
  93 
  94     if len(args) > 0 && args[0] == `--` {
  95         args = args[1:]
  96     }
  97 
  98     cfg.liveLines = !buffered
  99     if !buffered {
 100         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 101             cfg.liveLines = false
 102         }
 103     }
 104 
 105     if err := run(args, cfg); err != nil && err != io.EOF {
 106         os.Stderr.WriteString(`reprose: `)
 107         os.Stderr.WriteString(err.Error())
 108         os.Stderr.WriteString("\n")
 109         os.Exit(1)
 110         return
 111     }
 112 }
 113 
 114 func run(paths []string, cfg config) error {
 115     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 116     defer bw.Flush()
 117 
 118     for _, p := range paths {
 119         if err := handleFile(bw, p, &cfg); err != nil {
 120             return err
 121         }
 122     }
 123 
 124     if len(paths) == 0 {
 125         if err := handleReader(bw, os.Stdin, &cfg); err != nil {
 126             return err
 127         }
 128     }
 129 
 130     // end current line, if there's anything on it
 131     if cfg.pos > 0 {
 132         if bw.WriteByte('\n') != nil {
 133             return io.EOF
 134         }
 135     }
 136     return nil
 137 }
 138 
 139 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 140     f, err := os.Open(path)
 141     if err != nil {
 142         // on windows, file-not-found error messages may mention `CreateFile`,
 143         // even when trying to open files in read-only mode
 144         return errors.New(`can't open file named ` + path)
 145     }
 146     defer f.Close()
 147     return handleReader(w, f, cfg)
 148 }
 149 
 150 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 151     const gb = 1024 * 1024 * 1024
 152     sc := bufio.NewScanner(r)
 153     sc.Buffer(nil, 8*gb)
 154 
 155     for i := 0; sc.Scan(); i++ {
 156         s := sc.Bytes()
 157         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 158             s = s[3:]
 159         }
 160 
 161         s = trimTrailingSpaces(s)
 162 
 163         // keep empty(ish) lines as empty lines
 164         if len(s) == 0 {
 165             if cfg.pos > 0 && w.WriteByte('\n') != nil {
 166                 return io.EOF
 167             }
 168             if w.WriteByte('\n') != nil {
 169                 return io.EOF
 170             }
 171             if cfg.liveLines && w.Flush() != nil {
 172                 return io.EOF
 173             }
 174             cfg.pos = 0
 175             continue
 176         }
 177 
 178         // reflow all `words` from non-empty lines
 179         for len(s) > 0 {
 180             var word []byte
 181             s = trimLeadingSpaces(s)
 182             if i := bytes.IndexByte(s, ' '); i >= 0 {
 183                 word = s[:i]
 184                 s = s[i+1:]
 185             } else {
 186                 word = s
 187                 s = nil
 188             }
 189 
 190             word = trimLeadingSpaces(word)
 191             width := countWidth(word)
 192             if width == 0 {
 193                 continue
 194             }
 195 
 196             if cfg.pos+width > cfg.maxWidth {
 197                 if cfg.pos > 0 && w.WriteByte('\n') != nil {
 198                     return io.EOF
 199                 }
 200                 if cfg.liveLines && w.Flush() != nil {
 201                     return io.EOF
 202                 }
 203                 cfg.pos = 0
 204             }
 205 
 206             if cfg.pos > 0 {
 207                 w.WriteByte(' ')
 208                 cfg.pos++
 209             }
 210             w.Write(word)
 211             cfg.pos += width
 212         }
 213     }
 214 
 215     return sc.Err()
 216 }
 217 
 218 func trimLeadingSpaces(s []byte) []byte {
 219     for len(s) > 0 && s[0] == ' ' {
 220         s = s[1:]
 221     }
 222     return s
 223 }
 224 
 225 func trimTrailingSpaces(s []byte) []byte {
 226     for len(s) > 0 && s[len(s)-1] == ' ' {
 227         s = s[:len(s)-1]
 228     }
 229     return s
 230 }
 231 
 232 func countWidth(s []byte) int {
 233     width := 0
 234 
 235     for len(s) > 0 {
 236         i, j := indexEscapeSequence(s)
 237         if i < 0 {
 238             break
 239         }
 240         if j < 0 {
 241             j = len(s)
 242         }
 243 
 244         width += utf8.RuneCount(s[:i])
 245         s = s[j:]
 246     }
 247 
 248     // count trailing/all runes in strings which don't end with ANSI-sequences
 249     width += utf8.RuneCount(s)
 250     return width
 251 }
 252 
 253 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 254 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 255 // indices which can be independently negative when either the start/end of
 256 // a sequence isn't found; given their fairly-common use, even the hyperlink
 257 // ESC]8 sequences are supported
 258 func indexEscapeSequence(s []byte) (int, int) {
 259     var prev byte
 260 
 261     for i := range s {
 262         b := s[i]
 263 
 264         if prev == '\x1b' && b == '[' {
 265             j := indexLetter(s[i+1:])
 266             if j < 0 {
 267                 return i, -1
 268             }
 269             return i - 1, i + 1 + j + 1
 270         }
 271 
 272         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 273             j := indexPair(s[i+1:], '\x1b', '\\')
 274             if j < 0 {
 275                 return i, -1
 276             }
 277             return i - 1, i + 1 + j + 2
 278         }
 279 
 280         prev = b
 281     }
 282 
 283     return -1, -1
 284 }
 285 
 286 func indexLetter(s []byte) int {
 287     for i, b := range s {
 288         upper := b &^ 32
 289         if 'A' <= upper && upper <= 'Z' {
 290             return i
 291         }
 292     }
 293 
 294     return -1
 295 }
 296 
 297 func indexPair(s []byte, x byte, y byte) int {
 298     var prev byte
 299 
 300     for i := range s {
 301         b := s[i]
 302         if prev == x && b == y && i > 0 {
 303             return i
 304         }
 305         prev = b
 306     }
 307 
 308     return -1
 309 }
     File: ./restyle/restyle.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package restyle
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 restyle [options...] [style...]
  37 
  38 
  39 Change lines from the standard input, using ANSI-codes for the style/color
  40 name given. Existing ANSI-styles from input lines are ignored, except when
  41 inverse-styling lines.
  42 
  43 The options are, available both in single and double-dash versions
  44 
  45     -h, -help                 show this help message
  46 
  47 Some of the colors/styles available are:
  48 
  49     blue         blueback
  50     bold
  51     gray         grayback
  52     green        greenback
  53     inverse
  54     magenta      magentaback
  55     orange       orangeback
  56     purple       purpleback
  57     red          redback
  58     underline
  59 
  60 Some style aliases are:
  61 
  62     b       blue                  bb       blueback
  63     g       green                 gb       greenback
  64     m       magenta               mb       magentaback
  65     o       orange                ob       orangeback
  66     p       purple                pb       purpleback
  67     r       red                   rb       redback
  68     i       inverse (highlight)
  69     u       underline
  70 `
  71 
  72 var styleAliases = map[string]string{
  73     `b`: `blue`,
  74     `g`: `green`,
  75     `m`: `magenta`,
  76     `o`: `orange`,
  77     `p`: `purple`,
  78     `r`: `red`,
  79     `u`: `underline`,
  80 
  81     `bb`: `blueback`,
  82     `bg`: `greenback`,
  83     `bm`: `magentaback`,
  84     `bo`: `orangeback`,
  85     `bp`: `purpleback`,
  86     `br`: `redback`,
  87 
  88     `gb`: `greenback`,
  89     `mb`: `magentaback`,
  90     `ob`: `orangeback`,
  91     `pb`: `purpleback`,
  92     `rb`: `redback`,
  93 
  94     `hi`:  `inverse`,
  95     `inv`: `inverse`,
  96     `mag`: `magenta`,
  97 
  98     `du`: `doubleunderline`,
  99 
 100     `flip`: `inverse`,
 101     `swap`: `inverse`,
 102 
 103     `reset`:     `plain`,
 104     `highlight`: `inverse`,
 105     `hilite`:    `inverse`,
 106     `invert`:    `inverse`,
 107     `inverted`:  `inverse`,
 108     `swapped`:   `inverse`,
 109 
 110     `dunderline`:  `doubleunderline`,
 111     `dunderlined`: `doubleunderline`,
 112 
 113     `strikethrough`: `strike`,
 114     `strikethru`:    `strike`,
 115     `struck`:        `strike`,
 116 
 117     `underlined`: `underline`,
 118 
 119     `bblue`:    `blueback`,
 120     `bgray`:    `grayback`,
 121     `bgreen`:   `greenback`,
 122     `bmagenta`: `magentaback`,
 123     `borange`:  `orangeback`,
 124     `bpurple`:  `purpleback`,
 125     `bred`:     `redback`,
 126 
 127     `bgblue`:    `blueback`,
 128     `bggray`:    `grayback`,
 129     `bggreen`:   `greenback`,
 130     `bgmag`:     `magentaback`,
 131     `bgmagenta`: `magentaback`,
 132     `bgorange`:  `orangeback`,
 133     `bgpurple`:  `purpleback`,
 134     `bgred`:     `redback`,
 135 
 136     `bluebg`:    `blueback`,
 137     `graybg`:    `grayback`,
 138     `greenbg`:   `greenback`,
 139     `magbg`:     `magentaback`,
 140     `magentabg`: `magentaback`,
 141     `orangebg`:  `orangeback`,
 142     `purplebg`:  `purpleback`,
 143     `redbg`:     `redback`,
 144 
 145     `backblue`:    `blueback`,
 146     `backgray`:    `grayback`,
 147     `backgreen`:   `greenback`,
 148     `backmag`:     `magentaback`,
 149     `backmagenta`: `magentaback`,
 150     `backorange`:  `orangeback`,
 151     `backpurple`:  `purpleback`,
 152     `backred`:     `redback`,
 153 }
 154 
 155 var styles = map[string]string{
 156     `blue`:            "\x1b[38;2;0;95;215m",
 157     `bold`:            "\x1b[1m",
 158     `doubleunderline`: "\x1b[21m",
 159     `gray`:            "\x1b[38;2;168;168;168m",
 160     `green`:           "\x1b[38;2;0;135;95m",
 161     `inverse`:         "\x1b[7m",
 162     `magenta`:         "\x1b[38;2;215;0;255m",
 163     `orange`:          "\x1b[38;2;215;95;0m",
 164     `plain`:           "\x1b[0m",
 165     `purple`:          "\x1b[38;2;135;95;255m",
 166     `red`:             "\x1b[38;2;204;0;0m",
 167     `strike`:          "\x1b[9m",
 168     `underline`:       "\x1b[4m",
 169 
 170     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 171     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 172     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 173     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 174     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 175     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 176     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 177 }
 178 
 179 type config struct {
 180     // style is the ANSI-code to style output lines
 181     style string
 182 
 183     // live is whether output lines are flushed immediately
 184     live bool
 185 }
 186 
 187 func Main() {
 188     buffered := false
 189     args := os.Args[1:]
 190 
 191     for len(args) > 0 {
 192         switch args[0] {
 193         case `-b`, `--b`, `-buffered`, `--buffered`:
 194             buffered = true
 195             args = args[1:]
 196             continue
 197 
 198         case `-h`, `--h`, `-help`, `--help`:
 199             os.Stdout.WriteString(info[1:])
 200             return
 201         }
 202 
 203         break
 204     }
 205 
 206     if len(args) > 0 && args[0] == `--` {
 207         args = args[1:]
 208     }
 209 
 210     if len(args) == 0 {
 211         os.Stderr.WriteString("restyle: forgot the style name\n")
 212         os.Exit(1)
 213         return
 214     }
 215 
 216     sname := args[0]
 217     args = args[1:]
 218 
 219     if strings.HasPrefix(sname, `--`) {
 220         sname = sname[2:]
 221     } else if strings.HasPrefix(sname, `-`) {
 222         sname = sname[1:]
 223     }
 224 
 225     if alias, ok := styleAliases[sname]; ok {
 226         sname = alias
 227     }
 228 
 229     style, ok := styles[sname]
 230     if !ok {
 231         os.Stderr.WriteString("restyle: no style named `")
 232         os.Stderr.WriteString(sname)
 233         os.Stderr.WriteString("`\n")
 234         os.Exit(1)
 235         return
 236     }
 237 
 238     liveLines := !buffered
 239     if !buffered {
 240         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 241             liveLines = false
 242         }
 243     }
 244 
 245     var cfg config
 246     cfg.style = style
 247     cfg.live = liveLines
 248 
 249     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 250         os.Stderr.WriteString(`restyle: `)
 251         os.Stderr.WriteString(err.Error())
 252         os.Stderr.WriteString("\n")
 253         os.Exit(1)
 254         return
 255     }
 256 }
 257 
 258 func run(w io.Writer, paths []string, cfg config) error {
 259     bw := bufio.NewWriterSize(w, 32*1024)
 260     defer bw.Flush()
 261 
 262     for _, path := range paths {
 263         if err := handleFile(bw, path, cfg); err != nil {
 264             return err
 265         }
 266     }
 267 
 268     if len(paths) == 0 {
 269         return handleReader(bw, os.Stdin, cfg)
 270     }
 271     return nil
 272 }
 273 
 274 func handleFile(w *bufio.Writer, path string, cfg config) error {
 275     f, err := os.Open(path)
 276     if err != nil {
 277         return err
 278     }
 279     defer f.Close()
 280     return handleReader(w, f, cfg)
 281 }
 282 
 283 func handleReader(w *bufio.Writer, r io.Reader, cfg config) error {
 284     sc := bufio.NewScanner(os.Stdin)
 285     sc.Buffer(nil, 8*1024*1024*1024)
 286 
 287     invert := cfg.style == "\x1b[7m"
 288 
 289     for i := 0; sc.Scan(); i++ {
 290         s := sc.Bytes()
 291         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 292             s = s[3:]
 293         }
 294 
 295         if invert {
 296             writeInverted(w, s)
 297         } else {
 298             writePlain(w, s)
 299         }
 300 
 301         if w.WriteByte('\n') != nil {
 302             return io.EOF
 303         }
 304 
 305         if cfg.live && w.Flush() != nil {
 306             return io.EOF
 307         }
 308     }
 309 
 310     return nil
 311 }
 312 
 313 func writeInverted(w *bufio.Writer, s []byte) {
 314     w.WriteString("\x1b[7m")
 315 
 316     for len(s) > 0 {
 317         i, j := indexEscapeSequence(s)
 318         if i < 0 {
 319             w.Write(s)
 320             break
 321         }
 322 
 323         if j < 0 {
 324             j = len(s)
 325         }
 326 
 327         if i > 0 {
 328             w.Write(s[:i])
 329         }
 330 
 331         s = s[j:]
 332     }
 333 
 334     w.WriteString("\x1b[0m")
 335 }
 336 
 337 func writePlain(w *bufio.Writer, s []byte) {
 338     for len(s) > 0 {
 339         i, j := indexEscapeSequence(s)
 340         if i < 0 {
 341             w.Write(s)
 342             break
 343         }
 344 
 345         if j < 0 {
 346             j = len(s)
 347         }
 348 
 349         if i > 0 {
 350             w.Write(s[:i])
 351         }
 352 
 353         s = s[j:]
 354     }
 355 }
 356 
 357 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 358 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 359 // indices which can be independently negative when either the start/end of
 360 // a sequence isn't found; given their fairly-common use, even the hyperlink
 361 // ESC]8 sequences are supported
 362 func indexEscapeSequence(s []byte) (int, int) {
 363     var prev byte
 364 
 365     for i, b := range s {
 366         if prev == '\x1b' && b == '[' {
 367             j := indexLetter(s[i+1:])
 368             if j < 0 {
 369                 return i, -1
 370             }
 371             return i - 1, i + 1 + j + 1
 372         }
 373 
 374         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 375             j := indexPair(s[i+1:], '\x1b', '\\')
 376             if j < 0 {
 377                 return i, -1
 378             }
 379             return i - 1, i + 1 + j + 2
 380         }
 381 
 382         prev = b
 383     }
 384 
 385     return -1, -1
 386 }
 387 
 388 func indexLetter(s []byte) int {
 389     for i, b := range s {
 390         upper := b &^ 32
 391         if 'A' <= upper && upper <= 'Z' {
 392             return i
 393         }
 394     }
 395 
 396     return -1
 397 }
 398 
 399 func indexPair(s []byte, x byte, y byte) int {
 400     var prev byte
 401 
 402     for i, b := range s {
 403         if prev == x && b == y && i > 0 {
 404             return i
 405         }
 406         prev = b
 407     }
 408 
 409     return -1
 410 }
     File: ./same/same.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package same
  26 
  27 import (
  28     "bytes"
  29     "errors"
  30     "fmt"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 same [options...] [file 1] [file 2]
  37 
  38 
  39 Check if 2 files have the same contents. Output is emitted only for errors,
  40 and only on the standard error, if so.
  41 
  42 This tool is used for its return code, which is 0 when the files have the
  43 same contents, 1 when they don't, or 2 when not given exactly 2 files to
  44 compare, or if either file isn't found.
  45 
  46 The options are, available both in single and double-dash versions
  47 
  48     -h, -help    show this help message
  49 `
  50 
  51 func Main() {
  52     args := os.Args[1:]
  53 
  54     if len(args) > 0 {
  55         switch args[0] {
  56         case `-h`, `--h`, `-help`, `--help`:
  57             os.Stdout.WriteString(info[1:])
  58             return
  59         }
  60     }
  61 
  62     if len(args) > 0 && args[0] == `--` {
  63         args = args[1:]
  64     }
  65 
  66     if len(args) != 2 {
  67         fmt.Fprintf(os.Stderr, "given %d files, instead of 2\n", len(args))
  68         // exit-status 1 is only for when the files have different contents
  69         os.Exit(2)
  70         return
  71     }
  72 
  73     same, err := run(args[0], args[1])
  74     if err != nil && err != io.EOF {
  75         os.Stderr.WriteString(`same: `)
  76         os.Stderr.WriteString(err.Error())
  77         os.Stderr.WriteString("\n")
  78         // exit-status 1 is only for when the files have different contents
  79         os.Exit(2)
  80         return
  81     }
  82 
  83     if !same {
  84         os.Exit(1)
  85         return
  86     }
  87 }
  88 
  89 func run(path1 string, path2 string) (same bool, err error) {
  90     f1, err := os.Open(path1)
  91     if err != nil {
  92         // on windows, file-not-found error messages may mention `CreateFile`,
  93         // even when trying to open files in read-only mode
  94         return false, errors.New(`can't open file named ` + path1)
  95     }
  96     defer f1.Close()
  97 
  98     if path1 == path2 {
  99         return true, nil
 100     }
 101 
 102     f2, err := os.Open(path2)
 103     if err != nil {
 104         // on windows, file-not-found error messages may mention `CreateFile`,
 105         // even when trying to open files in read-only mode
 106         return false, errors.New(`can't open file named ` + path2)
 107     }
 108     defer f2.Close()
 109 
 110     same, err = sameContents(f1, f2)
 111     return same, err
 112 }
 113 
 114 // sameContents checks if 2 files have the same bytes/contents
 115 func sameContents(f1, f2 *os.File) (same bool, err error) {
 116     // don't read anything from files, if they're obviously different in size;
 117     // files are always/already opened to check if filenames given are valid
 118     if differentSize(f1, f2) {
 119         return false, nil
 120     }
 121 
 122     const bufSize = 16 * 1024
 123     var buf1 [bufSize]byte
 124     var buf2 [bufSize]byte
 125 
 126     for {
 127         n, err1 := f1.Read(buf1[:])
 128         chunk1 := buf1[:n]
 129 
 130         n, err2 := f2.Read(buf2[:])
 131         chunk2 := buf2[:n]
 132 
 133         if err1 == io.EOF && err2 == io.EOF {
 134             return bytes.Equal(chunk1, chunk2), nil
 135         }
 136 
 137         if err1 != nil {
 138             return false, err1
 139         }
 140         if err2 != nil {
 141             return false, err2
 142         }
 143 
 144         if !bytes.Equal(chunk1, chunk2) {
 145             return false, nil
 146         }
 147     }
 148 }
 149 
 150 // differentSize returns whether the 2 files given can be determined to have
 151 // different non-zero file-sizes: this is because some special files falsely
 152 // advertise being empty; in case of errors false is returned, but the files
 153 // can still have different sizes, but that isn't known for sure
 154 func differentSize(f1, f2 *os.File) bool {
 155     info1, err := f1.Stat()
 156     if err != nil {
 157         // can't tell size for sure
 158         return false
 159     }
 160 
 161     info2, err := f2.Stat()
 162     if err != nil {
 163         // can't tell size for sure
 164         return false
 165     }
 166 
 167     size1 := info1.Size()
 168     size2 := info2.Size()
 169 
 170     if size1 == 0 || size2 == 0 {
 171         // some special files falsely advertise being empty
 172         return false
 173     }
 174     return size1 != size2
 175 }
     File: ./sbs/config.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sbs
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 const (
  32     // tabstop is the space-count used for tab-expansion
  33     tabstop = 4
  34 
  35     // maxAutoWidth is the output max-width, chosen to fit very old monitors
  36     maxAutoWidth = 79
  37 )
  38 
  39 // chooseNumColumns implements heuristics to auto-pick the number of columns
  40 // to show: this func is used when the app is using data from standard-input
  41 func chooseNumColumns(lines []string, separator string) int {
  42     if len(lines) == 0 {
  43         return 1
  44     }
  45 
  46     // sepw is the separator width
  47     sepw := width(separator)
  48 
  49     // see if lines can even fit a single column
  50     if !columnsCanFit(1, lines, sepw) {
  51         return 1
  52     }
  53 
  54     start := int(maxAutoWidth)
  55     if sepw > 0 {
  56         start = int(maxAutoWidth / sepw)
  57     }
  58 
  59     // starting from the max possible columns which may fit, keep trying
  60     // with 1 fewer column, until the columns fit
  61     for ncols := start; ncols > 1; ncols-- {
  62         if columnsCanFit(ncols, lines, sepw) {
  63             // success: found the most columns which fit
  64             return ncols
  65         }
  66     }
  67 
  68     // avoid multiple columns if some lines are too wide
  69     return 1
  70 }
  71 
  72 // columnsCanFit checks whether the number of columns given would fit the
  73 // display max-width constant
  74 func columnsCanFit(ncols int, lines []string, gap int) bool {
  75     if ncols < 1 {
  76         // avoid surprises when called with non-sense column counts
  77         return true
  78     }
  79 
  80     // stack-allocate the backing-array behind slice maxw
  81     var buf [maxAutoWidth / 2]int
  82     maxw := buf[:0]
  83 
  84     // find the column max-height, to chunk lines into columns
  85     h := int(math.Ceil(float64(len(lines)) / float64(ncols)))
  86 
  87     // find column max-width by looping over chunks of lines
  88     for len(lines) >= h {
  89         w := findMaxWidth(lines[:h])
  90         maxw = append(maxw, w)
  91         lines = lines[h:]
  92     }
  93 
  94     // don't forget the last column
  95     if len(lines) > 0 {
  96         w := findMaxWidth(lines)
  97         maxw = append(maxw, w)
  98     }
  99 
 100     // remember to add the gaps/separators between columns, along with
 101     // all the individual column max-widths
 102     w := (ncols - 1) * gap
 103     for _, n := range maxw {
 104         w += n
 105     }
 106 
 107     // do the columns fit?
 108     return w <= maxAutoWidth
 109 }
 110 
 111 // findMaxWidth finds the max width in the slice given, ignoring ANSI codes
 112 func findMaxWidth(lines []string) int {
 113     maxw := 0
 114     for _, s := range lines {
 115         w := width(s)
 116         if w > maxw {
 117             maxw = w
 118         }
 119     }
 120     return maxw
 121 }
     File: ./sbs/info.txt
   1 sbs [options...] [columns...] [filenames...]
   2 
   3 
   4 Show lines Side By Side: this app is made for content which normally scrolls
   5 a long way downward. You can either just pipe text into it, or give multiple
   6 filenames to read lines from those: a common use-case is to pipe its results
   7 to `less -MKiCRS` or some other viewer command.
   8 
   9 When given an explicit `0` for the number of columns, it figures out the most
  10 columns which fit 80-symbols lines, or just 1 column if that's not possible.
  11 
  12 When given no filenames, this app reads from stdin; when giving it filenames,
  13 you can use a dash to use stdin along with the files.
  14 
  15 All (optional) leading options start with either single or double-dash:
  16 
  17     -h, -help               show this help message
  18     -s, -simple, -no-sep    don't put a separator symbol between columns
  19     -sep, -separator        use the next argument as the column-separator
     File: ./sbs/io.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 # Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sbs
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "strings"
  32 )
  33 
  34 // errDoneWriting is a dummy error used to signal the app should quit early
  35 // without showing an actual error
  36 var errDoneWriting = errors.New(`done writing`)
  37 
  38 // padding is a ring-buffer only used by func pad, to minimize calls to Write
  39 var padding = [64]byte{
  40     ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
  41     ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
  42     ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
  43     ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
  44     ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
  45     ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
  46     ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
  47     ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
  48 }
  49 
  50 // writeSpaces emits the number of spaces given, while minimizing calls
  51 // to Write by reusing a ring-buffer
  52 func writeSpaces(w *bufio.Writer, spaces int) (n int, err error) {
  53     if spaces <= 0 {
  54         return 0, nil
  55     }
  56 
  57     // emit all full-buffer writes
  58     for l := len(padding); spaces >= l; spaces -= l {
  59         m, err := w.Write(padding[:])
  60         n += m
  61 
  62         if err != nil {
  63             return n, err
  64         }
  65     }
  66 
  67     // emit any remainder bytes
  68     if spaces > 0 {
  69         m, err := w.Write(padding[:spaces])
  70         return n + m, err
  71     }
  72     return n, nil
  73 }
  74 
  75 // newScanner standardizes how line-scanners are setup in this app
  76 func newScanner(r io.Reader) *bufio.Scanner {
  77     const maxbufsize = 8 * 1024 * 1024 * 1024
  78     sc := bufio.NewScanner(r)
  79     sc.Buffer(nil, maxbufsize)
  80     return sc
  81 }
  82 
  83 // padWrite emits the string given, following it with spaces to fill the
  84 // width given if string is shorter than that
  85 func padWrite(w *bufio.Writer, s string, n int) {
  86     w.WriteString(s)
  87     writeSpaces(w, n-width(s))
  88 }
  89 
  90 // writeItem emits the string given, followed by any padding needed, as well
  91 // as ANSI-style clearing, again if needed
  92 func writeItem(w *bufio.Writer, s string, width int) {
  93     padWrite(w, s, width)
  94     if needsStyleReset(s) {
  95         w.WriteString("\x1b[0m")
  96     }
  97 }
  98 
  99 func needsStyleReset(s string) bool {
 100     return strings.Contains(s, "\x1b[") && !strings.HasSuffix(s, "\x1b[0m")
 101 }
     File: ./sbs/io_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sbs
  26 
  27 import (
  28     "bufio"
  29     "strconv"
  30     "strings"
  31     "testing"
  32 )
  33 
  34 func TestWriteSpaces(t *testing.T) {
  35     spaces := strings.Repeat(` `, 20_000)
  36 
  37     for n := -20; n < len(spaces); n++ {
  38         t.Run(strconv.Itoa(n), func(t *testing.T) {
  39             var sb strings.Builder
  40             w := bufio.NewWriter(&sb)
  41             writeSpaces(w, n)
  42             w.Flush()
  43 
  44             if n < 0 {
  45                 // avoid slicing with negative values
  46                 n = 0
  47             }
  48 
  49             expected := spaces[:n]
  50             got := sb.String()
  51 
  52             if got != expected {
  53                 const fs = `expected %d spaces, but got %d instead`
  54                 t.Fatalf(fs, len(expected), len(got))
  55             }
  56         })
  57     }
  58 }
     File: ./sbs/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sbs
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "math"
  32     "os"
  33     "strconv"
  34     "strings"
  35 
  36     _ "embed"
  37 )
  38 
  39 //go:embed info.txt
  40 var usage string
  41 
  42 func Main() {
  43     sep := ` █ `
  44     args := os.Args[1:]
  45 
  46     if len(args) > 0 {
  47         switch args[0] {
  48         case `-h`, `--h`, `-help`, `--help`:
  49             os.Stderr.WriteString(usage)
  50             return
  51 
  52         case `-s`, `--s`, `-simple`, `--simple`, `-no-sep`, `--no-sep`:
  53             sep = `  `
  54             args = args[1:]
  55 
  56         case `-sep`, `--sep`, `-separator`, `--separator`:
  57             if len(args) < 2 {
  58                 const msg = "missing value to change column separator\n"
  59                 os.Stderr.WriteString(msg)
  60                 os.Exit(1)
  61                 return
  62             }
  63             sep = args[1]
  64             args = args[2:]
  65         }
  66     }
  67 
  68     ncols := 0
  69     if len(args) > 0 {
  70         f, err := strconv.ParseFloat(args[0], 64)
  71         if err == nil && !math.IsInf(f, 0) && !math.IsNaN(f) {
  72             ncols = int(f)
  73             args = args[1:]
  74         }
  75     }
  76 
  77     if len(args) > 0 && args[0] == `--` {
  78         args = args[1:]
  79     }
  80 
  81     err := run(args, ncols, sep)
  82     if err != nil && err != errDoneWriting {
  83         os.Stderr.WriteString(`sbs: `)
  84         os.Stderr.WriteString(err.Error())
  85         os.Exit(1)
  86         return
  87     }
  88 }
  89 
  90 func run(args []string, ncols int, separator string) error {
  91     paths := args
  92     if len(paths) == 0 {
  93         paths = []string{`-`}
  94     }
  95 
  96     if separator == `` {
  97         separator = ` `
  98     }
  99 
 100     lines, err := gatherLines(paths)
 101     if err != nil {
 102         return err
 103     }
 104 
 105     // choose a default number of columns, if not given an explicit one
 106     if ncols < 1 {
 107         ncols = chooseNumColumns(lines, separator)
 108     }
 109 
 110     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 111     defer w.Flush()
 112     return showLines(w, lines, ncols, separator)
 113 }
 114 
 115 // gatherLines slurps all text-lines from all the filepaths given, expanding
 116 // any tabs found; a single dash as a pathname means stdin
 117 func gatherLines(paths []string) ([]string, error) {
 118     var lines []string
 119     var sb strings.Builder
 120 
 121     dashes := 0
 122     for _, s := range paths {
 123         if s == `-` {
 124             dashes++
 125         }
 126     }
 127     if dashes > 1 {
 128         return lines, errors.New(`can't use "-" (stdin) more than once`)
 129     }
 130 
 131     for _, s := range paths {
 132         err := handleNamedInput(s, func(r io.Reader) error {
 133             sc := newScanner(r)
 134 
 135             for sc.Scan() {
 136                 s := sc.Text()
 137                 if strings.IndexByte(s, '\t') >= 0 {
 138                     sb.Reset()
 139                     expand(s, tabstop, &sb)
 140                     s = strings.Clone(sb.String())
 141                 }
 142                 lines = append(lines, s)
 143             }
 144 
 145             return sc.Err()
 146         })
 147 
 148         if err != nil {
 149             return lines, err
 150         }
 151     }
 152 
 153     return lines, nil
 154 }
 155 
 156 // handleNamedInput makes opening/closing files more convenient by using
 157 // callbacks to handle processing; this func also handles recognizing `-`
 158 // as meaning stdin
 159 func handleNamedInput(path string, fn func(r io.Reader) error) error {
 160     if path == `-` {
 161         return fn(os.Stdin)
 162     }
 163 
 164     f, err := os.Open(path)
 165     if err != nil {
 166         return err
 167     }
 168     defer f.Close()
 169     return fn(f)
 170 }
 171 
 172 // showLines handles the use-case of showing/rearranging lines from a single
 173 // input source (presumably standard input) into several columns
 174 func showLines(w *bufio.Writer, lines []string, ncols int, sep string) error {
 175     if ncols < 1 {
 176         return nil
 177     }
 178 
 179     if ncols == 1 {
 180         for _, s := range lines {
 181             w.WriteString(s)
 182             err := w.WriteByte('\n')
 183             if err != nil {
 184                 // assume error probably results from a closed stdout
 185                 // pipe, so quit the app right away without complaining
 186                 return err
 187             }
 188         }
 189         return nil
 190     }
 191 
 192     // nothing to show, so don't even bother
 193     if len(lines) == 0 {
 194         return nil
 195     }
 196 
 197     cols, height := splitLines(lines, ncols)
 198     widths := make([]int, 0, len(cols))
 199     for _, c := range cols {
 200         // find the max width of all lines of the current column
 201         maxw := 0
 202         for _, v := range c {
 203             w := width(v)
 204             if w > maxw {
 205                 maxw = w
 206             }
 207         }
 208 
 209         widths = append(widths, maxw)
 210     }
 211 
 212     // endSep is right-trimmed to avoid unneeded trailing spaces on output
 213     // lines whose last column is an empty/missing input line
 214     endSep := strings.TrimRight(sep, ` `)
 215 
 216     // show columns side by side
 217     for r := 0; r < height; r++ {
 218         for c := 0; c < len(cols); c++ {
 219             badr := r >= len(cols[c])
 220 
 221             // clearly separate columns visually
 222             if c > 0 {
 223                 if c == len(cols)-1 && (badr || cols[c][r] == ``) {
 224                     // avoid unneeded trailing spaces
 225                     w.WriteString(endSep)
 226                 } else {
 227                     w.WriteString(sep)
 228                 }
 229             }
 230 
 231             if badr {
 232                 // exceeding items for this (last) column
 233                 continue
 234             }
 235 
 236             // pad all columns, except the last
 237             width := 0
 238             if c < len(cols)-1 {
 239                 width = widths[c]
 240             }
 241 
 242             // emit maybe-padded column
 243             writeItem(w, cols[c][r], width)
 244         }
 245 
 246         // end the line
 247         err := w.WriteByte('\n')
 248         if err != nil {
 249             // probably a pipe was closed
 250             return nil
 251         }
 252     }
 253 
 254     return nil
 255 }
     File: ./sbs/strings.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sbs
  26 
  27 import (
  28     "math"
  29     "strings"
  30     "unicode/utf8"
  31 )
  32 
  33 // expand replaces all tabs with correctly-padded tabstops, turning all tabs
  34 // each into 1 or more spaces, as appropriate
  35 func expand(s string, tabstop int, sb *strings.Builder) {
  36     sb.Reset()
  37     numrunes := 0
  38 
  39     for _, r := range s {
  40         switch r {
  41         case '\t':
  42             numspaces := tabstop - numrunes%tabstop
  43             for i := 0; i < numspaces; i++ {
  44                 sb.WriteRune(' ')
  45             }
  46             numrunes += numspaces
  47 
  48         default:
  49             sb.WriteRune(r)
  50             numrunes++
  51         }
  52     }
  53 }
  54 
  55 // width calculates visually-correct string widths
  56 func width(s string) int {
  57     return utf8.RuneCountInString(s) - ansiLength(s)
  58 }
  59 
  60 // ansiLength calculates how many bytes ANSI-codes take in the string given:
  61 // func width uses this to calculate visually-correct string widths
  62 func ansiLength(s string) int {
  63     n := 0
  64     prev := rune(0)
  65     ansi := false
  66     for _, r := range s {
  67         if ansi {
  68             n++
  69         }
  70 
  71         if ansi && r == 'm' {
  72             ansi = false
  73             continue
  74         }
  75 
  76         if prev == '\x1b' && r == '[' {
  77             n += 2 // count the 2-item starter-sequence `\x1b[`
  78             ansi = true
  79         }
  80         prev = r
  81     }
  82     return n
  83 }
  84 
  85 // splitLines turns an array of lines into sub-arrays of lines, so they can
  86 // be shown side by side later on
  87 func splitLines(lines []string, ncols int) (cols [][]string, maxheight int) {
  88     n := ncols
  89     hfrac := float64(len(lines)) / float64(n)
  90     h := int(math.Ceil(hfrac))
  91 
  92     cols = make([][]string, 0, n)
  93     for len(lines) > h {
  94         cols = append(cols, lines[:h])
  95         lines = lines[h:]
  96     }
  97     if len(lines) != 0 {
  98         cols = append(cols, lines)
  99     }
 100     return cols, h
 101 }
 102 
 103 // indexLine handles slice-indexing by returning an empty string when the
 104 // index given is invalid, which helps simplify other funcs' control-flow
 105 // func indexLine(lines []string, i int) (line string, ok bool) {
 106 //  if 0 <= i && i < len(lines) {
 107 //      return lines[i], true
 108 //  }
 109 //  return ``, false
 110 // }
     File: ./sbs/strings_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sbs
  26 
  27 import (
  28     "strings"
  29     "testing"
  30 )
  31 
  32 func TestExpand(t *testing.T) {
  33     tests := map[string]struct {
  34         input    string
  35         tabstop  int
  36         expected string
  37     }{
  38         `empty`:                    {``, 4, ``},
  39         `indent 1`:                 {"\tabc", 4, `    abc`},
  40         `indent 2`:                 {"\t\tabc", 4, `        abc`},
  41         `indent 2 (mix tab/space)`: {"\t \tabc", 4, `        abc`},
  42     }
  43 
  44     for name, tc := range tests {
  45         t.Run(name, func(t *testing.T) {
  46             var sb strings.Builder
  47             expand(tc.input, tc.tabstop, &sb)
  48 
  49             if got := strings.Clone(sb.String()); got != tc.expected {
  50                 const fs = `input %q, tabstop %d: got %q, instead of %q`
  51                 t.Fatalf(fs, tc.input, tc.tabstop, got, tc.expected)
  52             }
  53         })
  54     }
  55 }
  56 
  57 func TestANSILength(t *testing.T) {
  58     tests := map[string]struct {
  59         input    string
  60         expected int
  61     }{
  62         `empty`:               {``, 0},
  63         `no ansi escapes`:     {`abc def`, 0},
  64         `simple ansi escapes`: {"\x1b[38;5;120mabc def\x1b[0m", 15},
  65     }
  66 
  67     for name, tc := range tests {
  68         t.Run(name, func(t *testing.T) {
  69             if got := ansiLength(tc.input); got != tc.expected {
  70                 const fs = `input %q: got %d, instead of %d`
  71                 t.Fatalf(fs, tc.input, got, tc.expected)
  72             }
  73         })
  74     }
  75 }
     File: ./seq/seq.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package seq
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "math/big"
  31     "os"
  32     "strconv"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 seq [options...] [first...] [increment...] [last]
  38 
  39 Emit a sequence of numbers, one per line.
  40 
  41 Options
  42 
  43     -w    pad with leading zeros
  44 `
  45 
  46 func Main() {
  47     zeroPad := false
  48     var numbuf [3]*big.Rat
  49 
  50     args := os.Args[1:]
  51     nums := numbuf[:0]
  52 
  53     for len(args) > 0 {
  54         switch args[0] {
  55         case `-h`, `--h`, `-help`, `--help`, `help`:
  56             os.Stdout.WriteString(info[1:])
  57             return
  58 
  59         case `-w`:
  60             zeroPad = true
  61             args = args[1:]
  62             continue
  63         }
  64 
  65         n := big.NewRat(0, 1)
  66         if v, ok := n.SetString(args[0]); ok {
  67             if len(nums) == cap(numbuf) {
  68                 os.Stderr.WriteString(info[1:])
  69                 os.Exit(1)
  70                 return
  71             }
  72 
  73             nums = append(nums, v)
  74             args = args[1:]
  75             continue
  76         }
  77 
  78         break
  79     }
  80 
  81     if len(args) > 0 && args[0] == `--` {
  82         args = args[1:]
  83     }
  84 
  85     switch len(nums) {
  86     case 0:
  87         os.Stderr.WriteString(info[1:])
  88         os.Exit(1)
  89         return
  90 
  91     case 1:
  92         numbuf[2] = numbuf[0]
  93         numbuf[0] = big.NewRat(1, 1)
  94         numbuf[1] = big.NewRat(1, 1)
  95 
  96     case 2:
  97         numbuf[2] = numbuf[1]
  98         numbuf[1] = big.NewRat(1, 1)
  99     }
 100 
 101     if numbuf[1].Sign() == 0 {
 102         return
 103     }
 104 
 105     first, incr, last := numbuf[0], numbuf[1], numbuf[2]
 106     if err := seq(first, incr, last, zeroPad); err != nil && err != io.EOF {
 107         os.Stderr.WriteString(`seq: `)
 108         os.Stderr.WriteString(err.Error())
 109         os.Stderr.WriteString("\n")
 110         os.Exit(1)
 111         return
 112     }
 113 }
 114 
 115 func seq(first, incr, last *big.Rat, zeroPad bool) error {
 116     if first.IsInt() && incr.IsInt() && last.IsInt() {
 117         f := int(first.Num().Int64())
 118         i := int(incr.Num().Int64())
 119         l := int(last.Num().Int64())
 120         return seqInt(f, i, l, zeroPad)
 121     }
 122 
 123     return seqFrac(first, incr, last, zeroPad)
 124 }
 125 
 126 func seqInt(first, incr, last int, zeroPad bool) error {
 127     if incr == 0 {
 128         return nil
 129     }
 130 
 131     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 132     defer w.Flush()
 133 
 134     var buf [24]byte
 135     var maxlen int
 136 
 137     cur := first
 138     if zeroPad {
 139         var n int
 140         n = len(strconv.AppendInt(buf[:0], int64(first), 10))
 141         if maxlen < n {
 142             maxlen = n
 143         }
 144         n = len(strconv.AppendInt(buf[:0], int64(last), 10))
 145         if maxlen < n {
 146             maxlen = n
 147         }
 148     }
 149 
 150     for {
 151         if incr > 0 && cur > last {
 152             break
 153         }
 154         if incr < 0 && cur < last {
 155             break
 156         }
 157 
 158         s := strconv.AppendInt(buf[:0], int64(cur), 10)
 159         if maxlen > 0 {
 160             writeZeros(w, maxlen-len(s))
 161         }
 162         w.Write(s)
 163 
 164         if w.WriteByte('\n') != nil {
 165             return io.EOF
 166         }
 167 
 168         cur += incr
 169     }
 170 
 171     return nil
 172 }
 173 
 174 func seqFrac(first, incr, last *big.Rat, zeroPad bool) error {
 175     incrSign := incr.Sign()
 176     if incrSign == 0 {
 177         return nil
 178     }
 179 
 180     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 181     defer w.Flush()
 182 
 183     var maxlen, prec int
 184 
 185     var p int
 186     p = maxPrec(first)
 187     if prec < p {
 188         prec = p
 189     }
 190     p = maxPrec(incr)
 191     if prec < p {
 192         prec = p
 193     }
 194     p = maxPrec(last)
 195     if prec < p {
 196         prec = p
 197     }
 198 
 199     cur := first
 200     if zeroPad {
 201         var n int
 202         n = len(first.FloatString(prec))
 203         if maxlen < n {
 204             maxlen = n
 205         }
 206         n = len(last.FloatString(prec))
 207         if maxlen < n {
 208             maxlen = n
 209         }
 210     }
 211 
 212     for {
 213         if incrSign > 0 && cur.Cmp(last) > 0 {
 214             break
 215         }
 216         if incrSign < 0 && cur.Cmp(last) < 0 {
 217             break
 218         }
 219 
 220         s := cur.FloatString(prec)
 221         if maxlen > 0 {
 222             writeZeros(w, maxlen-len(s))
 223         }
 224         w.WriteString(s)
 225 
 226         if w.WriteByte('\n') != nil {
 227             return io.EOF
 228         }
 229 
 230         cur = cur.Add(cur, incr)
 231     }
 232 
 233     return nil
 234 }
 235 
 236 func maxPrec(n *big.Rat) int {
 237     if p, exact := n.FloatPrec(); exact {
 238         return p
 239     }
 240 
 241     s := n.FloatString(50)
 242     i := strings.IndexByte(s, '.')
 243     if i < 0 {
 244         return 0
 245     }
 246 
 247     s = s[i+1:]
 248     for len(s) > 0 && s[len(s)-1] == '0' {
 249         s = s[:len(s)-1]
 250     }
 251     return len(s)
 252 }
 253 
 254 func writeZeros(w *bufio.Writer, n int) {
 255     const (
 256         half  = `00000000000000000000000000000000`
 257         zeros = half + half
 258     )
 259 
 260     for n >= len(zeros) {
 261         w.WriteString(zeros)
 262         n -= len(zeros)
 263     }
 264     if n > 0 {
 265         w.WriteString(zeros[:n])
 266     }
 267 }
     File: ./setint/setint.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package setint
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 setint [options...] [files...]
  37 
  38 SET INTersection emits only the lines which are common to all the input
  39 files given, never emitting the same line more than once.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44     -i, -ins     match lines case-insensitively
  45 `
  46 
  47 type config struct {
  48     // w is a writer to start emitting known results during the last input
  49     w *bufio.Writer
  50 
  51     // live controls whether each line output is immediately flushed
  52     live bool
  53 
  54     // sensitive controls case-sensitivity for the tally across inputs
  55     sensitive bool
  56 }
  57 
  58 func Main() {
  59     var cfg config
  60     cfg.live = true
  61     cfg.sensitive = true
  62     args := os.Args[1:]
  63 
  64     if len(args) > 0 {
  65         switch args[0] {
  66         case `-b`, `--b`, `-buffered`, `--buffered`:
  67             cfg.live = false
  68             args = args[1:]
  69 
  70         case `-h`, `--h`, `-help`, `--help`:
  71             os.Stdout.WriteString(info[1:])
  72             return
  73 
  74         case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
  75             cfg.sensitive = false
  76             args = args[1:]
  77         }
  78     }
  79 
  80     if len(args) > 0 && args[0] == `--` {
  81         args = args[1:]
  82     }
  83 
  84     if cfg.live {
  85         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  86             cfg.live = false
  87         }
  88     }
  89 
  90     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  91         os.Stderr.WriteString(`setint: `)
  92         os.Stderr.WriteString(err.Error())
  93         os.Stderr.WriteString("\n")
  94         os.Exit(1)
  95         return
  96     }
  97 }
  98 
  99 type intersector struct {
 100     // lines has all the lines across inputs, without repetitions
 101     lines []string
 102 
 103     // tally counts how many inputs have each entry
 104     tally map[string]int
 105 
 106     // dedup is to ensure each input can only tally a value once
 107     dedup map[string]struct{}
 108 
 109     // target auto-increments to keep track of the target tally for items
 110     target int
 111 
 112     // leadKey remembers the calculated tally-key for the first eligible
 113     // line in the queued-up list
 114     leadKey string
 115 
 116     // lastInput is to start emitting output before the end of all inputs
 117     lastInput bool
 118 
 119     // empty keeps track of empty inputs, since these mean an empty output
 120     empty bool
 121 }
 122 
 123 // prepare does what's needed right before handling the next input
 124 func (in *intersector) prepare(r io.Reader, cfg config) {
 125     in.empty = true
 126     clear(in.dedup)
 127     in.target++
 128     in.seekLead(cfg)
 129 }
 130 
 131 // seekLead discards leading items which will provably never make it to the
 132 // output for sure due to their tally being too low; udpates `leadKey` to
 133 // avoid recalculating it in the update method
 134 func (in *intersector) seekLead(cfg config) {
 135     if !in.lastInput {
 136         return
 137     }
 138 
 139     for len(in.lines) > 0 {
 140         key := in.lines[0]
 141         if !cfg.sensitive {
 142             key = strings.ToLower(key)
 143         }
 144 
 145         if in.tally[key] >= in.target-1 {
 146             in.leadKey = key
 147             break
 148         }
 149 
 150         in.lines = in.lines[1:]
 151     }
 152 }
 153 
 154 // update handles the line given
 155 func (in *intersector) update(line string, cfg config) error {
 156     in.empty = false
 157 
 158     if _, ok := in.dedup[line]; ok {
 159         return nil
 160     }
 161 
 162     in.dedup[line] = struct{}{}
 163 
 164     key := line
 165     if !cfg.sensitive {
 166         key = strings.ToLower(key)
 167     }
 168 
 169     t, ok := in.tally[key]
 170     if !ok {
 171         in.lines = append(in.lines, line)
 172     }
 173 
 174     t++
 175     in.tally[key] = t
 176 
 177     sureLead := in.lastInput && t == in.target && key == in.leadKey
 178 
 179     if !sureLead {
 180         return nil
 181     }
 182 
 183     // the current lead makes it to the output
 184 
 185     in.lines = in.lines[1:]
 186 
 187     cfg.w.WriteString(line)
 188     if cfg.w.WriteByte('\n') != nil {
 189         return io.EOF
 190     }
 191 
 192     if cfg.live && cfg.w.Flush() != nil {
 193         return io.EOF
 194     }
 195 
 196     // the (former) lead making it may open room for an uninterrupted streak
 197     // of other lines whose tally makes them part of the output
 198 
 199     for len(in.lines) > 0 {
 200         line := in.lines[0]
 201         key := line
 202         if !cfg.sensitive {
 203             key = strings.ToLower(key)
 204         }
 205 
 206         if in.tally[key] != in.target {
 207             break
 208         }
 209         in.lines = in.lines[1:]
 210 
 211         cfg.w.WriteString(line)
 212         if cfg.w.WriteByte('\n') != nil {
 213             return io.EOF
 214         }
 215 
 216         if cfg.live && cfg.w.Flush() != nil {
 217             return io.EOF
 218         }
 219     }
 220 
 221     // don't forget there may be later leads about to make it
 222     in.seekLead(cfg)
 223 
 224     return nil
 225 }
 226 
 227 // show what's left of the output entries, after all inputs are handled
 228 func (in intersector) show(w *bufio.Writer, cfg config) error {
 229     defer w.Flush()
 230 
 231     if in.empty {
 232         return nil
 233     }
 234 
 235     for _, line := range in.lines {
 236         key := line
 237         if !cfg.sensitive {
 238             key = strings.ToLower(key)
 239         }
 240 
 241         if in.tally[key] != in.target {
 242             continue
 243         }
 244 
 245         w.WriteString(line)
 246         if w.WriteByte('\n') != nil {
 247             return io.EOF
 248         }
 249     }
 250 
 251     return nil
 252 }
 253 
 254 func run(w io.Writer, paths []string, cfg config) error {
 255     // ignore duplicate file names
 256     got := make(map[string]struct{})
 257     unique := make([]string, 0, len(paths))
 258     for _, s := range paths {
 259         if _, ok := got[s]; ok {
 260             continue
 261         }
 262         got[s] = struct{}{}
 263         unique = append(unique, s)
 264     }
 265 
 266     cfg.w = bufio.NewWriterSize(w, 32*1024)
 267     defer cfg.w.Flush()
 268 
 269     // with a single input, the tool simply avoids duplicates; use special
 270     // funcs to save memory, in that case
 271     if len(unique) == 0 {
 272         return dedup(cfg.w, os.Stdin, cfg)
 273     }
 274     if len(unique) == 1 {
 275         return dedupFile(cfg.w, unique[0], cfg)
 276     }
 277 
 278     var in intersector
 279     in.tally = make(map[string]int)
 280     in.dedup = make(map[string]struct{})
 281 
 282     for i, name := range unique {
 283         in.lastInput = i == len(unique)-1
 284         if err := handleFile(name, &in, cfg); err != nil {
 285             return err
 286         }
 287 
 288         // as soon as there's an empty input, the result is empty for sure
 289         if in.empty {
 290             break
 291         }
 292     }
 293 
 294     return in.show(cfg.w, cfg)
 295 }
 296 
 297 func handleFile(name string, in *intersector, cfg config) error {
 298     if name == `` || name == `-` {
 299         return handleReader(os.Stdin, in, cfg)
 300     }
 301 
 302     f, err := os.Open(name)
 303     if err != nil {
 304         return errors.New(`can't read from file named "` + name + `"`)
 305     }
 306     defer f.Close()
 307 
 308     return handleReader(f, in, cfg)
 309 }
 310 
 311 func handleReader(r io.Reader, in *intersector, cfg config) error {
 312     // any empty input makes the whole output empty
 313     if in.empty {
 314         return nil
 315     }
 316 
 317     in.prepare(r, cfg)
 318 
 319     const gb = 1024 * 1024 * 1024
 320     sc := bufio.NewScanner(r)
 321     sc.Buffer(nil, 8*gb)
 322 
 323     for sc.Scan() {
 324         in.update(sc.Text(), cfg)
 325     }
 326     return sc.Err()
 327 }
 328 
 329 func dedupFile(w *bufio.Writer, path string, cfg config) error {
 330     if path == `` || path == `-` {
 331         return dedup(w, os.Stdin, cfg)
 332     }
 333 
 334     f, err := os.Open(path)
 335     if err != nil {
 336         return errors.New(`can't read from file named "` + path + `"`)
 337     }
 338     defer f.Close()
 339 
 340     return dedup(w, f, cfg)
 341 }
 342 
 343 func dedup(w *bufio.Writer, r io.Reader, cfg config) error {
 344     defer w.Flush()
 345 
 346     const gb = 1024 * 1024 * 1024
 347     sc := bufio.NewScanner(r)
 348     sc.Buffer(nil, 8*gb)
 349 
 350     got := make(map[string]struct{})
 351 
 352     for sc.Scan() {
 353         line := sc.Text()
 354         key := line
 355         if !cfg.sensitive {
 356             key = strings.ToLower(key)
 357         }
 358 
 359         if _, ok := got[key]; ok {
 360             continue
 361         }
 362         got[key] = struct{}{}
 363 
 364         w.Write(sc.Bytes())
 365         if w.WriteByte('\n') != nil {
 366             return io.EOF
 367         }
 368 
 369         if cfg.live && w.Flush() != nil {
 370             return io.EOF
 371         }
 372     }
 373 
 374     return sc.Err()
 375 }
     File: ./setint/setint_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package setint
  26 
  27 import (
  28     "bufio"
  29     "encoding/base64"
  30     "strings"
  31     "testing"
  32 
  33     cryptorand "crypto/rand"
  34     mathrand "math/rand"
  35 )
  36 
  37 func TestIntersections(t *testing.T) {
  38     testIntersection(t,
  39         "3\n4\n5\n6\n", "3\n4\n5\n6\na\n3\n5\na\n", "2\n3\n4\n5\n6\n7\n")
  40 
  41     const m = 16
  42 
  43     for maxab := 0; maxab < m; maxab++ {
  44         for maxa := 0; maxa < m; maxa++ {
  45             for maxb := 0; maxb < m; maxb++ {
  46                 for maxlen := m / 2; maxlen < m; maxlen++ {
  47                     ab, a, b := mkInputs(maxab, maxa, maxb, maxlen)
  48                     testIntersection(t, ab, a, b)
  49                 }
  50             }
  51         }
  52     }
  53 }
  54 
  55 func testIntersection(t *testing.T, ab, a, b string) {
  56     var cfg config
  57     var dst strings.Builder
  58     cfg.w = bufio.NewWriterSize(&dst, 32*1024)
  59 
  60     var p intersector
  61     p.tally = make(map[string]int)
  62     p.dedup = make(map[string]struct{})
  63 
  64     handleReader(strings.NewReader(a), &p, cfg)
  65     handleReader(strings.NewReader(b), &p, cfg)
  66     p.show(cfg.w, cfg)
  67 
  68     res := dst.String()
  69     ab, _ = strings.CutSuffix(ab, "\n")
  70     res, _ = strings.CutSuffix(res, "\n")
  71     if ab == res {
  72         return
  73     }
  74 
  75     t.Errorf("results not matching:\n%s\nvs\n%s", ab, res)
  76     // t.Errorf(
  77     //  "results not matching (%d/%d/%d|%d//%d):\n%s\nvs\n%s",
  78     //  maxab, maxa, maxb, len(ab), len(res), ab, res,
  79     // )
  80 }
  81 
  82 func testRandomIntersection(t *testing.T, maxab, maxa, maxb, maxlen int) {
  83     ab, a, b := mkInputs(maxab, maxa, maxb, maxlen)
  84     testIntersection(t, ab, a, b)
  85 }
  86 
  87 func mkInputs(maxab, maxa, maxb, maxlen int) (ab, a, b string) {
  88     buf := make([]byte, maxlen)
  89 
  90     var abs []string
  91     dedup := make(map[string]struct{})
  92     for i := 0; i < maxab; i++ {
  93         s := makeJunkLine(buf)
  94         if _, ok := dedup[s]; ok {
  95             // resulting set-intersection can't have duplicates
  96             continue
  97         }
  98         abs = append(abs, s)
  99         dedup[s] = struct{}{}
 100     }
 101 
 102     var as []string
 103     for i := 0; i < maxa; i++ {
 104         s := makeJunkLine(buf)
 105         as = append(as, s)
 106     }
 107     as = append(as, as...)
 108     as = append(as, abs...)
 109 
 110     // don't shuffle lines from the first input, since output is supposed to
 111     // keep order of first appearance, and shuffling would prevent that
 112 
 113     var bs []string
 114     for i := 0; i < maxab; i++ {
 115         s := makeJunkLine(buf)
 116         bs = append(bs, s)
 117     }
 118     bs = append(bs, bs...)
 119     bs = append(bs, abs...)
 120 
 121     // shuffle lines from the second input, since order of later appearances
 122     // of common lines shouldn't matter
 123     mathrand.Shuffle(len(bs), func(i, j int) {
 124         bs[i], bs[j] = bs[j], bs[i]
 125     })
 126 
 127     if len(abs) > 0 {
 128         ab = strings.Join(abs, "\n") + "\n"
 129     }
 130     if len(as) > 0 {
 131         a = strings.Join(as, "\n") + "\n"
 132     }
 133     if len(bs) > 0 {
 134         b = strings.Join(bs, "\n") + "\n"
 135     }
 136     return ab, a, b
 137 }
 138 
 139 func makeJunkLine(buf []byte) string {
 140     // n, _ := cryptorand.Read(buf[:])
 141     // s := string(buf[:n])
 142     // s = strings.Replace(s, "\n", "", -1)
 143     // s = strings.Replace(s, "\r", "", -1)
 144     // return s
 145 
 146     n, _ := cryptorand.Read(buf[:])
 147     return base64.StdEncoding.EncodeToString(buf[:n])
 148 }
     File: ./setsub/setsub.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package setsub
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 setsub [options...] [files...]
  37 
  38 SET SUBtraction emits only the lines from the first input given which don't
  39 appear in later inputs, never emitting the same line more than once.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44     -i, -ins     match lines case-insensitively
  45 `
  46 
  47 type config struct {
  48     // sensitive controls case-sensitivity when comparing input lines
  49     sensitive bool
  50 
  51     // live controls whether each line output is immediately flushed; this
  52     // is used only when handling a single input
  53     live bool
  54 }
  55 
  56 func Main() {
  57     var cfg config
  58     cfg.sensitive = true
  59     cfg.live = true
  60     args := os.Args[1:]
  61 
  62     if len(args) > 0 {
  63         switch args[0] {
  64         case `-b`, `--b`, `-buffered`, `--buffered`:
  65             cfg.live = false
  66             args = args[1:]
  67 
  68         case `-h`, `--h`, `-help`, `--help`:
  69             os.Stdout.WriteString(info[1:])
  70             return
  71 
  72         case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
  73             cfg.sensitive = false
  74             args = args[1:]
  75         }
  76     }
  77 
  78     if len(args) > 0 && args[0] == `--` {
  79         args = args[1:]
  80     }
  81 
  82     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  83         os.Stderr.WriteString(`setsub: `)
  84         os.Stderr.WriteString(err.Error())
  85         os.Stderr.WriteString("\n")
  86         os.Exit(1)
  87         return
  88     }
  89 }
  90 
  91 type subtractor struct {
  92     // lines has all the lines from all the inputs, without repetitions
  93     lines []string
  94 
  95     // passing keeps track of whether a line/key makes it to the output
  96     passing map[string]bool
  97 
  98     // firstInput decides whether lines are added/allowed for output or not
  99     firstInput bool
 100 }
 101 
 102 func run(w io.Writer, paths []string, cfg config) error {
 103     // ignore duplicate file names
 104     got := make(map[string]struct{})
 105     unique := make([]string, 0, len(paths))
 106     for _, s := range paths {
 107         if _, ok := got[s]; ok {
 108             continue
 109         }
 110         got[s] = struct{}{}
 111         unique = append(unique, s)
 112     }
 113 
 114     bw := bufio.NewWriterSize(w, 32*1024)
 115     defer bw.Flush()
 116 
 117     // using a single input is equivalent to a `dedup`: this is handled
 118     // separately both for efficiency and for the `live` output of lines
 119     if len(unique) == 0 {
 120         return runDedup(bw, `-`, cfg)
 121     }
 122     if len(unique) == 1 {
 123         return runDedup(bw, unique[0], cfg)
 124     }
 125 
 126     // gather data from all inputs
 127 
 128     var sub subtractor
 129     sub.passing = make(map[string]bool)
 130 
 131     for i, name := range unique {
 132         sub.firstInput = i == 0
 133         if err := handleFile(name, &sub, cfg); err != nil {
 134             return err
 135         }
 136     }
 137 
 138     // show lines still remaining
 139 
 140     for _, line := range sub.lines {
 141         key := line
 142         if !cfg.sensitive {
 143             key = strings.ToLower(key)
 144         }
 145 
 146         if !sub.passing[key] {
 147             continue
 148         }
 149 
 150         bw.WriteString(line)
 151         if bw.WriteByte('\n') != nil {
 152             return nil
 153         }
 154     }
 155 
 156     return nil
 157 }
 158 
 159 func handleFile(name string, sub *subtractor, cfg config) error {
 160     if name == `` || name == `-` {
 161         return handleReader(os.Stdin, sub, cfg)
 162     }
 163 
 164     f, err := os.Open(name)
 165     if err != nil {
 166         return errors.New(`can't read from file named "` + name + `"`)
 167     }
 168     defer f.Close()
 169 
 170     return handleReader(f, sub, cfg)
 171 }
 172 
 173 func handleReader(r io.Reader, sub *subtractor, cfg config) error {
 174     const gb = 1024 * 1024 * 1024
 175     sc := bufio.NewScanner(r)
 176     sc.Buffer(nil, 8*gb)
 177 
 178     for sc.Scan() {
 179         line := sc.Text()
 180         key := line
 181         if !cfg.sensitive {
 182             key = strings.ToLower(key)
 183         }
 184 
 185         passing, ok := sub.passing[key]
 186 
 187         if sub.firstInput {
 188             // first input, and the line hasn't appeared before
 189             if !ok {
 190                 sub.lines = append(sub.lines, line)
 191                 sub.passing[key] = true
 192             }
 193             continue
 194         }
 195 
 196         // avoid storing more entries on keys which haven't appeared in
 197         // the first input
 198         if passing && ok {
 199             sub.passing[key] = false
 200         }
 201     }
 202 
 203     return sc.Err()
 204 }
 205 
 206 func runDedup(w *bufio.Writer, path string, cfg config) error {
 207     if cfg.live {
 208         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 209             cfg.live = false
 210         }
 211     }
 212 
 213     return dedupFile(w, path, cfg)
 214 }
 215 
 216 func dedupFile(w *bufio.Writer, path string, cfg config) error {
 217     if path == `` || path == `-` {
 218         return dedup(w, os.Stdin, cfg)
 219     }
 220 
 221     f, err := os.Open(path)
 222     if err != nil {
 223         return errors.New(`can't read from file named "` + path + `"`)
 224     }
 225     defer f.Close()
 226 
 227     return dedup(w, f, cfg)
 228 }
 229 
 230 func dedup(w *bufio.Writer, r io.Reader, cfg config) error {
 231     const gb = 1024 * 1024 * 1024
 232     sc := bufio.NewScanner(r)
 233     sc.Buffer(nil, 8*gb)
 234 
 235     got := make(map[string]struct{})
 236 
 237     for sc.Scan() {
 238         line := sc.Text()
 239         key := line
 240         if !cfg.sensitive {
 241             key = strings.ToLower(key)
 242         }
 243 
 244         if _, ok := got[key]; ok {
 245             continue
 246         }
 247         got[key] = struct{}{}
 248 
 249         w.Write(sc.Bytes())
 250         if w.WriteByte('\n') != nil {
 251             return io.EOF
 252         }
 253 
 254         if cfg.live && w.Flush() != nil {
 255             return io.EOF
 256         }
 257     }
 258 
 259     return sc.Err()
 260 }
     File: ./shame512/shame512.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package shame512
  26 
  27 import (
  28     "bufio"
  29     "crypto/sha512"
  30     "encoding/hex"
  31     "errors"
  32     "io"
  33     "io/fs"
  34     "os"
  35     "path/filepath"
  36     "runtime"
  37     "sort"
  38     "sync"
  39 )
  40 
  41 const info = `
  42 shame512 [options...] [files/folders...]
  43 
  44 Group files by common/same SHA-512 hashes: having the same hash means a high
  45 chance of having the same contents, even though that's not a guarantee.
  46 
  47 Files and folder names can be given together for convenience: folder names
  48 are searched recursively to find all files, including in any subfolders.
  49 
  50 All (optional) leading options start with either single or double-dash:
  51 
  52     -h, -help    show this help message
  53     -j, -json    show results as JSON instead
  54 `
  55 
  56 func Main() {
  57     args := os.Args[1:]
  58     show := showGroups
  59 
  60     if len(args) > 0 {
  61         switch args[0] {
  62         case `-h`, `--h`, `-help`, `--help`:
  63             os.Stdout.WriteString(info[1:])
  64             return
  65 
  66         case `-j`, `--j`, `-json`, `--json`:
  67             show = showJSON
  68             args = args[1:]
  69         }
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     if len(args) == 0 {
  77         args = []string{`.`}
  78     }
  79 
  80     // paths has all filepaths given, ignoring repetitions
  81     paths, ok := findAllFiles(args)
  82     if !ok {
  83         os.Exit(1)
  84         return
  85     }
  86 
  87     checksums := make([]string, len(paths))
  88     findChecksums(paths, checksums)
  89 
  90     // group filepaths by common checksum, remembering the index of the
  91     // first item for each group, so the groups can be sorted by the
  92     // original order their first filepath came from the command-line
  93     indices := make(map[string]int)
  94     groups := make(map[string][]string)
  95     for i, chsum := range checksums {
  96         if chsum == `` {
  97             continue
  98         }
  99         g, ok := groups[chsum]
 100         if !ok {
 101             indices[chsum] = i
 102         }
 103         groups[chsum] = append(g, paths[i])
 104     }
 105 
 106     // keys has the sorted checksums
 107     keys := make([]string, len(groups))
 108     for chsum := range groups {
 109         keys = append(keys, chsum)
 110     }
 111     sort.SliceStable(keys, func(i, j int) bool {
 112         x := indices[keys[i]]
 113         y := indices[keys[j]]
 114         return x < y
 115     })
 116 
 117     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 118     defer bw.Flush()
 119 
 120     show(bw, keys, groups)
 121 }
 122 
 123 // findAllFiles can be given a mix of file/folder paths, finding all files
 124 // recursively in folders, avoiding duplicates
 125 func findAllFiles(paths []string) (files []string, success bool) {
 126     walk := filepath.WalkDir
 127     got := make(map[string]struct{})
 128     success = true
 129 
 130     for _, path := range paths {
 131         if _, ok := got[path]; ok {
 132             continue
 133         }
 134         got[path] = struct{}{}
 135 
 136         // a dash means standard input
 137         if path == `-` {
 138             files = append(files, path)
 139             continue
 140         }
 141 
 142         info, err := os.Stat(path)
 143         if os.IsNotExist(err) {
 144             // on windows, file-not-found messages may mention `CreateFile`,
 145             // even when trying to open files in read-only mode
 146             err = errors.New(`can't find file/folder named ` + path)
 147         }
 148 
 149         if err != nil {
 150             showError(path, err)
 151             success = false
 152             continue
 153         }
 154 
 155         if !info.IsDir() {
 156             files = append(files, path)
 157             continue
 158         }
 159 
 160         err = walk(path, func(path string, info fs.DirEntry, err error) error {
 161             path, err = filepath.Abs(path)
 162             if err != nil {
 163                 showError(path, err)
 164                 success = false
 165                 return err
 166             }
 167 
 168             if _, ok := got[path]; ok {
 169                 if info.IsDir() {
 170                     return fs.SkipDir
 171                 }
 172                 return nil
 173             }
 174             got[path] = struct{}{}
 175 
 176             if info.IsDir() {
 177                 return nil
 178             }
 179 
 180             files = append(files, path)
 181             return nil
 182         })
 183 
 184         if err != nil {
 185             showError(path, err)
 186             success = false
 187         }
 188     }
 189 
 190     return files, success
 191 }
 192 
 193 func showError(path string, err error) {
 194     if path != `` {
 195         os.Stderr.WriteString(path)
 196         os.Stderr.WriteString(`: `)
 197     }
 198     os.Stderr.WriteString(err.Error())
 199     os.Stderr.WriteString("\n")
 200 }
 201 
 202 func findChecksums(paths []string, sums []string) {
 203     var tasks sync.WaitGroup
 204     // the number of tasks is always known in advance
 205     tasks.Add(len(paths))
 206 
 207     // permissions is buffered to limit concurrency to the core-count
 208     permissions := make(chan struct{}, runtime.NumCPU())
 209     defer close(permissions)
 210 
 211     for i, path := range paths {
 212         // wait until some concurrency-room is available, before proceeding
 213         permissions <- struct{}{}
 214 
 215         go func(i int, path string) {
 216             defer tasks.Done()
 217 
 218             chsum, err := sha(path)
 219             if err != nil {
 220                 chsum = ``
 221                 showError(path, err)
 222             }
 223 
 224             sums[i] = chsum
 225             <-permissions
 226         }(i, path)
 227     }
 228 
 229     // wait for all tasks to finish
 230     tasks.Wait()
 231 }
 232 
 233 // sha calculates a checksum for a file's contents
 234 func sha(path string) (string, error) {
 235     f, err := os.Open(path)
 236     if err != nil {
 237         return ``, err
 238     }
 239     defer f.Close()
 240 
 241     sha := sha512.New()
 242     _, err = io.Copy(sha, f)
 243     if err != nil {
 244         return ``, err
 245     }
 246 
 247     // buf has room to fit a SHA-512 hash exactly: while its hexadecimal-ASCII
 248     // rendition is 128 bytes, the checksum itself is 64 bytes
 249     var buf [64]byte
 250     return hex.EncodeToString(sha.Sum(buf[:0])), err
 251 }
 252 
 253 func showGroups(w *bufio.Writer, keys []string, groups map[string][]string) {
 254     shown := 0
 255 
 256     for _, chsum := range keys {
 257         if chsum == `` {
 258             continue
 259         }
 260 
 261         if shown > 0 && w.WriteByte('\n') != nil {
 262             return
 263         }
 264 
 265         w.WriteString(chsum)
 266         if w.WriteByte('\n') != nil {
 267             return
 268         }
 269 
 270         files := groups[chsum]
 271 
 272         for _, path := range files {
 273             w.WriteString(path)
 274             if w.WriteByte('\n') != nil {
 275                 return
 276             }
 277         }
 278 
 279         shown++
 280     }
 281 
 282     return
 283 }
 284 
 285 func showJSON(w *bufio.Writer, keys []string, groups map[string][]string) {
 286     if len(keys) == 0 {
 287         w.WriteString("{}\n")
 288         return
 289     }
 290 
 291     const indent = `    `
 292 
 293     shown := 0
 294 
 295     w.WriteString("{")
 296 
 297     for _, chsum := range keys {
 298         files := groups[chsum]
 299         if len(files) == 0 {
 300             continue
 301         }
 302 
 303         if shown > 0 {
 304             w.WriteByte(',')
 305         }
 306         if w.WriteByte('\n') != nil {
 307             return
 308         }
 309 
 310         w.WriteString(indent)
 311 
 312         // checksum strings never need escaping
 313 
 314         w.WriteByte('"')
 315         w.WriteString(chsum)
 316         if _, err := w.WriteString(`": [` + "\n"); err != nil {
 317             return
 318         }
 319 
 320         for i, path := range files {
 321             if i > 0 && w.WriteByte('\n') != nil {
 322                 return
 323             }
 324 
 325             w.WriteString(indent + indent)
 326 
 327             w.WriteByte('"')
 328             w.WriteString(path)
 329             if i == len(files)-1 {
 330                 w.WriteByte('"')
 331             } else {
 332                 w.WriteString(`",`)
 333             }
 334         }
 335 
 336         w.WriteString("\n" + indent + `]`)
 337 
 338         shown++
 339     }
 340 
 341     if shown > 0 {
 342         w.WriteString("\n}\n")
 343     } else {
 344         w.WriteString("}\n")
 345     }
 346     return
 347 }
     File: ./skip/skip.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package skip
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 skip [options...] [max lines...] [files...]
  37 
  38 Skip at most the first n lines, or skip the first line by default. When
  39 not given any filepaths, the standard input is used instead.
  40 
  41 Options
  42 
  43     -n [number]    change max number of lines to skip (default is 1)
  44 `
  45 
  46 type config struct {
  47     skip      int
  48     liveLines bool
  49 }
  50 
  51 func Main() {
  52     var cfg config
  53     cfg.skip = 1
  54     cfg.liveLines = true
  55 
  56     args := os.Args[1:]
  57     for len(args) > 0 {
  58         switch args[0] {
  59         case `-b`, `--b`, `-buffered`, `--buffered`:
  60             cfg.liveLines = false
  61             args = args[1:]
  62             continue
  63 
  64         case `-n`:
  65             args = args[1:]
  66             if len(args) == 0 {
  67                 os.Stderr.WriteString("missing number of lines\n")
  68                 os.Exit(1)
  69                 return
  70             }
  71 
  72             s := strings.Replace(args[0], `_`, ``, -1)
  73             n, err := strconv.ParseInt(s, 10, 64)
  74             if err != nil {
  75                 os.Stderr.WriteString(`skip: invalid number: `)
  76                 os.Stderr.WriteString(err.Error())
  77                 os.Stderr.WriteString("\n")
  78                 os.Exit(1)
  79                 return
  80             }
  81 
  82             args = args[1:]
  83             cfg.skip = int(n)
  84             continue
  85 
  86         case `--help`:
  87             os.Stderr.WriteString(info[1:])
  88             return
  89         }
  90 
  91         break
  92     }
  93 
  94     if len(args) > 0 {
  95         s := strings.Replace(args[0], `_`, ``, -1)
  96         if n, err := strconv.ParseInt(s, 10, 64); err == nil {
  97             args = args[1:]
  98             cfg.skip = int(n)
  99         }
 100     }
 101 
 102     if len(args) > 0 && args[0] == `--` {
 103         args = args[1:]
 104     }
 105 
 106     if cfg.liveLines {
 107         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 108             cfg.liveLines = false
 109         }
 110     }
 111 
 112     if err := run(args, &cfg); err != nil && err != io.EOF {
 113         os.Stderr.WriteString(`skip: `)
 114         os.Stderr.WriteString(err.Error())
 115         os.Stderr.WriteString("\n")
 116         os.Exit(1)
 117         return
 118     }
 119 }
 120 
 121 func run(paths []string, cfg *config) error {
 122     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 123     defer w.Flush()
 124 
 125     for _, path := range paths {
 126         if err := handleFile(w, path, cfg); err != nil {
 127             return err
 128         }
 129     }
 130 
 131     if len(paths) == 0 {
 132         if err := handleLines(w, os.Stdin, cfg); err != nil {
 133             return err
 134         }
 135     }
 136     return nil
 137 }
 138 
 139 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 140     f, err := os.Open(path)
 141     if err != nil {
 142         return err
 143     }
 144     defer f.Close()
 145     return handleLines(w, f, cfg)
 146 }
 147 
 148 func handleLines(w *bufio.Writer, r io.Reader, cfg *config) error {
 149     const gb = 1024 * 1024 * 1024
 150     sc := bufio.NewScanner(r)
 151     sc.Buffer(nil, 8*gb)
 152 
 153     for sc.Scan() {
 154         if cfg.skip > 0 {
 155             cfg.skip--
 156             continue
 157         }
 158 
 159         w.Write(sc.Bytes())
 160         if w.WriteByte('\n') != nil {
 161             return io.EOF
 162         }
 163 
 164         if cfg.liveLines && w.Flush() != nil {
 165             return io.EOF
 166         }
 167     }
 168 
 169     return sc.Err()
 170 }
     File: ./skiplast/skiplast.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package skiplast
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 skiplast [options...] [max lines...] [files...]
  37 
  38 Keep all but the last n lines, or skip the last line by default. When not
  39 given any filepaths, the standard input is used instead.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44 `
  45 
  46 type ringBuffer struct {
  47     next  int
  48     max   int
  49     items []string
  50 }
  51 
  52 func (rb *ringBuffer) append(s string) (previous string) {
  53     if rb.next < len(rb.items) {
  54         previous = rb.items[rb.next]
  55         rb.items[rb.next] = s
  56     } else if len(rb.items) < rb.max {
  57         rb.items = append(rb.items, s)
  58     } else if len(rb.items) > 0 {
  59         previous = rb.items[0]
  60         rb.items[0] = s
  61     }
  62 
  63     rb.next++
  64     if rb.next >= rb.max {
  65         rb.next = 0
  66     }
  67     return previous
  68 }
  69 
  70 func Main() {
  71     var latest ringBuffer
  72     latest.max = 1
  73     buffered := false
  74     args := os.Args[1:]
  75 
  76     if len(args) > 0 {
  77         switch args[0] {
  78         case `-b`, `--b`, `-buffered`, `--buffered`:
  79             buffered = true
  80             args = args[1:]
  81 
  82         case `-h`, `--h`, `-help`, `--help`:
  83             os.Stdout.WriteString(info[1:])
  84             return
  85         }
  86     }
  87 
  88     if len(args) > 0 {
  89         s := strings.Replace(args[0], `_`, ``, -1)
  90         n, err := strconv.ParseInt(s, 10, 64)
  91         if err == nil {
  92             latest.max = int(n)
  93             args = args[1:]
  94         }
  95     }
  96 
  97     if len(args) > 0 && args[0] == `--` {
  98         args = args[1:]
  99     }
 100 
 101     if latest.max <= 0 {
 102         return
 103     }
 104 
 105     if latest.max <= 1_000 {
 106         latest.items = make([]string, 0, latest.max)
 107     }
 108 
 109     liveLines := !buffered
 110     if !buffered {
 111         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 112             liveLines = false
 113         }
 114     }
 115 
 116     if err := run(args, &latest, liveLines); err != nil && err != io.EOF {
 117         os.Stderr.WriteString(`skiplast: `)
 118         os.Stderr.WriteString(err.Error())
 119         os.Stderr.WriteString("\n")
 120         os.Exit(1)
 121         return
 122     }
 123 }
 124 
 125 type config struct {
 126     w    *bufio.Writer
 127     live bool
 128 }
 129 
 130 func run(paths []string, rb *ringBuffer, live bool) error {
 131     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 132     defer bw.Flush()
 133 
 134     var cfg config
 135     cfg.w = bw
 136     cfg.live = live
 137 
 138     for _, path := range paths {
 139         if err := handleFile(path, rb, cfg); err != nil {
 140             return err
 141         }
 142     }
 143 
 144     if len(paths) == 0 {
 145         if err := visit(os.Stdin, rb, cfg); err != nil {
 146             return err
 147         }
 148     }
 149     return nil
 150 }
 151 
 152 func handleFile(path string, rb *ringBuffer, cfg config) error {
 153     f, err := os.Open(path)
 154     if err != nil {
 155         return err
 156     }
 157     defer f.Close()
 158     return visit(f, rb, cfg)
 159 }
 160 
 161 func visit(r io.Reader, rb *ringBuffer, cfg config) error {
 162     const gb = 1024 * 1024 * 1024
 163     sc := bufio.NewScanner(r)
 164     sc.Buffer(nil, 8*gb)
 165 
 166     for sc.Scan() {
 167         prev := rb.append(sc.Text())
 168         if len(rb.items) < rb.max {
 169             continue
 170         }
 171 
 172         cfg.w.WriteString(prev)
 173 
 174         if cfg.w.WriteByte('\n') != nil {
 175             return io.EOF
 176         }
 177 
 178         if !cfg.live {
 179             continue
 180         }
 181 
 182         if cfg.w.Flush() != nil {
 183             return io.EOF
 184         }
 185     }
 186 
 187     return sc.Err()
 188 }
     File: ./squeeze/squeeze.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package squeeze
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 squeeze [filenames...]
  37 
  38 Ignore leading/trailing spaces (and carriage-returns) on lines, also turning
  39 all runs of multiple consecutive spaces into single spaces. Spaces around
  40 tabs are ignored as well.
  41 `
  42 
  43 func Main() {
  44     buffered := false
  45     args := os.Args[1:]
  46 
  47     if len(args) > 0 {
  48         switch args[0] {
  49         case `-b`, `--b`, `-buffered`, `--buffered`:
  50             buffered = true
  51             args = args[1:]
  52 
  53         case `-h`, `--h`, `-help`, `--help`:
  54             os.Stdout.WriteString(info[1:])
  55             return
  56         }
  57     }
  58 
  59     if len(args) > 0 && args[0] == `--` {
  60         args = args[1:]
  61     }
  62 
  63     liveLines := !buffered
  64     if !buffered {
  65         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  66             liveLines = false
  67         }
  68     }
  69 
  70     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  71         os.Stderr.WriteString(`squeeze: `)
  72         os.Stderr.WriteString(err.Error())
  73         os.Stderr.WriteString("\n")
  74         os.Exit(1)
  75         return
  76     }
  77 }
  78 
  79 func run(w io.Writer, args []string, live bool) error {
  80     bw := bufio.NewWriterSize(w, 32*1024)
  81     defer bw.Flush()
  82 
  83     if len(args) == 0 {
  84         return squeeze(bw, os.Stdin, live)
  85     }
  86 
  87     for _, name := range args {
  88         if err := handleFile(bw, name, live); err != nil {
  89             return err
  90         }
  91     }
  92     return nil
  93 }
  94 
  95 func handleFile(w *bufio.Writer, name string, live bool) error {
  96     if name == `` || name == `-` {
  97         return squeeze(w, os.Stdin, live)
  98     }
  99 
 100     f, err := os.Open(name)
 101     if err != nil {
 102         return errors.New(`can't read from file named "` + name + `"`)
 103     }
 104     defer f.Close()
 105 
 106     return squeeze(w, f, live)
 107 }
 108 
 109 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
 110     const gb = 1024 * 1024 * 1024
 111     sc := bufio.NewScanner(r)
 112     sc.Buffer(nil, 8*gb)
 113 
 114     for i := 0; sc.Scan(); i++ {
 115         s := sc.Bytes()
 116         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 117             s = s[3:]
 118         }
 119 
 120         writeSqueezed(w, s)
 121         if w.WriteByte('\n') != nil {
 122             return io.EOF
 123         }
 124 
 125         if live && w.Flush() != nil {
 126             return io.EOF
 127         }
 128     }
 129 
 130     return sc.Err()
 131 }
 132 
 133 func writeSqueezed(w *bufio.Writer, s []byte) {
 134     // ignore leading spaces
 135     for len(s) > 0 && s[0] == ' ' {
 136         s = s[1:]
 137     }
 138 
 139     // ignore trailing spaces
 140     for len(s) > 0 && s[len(s)-1] == ' ' {
 141         s = s[:len(s)-1]
 142     }
 143 
 144     space := false
 145 
 146     for len(s) > 0 {
 147         switch s[0] {
 148         case ' ':
 149             s = s[1:]
 150             space = true
 151 
 152         case '\t':
 153             s = s[1:]
 154             space = false
 155             for len(s) > 0 && s[0] == ' ' {
 156                 s = s[1:]
 157             }
 158             w.WriteByte('\t')
 159 
 160         default:
 161             if space {
 162                 w.WriteByte(' ')
 163                 space = false
 164             }
 165             w.WriteByte(s[0])
 166             s = s[1:]
 167         }
 168     }
 169 }
     File: ./squomp/squomp.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package squomp
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 squomp [filenames...]
  37 
  38 Squeeze non-empty lines and stomp empty(-ish) lines.
  39 
  40 Ignore leading/trailing spaces (and carriage-returns) on lines, also turning
  41 all runs of multiple consecutive spaces into single spaces. Spaces around
  42 tabs are ignored as well.
  43 
  44 Also, ignore leading/trailing empty lines, and turn runs of multiple empty
  45 lines into single empty lines. Empty-ish lines, or lines with only spaces in
  46 them, are also treated like empty ones.
  47 `
  48 
  49 type config struct {
  50     nonEmpty  int
  51     emptyRun  int
  52     liveLines bool
  53 }
  54 
  55 func Main() {
  56     var cfg config
  57     cfg.liveLines = true
  58     args := os.Args[1:]
  59 
  60     for len(args) > 0 {
  61         switch args[0] {
  62         case `-b`, `--b`, `-buffered`, `--buffered`:
  63             cfg.liveLines = false
  64             args = args[1:]
  65             continue
  66 
  67         case `-h`, `--h`, `-help`, `--help`:
  68             os.Stdout.WriteString(info[1:])
  69             return
  70         }
  71 
  72         break
  73     }
  74 
  75     if len(args) > 0 && args[0] == `--` {
  76         args = args[1:]
  77     }
  78 
  79     if cfg.liveLines {
  80         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  81             cfg.liveLines = false
  82         }
  83     }
  84 
  85     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  86         os.Stderr.WriteString(`squomp: `)
  87         os.Stderr.WriteString(err.Error())
  88         os.Stderr.WriteString("\n")
  89         os.Exit(1)
  90         return
  91     }
  92 }
  93 
  94 func run(w io.Writer, args []string, cfg config) error {
  95     bw := bufio.NewWriterSize(w, 32*1024)
  96     defer bw.Flush()
  97 
  98     if len(args) == 0 {
  99         return squomp(bw, os.Stdin, &cfg)
 100     }
 101 
 102     for _, name := range args {
 103         if err := handleFile(bw, name, &cfg); err != nil {
 104             return err
 105         }
 106     }
 107     return nil
 108 }
 109 
 110 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 111     if name == `` || name == `-` {
 112         return squomp(w, os.Stdin, cfg)
 113     }
 114 
 115     f, err := os.Open(name)
 116     if err != nil {
 117         return errors.New(`can't read from file named "` + name + `"`)
 118     }
 119     defer f.Close()
 120 
 121     return squomp(w, f, cfg)
 122 }
 123 
 124 func squomp(w *bufio.Writer, r io.Reader, cfg *config) error {
 125     const gb = 1024 * 1024 * 1024
 126     sc := bufio.NewScanner(r)
 127     sc.Buffer(nil, 8*gb)
 128 
 129     for i := 0; sc.Scan(); i++ {
 130         s := sc.Bytes()
 131         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 132             s = s[3:]
 133         }
 134 
 135         // trim leading spaces, so the empty-length check which follows can
 136         // also detect/ignore empty-ish lines, that is lines with only spaces
 137         for len(s) > 0 && s[0] == ' ' {
 138             s = s[1:]
 139         }
 140 
 141         if len(s) == 0 {
 142             if cfg.nonEmpty == 0 {
 143                 continue
 144             }
 145             cfg.emptyRun++
 146         }
 147 
 148         if cfg.emptyRun > 0 {
 149             cfg.emptyRun = 0
 150             if w.WriteByte('\n') != nil {
 151                 return io.EOF
 152             }
 153         }
 154 
 155         cfg.nonEmpty++
 156 
 157         writeSqueezed(w, s)
 158         if w.WriteByte('\n') != nil {
 159             return io.EOF
 160         }
 161 
 162         if cfg.liveLines && w.Flush() != nil {
 163             return io.EOF
 164         }
 165     }
 166 
 167     return sc.Err()
 168 }
 169 
 170 func writeSqueezed(w *bufio.Writer, s []byte) {
 171     // ignore leading spaces
 172     for len(s) > 0 && s[0] == ' ' {
 173         s = s[1:]
 174     }
 175 
 176     // ignore trailing spaces
 177     for len(s) > 0 && s[len(s)-1] == ' ' {
 178         s = s[:len(s)-1]
 179     }
 180 
 181     space := false
 182 
 183     for len(s) > 0 {
 184         switch s[0] {
 185         case ' ':
 186             s = s[1:]
 187             space = true
 188 
 189         case '\t':
 190             s = s[1:]
 191             space = false
 192             for len(s) > 0 && s[0] == ' ' {
 193                 s = s[1:]
 194             }
 195             w.WriteByte('\t')
 196 
 197         default:
 198             if space {
 199                 w.WriteByte(' ')
 200                 space = false
 201             }
 202             w.WriteByte(s[0])
 203             s = s[1:]
 204         }
 205     }
 206 }
     File: ./stomp/stomp.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package stomp
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 stomp [files...]
  37 
  38 Turn runs of empty lines into single empty lines, effectively squeezing
  39 paragraphs vertically, so to speak; runs of empty lines both at the start
  40 and at the end are completely ignored.
  41 
  42 All (optional) leading options start with either single or double-dash:
  43 
  44     -h, -help    show this help message
  45 `
  46 
  47 type config struct {
  48     nonEmpty  int
  49     emptyRun  int
  50     liveLines bool
  51 }
  52 
  53 func Main() {
  54     var cfg config
  55     cfg.liveLines = true
  56     args := os.Args[1:]
  57 
  58     for len(args) > 0 {
  59         switch args[0] {
  60         case `-b`, `--b`, `-buffered`, `--buffered`:
  61             cfg.liveLines = false
  62             args = args[1:]
  63             continue
  64 
  65         case `-h`, `--h`, `-help`, `--help`:
  66             os.Stdout.WriteString(info[1:])
  67             return
  68         }
  69 
  70         break
  71     }
  72 
  73     if len(args) > 0 && args[0] == `--` {
  74         args = args[1:]
  75     }
  76 
  77     if cfg.liveLines {
  78         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  79             cfg.liveLines = false
  80         }
  81     }
  82 
  83     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  84         os.Stderr.WriteString(`stomp: `)
  85         os.Stderr.WriteString(err.Error())
  86         os.Stderr.WriteString("\n")
  87         os.Exit(1)
  88         return
  89     }
  90 }
  91 
  92 func run(w io.Writer, args []string, cfg config) error {
  93     bw := bufio.NewWriterSize(w, 32*1024)
  94     defer bw.Flush()
  95 
  96     dashes := 0
  97     for _, name := range args {
  98         if name == `-` {
  99             dashes++
 100         }
 101         if dashes > 1 {
 102             return errors.New(`can't read stdin (dash) more than once`)
 103         }
 104     }
 105 
 106     if len(args) == 0 {
 107         return stomp(bw, os.Stdin, &cfg)
 108     }
 109 
 110     for _, name := range args {
 111         if err := handleFile(bw, name, &cfg); err != nil {
 112             return err
 113         }
 114     }
 115     return nil
 116 }
 117 
 118 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 119     if name == `` || name == `-` {
 120         return stomp(w, os.Stdin, cfg)
 121     }
 122 
 123     f, err := os.Open(name)
 124     if err != nil {
 125         return errors.New(`can't read from file named "` + name + `"`)
 126     }
 127     defer f.Close()
 128 
 129     return stomp(w, f, cfg)
 130 }
 131 
 132 func stomp(w *bufio.Writer, r io.Reader, cfg *config) error {
 133     const gb = 1024 * 1024 * 1024
 134     sc := bufio.NewScanner(r)
 135     sc.Buffer(nil, 8*gb)
 136 
 137     for i := 0; sc.Scan(); i++ {
 138         s := sc.Bytes()
 139         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 140             s = s[3:]
 141         }
 142 
 143         if len(s) == 0 {
 144             if cfg.nonEmpty == 0 {
 145                 continue
 146             }
 147             cfg.emptyRun++
 148         }
 149 
 150         if cfg.emptyRun > 0 {
 151             cfg.emptyRun = 0
 152             if w.WriteByte('\n') != nil {
 153                 return io.EOF
 154             }
 155         }
 156 
 157         cfg.nonEmpty++
 158 
 159         w.Write(s)
 160         if w.WriteByte('\n') != nil {
 161             return io.EOF
 162         }
 163 
 164         if cfg.liveLines && w.Flush() != nil {
 165             return io.EOF
 166         }
 167     }
 168 
 169     return sc.Err()
 170 }
     File: ./sym/sym.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sym
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31 )
  32 
  33 const info = `
  34 sym [names...]
  35 
  36 Lookup various hard-to-type symbols by their common names/aliases. All/any
  37 dashes in the names given are ignored, and are thus supported, so to speak.
  38 
  39 All (optional) leading options start with either single or double-dash:
  40 
  41     -h, -help    show this help message
  42 `
  43 
  44 func Main() {
  45     args := os.Args[1:]
  46 
  47     if len(args) > 0 {
  48         switch args[0] {
  49         case `-h`, `--h`, `-help`, `--help`:
  50             os.Stdout.WriteString(info[1:])
  51             return
  52         }
  53     }
  54 
  55     if len(args) > 0 && args[0] == `--` {
  56         args = args[1:]
  57     }
  58 
  59     if err := run(args); err != nil && err != io.EOF {
  60         os.Stderr.WriteString(`sym: `)
  61         os.Stderr.WriteString(err.Error())
  62         os.Stderr.WriteString("\n")
  63         os.Exit(1)
  64         return
  65     }
  66 }
  67 
  68 func run(args []string) error {
  69     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  70     defer bw.Flush()
  71 
  72     if err := showSymbols(bw, args); err != nil {
  73         return err
  74     }
  75     return nil
  76 }
     File: ./sym/tables.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sym
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "sort"
  32     "strings"
  33 )
  34 
  35 const (
  36     base64 = `` +
  37         `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=`
  38 
  39     faces = `` +
  40         `😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐😑😒😓` +
  41         `😔😕😖😗😘😙😚😛😜😝😞😟😠😡😢😣😤😥😦😧` +
  42         `😨😩😪😫😬😭😮😯😰😱😲😳😴😵😶😷🙁🙂🙃🙄` +
  43         `🧐👶🤓🤐🤑🤒🤔🤕🤗🤠🤡🤢🤣🤤🤥🤧🤨🤩🤪🤫` +
  44         `🤬🤭🤮🤯`
  45 
  46     punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
  47 
  48     visibleASCII = `` +
  49         "!\"#$%&'()*+,-./0123456789:;<=>?@" +
  50         "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
  51 )
  52 
  53 // `crlf`:       "\r\n",
  54 // `lf`:         "\n",
  55 // `linefeed`:   "\n",
  56 // `tab`:        "\t",
  57 
  58 // names2symbols translates common names for common symbols
  59 var names2symbols = map[string]string{
  60     `altcolon`:       `:`,
  61     `altquestion`:    `?`,
  62     `altverticalbar`: `|`,
  63     `almost`:         `≈`,
  64     `ampersand`:      `&`,
  65     `apostrophe`:     `’`,
  66     `approximately`:  `≅`,
  67     `asterisk`:       `*`,
  68     `backquote`:      "`",
  69     `backslash`:      `\`,
  70     `ball`:           `●`,
  71     `bell`:           `🔔`,
  72     `block`:          `█`,
  73     `bomb`:           `💣`,
  74     `bullet`:         `•`,
  75     `caca`:           `💩`,
  76     `caret`:          `^`,
  77     `check`:          `✅`,
  78     `cherries`:       `🍒`,
  79     `cloud`:          `☁️`,
  80     `cloverleaf`:     `🍀`,
  81     `colon`:          `:`,
  82     `comma`:          `,`,
  83     `commaspace`:     `, `,
  84     `copyright`:      `©`,
  85     `cross`:          `❌`,
  86     `crying`:         `😭`,
  87     `decimals`:       `0123456789`,
  88     `degree`:         `°`,
  89     `diameter`:       `⌀`,
  90     `diamond`:        `💎`,
  91     `document`:       `📄`,
  92     `dollar`:         `$`,
  93     `doublequote`:    `"`,
  94     `ellipsis`:       `…`,
  95     `email`:          `@`,
  96     `emdash`:         `—`,
  97     `emptyset`:       `∅`,
  98     `endash`:         `–`,
  99     `equals`:         `=`,
 100     `escape`:         ``,
 101     `euro`:           `€`,
 102     `exclamation`:    `!`,
 103     `eyes`:           `👀`,
 104     `fire`:           `🔥`,
 105     `folder`:         `📂`,
 106     `fullmoon`:       `🌕`,
 107     `grapes`:         `🍇`,
 108     `greatereq`:      `≥`,
 109     `happy`:          `😀`,
 110     `heart`:          `❤️`,
 111     `hole`:           `○`,
 112     `hyphen`:         `-`,
 113     `laughter`:       `😄`,
 114     `lemon`:          `🍋`,
 115     `less`:           `<`,
 116     `lesseq`:         `≤`,
 117     `lightning`:      `🌩️`,
 118     `ldquote`:        `“`,
 119     `lol`:            `😂`,
 120     `lsquote`:        `‘`,
 121     `mdot`:           `·`,
 122     `more`:           `>`,
 123     `multiplication`: `×`,
 124     `musicalnote`:    `🎵`,
 125     `not`:            `¬`,
 126     `notequals`:      `≠`,
 127     `orange`:         `🍊`,
 128     `party`:          `🥳`,
 129     `partypopper`:    `🎉`,
 130     `percent`:        `%`,
 131     `period`:         `.`,
 132     `pineapple`:      `🍍`,
 133     `plus`:           `+`,
 134     `question`:       `?`,
 135     `rain`:           `🌧️`,
 136     `rdquote`:        `”`,
 137     `rofl`:           `🤣`,
 138     `semicolon`:      `;`,
 139     `sharp`:          `#`,
 140     `slash`:          `/`,
 141     `slasher`:        `⧸`,
 142     `smile`:          `🙂`,
 143     `smirk`:          `😏`,
 144     `snowflake`:      `❄️`,
 145     `space`:          ` `,
 146     `sparkles`:       `✨`,
 147     `square`:         `■`,
 148     `singlequote`:    `'`,
 149     `star`:           `⭐`,
 150     `sun`:            `☀️`,
 151     `supersmile`:     `😊`,
 152     `tilde`:          `~`,
 153     `verticalbar`:    `|`,
 154     `vellipsis`:      `⋮`,
 155     `warning`:        `⚠️`,
 156     `watermelon`:     `🍉`,
 157 
 158     `base64`: base64,
 159     `faces`:  faces,
 160 
 161     `visibleascii`: visibleASCII,
 162 
 163     `alpha`:   `α`,
 164     `beta`:    `β`,
 165     `delta`:   `δ`,
 166     `epsilon`: `ε`,
 167     `gamma`:   `γ`,
 168     `lambda`:  `λ`,
 169     `omega`:   `ω`,
 170     `pi`:      `π`,
 171     `sigma`:   `σ`,
 172     `tau`:     `τ`,
 173     `theta`:   `θ`,
 174 
 175     `capitalsigma`: `Σ`,
 176 
 177     `asciiletters`:   `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`,
 178     `greek`:          `αβγδεζηθικλμνξοπρστυφχψω`,
 179     `hexadecimals`:   `0123456789ABCDEF`,
 180     `infinity`:       `∞`,
 181     `lowercase`:      `abcdefghijklmnopqrstuvwxyz`,
 182     `lowercasehex`:   `0123456789abcdef`,
 183     `math`:           `+-×÷²³±`,
 184     `octaldigits`:    `01234567`,
 185     `other`:          `✓✗✔❌`,
 186     `plusminus`:      `±`,
 187     `product`:        `Π`,
 188     `punctuation`:    punctuation,
 189     `uppercase`:      `ABCDEFGHIJKLMNOPQRSTUVWXYZ`,
 190     `uppercasegreek`: `ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ`,
 191 
 192     `eur`: `🇪🇺`,
 193 
 194     `afg`: `🇦🇫`,
 195     `ala`: `🇦🇽`,
 196     `alb`: `🇦🇱`,
 197     `dza`: `🇩🇿`,
 198     `asm`: `🇦🇸`,
 199     `and`: `🇦🇩`,
 200     `ago`: `🇦🇴`,
 201     `aia`: `🇦🇮`,
 202     `ata`: `🇦🇶`,
 203     `atg`: `🇦🇬`,
 204     `arg`: `🇦🇷`,
 205     `arm`: `🇦🇲`,
 206     `abw`: `🇦🇼`,
 207     `aus`: `🇦🇺`,
 208     `aut`: `🇦🇹`,
 209     `aze`: `🇦🇿`,
 210     `bhs`: `🇧🇸`,
 211     `bhr`: `🇧🇭`,
 212     `bgd`: `🇧🇩`,
 213     `brb`: `🇧🇧`,
 214     `blr`: `🇧🇾`,
 215     `bel`: `🇧🇪`,
 216     `blz`: `🇧🇿`,
 217     `ben`: `🇧🇯`,
 218     `bmu`: `🇧🇲`,
 219     `btn`: `🇧🇹`,
 220     `bol`: `🇧🇴`,
 221     `bes`: `🇧🇶`,
 222     `bih`: `🇧🇦`,
 223     `bwa`: `🇧🇼`,
 224     `bvt`: `🇧🇻`,
 225     `bra`: `🇧🇷`,
 226     `iot`: `🇮🇴`,
 227     `brn`: `🇧🇳`,
 228     `bgr`: `🇧🇬`,
 229     `bfa`: `🇧🇫`,
 230     `bdi`: `🇧🇮`,
 231     `cpv`: `🇨🇻`,
 232     `khm`: `🇰🇭`,
 233     `cmr`: `🇨🇲`,
 234     `can`: `🇨🇦`,
 235     `cym`: `🇰🇾`,
 236     `caf`: `🇨🇫`,
 237     `tcd`: `🇹🇩`,
 238     `chl`: `🇨🇱`,
 239     `chn`: `🇨🇳`,
 240     `cxr`: `🇨🇽`,
 241     `cck`: `🇨🇨`,
 242     `col`: `🇨🇴`,
 243     `com`: `🇰🇲`,
 244     `cod`: `🇨🇩`,
 245     `cog`: `🇨🇬`,
 246     `cok`: `🇨🇰`,
 247     `cri`: `🇨🇷`,
 248     `civ`: `🇨🇮`,
 249     `hrv`: `🇭🇷`,
 250     `cub`: `🇨🇺`,
 251     `cuw`: `🇨🇼`,
 252     `cyp`: `🇨🇾`,
 253     `cze`: `🇨🇿`,
 254     `dnk`: `🇩🇰`,
 255     `dji`: `🇩🇯`,
 256     `dma`: `🇩🇲`,
 257     `dom`: `🇩🇴`,
 258     `ecu`: `🇪🇨`,
 259     `egy`: `🇪🇬`,
 260     `slv`: `🇸🇻`,
 261     `gnq`: `🇬🇶`,
 262     `eri`: `🇪🇷`,
 263     `est`: `🇪🇪`,
 264     `swz`: `🇸🇿`,
 265     `eth`: `🇪🇹`,
 266     `flk`: `🇫🇰`,
 267     `fro`: `🇫🇴`,
 268     `fji`: `🇫🇯`,
 269     `fin`: `🇫🇮`,
 270     `fra`: `🇫🇷`,
 271     `guf`: `🇬🇫`,
 272     `pyf`: `🇵🇫`,
 273     `atf`: `🇹🇫`,
 274     `gab`: `🇬🇦`,
 275     `gmb`: `🇬🇲`,
 276     `geo`: `🇬🇪`,
 277     `deu`: `🇩🇪`,
 278     `gha`: `🇬🇭`,
 279     `gib`: `🇬🇮`,
 280     `grc`: `🇬🇷`,
 281     `grl`: `🇬🇱`,
 282     `grd`: `🇬🇩`,
 283     `glp`: `🇬🇵`,
 284     `gum`: `🇬🇺`,
 285     `gtm`: `🇬🇹`,
 286     `ggy`: `🇬🇬`,
 287     `gin`: `🇬🇳`,
 288     `gnb`: `🇬🇼`,
 289     `guy`: `🇬🇾`,
 290     `hti`: `🇭🇹`,
 291     `hmd`: `🇭🇲`,
 292     `vat`: `🇻🇦`,
 293     `hnd`: `🇭🇳`,
 294     `hkg`: `🇭🇰`,
 295     `hun`: `🇭🇺`,
 296     `isl`: `🇮🇸`,
 297     `ind`: `🇮🇳`,
 298     `idn`: `🇮🇩`,
 299     `irn`: `🇮🇷`,
 300     `irq`: `🇮🇶`,
 301     `irl`: `🇮🇪`,
 302     `imn`: `🇮🇲`,
 303     `isr`: `🇮🇱`,
 304     `ita`: `🇮🇹`,
 305     `jam`: `🇯🇲`,
 306     `jpn`: `🇯🇵`,
 307     `jey`: `🇯🇪`,
 308     `jor`: `🇯🇴`,
 309     `kaz`: `🇰🇿`,
 310     `ken`: `🇰🇪`,
 311     `kir`: `🇰🇮`,
 312     `prk`: `🇰🇵`,
 313     `kor`: `🇰🇷`,
 314     `kwt`: `🇰🇼`,
 315     `kgz`: `🇰🇬`,
 316     `lao`: `🇱🇦`,
 317     `lva`: `🇱🇻`,
 318     `lbn`: `🇱🇧`,
 319     `lso`: `🇱🇸`,
 320     `lbr`: `🇱🇷`,
 321     `lby`: `🇱🇾`,
 322     `lie`: `🇱🇮`,
 323     `ltu`: `🇱🇹`,
 324     `lux`: `🇱🇺`,
 325     `mac`: `🇲🇴`,
 326     `mkd`: `🇲🇰`,
 327     `mdg`: `🇲🇬`,
 328     `mwi`: `🇲🇼`,
 329     `mys`: `🇲🇾`,
 330     `mdv`: `🇲🇻`,
 331     `mli`: `🇲🇱`,
 332     `mlt`: `🇲🇹`,
 333     `mhl`: `🇲🇭`,
 334     `mtq`: `🇲🇶`,
 335     `mrt`: `🇲🇷`,
 336     `mus`: `🇲🇺`,
 337     `myt`: `🇾🇹`,
 338     `mex`: `🇲🇽`,
 339     `fsm`: `🇫🇲`,
 340     `mda`: `🇲🇩`,
 341     `mco`: `🇲🇨`,
 342     `mng`: `🇲🇳`,
 343     `mne`: `🇲🇪`,
 344     `msr`: `🇲🇸`,
 345     `mar`: `🇲🇦`,
 346     `moz`: `🇲🇿`,
 347     `mmr`: `🇲🇲`,
 348     `nam`: `🇳🇦`,
 349     `nru`: `🇳🇷`,
 350     `npl`: `🇳🇵`,
 351     `nld`: `🇳🇱`,
 352     `ncl`: `🇳🇨`,
 353     `nzl`: `🇳🇿`,
 354     `nic`: `🇳🇮`,
 355     `ner`: `🇳🇪`,
 356     `nga`: `🇳🇬`,
 357     `niu`: `🇳🇺`,
 358     `nfk`: `🇳🇫`,
 359     `mnp`: `🇲🇵`,
 360     `nor`: `🇳🇴`,
 361     `omn`: `🇴🇲`,
 362     `pak`: `🇵🇰`,
 363     `plw`: `🇵🇼`,
 364     `pse`: `🇵🇸`,
 365     `pan`: `🇵🇦`,
 366     `png`: `🇵🇬`,
 367     `pry`: `🇵🇾`,
 368     `per`: `🇵🇪`,
 369     `phl`: `🇵🇭`,
 370     `pcn`: `🇵🇳`,
 371     `pol`: `🇵🇱`,
 372     `prt`: `🇵🇹`,
 373     `pri`: `🇵🇷`,
 374     `qat`: `🇶🇦`,
 375     `reu`: `🇷🇪`,
 376     `rou`: `🇷🇴`,
 377     `rus`: `🇷🇺`,
 378     `rwa`: `🇷🇼`,
 379     `blm`: `🇧🇱`,
 380     `shn`: `🇸🇭`,
 381     `kna`: `🇰🇳`,
 382     `lca`: `🇱🇨`,
 383     `maf`: `🇲🇫`,
 384     `spm`: `🇵🇲`,
 385     `vct`: `🇻🇨`,
 386     `wsm`: `🇼🇸`,
 387     `smr`: `🇸🇲`,
 388     `stp`: `🇸🇹`,
 389     `sau`: `🇸🇦`,
 390     `sen`: `🇸🇳`,
 391     `srb`: `🇷🇸`,
 392     `syc`: `🇸🇨`,
 393     `sle`: `🇸🇱`,
 394     `sgp`: `🇸🇬`,
 395     `sxm`: `🇸🇽`,
 396     `svk`: `🇸🇰`,
 397     `svn`: `🇸🇮`,
 398     `slb`: `🇸🇧`,
 399     `som`: `🇸🇴`,
 400     `zaf`: `🇿🇦`,
 401     `sgs`: `🇬🇸`,
 402     `ssd`: `🇸🇸`,
 403     `esp`: `🇪🇸`,
 404     `lka`: `🇱🇰`,
 405     `sdn`: `🇸🇩`,
 406     `sur`: `🇸🇷`,
 407     `sjm`: `🇸🇯`,
 408     `swe`: `🇸🇪`,
 409     `che`: `🇨🇭`,
 410     `syr`: `🇸🇾`,
 411     `twn`: `🇹🇼`,
 412     `tjk`: `🇹🇯`,
 413     `tza`: `🇹🇿`,
 414     `tha`: `🇹🇭`,
 415     `tls`: `🇹🇱`,
 416     `tgo`: `🇹🇬`,
 417     `tkl`: `🇹🇰`,
 418     `ton`: `🇹🇴`,
 419     `tto`: `🇹🇹`,
 420     `tun`: `🇹🇳`,
 421     `tur`: `🇹🇷`,
 422     `tkm`: `🇹🇲`,
 423     `tca`: `🇹🇨`,
 424     `tuv`: `🇹🇻`,
 425     `uga`: `🇺🇬`,
 426     `ukr`: `🇺🇦`,
 427     `gbr`: `🇬🇧`,
 428     `uae`: `🇦🇪`,
 429     `umi`: `🇺🇲`,
 430     `usa`: `🇺🇸`,
 431     `ury`: `🇺🇾`,
 432     `uzb`: `🇺🇿`,
 433     `vut`: `🇻🇺`,
 434     `ven`: `🇻🇪`,
 435     `vnm`: `🇻🇳`,
 436     `vgb`: `🇻🇬`,
 437     `vir`: `🇻🇮`,
 438     `wlf`: `🇼🇫`,
 439     `esh`: `🇪🇭`,
 440     `yem`: `🇾🇪`,
 441     `zmb`: `🇿🇲`,
 442     `zwe`: `🇿🇼`,
 443 }
 444 
 445 // symbolAliases resolves aliases for keys used in table name2symbols
 446 var symbolAliases = map[string]string{
 447     `adash`:          `hyphen`,
 448     `aeq`:            `almost`,
 449     `alphabet`:       `lowercase`,
 450     `altqmark`:       `altquestion`,
 451     `altque`:         `altquestion`,
 452     `altques`:        `altquestion`,
 453     `altquest`:       `altquestion`,
 454     `altvbar`:        `altverticalbar`,
 455     `altverbar`:      `altverticalbar`,
 456     `altvertbar`:     `altverticalbar`,
 457     `amp`:            `ampersand`,
 458     `apos`:           `apostrophe`,
 459     `approx`:         `approximately`,
 460     `ast`:            `asterisk`,
 461     `atleast`:        `greatereq`,
 462     `atmost`:         `lesseq`,
 463     `backtick`:       `backquote`,
 464     `bang`:           `exclamation`,
 465     `bigsigma`:       `capitalsigma`,
 466     `bquo`:           `backquote`,
 467     `bquote`:         `backquote`,
 468     `bslash`:         `backslash`,
 469     `capsigma`:       `capitalsigma`,
 470     `cdot`:           `mdot`,
 471     `cherry`:         `cherries`,
 472     `circle`:         `ball`,
 473     `cquote`:         `rdquote`,
 474     `crap`:           `caca`,
 475     `cry`:            `crying`,
 476     `cryin`:          `crying`,
 477     `dash`:           `emdash`,
 478     `decimal`:        `decimals`,
 479     `dec`:            `decimals`,
 480     `deg`:            `degree`,
 481     `digits`:         `decimals`,
 482     `doc`:            `document`,
 483     `dot`:            `period`,
 484     `dquo`:           `doublequote`,
 485     `dquote`:         `doublequote`,
 486     `ellip`:          `ellipsis`,
 487     `eps`:            `epsilon`,
 488     `eq`:             `equals`,
 489     `equal`:          `equals`,
 490     `esc`:            `escape`,
 491     `excl`:           `exclamation`,
 492     `exclam`:         `exclamation`,
 493     `file`:           `document`,
 494     `fullstop`:       `period`,
 495     `gem`:            `diamond`,
 496     `grape`:          `grapes`,
 497     `geq`:            `greatereq`,
 498     `grin`:           `happy`,
 499     `hash`:           `sharp`,
 500     `hellip`:         `ellipsis`,
 501     `hellipsis`:      `ellipsis`,
 502     `hex`:            `hexadecimals`,
 503     `hexa`:           `hexadecimals`,
 504     `hexadec`:        `hexadecimals`,
 505     `hexadecimal`:    `hexadecimals`,
 506     `hexdigits`:      `hexadecimals`,
 507     `inf`:            `infinity`,
 508     `infty`:          `infinity`,
 509     `latin`:          `lowercase`,
 510     `laugh`:          `laughter`,
 511     `ldquo`:          `ldquote`,
 512     `leq`:            `lesseq`,
 513     `letters`:        `lowercase`,
 514     `lower`:          `lowercase`,
 515     `loweralpha`:     `alpha`,
 516     `lowerbeta`:      `beta`,
 517     `lowercasegreek`: `greek`,
 518     `lowergreek`:     `greek`,
 519     `lowerdelta`:     `delta`,
 520     `lowerepsilon`:   `epsilon`,
 521     `lowergamma`:     `gamma`,
 522     `lowerhex`:       `lowercasehex`,
 523     `lowerlambda`:    `lambda`,
 524     `loweromega`:     `omega`,
 525     `lowerpi`:        `pi`,
 526     `lowersigma`:     `sigma`,
 527     `lowertau`:       `tau`,
 528     `lowertheta`:     `theta`,
 529     `lsquo`:          `lsquote`,
 530     `mdash`:          `emdash`,
 531     `midascii`:       `visibleascii`,
 532     `minus`:          `hyphen`,
 533     `moon`:           `fullmoon`,
 534     `multiply`:       `multiplication`,
 535     `music`:          `musicalnote`,
 536     `ndash`:          `endash`,
 537     `neq`:            `notequals`,
 538     `note`:           `musicalnote`,
 539     `notequal`:       `notequals`,
 540     `oct`:            `octaldigits`,
 541     `octal`:          `octaldigits`,
 542     `octals`:         `octaldigits`,
 543     `octdigits`:      `octaldigits`,
 544     `oquote`:         `ldquote`,
 545     `pipe`:           `verticalbar`,
 546     `prod`:           `product`,
 547     `punct`:          `punctuation`,
 548     `rdquo`:          `rdquote`,
 549     `rsquo`:          `apostrophe`,
 550     `rsquote`:        `apostrophe`,
 551     `semi`:           `semicolon`,
 552     `shit`:           `caca`,
 553     `slashier`:       `slasher`,
 554     `snow`:           `snowflake`,
 555     `sparkle`:        `sparkles`,
 556     `squo`:           `singlequote`,
 557     `squote`:         `singlequote`,
 558     `sum`:            `capitalsigma`,
 559     `summation`:      `capitalsigma`,
 560     `upper`:          `uppercase`,
 561     `uppercasehex`:   `hexadecimals`,
 562     `uppergreek`:     `uppercasegreek`,
 563     `upperhex`:       `hexadecimals`,
 564     `vbar`:           `verticalbar`,
 565     `vellip`:         `vellipsis`,
 566     `verbar`:         `verticalbar`,
 567     `vertbar`:        `verticalbar`,
 568     `visascii`:       `visibleascii`,
 569 }
 570 
 571 func showSymbols(w *bufio.Writer, names []string) error {
 572     if len(names) == 0 {
 573         return showAllSymbols(w)
 574     }
 575 
 576     for _, name := range names {
 577         // normalize symbol names
 578         name = strings.TrimSpace(name)
 579         name = strings.ToLower(name)
 580         name = strings.ReplaceAll(name, ` `, ``)
 581         name = strings.ReplaceAll(name, `-`, ``)
 582         name = strings.ReplaceAll(name, `_`, ``)
 583 
 584         if len(name) == 0 {
 585             return errors.New(`no symbol name given`)
 586         }
 587 
 588         // handle regular symbol-name lookups
 589         s := name
 590         if alias, ok := symbolAliases[name]; ok {
 591             s = alias
 592         }
 593         sym, ok := names2symbols[s]
 594         if !ok {
 595             return errors.New(`no symbol named ` + name + ` found`)
 596         }
 597 
 598         w.WriteString(sym)
 599         if w.WriteByte('\n') != nil {
 600             return io.EOF
 601         }
 602     }
 603 
 604     return nil
 605 }
 606 
 607 func showAllSymbols(w *bufio.Writer) error {
 608     var keys []string
 609     got := make(map[string]struct{}, len(names2symbols)+len(symbolAliases))
 610 
 611     for k := range names2symbols {
 612         got[k] = struct{}{}
 613         keys = append(keys, k)
 614     }
 615 
 616     for k := range symbolAliases {
 617         if _, ok := got[k]; ok {
 618             continue
 619         }
 620         got[k] = struct{}{}
 621         keys = append(keys, k)
 622     }
 623 
 624     sort.Strings(keys)
 625 
 626     for _, s := range keys {
 627         k := s
 628         if alias, ok := symbolAliases[k]; ok {
 629             k = alias
 630         }
 631 
 632         w.WriteString(s)
 633         w.WriteByte('\t')
 634         w.WriteString(names2symbols[k])
 635         if w.WriteByte('\n') != nil {
 636             return io.EOF
 637         }
 638     }
 639 
 640     return nil
 641 }
     File: ./sym/tables_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package sym
  26 
  27 import "testing"
  28 
  29 func TestAliases(t *testing.T) {
  30     for alias, name := range symbolAliases {
  31         t.Run(alias, func(t *testing.T) {
  32             const fs = `alias %q becomes %q, which has no associated symbol`
  33             if _, ok := names2symbols[name]; !ok {
  34                 t.Errorf(fs, alias, name)
  35             }
  36         })
  37     }
  38 }
  39 
  40 func TestUniqueness(t *testing.T) {
  41     reverse := make(map[string][]string)
  42     for k, v := range names2symbols {
  43         reverse[v] = append(reverse[v], k)
  44     }
  45 
  46     for v, keys := range reverse {
  47         t.Run(v, func(t *testing.T) {
  48             if len(keys) > 1 {
  49                 t.Errorf(`value %q appears %d times`, v, len(keys))
  50             }
  51         })
  52     }
  53 }
     File: ./tacl/tacl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package tacl
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 tacl [options...] [file...]
  38 
  39 TAC Lines emits text lines in backward-order, last ones first.
  40 
  41 Unlike "tac" (reverse-order "cat"), TAC Lines ensures lines across inputs
  42 are never joined by accident, when an input's last line doesn't end with a
  43 line-feed.
  44 
  45 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  46 feeds. Leading BOM (byte-order marks) on first lines are also ignored.
  47 
  48 All (optional) leading options start with either single or double-dash:
  49 
  50     -h, -help    show this help message
  51     -0, -null    turn null-byte-delimited chunks into proper lines
  52 `
  53 
  54 type config struct {
  55     lines []string
  56     null  bool
  57 }
  58 
  59 func Main() {
  60     var cfg config
  61     args := os.Args[1:]
  62 
  63     for len(args) > 0 {
  64         switch args[0] {
  65         case `-0`, `--0`, `-null`, `--null`:
  66             cfg.null = true
  67             args = args[1:]
  68             continue
  69 
  70         case `-h`, `--h`, `-help`, `--help`:
  71             os.Stdout.WriteString(info[1:])
  72             return
  73         }
  74 
  75         break
  76     }
  77 
  78     if len(args) > 0 && args[0] == `--` {
  79         args = args[1:]
  80     }
  81 
  82     if err := run(os.Stdout, args, &cfg); err != nil && err != io.EOF {
  83         os.Stderr.WriteString(`tacl: `)
  84         os.Stderr.WriteString(err.Error())
  85         os.Stderr.WriteString("\n")
  86         os.Exit(1)
  87         return
  88     }
  89 
  90     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  91     defer w.Flush()
  92 
  93     for i := len(cfg.lines) - 1; i >= 0; i-- {
  94         w.WriteString(cfg.lines[i])
  95         if w.WriteByte('\n') != nil {
  96             break
  97         }
  98     }
  99 }
 100 
 101 func run(w io.Writer, args []string, cfg *config) error {
 102     dashes := 0
 103     for _, name := range args {
 104         if name == `-` {
 105             dashes++
 106         }
 107         if dashes > 1 {
 108             return errors.New(`can't read stdin (dash) more than once`)
 109         }
 110     }
 111 
 112     if len(args) == 0 {
 113         return getLines(os.Stdin, cfg)
 114     }
 115 
 116     for _, name := range args {
 117         if name == `-` {
 118             if err := getLines(os.Stdin, cfg); err != nil {
 119                 return err
 120             }
 121             continue
 122         }
 123 
 124         if err := handleFile(name, cfg); err != nil {
 125             return err
 126         }
 127     }
 128     return nil
 129 }
 130 
 131 func handleFile(name string, cfg *config) error {
 132     if name == `` || name == `-` {
 133         return getLines(os.Stdin, cfg)
 134     }
 135 
 136     f, err := os.Open(name)
 137     if err != nil {
 138         return errors.New(`can't read from file named "` + name + `"`)
 139     }
 140     defer f.Close()
 141 
 142     return getLines(f, cfg)
 143 }
 144 
 145 func getLines(r io.Reader, cfg *config) error {
 146     const gb = 1024 * 1024 * 1024
 147     sc := bufio.NewScanner(r)
 148     sc.Buffer(nil, 8*gb)
 149     if cfg.null {
 150         sc.Split(splitNull)
 151     }
 152 
 153     for i := 0; sc.Scan(); i++ {
 154         s := sc.Text()
 155         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 156             s = s[3:]
 157         }
 158 
 159         cfg.lines = append(cfg.lines, sc.Text())
 160     }
 161 
 162     return sc.Err()
 163 }
 164 
 165 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
 166 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
 167     // handle leading null-terminated line, if found in the current chunk
 168     if i := bytes.IndexByte(data, 0); i >= 0 {
 169         return i + 1, data[:i], nil
 170     }
 171 
 172     // request more data, in case there's a null coming up later
 173     if !atEOF {
 174         return 0, nil, nil
 175     }
 176 
 177     // handle non-empty non-terminated last chunk
 178     if len(data) > 0 {
 179         return len(data), data, bufio.ErrFinalToken
 180     }
 181 
 182     // handle empty non-terminated last chunk
 183     return 0, nil, bufio.ErrFinalToken
 184 }
     File: ./tail/tail.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 Note
  27 
  28     A previous attempt was trying to act more like the standard `tail` app
  29     for efficiency, by going backwards on the list of files, reading chunks
  30     backwards to reach the starting position in the right file for (up to)
  31     the last n lines.
  32 
  33     That attempt had an intermittent bug, and was scrapped in favor of the
  34     naive/slow approach used here, forward-scanning lines and keeping them
  35     into a ring-buffer.
  36 */
  37 
  38 package tail
  39 
  40 import (
  41     "bufio"
  42     "io"
  43     "os"
  44     "strconv"
  45     "strings"
  46 )
  47 
  48 const info = `
  49 tail [options...] [files...]
  50 
  51 Keep at most the last n lines, or keep the last 10 lines by default. When not
  52 given any filepaths, the standard input is used instead.
  53 
  54 Options
  55 
  56     -n [number]    change max number of lines (default is 10)
  57 `
  58 
  59 type ringBuffer struct {
  60     next  int
  61     max   int
  62     items []string
  63 }
  64 
  65 func (rb *ringBuffer) append(s string) {
  66     if rb.next < len(rb.items) {
  67         rb.items[rb.next] = s
  68     } else if len(rb.items) < rb.max {
  69         rb.items = append(rb.items, s)
  70     } else if len(rb.items) > 0 {
  71         rb.items[0] = s
  72     }
  73 
  74     rb.next++
  75     if rb.next >= rb.max {
  76         rb.next = 0
  77     }
  78 }
  79 
  80 func Main() {
  81     var latest ringBuffer
  82     latest.max = 10
  83 
  84     args := os.Args[1:]
  85     for len(args) > 0 {
  86         switch args[0] {
  87         case `-n`:
  88             args = args[1:]
  89             if len(args) == 0 {
  90                 os.Stderr.WriteString("missing number of lines\n")
  91                 os.Exit(1)
  92                 return
  93             }
  94 
  95             s := strings.Replace(args[0], `_`, ``, -1)
  96             n, err := strconv.ParseInt(s, 10, 64)
  97             if err != nil {
  98                 os.Stderr.WriteString(`tail: invalid number: `)
  99                 os.Stderr.WriteString(err.Error())
 100                 os.Stderr.WriteString("\n")
 101                 os.Exit(1)
 102                 return
 103             }
 104 
 105             args = args[1:]
 106             latest.max = int(n)
 107             continue
 108 
 109         case `--help`:
 110             os.Stderr.WriteString(info[1:])
 111             return
 112         }
 113 
 114         break
 115     }
 116 
 117     if len(args) > 0 && args[0] == `--` {
 118         args = args[1:]
 119     }
 120 
 121     if latest.max <= 0 {
 122         return
 123     }
 124 
 125     if latest.max <= 1_000 {
 126         latest.items = make([]string, 0, latest.max)
 127     }
 128 
 129     if err := run(args, &latest); err != nil {
 130         os.Stderr.WriteString(`tail: `)
 131         os.Stderr.WriteString(err.Error())
 132         os.Stderr.WriteString("\n")
 133         os.Exit(1)
 134         return
 135     }
 136 
 137     show(os.Stdout, latest)
 138 }
 139 
 140 func run(paths []string, rb *ringBuffer) error {
 141     for _, path := range paths {
 142         if err := handleFile(path, rb); err != nil {
 143             return err
 144         }
 145     }
 146 
 147     if len(paths) == 0 {
 148         if err := visit(os.Stdin, rb); err != nil {
 149             return err
 150         }
 151     }
 152     return nil
 153 }
 154 
 155 func show(w io.Writer, rb ringBuffer) {
 156     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 157     defer bw.Flush()
 158 
 159     for i := rb.next; i < len(rb.items); i++ {
 160         bw.WriteString(rb.items[i])
 161         if err := bw.WriteByte('\n'); err != nil {
 162             return
 163         }
 164     }
 165 
 166     for i := 0; i < len(rb.items) && i < rb.next; i++ {
 167         bw.WriteString(rb.items[i])
 168         if err := bw.WriteByte('\n'); err != nil {
 169             return
 170         }
 171     }
 172 }
 173 
 174 func handleFile(path string, rb *ringBuffer) error {
 175     f, err := os.Open(path)
 176     if err != nil {
 177         return err
 178     }
 179     defer f.Close()
 180     return visit(f, rb)
 181 }
 182 
 183 func visit(r io.Reader, rb *ringBuffer) error {
 184     const gb = 1024 * 1024 * 1024
 185     sc := bufio.NewScanner(r)
 186     sc.Buffer(nil, 8*gb)
 187 
 188     for sc.Scan() {
 189         rb.append(sc.Text())
 190     }
 191     return sc.Err()
 192 }
     File: ./tam/tam.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package tam
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "os/exec"
  33     "time"
  34 )
  35 
  36 // unixInfo is the help message for the non-Windows versions of this tool
  37 const unixInfo = `
  38 tam [custom options...] [command] [arguments...]
  39 
  40 
  41 Time And Memory (and exit) runs the command given, then shows a single line
  42 with the following info on the standard error:
  43 
  44     - total time elapsed
  45     - total user-mode time spent overall
  46     - total system/kernel time spent overall
  47     - maximum actual memory used (RSS) in 1024-byte kilobytes
  48     - the exit/status code, where 0 means success, and anything else error
  49 
  50 The options are, available both in single and double-dash versions
  51 
  52     -h, -help    show this help message
  53     -n, -name    also show command name and arguments
  54 `
  55 
  56 func Main() {
  57     full := false
  58     buffered := false
  59     args := os.Args[1:]
  60 
  61     for len(args) > 0 {
  62         switch args[0] {
  63         case `-b`, `--b`, `-buffered`, `--buffered`:
  64             buffered = true
  65             args = args[1:]
  66             continue
  67 
  68         case `-f`, `--f`, `-full`, `--full`, `-n`, `--n`, `-name`, `--name`:
  69             full = true
  70             args = args[1:]
  71             continue
  72 
  73         case `-h`, `--h`, `-help`, `--help`:
  74             os.Stdout.WriteString(info[1:])
  75             return
  76         }
  77 
  78         break
  79     }
  80 
  81     if len(args) > 0 && args[0] == `--` {
  82         args = args[1:]
  83     }
  84 
  85     if len(args) == 0 {
  86         os.Stderr.WriteString(info[1:])
  87         os.Exit(1)
  88         return
  89     }
  90 
  91     liveLines := !buffered
  92     if len(args) > 0 && !buffered {
  93         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  94             liveLines = false
  95         }
  96     }
  97 
  98     res, err := run(args, liveLines)
  99     if _, ok := err.(*exec.ExitError); ok {
 100         err = nil
 101     }
 102 
 103     if err != nil {
 104         os.Stderr.WriteString(`tam: `)
 105         os.Stderr.WriteString(err.Error())
 106         os.Stderr.WriteString("\n")
 107         if res.exitCode == 0 {
 108             res.exitCode = 1
 109         }
 110     }
 111 
 112     if full {
 113         for i, s := range args {
 114             if i > 0 {
 115                 os.Stderr.WriteString(` `)
 116             }
 117             os.Stderr.WriteString(s)
 118         }
 119         os.Stderr.WriteString(`    `)
 120     }
 121     showResults(res)
 122     os.Exit(res.exitCode)
 123     return
 124 }
 125 
 126 func run(args []string, liveLines bool) (res results, err error) {
 127     if len(args) == 0 {
 128         return results{}, nil
 129     }
 130 
 131     start := time.Now()
 132 
 133     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 134     defer bw.Flush()
 135 
 136     cmd := exec.Command(args[0], args[1:]...)
 137     cmd.Stdin = os.Stdin
 138     if liveLines {
 139         cmd.Stdout = lineFlusher{bw}
 140     } else {
 141         cmd.Stdout = bw
 142     }
 143     cmd.Stderr = os.Stderr
 144 
 145     bw.Flush()
 146 
 147     if err := cmd.Start(); err != nil {
 148         return makeResults(cmd.ProcessState, start), err
 149     }
 150 
 151     if err := cmd.Wait(); err != nil {
 152         return makeResults(cmd.ProcessState, start), err
 153     }
 154 
 155     bw.Flush()
 156 
 157     return makeResults(cmd.ProcessState, start), err
 158 }
 159 
 160 type lineFlusher struct {
 161     bw *bufio.Writer
 162 }
 163 
 164 func (lf lineFlusher) Write(p []byte) (n int, err error) {
 165     n, err = lf.bw.Write(p)
 166     if err != nil {
 167         return n, err
 168     }
 169 
 170     if bytes.IndexByte(p, '\n') >= 0 {
 171         err = lf.bw.Flush()
 172     }
 173 
 174     return n, err
 175 }
     File: ./tam/tam_unix.go
   1 //go:build linux || freebsd || openbsd || netbsd || darwin
   2 // +build linux freebsd openbsd netbsd darwin
   3 
   4 /*
   5 The MIT License (MIT)
   6 
   7 Copyright (c) 2026 pacman64
   8 
   9 Permission is hereby granted, free of charge, to any person obtaining a copy of
  10 this software and associated documentation files (the "Software"), to deal
  11 in the Software without restriction, including without limitation the rights to
  12 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  13 of the Software, and to permit persons to whom the Software is furnished to do
  14 so, subject to the following conditions:
  15 
  16 The above copyright notice and this permission notice shall be included in all
  17 copies or substantial portions of the Software.
  18 
  19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  25 SOFTWARE.
  26 */
  27 
  28 package tam
  29 
  30 import (
  31     "os"
  32     "strconv"
  33     "syscall"
  34     "time"
  35 )
  36 
  37 const info = unixInfo
  38 
  39 type results struct {
  40     exitCode   int
  41     totalTime  time.Duration
  42     userTime   time.Duration
  43     systemTime time.Duration
  44     maxMemory  int
  45 }
  46 
  47 func makeResults(s *os.ProcessState, start time.Time) results {
  48     if s == nil {
  49         return results{exitCode: 127}
  50     }
  51 
  52     maxRSS := 0
  53     if info, ok := s.SysUsage().(*syscall.Rusage); ok {
  54         maxRSS = int(info.Maxrss)
  55     }
  56 
  57     return results{
  58         exitCode:   s.ExitCode(),
  59         userTime:   s.UserTime(),
  60         systemTime: s.SystemTime(),
  61         totalTime:  time.Now().Sub(start),
  62         maxMemory:  maxRSS,
  63     }
  64 }
  65 
  66 func showResults(res results) {
  67     const gap = `    `
  68     var buf [256]byte
  69     s := buf[:0]
  70 
  71     s = append(s, `real `...)
  72     s = strconv.AppendFloat(s, res.totalTime.Seconds(), 'f', 2, 64)
  73     s = append(s, gap...)
  74     s = append(s, `user `...)
  75     s = strconv.AppendFloat(s, res.userTime.Seconds(), 'f', 2, 64)
  76     s = append(s, gap...)
  77     s = append(s, `sys `...)
  78     s = strconv.AppendFloat(s, res.systemTime.Seconds(), 'f', 2, 64)
  79     s = append(s, gap...)
  80     s = append(s, `mem `...)
  81     s = strconv.AppendInt(s, int64(res.maxMemory), 10)
  82     s = append(s, gap...)
  83     s = append(s, `exit `...)
  84     s = strconv.AppendInt(s, int64(res.exitCode), 10)
  85     s = append(s, '\n')
  86 
  87     os.Stderr.Write(s)
  88 }
     File: ./tam/tam_windows.go
   1 //go:build windows
   2 // +build windows
   3 
   4 /*
   5 The MIT License (MIT)
   6 
   7 Copyright (c) 2026 pacman64
   8 
   9 Permission is hereby granted, free of charge, to any person obtaining a copy of
  10 this software and associated documentation files (the "Software"), to deal
  11 in the Software without restriction, including without limitation the rights to
  12 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  13 of the Software, and to permit persons to whom the Software is furnished to do
  14 so, subject to the following conditions:
  15 
  16 The above copyright notice and this permission notice shall be included in all
  17 copies or substantial portions of the Software.
  18 
  19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  25 SOFTWARE.
  26 */
  27 
  28 package tam
  29 
  30 import (
  31     "os"
  32     "strconv"
  33     "time"
  34 )
  35 
  36 const info = `
  37 tam [custom options...] [command] [arguments...]
  38 
  39 
  40 Time And Memory (and exit) runs the command given, then shows a single line
  41 with the following info on the standard error:
  42 
  43     - total time elapsed
  44     - total user-mode time spent overall
  45     - total system/kernel time spent overall
  46     - the exit/status code, where 0 means success, and anything else error
  47 
  48 Despite its name, no info on the max-memory used is available on the Windows
  49 version of this tool.
  50 
  51 The options are, available both in single and double-dash versions
  52 
  53     -h, -help    show this help message
  54 `
  55 
  56 type results struct {
  57     exitCode   int
  58     totalTime  time.Duration
  59     userTime   time.Duration
  60     systemTime time.Duration
  61 }
  62 
  63 func makeResults(s *os.ProcessState, start time.Time) results {
  64     if s == nil {
  65         return results{exitCode: 127}
  66     }
  67 
  68     return results{
  69         exitCode:   s.ExitCode(),
  70         userTime:   s.UserTime(),
  71         systemTime: s.SystemTime(),
  72         totalTime:  time.Now().Sub(start),
  73     }
  74 }
  75 
  76 func showResults(res results) {
  77     const gap = `    `
  78     var buf [256]byte
  79     s := buf[:0]
  80 
  81     s = append(s, `real `...)
  82     s = strconv.AppendFloat(s, res.totalTime.Seconds(), 'f', 2, 64)
  83     s = append(s, gap...)
  84     s = append(s, `user `...)
  85     s = strconv.AppendFloat(s, res.userTime.Seconds(), 'f', 2, 64)
  86     s = append(s, gap...)
  87     s = append(s, `sys `...)
  88     s = strconv.AppendFloat(s, res.systemTime.Seconds(), 'f', 2, 64)
  89     s = append(s, gap...)
  90     s = append(s, `exit `...)
  91     s = append(s, '\n')
  92 
  93     os.Stderr.Write(s)
  94 }
     File: ./tcatl/tcatl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package tcatl
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "unicode/utf8"
  34 )
  35 
  36 const info = `
  37 tcatl [options...] [file...]
  38 
  39 
  40 Title and Concatenate lines emits lines from all the named sources given,
  41 preceding each file's contents with its name, using an ANSI reverse style.
  42 
  43 The name "-" stands for the standard input. When no names are given, the
  44 standard input is used by default.
  45 
  46 All (optional) leading options start with either single or double-dash:
  47 
  48     -h, -help    show this help message
  49     -0, -null    turn null-byte-delimited chunks into proper lines
  50 `
  51 
  52 type config struct {
  53     null      bool
  54     liveLines bool
  55 }
  56 
  57 func Main() {
  58     var cfg config
  59     cfg.liveLines = true
  60     args := os.Args[1:]
  61 
  62     for len(args) > 0 {
  63         switch args[0] {
  64         case `-0`, `--0`, `-null`, `--null`:
  65             cfg.null = true
  66             args = args[1:]
  67             continue
  68 
  69         case `-b`, `--b`, `-buffered`, `--buffered`:
  70             cfg.liveLines = false
  71             args = args[1:]
  72             continue
  73 
  74         case `-h`, `--h`, `-help`, `--help`:
  75             os.Stdout.WriteString(info[1:])
  76             return
  77         }
  78 
  79         break
  80     }
  81 
  82     if len(args) > 0 && args[0] == `--` {
  83         args = args[1:]
  84     }
  85 
  86     if cfg.liveLines {
  87         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  88             cfg.liveLines = false
  89         }
  90     }
  91 
  92     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  93         os.Stderr.WriteString(`tcatl: `)
  94         os.Stderr.WriteString(err.Error())
  95         os.Stderr.WriteString("\n")
  96         os.Exit(1)
  97         return
  98     }
  99 }
 100 
 101 func run(w io.Writer, args []string, cfg config) error {
 102     bw := bufio.NewWriterSize(w, 32*1024)
 103     defer bw.Flush()
 104 
 105     dashes := 0
 106     for _, name := range args {
 107         if name == `-` {
 108             dashes++
 109         }
 110         if dashes > 1 {
 111             break
 112         }
 113     }
 114 
 115     if len(args) == 0 {
 116         return tcatl(bw, os.Stdin, `<stdin>`, cfg)
 117     }
 118 
 119     var stdin []byte
 120     gotStdin := false
 121 
 122     for _, name := range args {
 123         if name == `-` {
 124             if dashes == 1 {
 125                 if err := tcatl(bw, os.Stdin, `<stdin>`, cfg); err != nil {
 126                     return err
 127                 }
 128                 continue
 129             }
 130 
 131             if !gotStdin {
 132                 data, err := io.ReadAll(os.Stdin)
 133                 if err != nil {
 134                     return err
 135                 }
 136                 stdin = data
 137                 gotStdin = true
 138             }
 139 
 140             bw.Write(stdin)
 141             if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
 142                 bw.WriteByte('\n')
 143             }
 144 
 145             if cfg.liveLines && bw.Flush() != nil {
 146                 return io.EOF
 147             }
 148 
 149             continue
 150         }
 151 
 152         if err := handleFile(bw, name, cfg); err != nil {
 153             return err
 154         }
 155     }
 156     return nil
 157 }
 158 
 159 func handleFile(w *bufio.Writer, name string, cfg config) error {
 160     if name == `` || name == `-` {
 161         return tcatl(w, os.Stdin, `<stdin>`, cfg)
 162     }
 163 
 164     f, err := os.Open(name)
 165     if err != nil {
 166         return errors.New(`can't read from file named "` + name + `"`)
 167     }
 168     defer f.Close()
 169 
 170     return tcatl(w, f, name, cfg)
 171 }
 172 
 173 func tcatl(w *bufio.Writer, r io.Reader, name string, cfg config) error {
 174     w.WriteString("\x1b[7m")
 175     w.WriteString(name)
 176     writeSpaces(w, 80-utf8.RuneCountInString(name))
 177     w.WriteString("\x1b[0m\n")
 178 
 179     if w.Flush() != nil {
 180         // a write error may be the consequence of stdout being closed,
 181         // perhaps by another app along a pipe
 182         return io.EOF
 183     }
 184 
 185     if !cfg.liveLines {
 186         return catlFast(w, r, cfg.null)
 187     }
 188 
 189     const gb = 1024 * 1024 * 1024
 190     sc := bufio.NewScanner(r)
 191     sc.Buffer(nil, 8*gb)
 192     if cfg.null {
 193         sc.Split(splitNull)
 194     }
 195 
 196     for i := 0; sc.Scan(); i++ {
 197         s := sc.Bytes()
 198         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 199             s = s[3:]
 200         }
 201 
 202         w.Write(s)
 203         if w.WriteByte('\n') != nil {
 204             return io.EOF
 205         }
 206 
 207         if w.Flush() != nil {
 208             return io.EOF
 209         }
 210     }
 211 
 212     return sc.Err()
 213 }
 214 
 215 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
 216     var buf [32 * 1024]byte
 217     var last byte = '\n'
 218 
 219     for i := 0; true; i++ {
 220         n, err := r.Read(buf[:])
 221         if n > 0 && err == io.EOF {
 222             err = nil
 223         }
 224         if err == io.EOF {
 225             if last != '\n' {
 226                 w.WriteByte('\n')
 227             }
 228             return nil
 229         }
 230 
 231         if err != nil {
 232             return err
 233         }
 234 
 235         chunk := buf[:n]
 236         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 237             chunk = chunk[3:]
 238         }
 239 
 240         // change nulls into line-feeds to handle null-terminated lines
 241         if null {
 242             for i, b := range chunk {
 243                 if b == 0 {
 244                     chunk[i] = '\n'
 245                 }
 246             }
 247         }
 248 
 249         if len(chunk) >= 1 {
 250             if _, err := w.Write(chunk); err != nil {
 251                 return io.EOF
 252             }
 253             last = chunk[len(chunk)-1]
 254         }
 255     }
 256 
 257     return nil
 258 }
 259 
 260 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
 261 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
 262     // handle leading null-terminated line, if found in the current chunk
 263     if i := bytes.IndexByte(data, 0); i >= 0 {
 264         return i + 1, data[:i], nil
 265     }
 266 
 267     // request more data, in case there's a null coming up later
 268     if !atEOF {
 269         return 0, nil, nil
 270     }
 271 
 272     // handle non-empty non-terminated last chunk
 273     if len(data) > 0 {
 274         return len(data), data, bufio.ErrFinalToken
 275     }
 276 
 277     // handle empty non-terminated last chunk
 278     return 0, nil, bufio.ErrFinalToken
 279 }
 280 
 281 // writeSpaces bulk-emits the number of spaces given
 282 func writeSpaces(w *bufio.Writer, n int) {
 283     const spaces = `                                `
 284     for ; n > len(spaces); n -= len(spaces) {
 285         w.WriteString(spaces)
 286     }
 287     if n > 0 {
 288         w.WriteString(spaces[:n])
 289     }
 290 }
     File: ./teletype/teletype.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package teletype
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33     "time"
  34     "unicode/utf8"
  35 )
  36 
  37 const info = `
  38 teletype [options...] [files...]
  39 
  40 Simulate the cadence of old-fashioned teletype machines, by slowing down
  41 the output of ASCII/UTF-8 symbols from the inputs given.
  42 
  43 All (optional) leading options start with either single or double-dash:
  44 
  45     -h, -help    show this help message
  46 `
  47 
  48 func Main() {
  49     args := os.Args[1:]
  50 
  51     if len(args) > 0 {
  52         switch args[0] {
  53         case `-h`, `--h`, `-help`, `--help`:
  54             os.Stdout.WriteString(info[1:])
  55             return
  56         }
  57     }
  58 
  59     if len(args) > 0 && args[0] == `--` {
  60         args = args[1:]
  61     }
  62 
  63     if err := run(os.Stdout, args); err != nil && err != io.EOF {
  64         os.Stderr.WriteString(`teletype: `)
  65         os.Stderr.WriteString(err.Error())
  66         os.Stderr.WriteString("\n")
  67         os.Exit(1)
  68         return
  69     }
  70 }
  71 
  72 func run(w io.Writer, args []string) error {
  73     dashes := 0
  74     for _, name := range args {
  75         if name == `-` {
  76             dashes++
  77         }
  78         if dashes > 1 {
  79             return errors.New(`can't read stdin (dash) more than once`)
  80         }
  81     }
  82 
  83     if len(args) == 0 {
  84         return teletype(w, os.Stdin)
  85     }
  86 
  87     for _, name := range args {
  88         if name == `-` {
  89             if err := teletype(w, os.Stdin); err != nil {
  90                 return err
  91             }
  92             continue
  93         }
  94 
  95         if err := handleFile(w, name); err != nil {
  96             return err
  97         }
  98     }
  99     return nil
 100 }
 101 
 102 func handleFile(w io.Writer, name string) error {
 103     if name == `` || name == `-` {
 104         return teletype(w, os.Stdin)
 105     }
 106 
 107     f, err := os.Open(name)
 108     if err != nil {
 109         return errors.New(`can't read from file named "` + name + `"`)
 110     }
 111     defer f.Close()
 112 
 113     return teletype(w, f)
 114 }
 115 
 116 func teletype(w io.Writer, r io.Reader) error {
 117     const gb = 1024 * 1024 * 1024
 118     sc := bufio.NewScanner(r)
 119     sc.Buffer(nil, 8*gb)
 120 
 121     for i := 0; sc.Scan(); i++ {
 122         s := sc.Text()
 123         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 124             s = s[3:]
 125         }
 126 
 127         var buf [4]byte
 128         for _, r := range s {
 129             time.Sleep(15 * time.Millisecond)
 130             if _, err := w.Write(utf8.AppendRune(buf[:0], r)); err != nil {
 131                 return io.EOF
 132             }
 133         }
 134 
 135         time.Sleep(750 * time.Millisecond)
 136         if _, err := w.Write([]byte{'\n'}); err != nil {
 137             return io.EOF
 138         }
 139     }
 140 
 141     return sc.Err()
 142 }
     File: ./timer/timer.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package timer
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "os/exec"
  33     "os/signal"
  34     "strconv"
  35     "sync"
  36     "time"
  37 )
  38 
  39 const (
  40     // gap has the spaces between current timer value and current date/time
  41     gap = `    `
  42 
  43     // clear has enough spaces in it to cover any chronograph output
  44     clear = "\r" + `                                              ` + "\r"
  45 
  46     dateTimeFormat = `2006-01-02 15:04:05 Jan Mon`
  47 
  48     forceQuitCode = 255
  49 )
  50 
  51 const (
  52     // every = 100 * time.Millisecond
  53     // chronoFormat = `15:04:05.0`
  54 
  55     every        = 1000 * time.Millisecond
  56     chronoFormat = `15:04:05`
  57 )
  58 
  59 const info = `
  60 timer [options...] [command...] [args...]
  61 
  62 
  63 Run a live timer/chronograph on stderr, always showing below all lines
  64 from stdin, which update stdout as they come.
  65 
  66 When stdin (or the command run) is over, this app simply quits showing how
  67 long it ran, by extending its last visible timer line on stderr.
  68 
  69 You can also use this app without piping anything to its stdin or giving it
  70 any command to run, acting as a simple live clock until you force-quit it,
  71 via Control+C, or end the stdin stream via Control+D.
  72 
  73 To ensure any stderr lines don't clash with the chronograph output line,
  74 you can also run this app with arguments to have it run a command, thus
  75 guaranteeing the subtask's stdout and stderr lines show without ever being
  76 mixed up.
  77 
  78 Note: by default this app assumes command output is plain-text lines, so a
  79 command's non-empty binary stdout may be corrupted in subtle ways; use the
  80 binary option to avoid that.
  81 
  82 The options are, available both in single and double-dash versions
  83 
  84     -b, -bin, -binary    treat stdin or a command's stdout as binary data
  85     -h, -help            show this help message
  86 `
  87 
  88 func Main() {
  89     bin := false
  90     args := os.Args[1:]
  91 
  92     if len(args) > 0 {
  93         switch args[0] {
  94         case `-h`, `--h`, `-help`, `--help`:
  95             os.Stdout.WriteString(info[1:])
  96             return
  97 
  98         case `-b`, `--b`, `-bin`, `--bin`, `-binary`, `--binary`:
  99             bin = true
 100             args = args[1:]
 101         }
 102     }
 103 
 104     if len(args) > 0 && args[0] == `--` {
 105         args = args[1:]
 106     }
 107 
 108     if len(args) > 0 {
 109         os.Exit(runTask(args[0], args[1:], bin))
 110     }
 111 
 112     err := chronograph(os.Stdin, nil, bin)
 113     if exit, ok := err.(exit); ok {
 114         os.Exit(exit.code)
 115     }
 116 
 117     if err != nil {
 118         showError(err)
 119         os.Exit(1)
 120     }
 121 }
 122 
 123 // exit is a custom error-type which isn't for showing, but for quitting the
 124 // app right away instead
 125 type exit struct {
 126     code int
 127 }
 128 
 129 // Error is only to satisfy the error interface, and not for showing
 130 func (e exit) Error() string {
 131     os.Exit(e.code)
 132     return `quitting right away`
 133 }
 134 
 135 // runTask handles running the app in `subtask-mode`
 136 func runTask(name string, args []string, bin bool) (exitCode int) {
 137     cmd := exec.Command(name, args...)
 138     cmd.Stdin = os.Stdin
 139 
 140     stdout, err := cmd.StdoutPipe()
 141     if err != nil {
 142         showError(err)
 143         return 1
 144     }
 145     defer stdout.Close()
 146 
 147     stderr, err := cmd.StderrPipe()
 148     if err != nil {
 149         showError(err)
 150         return 1
 151     }
 152     defer stderr.Close()
 153 
 154     if err := cmd.Start(); err != nil {
 155         showError(err)
 156         return 1
 157     }
 158 
 159     err = chronograph(stdout, stderr, bin)
 160     if exit, ok := err.(exit); ok {
 161         return exit.code
 162     }
 163     if err != nil {
 164         showError(err)
 165     }
 166 
 167     if err := cmd.Wait(); err != nil {
 168         showError(err)
 169     }
 170     return cmd.ProcessState.ExitCode()
 171 }
 172 
 173 func showError(err error) {
 174     if err != nil {
 175         os.Stderr.WriteString(err.Error())
 176         os.Stderr.WriteString("\n")
 177     }
 178 }
 179 
 180 // chronograph runs a live chronograph, showing the time elapsed: 2 input
 181 // sources for lines are handled concurrently, one destined for the app's
 182 // stdout, the other for the app's stderr, without interfering with the
 183 // chronograph lines, which also show on stderr
 184 func chronograph(stdout io.Reader, stderr io.Reader, binaryOut bool) error {
 185     start := time.Now()
 186     t := time.NewTicker(every)
 187     // s is a buffer used to minimize actual writes to final stdout/stderr
 188     s := make([]byte, 0, 1024)
 189 
 190     // start showing the timer right away
 191     s = startChronoLine(s[:0], start, start)
 192     os.Stderr.Write(s)
 193 
 194     // stopped is a special event to handle force-quitting the app
 195     stopped := make(chan os.Signal, 1)
 196     defer close(stopped)
 197     signal.Notify(stopped, os.Interrupt)
 198 
 199     // errors will no longer travel when both input streams are over
 200     errors := make(chan error)
 201     var waitAllOutput sync.WaitGroup
 202     waitAllOutput.Add(2)
 203 
 204     // binary-mode only concerns stdin or stdout, and never stderr
 205     readOutput := readLines
 206     if binaryOut {
 207         readOutput = readBytes
 208     }
 209 
 210     // relay stdout data asynchronously
 211     outChunks := make(chan []byte)
 212     go func() {
 213         defer waitAllOutput.Done()
 214         if stdout != nil {
 215             errors <- readOutput(stdout, outChunks)
 216         }
 217     }()
 218 
 219     // relay stderr data asynchronously
 220     errLines := make(chan []byte)
 221     go func() {
 222         defer waitAllOutput.Done()
 223         if stderr != nil {
 224             errors <- readLines(stderr, errLines)
 225         }
 226     }()
 227 
 228     // quit is a special event which happens when both input streams are over
 229     quit := make(chan struct{})
 230     defer close(quit)
 231     go func() {
 232         waitAllOutput.Wait()
 233         close(errors)
 234         quit <- struct{}{}
 235     }()
 236 
 237     for {
 238         select {
 239         case now := <-t.C:
 240             s = append(s[:0], clear...)
 241             s = startChronoLine(s, start, now)
 242             os.Stderr.Write(s)
 243 
 244         case chunk := <-outChunks:
 245             if chunk == nil {
 246                 // filter out junk for needlessly-chatty pipes, while still
 247                 // keeping actual empty lines
 248                 continue
 249             }
 250 
 251             // write-order of the next 3 steps matters, to avoid mixing up
 252             // lines since stdout and stderr lines can show up together, if
 253             // not handled correctly
 254 
 255             os.Stderr.WriteString(clear)
 256 
 257             s = append(s[:0], chunk...)
 258             // add an extra line-feed byte only when in plain-text mode
 259             if !binaryOut {
 260                 s = append(s, '\n')
 261             }
 262 
 263             // assume (final) stdout write errors are due to a closed pipe,
 264             // so quit this app successfully right away
 265             if _, err := os.Stdout.Write(s); err != nil {
 266                 t.Stop()
 267                 s = startChronoLine(s[:0], start, time.Now())
 268                 s = endChronoLine(s, start)
 269                 os.Stderr.Write(s)
 270                 return nil
 271             }
 272 
 273             s = startChronoLine(s[:0], start, time.Now())
 274             os.Stderr.Write(s)
 275 
 276         case line := <-errLines:
 277             if line == nil {
 278                 // filter out junk for needlessly-chatty pipes, while still
 279                 // keeping actual empty lines
 280                 continue
 281             }
 282 
 283             s = append(append(append(s[:0], clear...), line...), '\n')
 284             s = startChronoLine(s, start, time.Now())
 285             s = endChronoLine(s, start)
 286             os.Stderr.Write(s)
 287 
 288         case err := <-errors:
 289             if err == nil {
 290                 continue
 291             }
 292             os.Stderr.WriteString(clear)
 293             showError(err)
 294             s = startChronoLine(s[:0], start, time.Now())
 295             os.Stderr.Write(s)
 296 
 297         case <-quit:
 298             os.Stderr.Write(endChronoLine(s[:0], start))
 299             return exit{0}
 300 
 301         case <-stopped:
 302             t.Stop()
 303             os.Stderr.Write(endChronoLine(s[:0], start))
 304             return exit{forceQuitCode}
 305         }
 306     }
 307 }
 308 
 309 // readBytes is the binary-mode alternative to func readLines for a command's
 310 // standard output
 311 func readBytes(r io.Reader, chunks chan<- []byte) error {
 312     defer close(chunks)
 313 
 314     if r == nil {
 315         return nil
 316     }
 317 
 318     var buf [16 * 1024]byte
 319 
 320     for {
 321         n, err := r.Read(buf[:])
 322 
 323         if err == io.EOF {
 324             if n > 0 {
 325                 chunks <- buf[:n]
 326             }
 327             return nil
 328         }
 329 
 330         if err != nil {
 331             return err
 332         }
 333 
 334         if n < 1 {
 335             continue
 336         }
 337 
 338         chunks <- buf[:n]
 339     }
 340 }
 341 
 342 // readLines is run twice asynchronously, so that both stdin and stderr lines
 343 // are handled independently, which matters when running a subtask
 344 func readLines(r io.Reader, lines chan<- []byte) error {
 345     defer close(lines)
 346 
 347     // when not handling a subtask, this func will be called with a nil
 348     // reader, since without a subtask, there's no stderr to read from
 349     if r == nil {
 350         return nil
 351     }
 352 
 353     const gb = 1024 * 1024 * 1024
 354     sc := bufio.NewScanner(r)
 355     sc.Buffer(nil, 8*gb)
 356 
 357     for i := 0; sc.Scan(); i++ {
 358         s := sc.Bytes()
 359         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 360             s = s[3:]
 361         }
 362         lines <- s
 363     }
 364     return sc.Err()
 365 }
 366 
 367 func startChronoLine(buf []byte, start, now time.Time) []byte {
 368     dt := now.Sub(start)
 369     buf = time.Time{}.Add(dt).AppendFormat(buf, chronoFormat)
 370     buf = append(buf, gap...)
 371     buf = now.AppendFormat(buf, dateTimeFormat)
 372     return buf
 373 }
 374 
 375 func endChronoLine(buf []byte, start time.Time) []byte {
 376     secs := time.Since(start).Seconds()
 377     buf = append(buf, gap...)
 378     buf = strconv.AppendFloat(buf, secs, 'f', 4, 64)
 379     buf = append(buf, " seconds\n"...)
 380     return buf
 381 }
     File: ./tinytools.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package main
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "math"
  31     "os"
  32     "sort"
  33     "strconv"
  34     "strings"
  35     "time"
  36     "unicode/utf8"
  37 
  38     "./ngron"
  39     "./now"
  40 )
  41 
  42 func args() {
  43     args := os.Args[1:]
  44     if len(args) == 0 {
  45         return
  46     }
  47 
  48     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  49     defer w.Flush()
  50 
  51     for _, s := range args {
  52         w.WriteString(s)
  53         if w.WriteByte('\n') != nil {
  54             break
  55         }
  56     }
  57 }
  58 
  59 func clear() {
  60     if len(os.Args) > 1 {
  61         switch os.Args[1] {
  62         case `-h`, `--h`, `-help`, `--help`, `help`:
  63             os.Stdout.WriteString("Clear the screen\n")
  64             return
  65 
  66         case `-x`:
  67             // clear all but the scrollback buffer
  68             os.Stdout.WriteString("\x1b[H\x1b[2J")
  69             return
  70         }
  71     }
  72 
  73     os.Stdout.WriteString("\x1b[H\x1b[2J\x1b[3J")
  74 }
  75 
  76 func cls() {
  77     if len(os.Args) > 1 {
  78         switch os.Args[1] {
  79         case `-h`, `--h`, `-help`, `--help`, `help`:
  80             os.Stdout.WriteString("Clear the Screen\n")
  81             return
  82         }
  83     }
  84 
  85     os.Stdout.WriteString("\x1b[H\x1b[2J\x1b[3J")
  86 }
  87 
  88 const divInfo = `
  89 div [options...] [x...] [y]
  90 
  91 DIVide 2 numbers in 3 different ways, showing each result in its own line.
  92 
  93 When given 2 numbers, the results are x / y, y / x, and 1 - (x / y), where
  94 x is the smaller number, and y is the larger number.
  95 
  96 When given just 1 number, the second number is 1 by default, showing you the
  97 inverse of the number given explicitly, among the other results.
  98 
  99 The options are, available both in single and double-dash versions
 100 
 101     -h, -help    show this help message
 102 `
 103 
 104 func div() {
 105     args := os.Args[1:]
 106 
 107     if len(args) > 0 {
 108         switch args[0] {
 109         case `-h`, `--h`, `-help`, `--help`:
 110             os.Stdout.WriteString(divInfo[1:])
 111             return
 112         }
 113     }
 114 
 115     if len(args) > 0 && args[0] == `--` {
 116         args = args[1:]
 117     }
 118 
 119     var nums []float64
 120 
 121     for len(args) > 0 {
 122         f, err := strconv.ParseFloat(args[0], 64)
 123         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 124             nums = append(nums, f)
 125             args = args[1:]
 126             continue
 127         }
 128 
 129         break
 130     }
 131 
 132     switch len(nums) {
 133     case 0:
 134         os.Stderr.WriteString(divInfo[1:])
 135         os.Exit(1)
 136         return
 137 
 138     case 1:
 139         nums = append(nums, nums[0])
 140         nums[0] = 1
 141     }
 142 
 143     if len(nums)%2 != 0 {
 144         os.Stderr.WriteString(divInfo[1:])
 145         os.Exit(1)
 146         return
 147     }
 148 
 149     for len(nums) >= 2 {
 150         x, y := nums[0], nums[1]
 151         nums = nums[2:]
 152 
 153         if x > y {
 154             x, y = y, x
 155         }
 156         comp := 1 - (x / y)
 157 
 158         const prec = 6
 159         os.Stdout.WriteString(strconv.FormatFloat(x/y, 'f', prec, 64) + "\n")
 160         os.Stdout.WriteString(strconv.FormatFloat(y/x, 'f', prec, 64) + "\n")
 161         os.Stdout.WriteString(strconv.FormatFloat(comp, 'f', prec, 64) + "\n")
 162     }
 163 }
 164 
 165 func echo() {
 166     args := os.Args[1:]
 167     if len(args) == 0 {
 168         os.Stdout.WriteString("\n")
 169         return
 170     }
 171 
 172     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 173     defer w.Flush()
 174 
 175     for i, s := range args {
 176         if i > 0 {
 177             if w.WriteByte(' ') != nil {
 178                 return
 179             }
 180         }
 181         w.WriteString(s)
 182     }
 183 
 184     w.WriteByte('\n')
 185 }
 186 
 187 func echobar() {
 188     const (
 189         half   = `                                        `
 190         spaces = half + half
 191     )
 192 
 193     args := os.Args[1:]
 194     if len(args) == 0 {
 195         os.Stdout.WriteString("\x1b[7m" + spaces + "\x1b[0m\n")
 196         return
 197     }
 198 
 199     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 200     defer w.Flush()
 201 
 202     w.WriteString("\x1b[7m")
 203 
 204     left := len(spaces)
 205 
 206     for i, s := range args {
 207         if i > 0 {
 208             if w.WriteByte(' ') != nil {
 209                 return
 210             }
 211             left--
 212         }
 213         w.WriteString(s)
 214         left -= utf8.RuneCountInString(s)
 215     }
 216 
 217     if 0 < left && left < len(spaces) {
 218         w.WriteString(spaces[:left])
 219     }
 220     w.WriteString("\x1b[0m\n")
 221 }
 222 
 223 const falseInfo = `
 224 false [options...]
 225 
 226 Quit right away, using exit code 1.
 227 
 228 All (optional) leading options start with either single or double-dash:
 229 
 230     -h, -help    show this help message
 231 `
 232 
 233 func falseMain() {
 234     if len(os.Args) > 1 {
 235         switch os.Args[1] {
 236         case `-h`, `--h`, `-help`, `--help`, `help`:
 237             os.Stdout.WriteString(falseInfo[1:])
 238             return
 239         }
 240     }
 241 
 242     os.Exit(1)
 243     return
 244 }
 245 
 246 func hecho() {
 247     args := os.Args[1:]
 248     if len(args) == 0 {
 249         os.Stdout.WriteString("\n")
 250         return
 251     }
 252 
 253     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 254     defer w.Flush()
 255 
 256     w.WriteString("\x1b[7m")
 257 
 258     for i, s := range args {
 259         if i > 0 {
 260             if err := w.WriteByte(' '); err != nil {
 261                 return
 262             }
 263         }
 264         w.WriteString(s)
 265     }
 266 
 267     w.WriteString("\x1b[0m\n")
 268 }
 269 
 270 const mopInfo = `
 271 mop [options...] [x...] [y]
 272 
 273 Multiple OPerations runs multiple arithmetic calculations on 2 numbers,
 274 showing each result in its own line.
 275 
 276 The options are, available both in single and double-dash versions
 277 
 278     -h, -help    show this help message
 279 `
 280 
 281 func mop() {
 282     args := os.Args[1:]
 283 
 284     if len(args) > 0 {
 285         switch args[0] {
 286         case `-h`, `--h`, `-help`, `--help`:
 287             os.Stdout.WriteString(mopInfo[1:])
 288             return
 289         }
 290     }
 291 
 292     if len(args) > 0 && args[0] == `--` {
 293         args = args[1:]
 294     }
 295 
 296     var nums []float64
 297 
 298     for len(args) > 0 {
 299         f, err := strconv.ParseFloat(args[0], 64)
 300         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 301             nums = append(nums, f)
 302             args = args[1:]
 303             continue
 304         }
 305 
 306         break
 307     }
 308 
 309     if len(nums) == 0 || len(nums)%2 != 0 {
 310         os.Stderr.WriteString(mopInfo[1:])
 311         os.Exit(1)
 312         return
 313     }
 314 
 315     for len(nums) >= 2 {
 316         x, y := nums[0], nums[1]
 317         nums = nums[2:]
 318 
 319         if x > y {
 320             x, y = y, x
 321         }
 322 
 323         fmt.Printf("%.6f + %.6f  ~  %.6f\n", x, y, x+y)
 324         fmt.Printf("%.6f - %.6f  ~  %.6f\n", x, y, x-y)
 325         fmt.Printf("%.6f * %.6f  ~  %.6f\n", x, y, x*y)
 326         fmt.Printf("%.6f / %.6f  ~  %.6f\n", x, y, x/y)
 327         fmt.Printf("%.6f / %.6f  ~  %.6f\n", y, x, y/x)
 328         fmt.Printf("%.6f %% %.6f  ~  %.6f\n", x, y, math.Mod(x, y))
 329         fmt.Printf("%.6f %% %.6f  ~  %.6f\n", y, x, math.Mod(y, x))
 330         fmt.Printf("%.6f ** %.6f  ~  %.6f\n", x, y, math.Pow(x, y))
 331         fmt.Printf("%.6f ** %.6f  ~  %.6f\n", y, x, math.Pow(y, x))
 332     }
 333 }
 334 
 335 func nothing() {
 336     // deliberately do nothing
 337 }
 338 
 339 const rulerInfo = `
 340 ruler [width...]
 341 
 342 Emit a line with a ruler-like pattern, which helps check the width of output
 343 lines right above it. If given a valid number, it's used as the ruler width,
 344 or 80 is used as the default width.
 345 
 346 The options are, available both in single and double-dash versions
 347 
 348     -h, -help    show this help message
 349 `
 350 
 351 func ruler() {
 352     args := os.Args[1:]
 353 
 354     if len(args) > 0 {
 355         switch args[0] {
 356         case `-h`, `--h`, `-help`, `--help`, `help`:
 357             os.Stdout.WriteString(rulerInfo[1:])
 358             return
 359         }
 360     }
 361 
 362     if len(args) > 0 && args[0] == `--` {
 363         args = args[1:]
 364     }
 365 
 366     width := 80
 367     if len(args) > 0 {
 368         if n, err := strconv.Atoi(args[0]); err == nil {
 369             width = n
 370         }
 371     }
 372 
 373     // avoid a single line-feed byte for empty rulers
 374     if width < 1 {
 375         return
 376     }
 377 
 378     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 379     defer bw.Flush()
 380 
 381     for width >= 10 {
 382         if _, err := bw.WriteString(`····╵····│`); err != nil {
 383             return
 384         }
 385         width -= 10
 386     }
 387 
 388     if width >= 5 {
 389         bw.WriteString(`····╵`)
 390         width -= 5
 391     }
 392 
 393     for width > 0 {
 394         bw.WriteRune('·')
 395         width--
 396     }
 397 
 398     bw.WriteByte('\n')
 399 }
 400 
 401 const sleepInfo = `
 402 sleep [options...] [duration/seconds...]
 403 
 404 Wait for the amount of time given.
 405 
 406 All (optional) leading options start with either single or double-dash:
 407 
 408     -h, -help             show this help message
 409 `
 410 
 411 func sleep() {
 412     args := os.Args[1:]
 413 
 414     if len(args) > 0 {
 415         switch args[0] {
 416         case `-h`, `--h`, `-help`, `--help`, `help`:
 417             os.Stdout.WriteString(sleepInfo[1:])
 418             return
 419         }
 420     }
 421 
 422     if len(args) > 0 && args[0] == `--` {
 423         args = args[1:]
 424     }
 425 
 426     if len(args) == 0 {
 427         os.Stderr.WriteString(sleepInfo[1:])
 428         // os.Stderr.WriteString("forgot the duration to wait for\n")
 429         os.Exit(1)
 430         return
 431     }
 432 
 433     var delay time.Duration
 434 
 435     for _, s := range args {
 436         f, err := strconv.ParseFloat(s, 64)
 437         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 438             if f < 0 {
 439                 f = 0
 440             }
 441             delay += time.Duration(f * float64(time.Second))
 442             continue
 443         }
 444 
 445         if d, err := time.ParseDuration(s); err == nil {
 446             delay += d
 447             continue
 448         }
 449 
 450         os.Stderr.WriteString("invalid duration " + s)
 451         os.Stderr.WriteString("\n")
 452         os.Exit(1)
 453         return
 454     }
 455 
 456     time.Sleep(delay)
 457 }
 458 
 459 const timezonesInfo = `
 460 timezones [options...] [places...]
 461 
 462 Lookup full timezone names from the city/place names given.
 463 
 464 All (optional) leading options start with either single or double-dash:
 465 
 466     -h, -help             show this help message
 467 `
 468 
 469 func timezones() {
 470     args := os.Args[1:]
 471 
 472     if len(args) > 0 {
 473         switch args[0] {
 474         case `-h`, `--h`, `-help`, `--help`, `help`:
 475             os.Stdout.WriteString(timezonesInfo[1:])
 476             return
 477         }
 478     }
 479 
 480     if len(args) > 0 && args[0] == `--` {
 481         args = args[1:]
 482     }
 483 
 484     if len(args) == 0 {
 485         os.Stderr.WriteString(timezonesInfo[1:])
 486         return
 487     }
 488 
 489     nerr := 0
 490     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 491     defer w.Flush()
 492 
 493     for _, place := range args {
 494         name, ok := now.LookupName(place)
 495 
 496         if !ok {
 497             w.Flush()
 498             fmt.Fprintf(os.Stderr, "timezone for %q not found\n", place)
 499             nerr++
 500             continue
 501         }
 502 
 503         w.WriteString(name)
 504         w.WriteByte('\n')
 505     }
 506 
 507     if nerr > 0 {
 508         w.Flush()
 509         os.Exit(1)
 510         return
 511     }
 512 }
 513 
 514 const toolsInfo = `
 515 tools [options...]
 516 
 517 Show all tools (officially) available.
 518 
 519 All (optional) leading options start with either single or double-dash:
 520 
 521     -a, -aliases, -all    also show all aliases available, after the tools
 522     -h, -help             show this help message
 523 `
 524 
 525 func tools() {
 526     showAliases := false
 527     if len(os.Args) > 1 {
 528         switch os.Args[1] {
 529         case `-a`, `--a`, `-aliases`, `--aliases`, `-all`, `--all`:
 530             showAliases = true
 531 
 532         case `-h`, `--h`, `-help`, `--help`, `help`:
 533             os.Stdout.WriteString(toolsInfo[1:])
 534             return
 535         }
 536     }
 537 
 538     n := len(mains)
 539     if n < len(aliases) {
 540         n = len(aliases)
 541     }
 542 
 543     names := make([]string, 0, n)
 544     for k := range mains {
 545         names = append(names, k)
 546     }
 547 
 548     sort.Strings(names)
 549 
 550     for _, s := range names {
 551         fmt.Fprintln(os.Stdout, s)
 552     }
 553 
 554     if !showAliases {
 555         return
 556     }
 557 
 558     names = names[:0]
 559     for k := range aliases {
 560         names = append(names, k)
 561     }
 562 
 563     sort.Strings(names)
 564 
 565     for _, s := range names {
 566         fmt.Fprintf(os.Stdout, "%s -> %s\n", s, aliases[s])
 567     }
 568 }
 569 
 570 const trueInfo = `
 571 true [options...]
 572 
 573 Quit right away successfully, using exit code 0.
 574 
 575 All (optional) leading options start with either single or double-dash:
 576 
 577     -h, -help    show this help message
 578 `
 579 
 580 func trueMain() {
 581     if len(os.Args) > 1 {
 582         switch os.Args[1] {
 583         case `-h`, `--h`, `-help`, `--help`, `help`:
 584             os.Stdout.WriteString(trueInfo[1:])
 585             return
 586         }
 587     }
 588 }
 589 
 590 const ungronInfo = `
 591 ungron [options...]
 592 
 593 Turn GRON-style lines into valid JSON. This is the same as directly running
 594 the 'ngron' tool with the '-u' option.
 595 
 596 All (optional) leading options start with either single or double-dash:
 597 
 598     -h, -help    show this help message
 599 `
 600 
 601 func ungronMain() {
 602     args := os.Args[1:]
 603 
 604     for _, s := range args {
 605         switch s {
 606         case `-h`, `--h`, `-help`, `--help`, `help`:
 607             os.Stdout.WriteString(ungronInfo[1:])
 608             return
 609         }
 610     }
 611 
 612     gronArgs := make([]string, 0, len(os.Args)+1)
 613     gronArgs = append(gronArgs, os.Args[0])
 614     gronArgs = append(gronArgs, `-u`)
 615     gronArgs = append(gronArgs, args...)
 616     os.Args = gronArgs
 617     ngron.Main()
 618 }
 619 
 620 const yesInfo = `
 621 yes [options...] [message...]
 622 
 623 Keep emitting the line with the message given, or "y" (short for "yes") by
 624 default.
 625 
 626 All (optional) leading options start with either single or double-dash:
 627 
 628     -h, -help    show this help message
 629 `
 630 
 631 func yes() {
 632     args := os.Args[1:]
 633 
 634     if len(args) > 0 {
 635         switch args[0] {
 636         case `-h`, `--h`, `-help`, `--help`, `help`:
 637             os.Stdout.WriteString(yesInfo[1:])
 638             return
 639         }
 640     }
 641 
 642     if len(args) > 0 && args[0] == `--` {
 643         args = args[1:]
 644     }
 645 
 646     msg := `y`
 647     if len(args) > 0 {
 648         msg = strings.Join(args, ` `)
 649     }
 650 
 651     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 652     defer w.Flush()
 653 
 654     for {
 655         w.WriteString(msg)
 656         if w.WriteByte('\n') != nil {
 657             break
 658         }
 659     }
 660 }
     File: ./tolower/tolower.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package tolower
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "unicode"
  34     "unicode/utf8"
  35 )
  36 
  37 const info = `
  38 tolower [options...] [files...]
  39 
  40 Turn all uppercase letters into lowercase ones.
  41 
  42 All (optional) leading options start with either single or double-dash:
  43 
  44     -h, -help    show this help message
  45 `
  46 
  47 func Main() {
  48     buffered := false
  49     args := os.Args[1:]
  50 
  51     if len(args) > 0 {
  52         switch args[0] {
  53         case `-b`, `--b`, `-buffered`, `--buffered`:
  54             buffered = true
  55             args = args[1:]
  56 
  57         case `-h`, `--h`, `-help`, `--help`:
  58             os.Stdout.WriteString(info[1:])
  59             return
  60         }
  61     }
  62 
  63     if len(args) > 0 && args[0] == `--` {
  64         args = args[1:]
  65     }
  66 
  67     liveLines := !buffered
  68     if !buffered {
  69         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  70             liveLines = false
  71         }
  72     }
  73 
  74     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  75         os.Stderr.WriteString(`tolower: `)
  76         os.Stderr.WriteString(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79         return
  80     }
  81 }
  82 
  83 func run(w io.Writer, args []string, live bool) error {
  84     bw := bufio.NewWriterSize(w, 32*1024)
  85     defer bw.Flush()
  86 
  87     if len(args) == 0 {
  88         return toLower(bw, os.Stdin, live)
  89     }
  90 
  91     for _, name := range args {
  92         if err := handleFile(bw, name, live); err != nil {
  93             return err
  94         }
  95     }
  96     return nil
  97 }
  98 
  99 func handleFile(w *bufio.Writer, name string, live bool) error {
 100     if name == `` || name == `-` {
 101         return toLower(w, os.Stdin, live)
 102     }
 103 
 104     f, err := os.Open(name)
 105     if err != nil {
 106         return errors.New(`can't read from file named "` + name + `"`)
 107     }
 108     defer f.Close()
 109 
 110     return toLower(w, f, live)
 111 }
 112 
 113 func toLower(w *bufio.Writer, r io.Reader, live bool) error {
 114     const gb = 1024 * 1024 * 1024
 115     sc := bufio.NewScanner(r)
 116     sc.Buffer(nil, 8*gb)
 117 
 118     for i := 0; sc.Scan(); i++ {
 119         s := sc.Bytes()
 120         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 121             s = s[3:]
 122         }
 123 
 124         if needsLowercasing(s) {
 125             writeLowercase(w, s)
 126         } else {
 127             w.Write(s)
 128         }
 129 
 130         if w.WriteByte('\n') != nil {
 131             return io.EOF
 132         }
 133 
 134         if live && w.Flush() != nil {
 135             return io.EOF
 136         }
 137     }
 138 
 139     return sc.Err()
 140 }
 141 
 142 func needsLowercasing(src []byte) bool {
 143     for _, b := range src {
 144         if b > 127 {
 145             return true
 146         }
 147         if 'A' <= b && b <= 'Z' {
 148             return true
 149         }
 150     }
 151 
 152     return false
 153 }
 154 
 155 func writeLowercase(w *bufio.Writer, src []byte) {
 156     for len(src) > 0 {
 157         r, size := utf8.DecodeRune(src)
 158         w.WriteRune(unicode.ToLower(r))
 159         src = src[size:]
 160     }
 161 }
     File: ./transpose/transpose.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package transpose
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strings"
  32 )
  33 
  34 const info = `
  35 transpose [options...] [files...]
  36 
  37 Switch rows and columns from tables. Output is always TSV (tab-separated
  38 values) lines. Input lines are auto-detected as either space(s)-separated
  39 fields or TSV by default, but there's an option to force splitting by tabs,
  40 should TSV-auto-detection fail for some inputs.
  41 
  42 All (optional) leading options start with either single or double-dash:
  43 
  44     -h, -help    show this help message
  45     -t, -tsv     split lines by tabs, no matter the first input line
  46 `
  47 
  48 func Main() {
  49     tsv := false
  50     args := os.Args[1:]
  51 
  52     if len(args) > 0 {
  53         switch args[0] {
  54         case `-h`, `--h`, `-help`, `--help`:
  55             os.Stdout.WriteString(info[1:])
  56             return
  57 
  58         case `-t`, `--t`, `-tsv`, `--tsv`:
  59             tsv = true
  60             args = args[1:]
  61         }
  62     }
  63 
  64     if len(args) > 0 && args[0] == `--` {
  65         args = args[1:]
  66     }
  67 
  68     if err := run(args, tsv); err != nil && err != io.EOF {
  69         os.Stderr.WriteString(`transpose: `)
  70         os.Stderr.WriteString(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73         return
  74     }
  75 }
  76 
  77 type table struct {
  78     // maxWidth is the highest item-count in any row read
  79     maxWidth int
  80 
  81     // rows remembers all split items for later transposition
  82     rows [][]string
  83 
  84     // split is the func to split items from input lines
  85     split func(s string) []string
  86 }
  87 
  88 func run(paths []string, tsv bool) error {
  89     var t table
  90     if tsv {
  91         t.split = splitTSV
  92     }
  93 
  94     // gather items
  95 
  96     for _, path := range paths {
  97         if err := handleFile(path, &t); err != nil {
  98             return err
  99         }
 100     }
 101 
 102     if len(paths) == 0 {
 103         if err := handleReader(os.Stdin, &t); err != nil {
 104             return err
 105         }
 106     }
 107 
 108     // show transpose
 109 
 110     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 111     defer w.Flush()
 112 
 113     for i := 0; i < t.maxWidth; i++ {
 114         for j, row := range t.rows {
 115             if j > 0 {
 116                 w.WriteByte('\t')
 117             }
 118             if i < len(row) {
 119                 w.WriteString(row[i])
 120             }
 121         }
 122 
 123         if w.WriteByte('\n') != nil {
 124             return io.EOF
 125         }
 126     }
 127 
 128     return nil
 129 }
 130 
 131 func handleFile(path string, t *table) error {
 132     f, err := os.Open(path)
 133     if err != nil {
 134         return err
 135     }
 136     defer f.Close()
 137     return handleReader(f, t)
 138 }
 139 
 140 func handleReader(r io.Reader, t *table) error {
 141     const gb = 1024 * 1024 * 1024
 142     sc := bufio.NewScanner(r)
 143     sc.Buffer(nil, 8*gb)
 144 
 145     for i := 0; sc.Scan(); i++ {
 146         s := sc.Text()
 147         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 148             s = s[3:]
 149         }
 150 
 151         if t.split == nil {
 152             if strings.IndexByte(s, '\t') >= 0 {
 153                 t.split = splitTSV
 154             } else {
 155                 t.split = strings.Fields
 156             }
 157         }
 158 
 159         items := t.split(s)
 160         if t.maxWidth < len(items) {
 161             t.maxWidth = len(items)
 162         }
 163         t.rows = append(t.rows, items)
 164     }
 165 
 166     return sc.Err()
 167 }
 168 
 169 func splitTSV(s string) []string {
 170     if len(s) == 0 {
 171         return nil
 172     }
 173 
 174     n := count(s, '\t') + 1
 175     tsv := make([]string, 0, n)
 176 
 177     for len(s) > 0 {
 178         i := strings.IndexByte(s, '\t')
 179         if i < 0 {
 180             tsv = append(tsv, s)
 181             break
 182         }
 183         tsv = append(tsv, s[:i])
 184         s = s[i+1:]
 185     }
 186 
 187     return tsv
 188 }
 189 
 190 func count(src string, what byte) int {
 191     got := 0
 192     for i := range src {
 193         if src[i] == what {
 194             got++
 195         }
 196     }
 197     return got
 198 }
     File: ./underline/underline.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package underline
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34 )
  35 
  36 const info = `
  37 underline [options...] [every...] [files...]
  38 
  39 Underline every nth line, or every 5th line by default.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help            show this help message
  44     -header, -t, -top    start by underlining the first line instead
  45 `
  46 
  47 type config struct {
  48     n         int
  49     every     int
  50     top       bool
  51     liveLines bool
  52 }
  53 
  54 func Main() {
  55     var cfg config
  56     cfg.every = 5
  57     cfg.liveLines = true
  58     args := os.Args[1:]
  59 
  60     for len(args) > 0 {
  61         switch args[0] {
  62         case `-b`, `--b`, `-buffered`, `--buffered`:
  63             cfg.liveLines = false
  64             args = args[1:]
  65             continue
  66 
  67         case `-h`, `--h`, `-help`, `--help`:
  68             os.Stdout.WriteString(info[1:])
  69             return
  70 
  71         case `-header`, `--header`, `-t`, `--t`, `-top`, `--top`:
  72             cfg.top = true
  73             args = args[1:]
  74             continue
  75         }
  76 
  77         break
  78     }
  79 
  80     if len(args) > 0 {
  81         if n, err := strconv.ParseUint(args[0], 10, 64); err == nil {
  82             cfg.every = int(n)
  83             args = args[1:]
  84         }
  85     }
  86 
  87     if len(args) > 0 && args[0] == `--` {
  88         args = args[1:]
  89     }
  90 
  91     if cfg.liveLines {
  92         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  93             cfg.liveLines = false
  94         }
  95     }
  96 
  97     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  98         os.Stderr.WriteString(`underline: `)
  99         os.Stderr.WriteString(err.Error())
 100         os.Stderr.WriteString("\n")
 101         os.Exit(1)
 102         return
 103     }
 104 }
 105 
 106 func run(w io.Writer, args []string, cfg config) error {
 107     bw := bufio.NewWriterSize(w, 32*1024)
 108     defer bw.Flush()
 109 
 110     dashes := 0
 111     for _, name := range args {
 112         if name == `-` {
 113             dashes++
 114         }
 115         if dashes > 1 {
 116             return errors.New(`can't read stdin (dash) more than once`)
 117         }
 118     }
 119 
 120     if len(args) == 0 {
 121         return handleReader(bw, os.Stdin, &cfg)
 122     }
 123 
 124     for _, name := range args {
 125         if name == `-` {
 126             if err := handleReader(bw, os.Stdin, &cfg); err != nil {
 127                 return err
 128             }
 129             continue
 130         }
 131 
 132         if err := handleFile(bw, name, &cfg); err != nil {
 133             return err
 134         }
 135     }
 136     return nil
 137 }
 138 
 139 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 140     if name == `` || name == `-` {
 141         return handleReader(w, os.Stdin, cfg)
 142     }
 143 
 144     f, err := os.Open(name)
 145     if err != nil {
 146         return errors.New(`can't read from file named "` + name + `"`)
 147     }
 148     defer f.Close()
 149 
 150     return handleReader(w, f, cfg)
 151 }
 152 
 153 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 154     const gb = 1024 * 1024 * 1024
 155     sc := bufio.NewScanner(r)
 156     sc.Buffer(nil, 8*gb)
 157 
 158     every := cfg.every
 159 
 160     for i := 0; sc.Scan(); i++ {
 161         s := sc.Bytes()
 162         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 163             s = s[3:]
 164         }
 165 
 166         cfg.n++
 167         var u bool
 168         if cfg.top {
 169             u = every > 0 && (cfg.n == 1 || ((cfg.n-1)%every == 0))
 170         } else {
 171             u = (every > 0 && cfg.n%every == 0 && cfg.n != 1) || every == 1
 172         }
 173 
 174         if u {
 175             if underline(w, s) != nil {
 176                 return io.EOF
 177             }
 178         } else {
 179             w.Write(s)
 180             if w.WriteByte('\n') != nil {
 181                 return io.EOF
 182             }
 183         }
 184 
 185         if cfg.liveLines && w.Flush() != nil {
 186             return io.EOF
 187         }
 188     }
 189 
 190     return sc.Err()
 191 }
 192 
 193 func underline(w *bufio.Writer, s []byte) error {
 194     w.WriteString("\x1b[4m")
 195     for len(s) > 0 {
 196         i := bytes.Index(s, []byte("\x1b[0m"))
 197         if i < 0 {
 198             break
 199         }
 200 
 201         j := i + len("\x1b[0m")
 202         w.Write(s[:j])
 203         w.WriteString("\x1b[4m")
 204         s = s[j:]
 205     }
 206 
 207     if len(s) > 0 {
 208         w.Write(s)
 209     }
 210 
 211     if _, err := w.WriteString("\x1b[0m\n"); err != nil {
 212         return io.EOF
 213     }
 214     return nil
 215 }
     File: ./units/units.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package units
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "io"
  31     "math"
  32     "os"
  33     "sort"
  34     "strconv"
  35     "strings"
  36 )
  37 
  38 const info = `
  39 units [options...] [quantities / source units...]
  40 
  41 Convert quantities from weird units into equivalent better ones, usually from
  42 the international systems of measurements: think kilometers instead of miles.
  43 
  44 All (optional) leading options start with either single or double-dash:
  45 
  46     -h, -help    show this help message
  47 `
  48 
  49 func Main() {
  50     args := os.Args[1:]
  51     if len(args) > 0 {
  52         switch args[0] {
  53         case `-h`, `--h`, `-help`, `--help`:
  54             os.Stdout.WriteString(info[1:])
  55             return
  56         }
  57     }
  58 
  59     if len(args) > 0 && args[0] == `--` {
  60         args = args[1:]
  61     }
  62 
  63     if err := run(os.Stdout, args); err != nil {
  64         os.Stderr.WriteString(`units: `)
  65         os.Stderr.WriteString(err.Error())
  66         os.Stderr.WriteString("\n")
  67         os.Exit(1)
  68         return
  69     }
  70 }
  71 
  72 func run(w io.Writer, args []string) error {
  73     bw := bufio.NewWriterSize(w, 32*1024)
  74     defer bw.Flush()
  75 
  76     if len(args) == 0 {
  77         dumpAll(bw, []float64{1})
  78         return nil
  79     }
  80 
  81     from := ``
  82     var values []float64
  83 
  84     for _, s := range args {
  85         f, err := strconv.ParseFloat(s, 64)
  86         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
  87             values = append(values, f)
  88             continue
  89         }
  90 
  91         if len(values) == 0 {
  92             values = append(values, 1)
  93         }
  94         from = strings.ToLower(s)
  95 
  96         if dump(bw, values, from) {
  97             values = values[:0]
  98             from = ``
  99             continue
 100         }
 101 
 102         return fmt.Errorf("unit %q not supported", from)
 103     }
 104 
 105     if len(values) > 0 {
 106         dumpAll(bw, values)
 107         return nil
 108     }
 109 
 110     if from != `` && !dump(bw, values, from) {
 111         return fmt.Errorf("unit %q not supported", from)
 112     }
 113     return nil
 114 }
 115 
 116 func dump(w *bufio.Writer, values []float64, unit string) bool {
 117     if s, ok := aliases[unit]; ok {
 118         unit = s
 119     }
 120 
 121     c, ok := mainConverters[unit]
 122     if !ok {
 123         c, ok = otherConverters[unit]
 124         if !ok {
 125             return false
 126         }
 127     }
 128 
 129     const fs = "%.4f  %-4s =  %.4f  %s\n"
 130 
 131     if len(values) == 0 {
 132         res := c.Mul + c.Add
 133         fmt.Fprintf(w, fs, 1.0, unit, res, c.To)
 134         return true
 135     }
 136 
 137     for _, v := range values {
 138         res := c.Mul*v + c.Add
 139         fmt.Fprintf(w, fs, v, unit, res, c.To)
 140     }
 141     return true
 142 }
 143 
 144 func dumpAll(w *bufio.Writer, values []float64) bool {
 145     if len(values) == 0 {
 146         values = []float64{1}
 147     }
 148 
 149     units := make([]string, 0, len(mainConverters))
 150     for k := range mainConverters {
 151         units = append(units, k)
 152     }
 153     sort.Strings(units)
 154 
 155     for _, from := range units {
 156         dump(w, values, from)
 157     }
 158     return true
 159 }
 160 
 161 var aliases = map[string]string{
 162     `acre`:    `ac`,
 163     `acres`:   `ac`,
 164     `days`:    `day`,
 165     `foot`:    `ft`,
 166     `feet`:    `ft`,
 167     `feet2`:   `ft²`,
 168     `feet3`:   `ft³`,
 169     `foot2`:   `ft²`,
 170     `foot3`:   `ft³`,
 171     `ft2`:     `ft²`,
 172     `ft3`:     `ft³`,
 173     `gallon`:  `gal`,
 174     `gallons`: `gal`,
 175     `gals`:    `gal`,
 176     `inch`:    `in`,
 177     `inches`:  `in`,
 178     `mile`:    `mi`,
 179     `miles`:   `mi`,
 180     `mile²`:   `mi²`,
 181     `miles²`:  `mi²`,
 182     `minute`:  `min`,
 183     `minutes`: `min`,
 184     `nmile`:   `nmi`,
 185     `nmiles`:  `nmi`,
 186     `ounce`:   `oz`,
 187     `ounces`:  `oz`,
 188     `ozs`:     `oz`,
 189     `weeks`:   `week`,
 190     `wk`:      `week`,
 191     `wks`:     `week`,
 192     `yard`:    `yd`,
 193     `yards`:   `yd`,
 194     `yard2`:   `yd²`,
 195     `yard²`:   `yd²`,
 196     `yards2`:  `yd²`,
 197     `yards²`:  `yd²`,
 198     `yds`:     `yd`,
 199     `yds2`:    `yd²`,
 200     `yds²`:    `yd²`,
 201 }
 202 
 203 type converter struct {
 204     To  string
 205     Mul float64
 206     Add float64
 207 }
 208 
 209 var mainConverters = map[string]converter{
 210     `ft`:  converter{`m`, 0.3048, 0},
 211     `gal`: converter{`L`, 3.785411784, 0},
 212     `in`:  converter{`cm`, 2.54, 0},
 213     `mi`:  converter{`km`, 1.609344, 0},
 214     `mpg`: converter{`kpl`, 0.425143707, 0},
 215     `mph`: converter{`kph`, 1.609344, 0},
 216     `nmi`: converter{`km`, 1.852, 0},
 217     `oz`:  converter{`g`, 28.349523125, 0},
 218     `yd`:  converter{`m`, 0.9144, 0},
 219 }
 220 
 221 var otherConverters = map[string]converter{
 222     `ac`:   converter{`m²`, 4046.8564224, 0},
 223     `day`:  converter{`s`, 24 * 60 * 60, 0},
 224     `ft²`:  converter{`m²`, 0.09290304, 0},
 225     `ft³`:  converter{`m³`, 0.028316846592, 0},
 226     `mi²`:  converter{`km²`, 2.5899881103360, 0},
 227     `min`:  converter{`s`, 60, 0},
 228     `psi`:  converter{`Pa`, 6894.757293168, 0},
 229     `week`: converter{`s`, 7 * 24 * 60 * 60, 0},
 230     `yd²`:  converter{`m²`, 0.83612736, 0},
 231 }
     File: ./utfate/utfate.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package utfate
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "encoding/binary"
  31     "errors"
  32     "io"
  33     "os"
  34     "unicode"
  35     "unicode/utf16"
  36 )
  37 
  38 const info = `
  39 utfate [options...] [file...]
  40 
  41 This app turns plain-text input into UTF-8. Supported input formats are
  42 
  43     - ASCII
  44     - UTF-8
  45     - UTF-8 with a leading BOM
  46     - UTF-16 BE
  47     - UTF-16 LE
  48     - UTF-32 BE
  49     - UTF-32 LE
  50 
  51 All (optional) leading options start with either single or double-dash:
  52 
  53     -h, -help    show this help message
  54 `
  55 
  56 func Main() {
  57     if len(os.Args) > 1 {
  58         switch os.Args[1] {
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62         }
  63     }
  64 
  65     if err := run(os.Stdout, os.Args[1:]); err != nil && err != io.EOF {
  66         os.Stderr.WriteString(`utfate: `)
  67         os.Stderr.WriteString(err.Error())
  68         os.Stderr.WriteString("\n")
  69         os.Exit(1)
  70         return
  71     }
  72 }
  73 
  74 func run(w io.Writer, args []string) error {
  75     bw := bufio.NewWriterSize(w, 32*1024)
  76     defer bw.Flush()
  77 
  78     for _, path := range args {
  79         if err := handleFile(bw, path); err != nil {
  80             return err
  81         }
  82     }
  83 
  84     if len(args) == 0 {
  85         return utfate(bw, os.Stdin)
  86     }
  87     return nil
  88 }
  89 
  90 func handleFile(w *bufio.Writer, name string) error {
  91     if name == `-` {
  92         return utfate(w, os.Stdin)
  93     }
  94 
  95     f, err := os.Open(name)
  96     if err != nil {
  97         return errors.New(`can't read from file named "` + name + `"`)
  98     }
  99     defer f.Close()
 100 
 101     return utfate(w, f)
 102 }
 103 
 104 func utfate(w io.Writer, r io.Reader) error {
 105     br := bufio.NewReader(r)
 106     bw := bufio.NewWriterSize(w, 32*1024)
 107     defer bw.Flush()
 108 
 109     lead, err := br.Peek(4)
 110     if err != nil && err != io.EOF {
 111         return err
 112     }
 113 
 114     if bytes.HasPrefix(lead, []byte{'\x00', '\x00', '\xfe', '\xff'}) {
 115         br.Discard(4)
 116         return utf32toUTF8(bw, br, binary.BigEndian)
 117     }
 118 
 119     if bytes.HasPrefix(lead, []byte{'\xff', '\xfe', '\x00', '\x00'}) {
 120         br.Discard(4)
 121         return utf32toUTF8(bw, br, binary.LittleEndian)
 122     }
 123 
 124     if bytes.HasPrefix(lead, []byte{'\xfe', '\xff'}) {
 125         br.Discard(2)
 126         return utf16toUTF8(bw, br, readBytePairBE)
 127     }
 128 
 129     if bytes.HasPrefix(lead, []byte{'\xff', '\xfe'}) {
 130         br.Discard(2)
 131         return utf16toUTF8(bw, br, readBytePairLE)
 132     }
 133 
 134     if bytes.HasPrefix(lead, []byte{'\xef', '\xbb', '\xbf'}) {
 135         br.Discard(3)
 136         return handleUTF8(bw, br)
 137     }
 138 
 139     return handleUTF8(bw, br)
 140 }
 141 
 142 func handleUTF8(w *bufio.Writer, r *bufio.Reader) error {
 143     for {
 144         c, _, err := r.ReadRune()
 145         if c == unicode.ReplacementChar {
 146             return errors.New(`invalid UTF-8 stream`)
 147         }
 148         if err == io.EOF {
 149             return nil
 150         }
 151         if err != nil {
 152             return err
 153         }
 154 
 155         if _, err := w.WriteRune(c); err != nil {
 156             return io.EOF
 157         }
 158     }
 159 }
 160 
 161 // fancyHandleUTF8 is kept only for reference, as its attempts at being clever
 162 // don't seem to speed things up much when given ASCII input
 163 func fancyHandleUTF8(w *bufio.Writer, r *bufio.Reader) error {
 164     lookahead := 1
 165     maxAhead := r.Size() / 2
 166 
 167     for {
 168         // look ahead to check for ASCII runs
 169         ahead, err := r.Peek(lookahead)
 170         if err == io.EOF {
 171             return nil
 172         }
 173         if err != nil {
 174             return err
 175         }
 176 
 177         // copy leading ASCII runs
 178         n := leadASCII(ahead)
 179         if n > 0 {
 180             w.Write(ahead[:n])
 181             r.Discard(n)
 182         }
 183 
 184         // adapt lookahead size
 185         if n == len(ahead) && lookahead < maxAhead {
 186             lookahead *= 2
 187         } else if lookahead > 1 {
 188             lookahead /= 2
 189         }
 190 
 191         if n == len(ahead) {
 192             continue
 193         }
 194 
 195         c, _, err := r.ReadRune()
 196         if c == unicode.ReplacementChar {
 197             return errors.New(`invalid UTF-8 stream`)
 198         }
 199         if err == io.EOF {
 200             return nil
 201         }
 202         if err != nil {
 203             return err
 204         }
 205 
 206         if _, err := w.WriteRune(c); err != nil {
 207             return io.EOF
 208         }
 209     }
 210 }
 211 
 212 // leadASCII is used by func fancyHandleUTF8
 213 func leadASCII(buf []byte) int {
 214     for i, b := range buf {
 215         if b >= 128 {
 216             return i
 217         }
 218     }
 219     return len(buf)
 220 }
 221 
 222 // readPairFunc narrows source-code lines below
 223 type readPairFunc func(*bufio.Reader) (byte, byte, error)
 224 
 225 // utf16toUTF8 handles UTF-16 inputs for func utfate
 226 func utf16toUTF8(w *bufio.Writer, r *bufio.Reader, read2 readPairFunc) error {
 227     for {
 228         a, b, err := read2(r)
 229         if err == io.EOF {
 230             return nil
 231         }
 232         if err != nil {
 233             return err
 234         }
 235 
 236         c := rune(256*int(a) + int(b))
 237         if utf16.IsSurrogate(c) {
 238             a, b, err := read2(r)
 239             if err == io.EOF {
 240                 return nil
 241             }
 242             if err != nil {
 243                 return err
 244             }
 245 
 246             next := rune(256*int(a) + int(b))
 247             c = utf16.DecodeRune(c, next)
 248         }
 249 
 250         if _, err := w.WriteRune(c); err != nil {
 251             return io.EOF
 252         }
 253     }
 254 }
 255 
 256 // readBytePairBE gets you a pair of bytes in big-endian (original) order
 257 func readBytePairBE(br *bufio.Reader) (byte, byte, error) {
 258     a, err := br.ReadByte()
 259     if err != nil {
 260         return a, 0, err
 261     }
 262 
 263     b, err := br.ReadByte()
 264     return a, b, err
 265 }
 266 
 267 // readBytePairLE gets you a pair of bytes in little-endian order
 268 func readBytePairLE(br *bufio.Reader) (byte, byte, error) {
 269     a, b, err := readBytePairBE(br)
 270     return b, a, err
 271 }
 272 
 273 // utf32toUTF8 handles UTF-32 inputs for func utfate
 274 func utf32toUTF8(w *bufio.Writer, r *bufio.Reader, o binary.ByteOrder) error {
 275     var n uint32
 276     for {
 277         err := binary.Read(r, o, &n)
 278         if err == io.EOF {
 279             return nil
 280         }
 281         if err != nil {
 282             return err
 283         }
 284 
 285         if _, err := w.WriteRune(rune(n)); err != nil {
 286             return io.EOF
 287         }
 288     }
 289 }
     File: ./waveout/bytes.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "encoding/binary"
  29     "fmt"
  30     "io"
  31     "math"
  32 )
  33 
  34 // aiff header format
  35 //
  36 // http://paulbourke.net/dataformats/audio/
  37 //
  38 // wav header format
  39 //
  40 // http://soundfile.sapp.org/doc/WaveFormat/
  41 // http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
  42 // https://docs.fileformat.com/audio/wav/
  43 
  44 const (
  45     // maxInt helps convert float64 values into int16 ones
  46     maxInt = 1<<15 - 1
  47 
  48     // wavIntPCM declares integer PCM sound-data in a wav header
  49     wavIntPCM = 1
  50 
  51     // wavFloatPCM declares floating-point PCM sound-data in a wav header
  52     wavFloatPCM = 3
  53 )
  54 
  55 // emitInt16LE writes a 16-bit signed integer in little-endian byte order
  56 func emitInt16LE(w io.Writer, f float64) {
  57     // binary.Write(w, binary.LittleEndian, int16(maxInt*f))
  58     var buf [2]byte
  59     binary.LittleEndian.PutUint16(buf[:2], uint16(int16(maxInt*f)))
  60     w.Write(buf[:2])
  61 }
  62 
  63 // emitFloat32LE writes a 32-bit float in little-endian byte order
  64 func emitFloat32LE(w io.Writer, f float64) {
  65     var buf [4]byte
  66     binary.LittleEndian.PutUint32(buf[:4], math.Float32bits(float32(f)))
  67     w.Write(buf[:4])
  68 }
  69 
  70 // emitInt16BE writes a 16-bit signed integer in big-endian byte order
  71 func emitInt16BE(w io.Writer, f float64) {
  72     // binary.Write(w, binary.BigEndian, int16(maxInt*f))
  73     var buf [2]byte
  74     binary.BigEndian.PutUint16(buf[:2], uint16(int16(maxInt*f)))
  75     w.Write(buf[:2])
  76 }
  77 
  78 // emitFloat32BE writes a 32-bit float in big-endian byte order
  79 func emitFloat32BE(w io.Writer, f float64) {
  80     var buf [4]byte
  81     binary.BigEndian.PutUint32(buf[:4], math.Float32bits(float32(f)))
  82     w.Write(buf[:4])
  83 }
  84 
  85 // wavSettings is an item in the type2wavSettings table
  86 type wavSettings struct {
  87     Type          byte
  88     BitsPerSample byte
  89 }
  90 
  91 // type2wavSettings encodes values used when emitting wav headers
  92 var type2wavSettings = map[sampleFormat]wavSettings{
  93     int16LE:   {wavIntPCM, 16},
  94     float32LE: {wavFloatPCM, 32},
  95 }
  96 
  97 // emitWaveHeader writes the start of a valid .wav file: since it also starts
  98 // the wav data section and emits its size, you only need to write all samples
  99 // after calling this func
 100 func emitWaveHeader(w io.Writer, cfg outputConfig) error {
 101     const fmtChunkSize = 16
 102     duration := cfg.MaxTime
 103     numchan := uint32(len(cfg.Scripts))
 104     sampleRate := cfg.SampleRate
 105 
 106     ws, ok := type2wavSettings[cfg.Samples]
 107     if !ok {
 108         const fs = `internal error: invalid output-format code %d`
 109         return fmt.Errorf(fs, cfg.Samples)
 110     }
 111     kind := uint16(ws.Type)
 112     bps := uint32(ws.BitsPerSample)
 113 
 114     // byte rate
 115     br := sampleRate * bps * numchan / 8
 116     // data size in bytes
 117     dataSize := uint32(float64(br) * duration)
 118     // total file size
 119     totalSize := uint32(dataSize + 44)
 120 
 121     // general descriptor
 122     w.Write([]byte(`RIFF`))
 123     binary.Write(w, binary.LittleEndian, uint32(totalSize))
 124     w.Write([]byte(`WAVE`))
 125 
 126     // fmt chunk
 127     w.Write([]byte(`fmt `))
 128     binary.Write(w, binary.LittleEndian, uint32(fmtChunkSize))
 129     binary.Write(w, binary.LittleEndian, uint16(kind))
 130     binary.Write(w, binary.LittleEndian, uint16(numchan))
 131     binary.Write(w, binary.LittleEndian, uint32(sampleRate))
 132     binary.Write(w, binary.LittleEndian, uint32(br))
 133     binary.Write(w, binary.LittleEndian, uint16(bps*numchan/8))
 134     binary.Write(w, binary.LittleEndian, uint16(bps))
 135 
 136     // start data chunk
 137     w.Write([]byte(`data`))
 138     binary.Write(w, binary.LittleEndian, uint32(dataSize))
 139     return nil
 140 }
     File: ./waveout/config.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "math"
  31     "os"
  32     "strconv"
  33     "strings"
  34     "time"
  35 )
  36 
  37 // config has all the parsed cmd-line options
  38 type config struct {
  39     // Scripts has the source codes of all scripts for all channels
  40     Scripts []string
  41 
  42     // To is the output format
  43     To string
  44 
  45     // MaxTime is the play duration of the resulting sound
  46     MaxTime float64
  47 
  48     // SampleRate is the number of samples per second for all channels
  49     SampleRate uint
  50 }
  51 
  52 // parseFlags is the constructor for type config
  53 func parseFlags(usage string) (config, error) {
  54     cfg := config{
  55         To:         `wav`,
  56         MaxTime:    math.NaN(),
  57         SampleRate: 48_000,
  58     }
  59 
  60     args := os.Args[1:]
  61     if len(args) == 0 {
  62         fmt.Fprint(os.Stderr, usage)
  63         os.Exit(0)
  64         return cfg, nil
  65     }
  66 
  67     for _, s := range args {
  68         switch s {
  69         case `help`, `-h`, `--h`, `-help`, `--help`:
  70             fmt.Fprint(os.Stdout, usage)
  71             os.Exit(0)
  72             return cfg, nil
  73         }
  74 
  75         err := cfg.handleArg(s)
  76         if err != nil {
  77             return cfg, err
  78         }
  79     }
  80 
  81     if math.IsNaN(cfg.MaxTime) {
  82         cfg.MaxTime = 1
  83     }
  84     if cfg.MaxTime < 0 {
  85         const fs = `error: given negative duration %f`
  86         return cfg, fmt.Errorf(fs, cfg.MaxTime)
  87     }
  88     return cfg, nil
  89 }
  90 
  91 func (c *config) handleArg(s string) error {
  92     switch s {
  93     case `44.1k`, `44.1K`:
  94         c.SampleRate = 44_100
  95         return nil
  96 
  97     case `48k`, `48K`:
  98         c.SampleRate = 48_000
  99         return nil
 100 
 101     case `dat`, `DAT`:
 102         c.SampleRate = 48_000
 103         return nil
 104 
 105     case `cd`, `cda`, `CD`, `CDA`:
 106         c.SampleRate = 44_100
 107         return nil
 108     }
 109 
 110     // handle output-format names and their aliases
 111     if kind, ok := name2type[s]; ok {
 112         c.To = kind
 113         return nil
 114     }
 115 
 116     // handle time formats, except when they're pure numbers
 117     if math.IsNaN(c.MaxTime) {
 118         dur, derr := ParseDuration(s)
 119         if derr == nil {
 120             c.MaxTime = float64(dur) / float64(time.Second)
 121             return nil
 122         }
 123     }
 124 
 125     // handle sample-rate, given either in hertz or kilohertz
 126     lc := strings.ToLower(s)
 127     if strings.HasSuffix(lc, `khz`) {
 128         lc = strings.TrimSuffix(lc, `khz`)
 129         khz, err := strconv.ParseFloat(lc, 64)
 130         if err != nil || isBadNumber(khz) || khz <= 0 {
 131             const fs = `invalid sample-rate frequency %q`
 132             return fmt.Errorf(fs, s)
 133         }
 134         c.SampleRate = uint(1_000 * khz)
 135         return nil
 136     } else if strings.HasSuffix(lc, `hz`) {
 137         lc = strings.TrimSuffix(lc, `hz`)
 138         hz, err := strconv.ParseUint(lc, 10, 64)
 139         if err != nil {
 140             const fs = `invalid sample-rate frequency %q`
 141             return fmt.Errorf(fs, s)
 142         }
 143         c.SampleRate = uint(hz)
 144         return nil
 145     }
 146 
 147     c.Scripts = append(c.Scripts, s)
 148     return nil
 149 }
 150 
 151 type encoding byte
 152 type headerType byte
 153 type sampleFormat byte
 154 
 155 const (
 156     directEncoding encoding = 1
 157     uriEncoding    encoding = 2
 158 
 159     noHeader  headerType = 1
 160     wavHeader headerType = 2
 161 
 162     int16BE   sampleFormat = 1
 163     int16LE   sampleFormat = 2
 164     float32BE sampleFormat = 3
 165     float32LE sampleFormat = 4
 166 )
 167 
 168 // name2type normalizes keys used for type2settings
 169 var name2type = map[string]string{
 170     `datauri`:  `data-uri`,
 171     `dataurl`:  `data-uri`,
 172     `data-uri`: `data-uri`,
 173     `data-url`: `data-uri`,
 174     `uri`:      `data-uri`,
 175     `url`:      `data-uri`,
 176 
 177     `raw`:     `raw`,
 178     `raw16be`: `raw16be`,
 179     `raw16le`: `raw16le`,
 180     `raw32be`: `raw32be`,
 181     `raw32le`: `raw32le`,
 182 
 183     `audio/x-wav`:  `wave-16`,
 184     `audio/x-wave`: `wave-16`,
 185     `wav`:          `wave-16`,
 186     `wave`:         `wave-16`,
 187     `wav16`:        `wave-16`,
 188     `wave16`:       `wave-16`,
 189     `wav-16`:       `wave-16`,
 190     `wave-16`:      `wave-16`,
 191     `x-wav`:        `wave-16`,
 192     `x-wave`:       `wave-16`,
 193 
 194     `wav16uri`:    `wave-16-uri`,
 195     `wave-16-uri`: `wave-16-uri`,
 196 
 197     `wav32uri`:    `wave-32-uri`,
 198     `wave-32-uri`: `wave-32-uri`,
 199 
 200     `wav32`:   `wave-32`,
 201     `wave32`:  `wave-32`,
 202     `wav-32`:  `wave-32`,
 203     `wave-32`: `wave-32`,
 204 }
 205 
 206 // outputSettings are format-specific settings which are controlled by the
 207 // output-format option on the cmd-line
 208 type outputSettings struct {
 209     Encoding encoding
 210     Header   headerType
 211     Samples  sampleFormat
 212 }
 213 
 214 // type2settings translates output-format names into the specific settings
 215 // these imply
 216 var type2settings = map[string]outputSettings{
 217     ``: {directEncoding, wavHeader, int16LE},
 218 
 219     `data-uri`:    {uriEncoding, wavHeader, int16LE},
 220     `raw`:         {directEncoding, noHeader, int16LE},
 221     `raw16be`:     {directEncoding, noHeader, int16BE},
 222     `raw16le`:     {directEncoding, noHeader, int16LE},
 223     `wave-16`:     {directEncoding, wavHeader, int16LE},
 224     `wave-16-uri`: {uriEncoding, wavHeader, int16LE},
 225 
 226     `raw32be`:     {directEncoding, noHeader, float32BE},
 227     `raw32le`:     {directEncoding, noHeader, float32LE},
 228     `wave-32`:     {directEncoding, wavHeader, float32LE},
 229     `wave-32-uri`: {uriEncoding, wavHeader, float32LE},
 230 }
 231 
 232 // outputConfig has all the info the core of this app needs to make sound
 233 type outputConfig struct {
 234     // Scripts has the source codes of all scripts for all channels
 235     Scripts []string
 236 
 237     // MaxTime is the play duration of the resulting sound
 238     MaxTime float64
 239 
 240     // SampleRate is the number of samples per second for all channels
 241     SampleRate uint32
 242 
 243     // all the configuration details needed to emit output
 244     outputSettings
 245 }
 246 
 247 // newOutputConfig is the constructor for type outputConfig, translating the
 248 // cmd-line info from type config
 249 func newOutputConfig(cfg config) (outputConfig, error) {
 250     oc := outputConfig{
 251         Scripts:    cfg.Scripts,
 252         MaxTime:    cfg.MaxTime,
 253         SampleRate: uint32(cfg.SampleRate),
 254     }
 255 
 256     if len(oc.Scripts) == 0 {
 257         return oc, errors.New(`no formulas given`)
 258     }
 259 
 260     outFmt := strings.ToLower(strings.TrimSpace(cfg.To))
 261     if alias, ok := name2type[outFmt]; ok {
 262         outFmt = alias
 263     }
 264 
 265     set, ok := type2settings[outFmt]
 266     if !ok {
 267         const fs = `unsupported output format %q`
 268         return oc, fmt.Errorf(fs, cfg.To)
 269     }
 270 
 271     oc.outputSettings = set
 272     return oc, nil
 273 }
 274 
 275 // mimeType gives the format's corresponding MIME type, or an empty string
 276 // if the type isn't URI-encodable
 277 func (oc outputConfig) mimeType() string {
 278     if oc.Header == wavHeader {
 279         return `audio/x-wav`
 280     }
 281     return ``
 282 }
 283 
 284 func isBadNumber(f float64) bool {
 285     return math.IsNaN(f) || math.IsInf(f, 0)
 286 }
     File: ./waveout/config_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import "testing"
  28 
  29 func TestTables(t *testing.T) {
  30     for _, kind := range name2type {
  31         // ensure all canonical format values are aliased to themselves
  32         if _, ok := name2type[kind]; !ok {
  33             const fs = `canonical format %q not set`
  34             t.Fatalf(fs, kind)
  35         }
  36     }
  37 
  38     for k, kind := range name2type {
  39         // ensure each setting leads somewhere
  40         set, ok := type2settings[kind]
  41         if !ok {
  42             const fs = `type alias %q has no setting for it`
  43             t.Fatalf(fs, k)
  44         }
  45 
  46         // ensure all encoding codes are valid in the next step
  47         switch set.Encoding {
  48         case directEncoding, uriEncoding:
  49             // ok
  50         default:
  51             const fs = `invalid encoding (code %d) from settings for %q`
  52             t.Fatalf(fs, set.Encoding, kind)
  53         }
  54 
  55         // also ensure all header codes are valid
  56         switch set.Header {
  57         case noHeader, wavHeader:
  58             // ok
  59         default:
  60             const fs = `invalid header (code %d) from settings for %q`
  61             t.Fatalf(fs, set.Header, kind)
  62         }
  63 
  64         // as well as all sample-format codes
  65         switch set.Samples {
  66         case int16BE, int16LE, float32BE, float32LE:
  67             // ok
  68         default:
  69             const fs = `invalid sample-format (code %d) from settings for %q`
  70             t.Fatalf(fs, set.Header, kind)
  71         }
  72     }
  73 }
     File: ./waveout/durations.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "errors"
  29     "math"
  30     "strconv"
  31     "strings"
  32     "time"
  33 )
  34 
  35 const (
  36     day        = 24 * time.Hour
  37     week       = 7 * day
  38     normalYear = 365 * day
  39 
  40     secondsInMinute = 60
  41     secondsInHour   = 3_600
  42     secondsInDay    = 24 * secondsInHour
  43     secondsInWeek   = 7 * secondsInDay
  44 )
  45 
  46 var (
  47     ErrMisplacedDots = errors.New(`misplaced decimal dot in time-duration`)
  48 )
  49 
  50 // ParseDuration extends the stdlib time-duration parser to also allow some
  51 // commonly-used time notations like
  52 //
  53 //  MM:SS           minutes and seconds
  54 //  HH:MM:SS        hours, minutes, and seconds
  55 //  DD:HH:MM:SS     days, hours, minutes, and seconds
  56 //  WW:DD:HH:MM:SS  weeks, days, hours, minutes, and seconds
  57 //
  58 // Decimals are also supported, but only for the final seconds field.
  59 // Again, this function also supports the stdlib time-duration notation.
  60 func ParseDuration(s string) (time.Duration, error) {
  61     s = strings.TrimSpace(s)
  62     if s == "" {
  63         return 0, errors.New(`can't parse time values from empty strings`)
  64     }
  65 
  66     // handle shortcuts for time units such as weeks and (normal) years
  67     if s[len(s)-1] == 'w' {
  68         f, err := strconv.ParseFloat(s[:len(s)-1], 64)
  69         if err != nil {
  70             return 0, err
  71         }
  72         return time.Duration(float64(week) * f), nil
  73     }
  74     if s[len(s)-1] == 'y' {
  75         f, err := strconv.ParseFloat(s[:len(s)-1], 64)
  76         if err != nil {
  77             return 0, err
  78         }
  79         return time.Duration(float64(normalYear) * f), nil
  80     }
  81 
  82     // see if the stdlib can handle it directly
  83     d, err := time.ParseDuration(s)
  84     if err == nil {
  85         return d, nil
  86     }
  87 
  88     return parseColonDuration(s)
  89 }
  90 
  91 // durationFragments helps func parseDuration keep track of all fields without
  92 // depending on functionality from external packages, such as strings.Split
  93 type durationFragments struct {
  94     weeks   int
  95     days    int
  96     hours   int
  97     minutes int
  98     seconds int
  99 
 100     numfields int
 101 }
 102 
 103 func (f *durationFragments) update(n int) error {
 104     f.numfields++
 105     switch f.numfields {
 106     case 1:
 107         f.seconds = n
 108         return nil
 109 
 110     case 2:
 111         f.minutes = f.seconds
 112         f.seconds = n
 113         return nil
 114 
 115     case 3:
 116         f.hours = f.minutes
 117         f.minutes = f.seconds
 118         f.seconds = n
 119         return nil
 120 
 121     case 4:
 122         f.days = f.hours
 123         f.hours = f.minutes
 124         f.minutes = f.seconds
 125         f.seconds = n
 126         return nil
 127 
 128     case 5:
 129         f.weeks = f.days
 130         f.days = f.hours
 131         f.hours = f.minutes
 132         f.minutes = f.seconds
 133         f.seconds = n
 134         return nil
 135 
 136     default:
 137         // weeks are the largest constant time unit there is
 138         return errors.New(`semicolon-separated time fields stop at weeks`)
 139     }
 140 }
 141 
 142 func (f durationFragments) duration() time.Duration {
 143     d := time.Duration(f.weeks) * week
 144     d += time.Duration(f.days) * day
 145     d += time.Duration(f.hours) * time.Hour
 146     d += time.Duration(f.minutes) * time.Minute
 147     d += time.Duration(f.seconds) * time.Second
 148     return d
 149 }
 150 
 151 // parseColonDuration handles HH:MM:SS-like strings for func ParseDuration
 152 func parseColonDuration(s string) (time.Duration, error) {
 153     n := 0         // value for current field
 154     dec := false   // was a decimal point found?
 155     numdigits := 0 // how many digits current field has
 156     frags := durationFragments{}
 157 
 158     for _, r := range s {
 159         switch r {
 160         case '.':
 161             // handle decimals
 162             if dec {
 163                 return 0, ErrMisplacedDots
 164             }
 165             dec = true
 166             // remember value for seconds
 167             if err := frags.update(n); err != nil {
 168                 return 0, err
 169             }
 170             numdigits = 0
 171             n = 0
 172 
 173         case ':':
 174             // switch to next fragment/group
 175             if dec {
 176                 return 0, ErrMisplacedDots
 177             }
 178             if err := frags.update(n); err != nil {
 179                 return 0, err
 180             }
 181             numdigits = 0
 182             n = 0
 183 
 184         default:
 185             // update value in current field
 186             if r < '0' || r > '9' {
 187                 const m1 = `non-digits found in what's supposed`
 188                 const m2 = `to be a valid numeric substring`
 189                 const msg = m1 + ` ` + m2
 190                 return 0, errors.New(msg)
 191             }
 192             n *= 10
 193             n += int(r - '0')
 194             numdigits++
 195         }
 196     }
 197 
 198     // handle subsecond values: seconds are already counted for in this case
 199     if dec {
 200         return frags.duration() + fractionalSecond(n, numdigits), nil
 201     }
 202 
 203     // remember value for seconds
 204     if err := frags.update(n); err != nil {
 205         return 0, err
 206     }
 207     return frags.duration(), nil
 208 }
 209 
 210 // fractionalSecond turns the int-pair (mantissa, -log10) into the sub-second
 211 // time-duration it represents
 212 func fractionalSecond(fraction int, numdigits int) time.Duration {
 213     nd := math.Pow10(numdigits)
 214     return time.Duration(fraction) * time.Second / time.Duration(int64(nd))
 215 }
     File: ./waveout/info.txt
   1 waveout [options...] [duration...] [formulas...]
   2 
   3 
   4 This app emits wave-sound binary data using the script(s) given. Scripts
   5 give you the float64-related functionality you may expect, from numeric
   6 operations to several math functions. When given 1 formula, the result is
   7 mono; when given 2 formulas (left and right), the result is stereo, and so
   8 on.
   9 
  10 Output is always uncompressed audio: `waveout` can emit that as is, or as a
  11 base64-encoded data-URI, which you can use as a `src` attribute value in an
  12 HTML audio tag. Output duration is 1 second by default, but you can change
  13 that too by using a recognized time format.
  14 
  15 The first recognized time format is the familiar hh:mm:ss, where the hours
  16 are optional, and where seconds can have a decimal part after it.
  17 
  18 The second recognized time format uses 1-letter shortcuts instead of colons
  19 for each time component, each of which is optional: `h` stands for hour, `m`
  20 for minutes, and `s` for seconds.
  21 
  22 
  23 Output Formats
  24 
  25              encoding  header  samples  endian   more info
  26 
  27     wav      direct    wave    int16    little   default format
  28 
  29     wav16    direct    wave    int16    little   alias for `wav`
  30     wav32    direct    wave    float32  little
  31     uri      data-URI  wave    int16    little   MIME type is audio/x-wav
  32 
  33     raw      direct    none    int16    little
  34     raw16le  direct    none    int16    little   alias for `raw`
  35     raw32le  direct    none    float32  little
  36     raw16be  direct    none    int16    big
  37     raw32be  direct    none    float32  big
  38 
  39 
  40 Concrete Examples
  41 
  42 # low-tones commonly used in club music as beats
  43 waveout 2s 'sin(10 * tau * exp(-20 * u)) * exp(-2 * u)' > club-beats.wav
  44 
  45 # 1 minute and 5 seconds of static-like random noise
  46 waveout 1m5s 'rand()' > random-noise.wav
  47 
  48 # many bell-like clicks in quick succession; can be a cellphone's ringtone
  49 waveout 'sin(2048 * tau * t) * exp(-50 * (t%0.1))' > ringtone.wav
  50 
  51 # 10-second sound resembling a door-ajar warning
  52 waveout 10 'sin(660 * tau * (t%1)) * exp(-10 * (t%1))' > door-ajar.wav
  53 
  54 # (vaguely) similar to the door-opening sound from a home alarm
  55 waveout 'sin(4096 * tau * t) * exp(-10 * (t%0.1))' > home-alarm.wav
  56 
  57 # watch your ears: quickly increases frequency up to 2khz
  58 waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav
  59 
  60 # 1-second 400hz test tone
  61 waveout 'sin(400 * tau * t)' > test-tone-400.wav
  62 
  63 # 2s of a 440hz test tone, also called an A440 sound
  64 waveout 2s 'sin(440 * tau * t)' > a440.wav
  65 
  66 # 1s 400hz test tone with sudden volume drop at the end, to avoid clip
  67 waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav
  68 
  69 # old ringtone used in north america
  70 waveout '0.5*sin(350 * tau * t) + 0.5*sin(450 * tau * t)' > na-ringtone.wav
  71 
  72 # 20 seconds of periodic pings
  73 waveout 20s 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav
  74 
  75 # 2 seconds of a european-style dial-tone
  76 waveout 2s '(sin(350 * tau * t) + sin(450 * tau * t)) / 2' > dial-tone.wav
  77 
  78 # 4 seconds of a north-american-style busy-phone signal
  79 waveout 4s '(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' > na-busy.wav
  80 
  81 # hit the 51st key on a synthetic piano-like instrument
  82 waveout 'sin(tau * 440 * 2**((51 - 49)/12) * t) * exp(-10*u)' > piano-key.wav
  83 
  84 # hit of a synthetic snare-like sound
  85 waveout 'random() * exp(-10 * t)' > synth-snare.wav
  86 
  87 # a stereotypical `laser` sound
  88 waveout 'sin(100 * tau * exp(-40 * t))' > laser.wav
     File: ./waveout/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "bufio"
  29     "encoding/base64"
  30     "errors"
  31     "fmt"
  32     "io"
  33     "os"
  34 
  35     _ "embed"
  36 )
  37 
  38 //go:embed info.txt
  39 var usage string
  40 
  41 func Main() {
  42     cfg, err := parseFlags(usage)
  43     if err != nil {
  44         fmt.Fprintln(os.Stderr, err.Error())
  45         os.Exit(1)
  46         return
  47     }
  48 
  49     oc, err := newOutputConfig(cfg)
  50     if err != nil {
  51         fmt.Fprintln(os.Stderr, err.Error())
  52         os.Exit(1)
  53         return
  54     }
  55 
  56     addDetermFuncs()
  57 
  58     if err := run(oc); err != nil {
  59         fmt.Fprintln(os.Stderr, err.Error())
  60         os.Exit(1)
  61         return
  62     }
  63 }
  64 
  65 func run(cfg outputConfig) error {
  66     // f, err := os.Create(`waveout.prof`)
  67     // if err != nil {
  68     //  return err
  69     // }
  70     // defer f.Close()
  71 
  72     // pprof.StartCPUProfile(f)
  73     // defer pprof.StopCPUProfile()
  74 
  75     w := bufio.NewWriterSize(os.Stdout, 64*1024)
  76     defer w.Flush()
  77 
  78     switch cfg.Encoding {
  79     case directEncoding:
  80         return runDirect(w, cfg)
  81 
  82     case uriEncoding:
  83         mtype := cfg.mimeType()
  84         if mtype == `` {
  85             return errors.New(`internal error: no MIME type`)
  86         }
  87 
  88         fmt.Fprintf(w, `data:%s;base64,`, mtype)
  89         enc := base64.NewEncoder(base64.StdEncoding, w)
  90         defer enc.Close()
  91         return runDirect(enc, cfg)
  92 
  93     default:
  94         const fs = `internal error: wrong output-encoding code %d`
  95         return fmt.Errorf(fs, cfg.Encoding)
  96     }
  97 }
  98 
  99 // type2emitter chooses sample-emitter funcs from the format given
 100 var type2emitter = map[sampleFormat]func(io.Writer, float64){
 101     int16LE:   emitInt16LE,
 102     int16BE:   emitInt16BE,
 103     float32LE: emitFloat32LE,
 104     float32BE: emitFloat32BE,
 105 }
 106 
 107 // runDirect emits sound-data bytes: this func can be called with writers
 108 // which keep bytes as given, or with re-encoders, such as base64 writers
 109 func runDirect(w io.Writer, cfg outputConfig) error {
 110     switch cfg.Header {
 111     case noHeader:
 112         // do nothing, while avoiding error
 113 
 114     case wavHeader:
 115         emitWaveHeader(w, cfg)
 116 
 117     default:
 118         const fs = `internal error: wrong header code %d`
 119         return fmt.Errorf(fs, cfg.Header)
 120     }
 121 
 122     emitter, ok := type2emitter[cfg.Samples]
 123     if !ok {
 124         const fs = `internal error: wrong output-format code %d`
 125         return fmt.Errorf(fs, cfg.Samples)
 126     }
 127 
 128     if len(cfg.Scripts) == 1 {
 129         return emitMono(w, cfg, emitter)
 130     }
 131     return emit(w, cfg, emitter)
 132 }
     File: ./waveout/scripts.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "io"
  29     "math"
  30     "math/rand"
  31     "time"
  32 
  33     "../fmscripts"
  34 )
  35 
  36 // makeDefs makes extra funcs and values available to scripts
  37 func makeDefs(cfg outputConfig) map[string]any {
  38     // copy extra built-in funcs
  39     defs := make(map[string]any, len(extras)+6+5)
  40     for k, v := range extras {
  41         defs[k] = v
  42     }
  43 
  44     // add extra variables
  45     defs[`t`] = 0.0
  46     defs[`u`] = 0.0
  47     defs[`d`] = cfg.MaxTime
  48     defs[`dur`] = cfg.MaxTime
  49     defs[`duration`] = cfg.MaxTime
  50     defs[`end`] = cfg.MaxTime
  51 
  52     // add pseudo-random funcs
  53 
  54     seed := time.Now().UnixNano()
  55     r := rand.New(rand.NewSource(seed))
  56 
  57     rand := func() float64 {
  58         return random01(r)
  59     }
  60     randomf := func() float64 {
  61         return random(r)
  62     }
  63     rexpf := func(scale float64) float64 {
  64         return rexp(r, scale)
  65     }
  66     rnormf := func(mu, sigma float64) float64 {
  67         return rnorm(r, mu, sigma)
  68     }
  69 
  70     defs[`rand`] = rand
  71     defs[`rand01`] = rand
  72     defs[`random`] = randomf
  73     defs[`rexp`] = rexpf
  74     defs[`rnorm`] = rnormf
  75 
  76     return defs
  77 }
  78 
  79 type emitFunc = func(io.Writer, float64)
  80 
  81 // emit runs the formulas given to emit all wave samples
  82 func emit(w io.Writer, cfg outputConfig, emit emitFunc) error {
  83     var c fmscripts.Compiler
  84     defs := makeDefs(cfg)
  85 
  86     programs := make([]fmscripts.Program, 0, len(cfg.Scripts))
  87     tvars := make([]*float64, 0, len(cfg.Scripts))
  88     uvars := make([]*float64, 0, len(cfg.Scripts))
  89 
  90     for _, s := range cfg.Scripts {
  91         p, err := c.Compile(s, defs)
  92         if err != nil {
  93             return err
  94         }
  95         programs = append(programs, p)
  96         t, _ := p.Get(`t`)
  97         u, _ := p.Get(`u`)
  98         tvars = append(tvars, t)
  99         uvars = append(uvars, u)
 100     }
 101 
 102     dt := 1.0 / float64(cfg.SampleRate)
 103     end := cfg.MaxTime
 104 
 105     for i := 0.0; true; i++ {
 106         now := dt * i
 107         if now >= end {
 108             return nil
 109         }
 110 
 111         _, u := math.Modf(now)
 112 
 113         for j, p := range programs {
 114             *tvars[j] = now
 115             *uvars[j] = u
 116             emit(w, p.Run())
 117         }
 118     }
 119     return nil
 120 }
 121 
 122 // emitMono runs the formula given to emit all single-channel wave samples
 123 func emitMono(w io.Writer, cfg outputConfig, emit emitFunc) error {
 124     var c fmscripts.Compiler
 125     mono, err := c.Compile(cfg.Scripts[0], makeDefs(cfg))
 126     if err != nil {
 127         return err
 128     }
 129 
 130     t, _ := mono.Get(`t`)
 131     u, needsu := mono.Get(`u`)
 132 
 133     dt := 1.0 / float64(cfg.SampleRate)
 134     end := cfg.MaxTime
 135 
 136     // update variable `u` only if script uses it: this can speed things
 137     // up considerably when that variable isn't used
 138     if needsu {
 139         for i := 0.0; true; i++ {
 140             now := dt * i
 141             if now >= end {
 142                 return nil
 143             }
 144 
 145             *t = now
 146             _, *u = math.Modf(now)
 147             emit(w, mono.Run())
 148         }
 149         return nil
 150     }
 151 
 152     for i := 0.0; true; i++ {
 153         now := dt * i
 154         if now >= end {
 155             return nil
 156         }
 157 
 158         *t = now
 159         emit(w, mono.Run())
 160     }
 161     return nil
 162 }
 163 
 164 // // emitStereo runs the formula given to emit all 2-channel wave samples
 165 // func emitStereo(w io.Writer, cfg outputConfig, emit emitFunc) error {
 166 //  defs := makeDefs(cfg)
 167 //  var c fmscripts.Compiler
 168 //  left, err := c.Compile(cfg.Scripts[0], defs)
 169 //  if err != nil {
 170 //      return err
 171 //  }
 172 //  right, err := c.Compile(cfg.Scripts[1], defs)
 173 //  if err != nil {
 174 //      return err
 175 //  }
 176 
 177 //  lt, _ := left.Get(`t`)
 178 //  rt, _ := right.Get(`t`)
 179 //  lu, luok := left.Get(`u`)
 180 //  ru, ruok := right.Get(`u`)
 181 
 182 //  dt := 1.0 / float64(cfg.SampleRate)
 183 //  end := cfg.MaxTime
 184 
 185 //  // update variable `u` only if script uses it: this can speed things
 186 //  // up considerably when that variable isn't used
 187 //  updateu := func(float64) {}
 188 //  if luok || ruok {
 189 //      updateu = func(now float64) {
 190 //          _, u := math.Modf(now)
 191 //          *lu = u
 192 //          *ru = u
 193 //      }
 194 //  }
 195 
 196 //  for i := 0.0; true; i++ {
 197 //      now := dt * i
 198 //      if now >= end {
 199 //          return nil
 200 //      }
 201 
 202 //      *rt = now
 203 //      *lt = now
 204 //      updateu(now)
 205 
 206 //      // most software seems to emit stereo pairs in left-right order
 207 //      emit(w, left.Run())
 208 //      emit(w, right.Run())
 209 //  }
 210 
 211 //  // keep the compiler happy
 212 //  return nil
 213 // }
     File: ./waveout/stdlib.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "math"
  29     "math/rand"
  30 
  31     "../fmscripts"
  32     "../mathplus"
  33 )
  34 
  35 // tau is exactly 1 loop around a circle, which is handy to turn frequencies
  36 // into trigonometric angles, since they're measured in radians
  37 const tau = 2 * math.Pi
  38 
  39 // extras has funcs beyond what the script built-ins offer: those built-ins
  40 // are for general math calculations, while these are specific for sound
  41 // effects, other sound-related calculations, or to make pseudo-random values
  42 var extras = map[string]any{
  43     `hihat`: hihat,
  44 }
  45 
  46 // addDetermFuncs does what it says, ensuring these funcs are optimizable when
  47 // they're given all-constant expressions as inputs
  48 func addDetermFuncs() {
  49     fmscripts.DefineDetFuncs(map[string]any{
  50         `ascale`:       mathplus.AnchoredScale,
  51         `awrap`:        mathplus.AnchoredWrap,
  52         `clamp`:        mathplus.Clamp,
  53         `epa`:          mathplus.Epanechnikov,
  54         `epanechnikov`: mathplus.Epanechnikov,
  55         `fract`:        mathplus.Fract,
  56         `gauss`:        mathplus.Gauss,
  57         `horner`:       mathplus.Polyval,
  58         `logistic`:     mathplus.Logistic,
  59         `mix`:          mathplus.Mix,
  60         `polyval`:      mathplus.Polyval,
  61         `scale`:        mathplus.Scale,
  62         `sign`:         mathplus.Sign,
  63         `sinc`:         mathplus.Sinc,
  64         `smoothstep`:   mathplus.SmoothStep,
  65         `step`:         mathplus.Step,
  66         `tricube`:      mathplus.Tricube,
  67         `unwrap`:       mathplus.Unwrap,
  68         `wrap`:         mathplus.Wrap,
  69 
  70         `drop`:       dropsince,
  71         `dropfrom`:   dropsince,
  72         `dropoff`:    dropsince,
  73         `dropsince`:  dropsince,
  74         `kick`:       kick,
  75         `kicklow`:    kicklow,
  76         `piano`:      piano,
  77         `pianokey`:   piano,
  78         `pickval`:    pickval,
  79         `pickvalue`:  pickval,
  80         `sched`:      schedule,
  81         `schedule`:   schedule,
  82         `timeval`:    timeval,
  83         `timevalues`: timeval,
  84     })
  85 }
  86 
  87 // random01 returns a random value in 0 .. 1
  88 func random01(r *rand.Rand) float64 {
  89     return r.Float64()
  90 }
  91 
  92 // random returns a random value in -1 .. +1
  93 func random(r *rand.Rand) float64 {
  94     return (2 * r.Float64()) - 1
  95 }
  96 
  97 // rexp returns an exponentially-distributed random value using the scale
  98 // (expected value) given
  99 func rexp(r *rand.Rand, scale float64) float64 {
 100     return scale * r.ExpFloat64()
 101 }
 102 
 103 // rnorm returns a normally-distributed random value using the mean and
 104 // standard deviation given
 105 func rnorm(r *rand.Rand, mu, sigma float64) float64 {
 106     return r.NormFloat64()*sigma + mu
 107 }
 108 
 109 // make sample for a synthetic-drum kick
 110 func kick(t float64, f, k float64) float64 {
 111     const p = 0.085
 112     return math.Sin(tau*f*math.Pow(p, t)) * math.Exp(-k*t)
 113 }
 114 
 115 // make sample for a heavier-sounding synthetic-drum kick
 116 func kicklow(t float64, f, k float64) float64 {
 117     const p = 0.08
 118     return math.Sin(tau*f*math.Pow(p, t)) * math.Exp(-k*t)
 119 }
 120 
 121 // make sample for a synthetic hi-hat hit
 122 func hihat(t float64, k float64) float64 {
 123     return rand.Float64() * math.Exp(-k*t)
 124 }
 125 
 126 // schedule rearranges time, without being a time machine
 127 func schedule(t float64, period, delay float64) float64 {
 128     v := t + (1 - delay)
 129     if v < 0 {
 130         return 0
 131     }
 132     return math.Mod(v*period, period)
 133 }
 134 
 135 // make sample for a synthetic piano key being hit
 136 func piano(t float64, n float64) float64 {
 137     p := (math.Floor(n) - 49) / 12
 138     f := 440 * math.Pow(2, p)
 139     return math.Sin(tau * f * t)
 140 }
 141 
 142 // multiply rest of a formula with this for a quick volume drop at the end:
 143 // this is handy to avoid clips when sounds end playing
 144 func dropsince(t float64, start float64) float64 {
 145     // return math.Min(1, math.Exp(-100*(t-start)))
 146     if t <= start {
 147         return 1
 148     }
 149     return math.Exp(-100 * (t - start))
 150 }
 151 
 152 // pickval requires at least 3 args, the first 2 being the current time and
 153 // each slot's duration, respectively: these 2 are followed by all the values
 154 // to pick for all time slots
 155 func pickval(args ...float64) float64 {
 156     if len(args) < 3 {
 157         return 0
 158     }
 159 
 160     t := args[0]
 161     slotdur := args[1]
 162     values := args[2:]
 163 
 164     u, _ := math.Modf(t / slotdur)
 165     n := len(values)
 166     i := int(u) % n
 167     if 0 <= i && i < n {
 168         return values[i]
 169     }
 170     return 0
 171 }
 172 
 173 // timeval requires at least 2 args, the first 2 being the current time and
 174 // the total looping-period, respectively: these 2 are followed by pairs of
 175 // numbers, each consisting of a timestamp and a matching value, in order
 176 func timeval(args ...float64) float64 {
 177     if len(args) < 2 {
 178         return 0
 179     }
 180 
 181     t := args[0]
 182     period := args[1]
 183     u, _ := math.Modf(t / period)
 184 
 185     // find the first value whose periodic timestamp is due
 186     for rest := args[2:]; len(rest) >= 2; rest = rest[2:] {
 187         if u >= rest[0]/period {
 188             return rest[1]
 189         }
 190     }
 191     return 0
 192 }
     File: ./zcat/zcat.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package zcat
  26 
  27 import (
  28     "compress/gzip"
  29     "io"
  30     "os"
  31 )
  32 
  33 const info = `
  34 zcat [options...] [files...]
  35 
  36 Concatenate gzip-decompressed files/data to the standard output. To put it in
  37 other words: all inputs are expected to be gzip-compressed, while the output
  38 is decompressed/normal.
  39 
  40 Options
  41 
  42     --help    show this help message
  43 `
  44 
  45 func Main() {
  46     args := os.Args[1:]
  47     for len(args) > 0 {
  48         switch args[0] {
  49         case `--help`:
  50             os.Stderr.WriteString(info[1:])
  51             return
  52         }
  53 
  54         break
  55     }
  56 
  57     if len(args) > 0 && args[0] == `--` {
  58         args = args[1:]
  59     }
  60 
  61     for _, path := range args {
  62         if err := handleFile(os.Stdout, path); err != nil && err != io.EOF {
  63             os.Stderr.WriteString(`zcat: `)
  64             os.Stderr.WriteString(err.Error())
  65             os.Stderr.WriteString("\n")
  66             os.Exit(1)
  67             return
  68         }
  69     }
  70 
  71     if len(args) == 0 {
  72         if err := zcat(os.Stdout, os.Stdin); err != nil && err != io.EOF {
  73             os.Stderr.WriteString(`zcat: `)
  74             os.Stderr.WriteString(err.Error())
  75             os.Stderr.WriteString("\n")
  76             os.Exit(1)
  77             return
  78         }
  79     }
  80 }
  81 
  82 func handleFile(w io.Writer, path string) error {
  83     f, err := os.Open(path)
  84     if err != nil {
  85         return err
  86     }
  87     defer f.Close()
  88     return zcat(w, f)
  89 }
  90 
  91 func zcat(w io.Writer, r io.Reader) error {
  92     r, err := gzip.NewReader(r)
  93     if err != nil {
  94         return err
  95     }
  96     return cat(w, r)
  97 }
  98 
  99 func cat(w io.Writer, r io.Reader) error {
 100     var buf [32 * 1024]byte
 101 
 102     for {
 103         got, err := r.Read(buf[:])
 104         if err == io.EOF {
 105             if got > 0 {
 106                 w.Write(buf[:got])
 107             }
 108             break
 109         }
 110 
 111         if err != nil {
 112             return err
 113         }
 114 
 115         if _, err := w.Write(buf[:got]); err != nil {
 116             return io.EOF
 117         }
 118     }
 119 
 120     return nil
 121 }
     File: ./zj/zj.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package zj
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34     "strings"
  35     "unicode/utf8"
  36 )
  37 
  38 const info = `
  39 zj [keys/indices...]
  40 
  41 Zoom Json digs into subsets of the JSON data read from the standard input.
  42 `
  43 
  44 // sets keeps track of 2 sets: one for integer indices, the other for string
  45 // keys; the sets are reused across recursive calls of func `zoom` to save
  46 // on memory/allocations
  47 type sets struct {
  48     indices map[int]struct{}
  49     keys    map[string]struct{}
  50 }
  51 
  52 // dictionary is a map which also remembers the order of its keys
  53 type dictionary struct {
  54     Keys []string
  55     Map  map[string]any
  56 }
  57 
  58 func Main() {
  59     args := os.Args[1:]
  60 
  61     if len(args) > 0 {
  62         switch args[0] {
  63         case `-h`, `--h`, `-help`, `--help`:
  64             os.Stdout.WriteString(info[1:])
  65             return
  66         }
  67     }
  68 
  69     if len(args) > 0 && args[0] == `--` {
  70         args = args[1:]
  71     }
  72 
  73     data, err := load(os.Stdin)
  74     if err != nil {
  75         os.Stderr.WriteString(`zj: `)
  76         os.Stderr.WriteString(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79         return
  80     }
  81 
  82     var avoid sets
  83     avoid.indices = make(map[int]struct{})
  84     avoid.keys = make(map[string]struct{})
  85 
  86     data, err = zoom(data, args, &avoid)
  87     if err != nil && err != io.EOF {
  88         os.Stderr.WriteString(`zj: `)
  89         os.Stderr.WriteString(err.Error())
  90         os.Stderr.WriteString("\n")
  91         os.Exit(1)
  92         return
  93     }
  94 
  95     if err := json0(os.Stdout, data); err != nil && err != io.EOF {
  96         os.Stderr.WriteString(`zj: `)
  97         os.Stderr.WriteString(err.Error())
  98         os.Stderr.WriteString("\n")
  99         os.Exit(1)
 100         return
 101     }
 102 }
 103 
 104 func load(r io.Reader) (any, error) {
 105     // dec := json.NewDecoder(r)
 106     dec := json.NewDecoder(bufio.NewReaderSize(r, 32*1024))
 107     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 108     // even if JSON parsers aren't required to guarantee such input-fidelity
 109     // for numbers
 110     dec.UseNumber()
 111 
 112     t, err := dec.Token()
 113     if err == io.EOF {
 114         return nil, errors.New(`input has no JSON values`)
 115     }
 116 
 117     data, err := loadToken(dec, t)
 118     if err != nil {
 119         return data, err
 120     }
 121 
 122     _, err = dec.Token()
 123     if err == io.EOF {
 124         // input is over, so it's a success
 125         return data, nil
 126     }
 127 
 128     if err == nil {
 129         // a successful `read` is a failure, as it means there are
 130         // trailing JSON tokens
 131         return data, errors.New(`unexpected trailing data`)
 132     }
 133 
 134     // any other error
 135     return data, err
 136 }
 137 
 138 // loadToken handles recursion for func load
 139 func loadToken(dec *json.Decoder, t json.Token) (any, error) {
 140     switch t := t.(type) {
 141     case json.Delim:
 142         switch t {
 143         case json.Delim('['):
 144             return loadArray(dec)
 145         case json.Delim('{'):
 146             return loadObject(dec)
 147         default:
 148             return nil, errors.New(`unsupported JSON syntax ` + string(t))
 149         }
 150 
 151     case nil, bool, json.Number, string:
 152         return t, nil
 153 
 154     default:
 155         // return nil, fmt.Errorf(`unsupported token type %T`, t)
 156         return nil, errors.New(`invalid JSON token`)
 157     }
 158 }
 159 
 160 // loadArray handles arrays for func loadToken
 161 func loadArray(dec *json.Decoder) ([]any, error) {
 162     var items []any
 163 
 164     for i := 0; true; i++ {
 165         t, err := dec.Token()
 166         if err == io.EOF {
 167             return items, errors.New(`end of JSON before array was closed`)
 168         }
 169         if err != nil {
 170             return items, err
 171         }
 172 
 173         if t == json.Delim(']') {
 174             return items, nil
 175         }
 176 
 177         v, err := loadToken(dec, t)
 178         if err != nil {
 179             return items, err
 180         }
 181         items = append(items, v)
 182     }
 183 
 184     // make the compiler happy
 185     return items, nil
 186 }
 187 
 188 // loadObject handles objects for func loadToken
 189 func loadObject(dec *json.Decoder) (dictionary, error) {
 190     var items dictionary
 191 
 192     for i := 0; true; i++ {
 193         t, err := dec.Token()
 194         if err == io.EOF {
 195             return items, errors.New(`end of JSON before object was closed`)
 196         }
 197         if err != nil {
 198             return items, err
 199         }
 200 
 201         if t == json.Delim('}') {
 202             return items, nil
 203         }
 204 
 205         k, ok := t.(string)
 206         if !ok {
 207             return items, errors.New(`expected a string for a key-value pair`)
 208         }
 209 
 210         t, err = dec.Token()
 211         if err == io.EOF {
 212             return items, errors.New(`expected a value for a key-value pair`)
 213         }
 214 
 215         v, err := loadToken(dec, t)
 216         if err != nil {
 217             return items, err
 218         }
 219 
 220         if i == 0 {
 221             items.Map = make(map[string]any)
 222         }
 223         if _, ok := items.Map[k]; !ok {
 224             items.Keys = append(items.Keys, k)
 225         }
 226         items.Map[k] = v
 227     }
 228 
 229     // make the compiler happy
 230     return items, nil
 231 }
 232 
 233 func zoom(data any, keys []string, avoid *sets) (any, error) {
 234     for i, k := range keys {
 235         switch v := data.(type) {
 236         case nil:
 237             return v, errors.New(`too many keys: can't zoom a null value`)
 238 
 239         case bool:
 240             return v, errors.New(`too many keys: can't zoom a boolean value`)
 241 
 242         case json.Number:
 243             return v, errors.New(`too many keys: can't zoom a number`)
 244 
 245         case string:
 246             v, err := zoomString(v, k)
 247             if err != nil {
 248                 return v, err
 249             }
 250             data = v
 251 
 252         case []any:
 253             switch k {
 254             case `.`:
 255                 return loopZoomArray(v, keys[i+1:], avoid)
 256             case `+`:
 257                 return pickArrayItems(v, keys[i+1:])
 258             case `-`:
 259                 clear(avoid.indices)
 260                 appendIndices(avoid.indices, v, keys[i+1:])
 261                 return dropArrayItems(v, avoid.indices)
 262             }
 263 
 264             res, err := zoomArray(v, k)
 265             if err != nil {
 266                 return data, err
 267             }
 268             data = res
 269 
 270         case dictionary:
 271             if v, ok := v.Map[k]; ok {
 272                 data = v
 273                 continue
 274             }
 275 
 276             switch k {
 277             case `+`:
 278                 return pickObjectItems(v, keys[i+1:])
 279             case `-`:
 280                 clear(avoid.keys)
 281                 for _, k := range keys[i+1:] {
 282                     avoid.keys[k] = struct{}{}
 283                 }
 284                 return dropObjectItems(v, avoid.keys)
 285             }
 286 
 287             if _, v, ok := matchObjectKey(v, k); ok {
 288                 data = v
 289             } else {
 290                 data = nil
 291             }
 292 
 293         default:
 294             return v, errors.New(`too many keys: can't zoom basic values`)
 295         }
 296     }
 297     return data, nil
 298 }
 299 
 300 func zoomArray(items []any, k string) (any, error) {
 301     // trim leading spaces
 302     for len(k) > 0 && k[0] == ' ' {
 303         k = k[1:]
 304     }
 305 
 306     // trim trailing spaces
 307     for len(k) > 0 && k[len(k)-1] == ' ' {
 308         k = k[:len(k)-1]
 309     }
 310 
 311     if i, j, ok := tryArraySlice(items, k); ok {
 312         if j >= len(items) {
 313             j = len(items)
 314         }
 315         if i < 0 || j < 0 || i > j {
 316             return []any(nil), nil
 317         }
 318         return items[i:j], nil
 319     }
 320 
 321     i, err := strconv.ParseInt(k, 10, 64)
 322     if err != nil {
 323         return nil, nil
 324     }
 325 
 326     if i < 0 {
 327         i += int64(len(items))
 328     }
 329 
 330     if 0 <= i && i < int64(len(items)) {
 331         return items[i], nil
 332     }
 333     return nil, nil
 334 }
 335 
 336 func tryArraySlice(items []any, k string) (i int, j int, ok bool) {
 337     if k == `` {
 338         return 0, 0, false
 339     }
 340 
 341     colon := strings.IndexByte(k, ':')
 342     if colon < 0 {
 343         if dots := indexPair(k, '.', '.'); dots >= 0 {
 344             return tryIncArraySlice(items, k, dots)
 345         }
 346 
 347         return 0, 0, false
 348     }
 349 
 350     // handle omitted/implied starting 0
 351     if colon == 0 {
 352         j, err := strconv.ParseInt(k[colon+1:], 10, 64)
 353         if err != nil {
 354             return 0, 0, false
 355         }
 356 
 357         if j < 0 {
 358             j += int64(len(items))
 359         }
 360 
 361         return 0, int(j), true
 362     }
 363 
 364     // handle omitted/implied until the end
 365     if colon == len(k)-1 {
 366         i, err := strconv.ParseInt(k[:colon], 10, 64)
 367         if err != nil {
 368             return 0, 0, false
 369         }
 370 
 371         if i < 0 {
 372             i += int64(len(items))
 373         }
 374 
 375         return int(i), len(items), true
 376     }
 377 
 378     start, err := strconv.ParseInt(k[:colon], 10, 64)
 379     if err != nil {
 380         return 0, 0, false
 381     }
 382 
 383     if start < 0 {
 384         start += int64(len(items))
 385     }
 386 
 387     end, err := strconv.ParseInt(k[colon+1:], 10, 64)
 388     if err != nil {
 389         return 0, 0, false
 390     }
 391 
 392     if end < 0 {
 393         end += int64(len(items))
 394     }
 395 
 396     return int(start), int(end), ok
 397 }
 398 
 399 func tryIncArraySlice(items []any, k string, dots int) (i int, j int, ok bool) {
 400     if k == `` {
 401         return 0, 0, false
 402     }
 403 
 404     if dots < 0 {
 405         return 0, 0, false
 406     }
 407 
 408     // handle omitted/implied starting 0
 409     if dots == 0 {
 410         j, err := strconv.ParseInt(k[dots+2:], 10, 64)
 411         if err != nil {
 412             return 0, 0, false
 413         }
 414 
 415         if j < 0 {
 416             j += int64(len(items))
 417         }
 418         if j >= 0 {
 419             j++
 420         }
 421 
 422         return 0, int(j), true
 423     }
 424 
 425     // handle omitted/implied until the end
 426     if dots == len(k)-1 {
 427         i, err := strconv.ParseInt(k[:dots], 10, 64)
 428         if err != nil {
 429             return 0, 0, false
 430         }
 431 
 432         if i < 0 {
 433             i += int64(len(items))
 434         }
 435 
 436         return int(i), len(items), true
 437     }
 438 
 439     start, err := strconv.ParseInt(k[:dots], 10, 64)
 440     if err != nil {
 441         return 0, 0, false
 442     }
 443 
 444     if start < 0 {
 445         start += int64(len(items))
 446     }
 447 
 448     end, err := strconv.ParseInt(k[dots+2:], 10, 64)
 449     if err != nil {
 450         return 0, 0, false
 451     }
 452 
 453     if end < 0 {
 454         end += int64(len(items))
 455     }
 456     if end >= 0 {
 457         end++
 458     }
 459     return int(start), int(end), ok
 460 }
 461 
 462 func matchObjectKey(items dictionary, k string) (match string, v any, ok bool) {
 463     // first, try direct key lookup
 464     if v, ok := items.Map[k]; ok {
 465         return k, v, true
 466     }
 467 
 468     // second, try case-insensitive key lookup
 469     for s := range items.Map {
 470         if strings.EqualFold(k, s) {
 471             return s, items.Map[s], true
 472         }
 473     }
 474 
 475     // finally, try integer/index lookup
 476     i, err := strconv.ParseInt(k, 10, 64)
 477     if err != nil {
 478         return ``, nil, false
 479     }
 480 
 481     if i < 0 {
 482         i += int64(len(items.Keys))
 483     }
 484 
 485     if 0 <= i && i < int64(len(items.Keys)) {
 486         k := items.Keys[i]
 487         return k, items.Map[k], true
 488     }
 489 
 490     // nothing worked
 491     return ``, nil, false
 492 }
 493 
 494 func zoomString(s string, k string) (string, error) {
 495     if i, j, ok := tryRuneSlice(s, k); ok {
 496         if i > j {
 497             return ``, nil
 498         }
 499         return sliceRunes(s, i, j), nil
 500     }
 501 
 502     i, err := strconv.ParseInt(k, 10, 64)
 503     if err != nil {
 504         return ``, err
 505     }
 506 
 507     // don't bother looping when the index given is obviously out of bounds
 508     if int(i) >= len(s) || int(-i) > len(s) {
 509         return ``, nil
 510     }
 511 
 512     if i < 0 {
 513         // shrink string backward from the end
 514         for len(s) > 0 && i < 0 {
 515             _, size := utf8.DecodeLastRuneInString(s)
 516             s = s[:len(s)-size]
 517             i++
 518         }
 519 
 520         if len(s) > 0 && i == 0 {
 521             _, size := utf8.DecodeLastRuneInString(s)
 522             return s[len(s)-size:], nil
 523         }
 524         return ``, nil
 525     }
 526 
 527     // shrink string forward from the start
 528     for len(s) > 0 && i > 0 {
 529         _, size := utf8.DecodeRuneInString(s)
 530         s = s[size:]
 531         i--
 532     }
 533 
 534     if len(s) > 0 && i == 0 {
 535         _, size := utf8.DecodeRuneInString(s)
 536         return s[:size], nil
 537     }
 538     return ``, nil
 539 }
 540 
 541 func tryRuneSlice(s string, k string) (i int, j int, ok bool) {
 542     if k == `` {
 543         return 0, 0, false
 544     }
 545 
 546     colon := strings.IndexByte(k, ':')
 547     if colon < 0 {
 548         return 0, 0, false
 549     }
 550 
 551     // handle omitted/implied starting 0
 552     if colon == 0 {
 553         j, err := strconv.ParseInt(k[colon+1:], 10, 64)
 554         if err != nil {
 555             return 0, 0, false
 556         }
 557 
 558         return 0, int(j), true
 559     }
 560 
 561     // handle omitted/implied until the end
 562     if colon == len(k)-1 {
 563         i, err := strconv.ParseInt(k[:colon], 10, 64)
 564         if err != nil {
 565             return 0, 0, false
 566         }
 567 
 568         return int(i), len(s), true
 569     }
 570 
 571     start, err := strconv.ParseInt(k[:colon], 10, 64)
 572     if err != nil {
 573         return 0, 0, false
 574     }
 575 
 576     end, err := strconv.ParseInt(k[colon+1:], 10, 64)
 577     if err != nil {
 578         return 0, 0, false
 579     }
 580 
 581     return int(start), int(end), ok
 582 }
 583 
 584 func sliceRunes(s string, i int, j int) string {
 585     if i >= j {
 586         return ``
 587     }
 588 
 589     // to do: backward-indexing
 590     if i < 0 || j < 0 {
 591         return ``
 592     }
 593 
 594     // don't bother looping when the index given is obviously out of bounds
 595     if int(i) >= len(s) || int(-i) > len(s) {
 596         return ``
 597     }
 598 
 599     // skip leading runes, according to the first index
 600     for len(s) > 0 && i > 0 {
 601         _, size := utf8.DecodeRuneInString(s)
 602         s = s[size:]
 603         i--
 604     }
 605 
 606     if len(s) == 0 {
 607         return ``
 608     }
 609 
 610     end := 0
 611     rest := s
 612     for len(rest) > 0 && j > 0 {
 613         _, size := utf8.DecodeRuneInString(rest)
 614         rest = rest[size:]
 615         end += size
 616         j--
 617     }
 618 
 619     if len(s) > 0 && j == 0 {
 620         return s[:end]
 621     }
 622     return ``
 623 }
 624 
 625 func json0(w io.Writer, data any) error {
 626     bw := bufio.NewWriterSize(w, 32*1024)
 627     defer bw.Flush()
 628 
 629     err := writeValue(bw, data)
 630     bw.WriteByte('\n')
 631 
 632     if err == io.EOF {
 633         return nil
 634     }
 635     return err
 636 }
 637 
 638 func loopZoomArray(items []any, keys []string, avoid *sets) (any, error) {
 639     res := items[:0]
 640     for _, v := range items {
 641         v, err := zoom(v, keys, avoid)
 642         if err != nil {
 643             return res, err
 644         }
 645         res = append(res, v)
 646     }
 647     return res, nil
 648 }
 649 
 650 func pickArrayItems(items []any, keys []string) (any, error) {
 651     res := items[:0]
 652     for _, k := range keys {
 653         v, err := zoomArray(items, k)
 654         if err != nil {
 655             return res, err
 656         }
 657         res = append(res, v)
 658     }
 659     return res, nil
 660 }
 661 
 662 func dropArrayItems(items []any, avoid map[int]struct{}) (any, error) {
 663     res := items[:0]
 664     for i, v := range items {
 665         if _, ok := avoid[i]; ok {
 666             continue
 667         }
 668         res = append(res, v)
 669     }
 670     return res, nil
 671 }
 672 
 673 func pickObjectItems(items dictionary, keys []string) (any, error) {
 674     var res dictionary
 675     res.Keys = items.Keys[:0]
 676     res.Map = items.Map
 677 
 678     for _, k := range keys {
 679         match, _, ok := matchObjectKey(items, k)
 680         if !ok {
 681             continue
 682         }
 683 
 684         if _, ok := res.Map[match]; !ok {
 685             res.Keys = append(res.Keys, match)
 686         }
 687     }
 688 
 689     return res, nil
 690 }
 691 
 692 func dropObjectItems(items dictionary, avoid map[string]struct{}) (any, error) {
 693     var res dictionary
 694     res.Keys = items.Keys[:0]
 695     res.Map = items.Map
 696 
 697     for _, k := range items.Keys {
 698         if hasFold(avoid, k) {
 699             continue
 700         }
 701 
 702         if _, ok := res.Map[k]; !ok {
 703             res.Keys = append(res.Keys, k)
 704         }
 705     }
 706 
 707     return res, nil
 708 }
 709 
 710 func hasFold(avoid map[string]struct{}, s string) bool {
 711     for v := range avoid {
 712         if v == s || strings.EqualFold(v, s) {
 713             return true
 714         }
 715     }
 716     return false
 717 }
 718 
 719 func appendIndices(dest map[int]struct{}, items []any, keys []string) {
 720     for _, k := range keys {
 721         i, err := strconv.ParseInt(k, 10, 64)
 722         if err != nil {
 723             continue
 724         }
 725 
 726         if i < 0 {
 727             i += int64(len(items))
 728         }
 729 
 730         if 0 <= i && i < int64(len(items)) {
 731             dest[int(i)] = struct{}{}
 732         }
 733     }
 734 }
 735 
 736 func writeValue(w *bufio.Writer, data any) error {
 737     switch data := data.(type) {
 738     case nil:
 739         return writeKeyword(w, `null`)
 740     case bool:
 741         if data {
 742             return writeKeyword(w, `true`)
 743         }
 744         return writeKeyword(w, `false`)
 745     case json.Number:
 746         if _, err := w.WriteString(data.String()); err != nil {
 747             return io.EOF
 748         }
 749         return nil
 750     case string:
 751         return writeEscapedString(w, data)
 752     case []any:
 753         return writeArray(w, data)
 754     case dictionary:
 755         return writeObject(w, data)
 756     default:
 757         return errors.New(`invalid JSON value`)
 758     }
 759 }
 760 
 761 func writeByte(w *bufio.Writer, b byte) error {
 762     err := w.WriteByte(b)
 763     if err != nil {
 764         return io.EOF
 765     }
 766     return nil
 767 }
 768 
 769 func writeKeyword(w *bufio.Writer, s string) error {
 770     if _, err := w.WriteString(s); err == nil {
 771         return nil
 772     }
 773     return io.EOF
 774 }
 775 
 776 func writeEscapedString(w *bufio.Writer, s string) error {
 777     if !needsEscaping(s) {
 778         w.WriteByte('"')
 779         w.WriteString(s)
 780         return writeByte(w, '"')
 781     }
 782 
 783     w.WriteByte('"')
 784 
 785     for _, r := range s {
 786         if ' ' <= r && r <= '~' && r != '\\' && r != '"' {
 787             w.WriteRune(r)
 788             continue
 789         }
 790 
 791         switch r {
 792         case '\\':
 793             w.WriteString(`\\`)
 794         case '"':
 795             w.WriteString(`\"`)
 796         default:
 797             writeEscapedHex(w, r)
 798         }
 799     }
 800 
 801     return writeByte(w, '"')
 802 }
 803 
 804 func writeEscapedHex(w *bufio.Writer, r rune) error {
 805     w.WriteByte('\\')
 806     w.WriteByte('u')
 807     writeHex(w, byte(r>>24))
 808     writeHex(w, byte(r>>16))
 809     writeHex(w, byte(r>>8))
 810     writeHex(w, byte(r))
 811     return nil
 812 }
 813 
 814 // writeHex is faster than calling fmt.Fprintf(w, `%04x`, b)
 815 func writeHex(w *bufio.Writer, b byte) {
 816     const hexDigits = `0123456789abcdef`
 817     w.WriteByte(hexDigits[b>>4])
 818     w.WriteByte(hexDigits[b&0x0f])
 819 }
 820 
 821 func needsEscaping(s string) bool {
 822     for i := range s {
 823         if b := s[i]; ' ' <= b && b <= '~' && b != '\\' && b != '"' {
 824             continue
 825         }
 826         return true
 827     }
 828 
 829     return false
 830 }
 831 
 832 func writeArray(w *bufio.Writer, items []any) error {
 833     w.WriteByte('[')
 834     for i, v := range items {
 835         if i > 0 {
 836             if err := writeByte(w, ','); err != nil {
 837                 return err
 838             }
 839         }
 840         if err := writeValue(w, v); err != nil {
 841             return err
 842         }
 843     }
 844     return writeByte(w, ']')
 845 }
 846 
 847 func writeObject(w *bufio.Writer, items dictionary) error {
 848     w.WriteByte('{')
 849     for i, k := range items.Keys {
 850         if i > 0 {
 851             if err := writeByte(w, ','); err != nil {
 852                 return err
 853             }
 854         }
 855         writeEscapedString(w, k)
 856         w.WriteByte(':')
 857         if err := writeValue(w, items.Map[k]); err != nil {
 858             return err
 859         }
 860     }
 861     return writeByte(w, '}')
 862 }
 863 
 864 func indexPair(s string, x byte, y byte) int {
 865     var cur, prev byte
 866 
 867     for i := range s {
 868         cur = s[i]
 869         if prev == x && cur == y && i > 0 {
 870             return i
 871         }
 872         prev = cur
 873     }
 874 
 875     return -1
 876 }
     File: ./zj/zj_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package zj
  26 
  27 import (
  28     "io"
  29     "strings"
  30     "testing"
  31 )
  32 
  33 func TestZoomJson(t *testing.T) {
  34     tests := map[string]struct {
  35         input    string
  36         expected string
  37         zoom     string
  38     }{
  39         `no-zoom number`: {`123.456`, `123.456`, ``},
  40     }
  41 
  42     for name, tc := range tests {
  43         t.Run(name, func(t *testing.T) {
  44             data, err := load(strings.NewReader(tc.input))
  45             if err != nil {
  46                 t.Error(err)
  47                 return
  48             }
  49 
  50             var avoid sets
  51             avoid.indices = make(map[int]struct{})
  52             avoid.keys = make(map[string]struct{})
  53 
  54             var keys []string
  55             if tc.zoom != `` {
  56                 keys = strings.Split(tc.zoom, ` `)
  57             }
  58 
  59             data, err = zoom(data, keys, &avoid)
  60             if err != nil {
  61                 t.Error(err)
  62                 return
  63             }
  64 
  65             var sb strings.Builder
  66             err = json0(&sb, data)
  67             if err != nil && err != io.EOF {
  68                 t.Error(err)
  69                 return
  70             }
  71 
  72             got := sb.String()
  73             got, _ = strings.CutSuffix(got, "\n")
  74             if got != tc.expected {
  75                 t.Errorf(`expected %q but got %q instead`, tc.expected, got)
  76                 return
  77             }
  78         })
  79     }
  80 }