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 )
  34 
  35 const info = `
  36 avoid [options...] [regular expressions...]
  37 
  38 Avoid/ignore lines which match any of the extended-mode regular expressions
  39 given. When not given any regex, all empty lines are ignored by default.
  40 
  41 The options are, available both in single and double-dash versions
  42 
  43     -h, -help    show this help message
  44     -i, -ins     match regexes case-insensitively
  45 `
  46 
  47 func Main() {
  48     nerr := 0
  49     buffered := false
  50     sensitive := true
  51     args := os.Args[1:]
  52 
  53     for len(args) > 0 {
  54         switch args[0] {
  55         case `-b`, `--b`, `-buffered`, `--buffered`:
  56             buffered = true
  57             args = args[1:]
  58             continue
  59 
  60         case `-h`, `--h`, `-help`, `--help`:
  61             os.Stdout.WriteString(info[1:])
  62             return
  63 
  64         case `-i`, `--i`, `-ins`, `--ins`:
  65             sensitive = false
  66             args = args[1:]
  67             continue
  68         }
  69 
  70         break
  71     }
  72 
  73     if len(args) > 0 && args[0] == `--` {
  74         args = args[1:]
  75     }
  76 
  77     liveLines := !buffered
  78     if !buffered {
  79         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  80             liveLines = false
  81         }
  82     }
  83 
  84     if len(args) == 0 {
  85         args = []string{`^$`}
  86     }
  87 
  88     exprs := make([]*regexp.Regexp, 0, len(args))
  89 
  90     for _, src := range args {
  91         var err error
  92         var exp *regexp.Regexp
  93         if !sensitive {
  94             exp, err = regexp.Compile(`(?i)` + src)
  95         } else {
  96             exp, err = regexp.Compile(src)
  97         }
  98 
  99         if err != nil {
 100             os.Stderr.WriteString(err.Error())
 101             os.Stderr.WriteString("\n")
 102             nerr++
 103         }
 104 
 105         exprs = append(exprs, exp)
 106     }
 107 
 108     if nerr > 0 {
 109         os.Exit(1)
 110         return
 111     }
 112 
 113     var buf []byte
 114     sc := bufio.NewScanner(os.Stdin)
 115     sc.Buffer(nil, 8*1024*1024*1024)
 116     bw := bufio.NewWriter(os.Stdout)
 117 
 118     for i := 0; sc.Scan(); i++ {
 119         line := sc.Bytes()
 120         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 121             line = line[3:]
 122         }
 123 
 124         s := line
 125         if bytes.IndexByte(s, '\x1b') >= 0 {
 126             buf = plain(buf[:0], s)
 127             s = buf
 128         }
 129 
 130         if !match(s, exprs) {
 131             bw.Write(line)
 132             bw.WriteByte('\n')
 133 
 134             if !liveLines {
 135                 continue
 136             }
 137 
 138             if err := bw.Flush(); err != nil {
 139                 return
 140             }
 141         }
 142     }
 143 }
 144 
 145 func match(what []byte, with []*regexp.Regexp) bool {
 146     for _, e := range with {
 147         if e.Match(what) {
 148             return true
 149         }
 150     }
 151     return false
 152 }
 153 
 154 func plain(dst []byte, src []byte) []byte {
 155     for len(src) > 0 {
 156         i, j := indexEscapeSequence(src)
 157         if i < 0 {
 158             dst = append(dst, src...)
 159             break
 160         }
 161         if j < 0 {
 162             j = len(src)
 163         }
 164 
 165         if i > 0 {
 166             dst = append(dst, src[:i]...)
 167         }
 168 
 169         src = src[j:]
 170     }
 171 
 172     return dst
 173 }
 174 
 175 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 176 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 177 // indices which can be independently negative when either the start/end of
 178 // a sequence isn't found; given their fairly-common use, even the hyperlink
 179 // ESC]8 sequences are supported
 180 func indexEscapeSequence(s []byte) (int, int) {
 181     var prev byte
 182 
 183     for i, b := range s {
 184         if prev == '\x1b' && b == '[' {
 185             j := indexLetter(s[i+1:])
 186             if j < 0 {
 187                 return i, -1
 188             }
 189             return i - 1, i + 1 + j + 1
 190         }
 191 
 192         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 193             j := indexPair(s[i+1:], '\x1b', '\\')
 194             if j < 0 {
 195                 return i, -1
 196             }
 197             return i - 1, i + 1 + j + 2
 198         }
 199 
 200         prev = b
 201     }
 202 
 203     return -1, -1
 204 }
 205 
 206 func indexLetter(s []byte) int {
 207     for i, b := range s {
 208         upper := b &^ 32
 209         if 'A' <= upper && upper <= 'Z' {
 210             return i
 211         }
 212     }
 213 
 214     return -1
 215 }
 216 
 217 func indexPair(s []byte, x byte, y byte) int {
 218     var prev byte
 219 
 220     for i, b := range s {
 221         if prev == x && b == y && i > 0 {
 222             return i
 223         }
 224         prev = b
 225     }
 226 
 227     return -1
 228 }
     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(err.Error())
  72         os.Stderr.WriteString("\n")
  73         os.Exit(1)
  74         return
  75     }
  76 }
  77 
  78 type handler func(w io.Writer, r io.Reader) error
  79 
  80 func run(w io.Writer, paths []string, handle handler) error {
  81     for _, path := range paths {
  82         if err := handleFile(os.Stdout, path, handle); err != nil {
  83             return err
  84         }
  85     }
  86 
  87     if len(paths) == 0 {
  88         if err := handle(os.Stdout, os.Stdin); err != nil {
  89             return err
  90         }
  91     }
  92 
  93     return nil
  94 }
  95 
  96 func handleFile(w io.Writer, path string, handle handler) error {
  97     f, err := os.Open(path)
  98     if err != nil {
  99         return err
 100     }
 101     defer f.Close()
 102     return handle(w, f)
 103 }
 104 
 105 func decode(w io.Writer, r io.Reader) error {
 106     return debase64(w, r)
 107 }
 108 
 109 func encode(w io.Writer, r io.Reader) error {
 110     enc := base64.NewEncoder(base64.StdEncoding, w)
 111     _, err := io.Copy(enc, r)
 112     enc.Close()
 113 
 114     if err == nil {
 115         w.Write([]byte{'\n'})
 116     }
 117     return err
 118 }
 119 
 120 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
 121 // apart from output-writing ones
 122 func debase64(w io.Writer, r io.Reader) error {
 123     br := bufio.NewReaderSize(r, 32*1024)
 124     start, err := br.Peek(64)
 125     if err != nil && err != io.EOF {
 126         return err
 127     }
 128 
 129     skip, err := skipIntroDataURI(start)
 130     if err != nil {
 131         return err
 132     }
 133 
 134     if skip > 0 {
 135         br.Discard(skip)
 136     }
 137 
 138     dec := base64.NewDecoder(base64.StdEncoding, br)
 139     _, err = io.Copy(w, dec)
 140     return err
 141 }
 142 
 143 func skipIntroDataURI(chunk []byte) (skip int, err error) {
 144     if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 145         chunk = chunk[3:]
 146         skip += 3
 147     }
 148 
 149     if !bytes.HasPrefix(chunk, []byte(`data:`)) {
 150         return skip, nil
 151     }
 152 
 153     const l = len(`data:,`)
 154     if len(chunk) == l && chunk[l-1] == ',' {
 155         return l, nil
 156     }
 157 
 158     start := chunk
 159     if len(start) > 64 {
 160         start = start[:64]
 161     }
 162 
 163     i := bytes.Index(start, []byte(`;base64,`))
 164     if i < 0 {
 165         return skip, errors.New(`invalid data URI`)
 166     }
 167 
 168     skip += i + len(`;base64,`)
 169     return skip, nil
 170 }
     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(err.Error())
 139         os.Stderr.WriteString("\n")
 140         os.Exit(1)
 141         return
 142     }
 143 }
 144 
 145 type params struct {
 146     offset     *int64
 147     emitOffset func(w *bufio.Writer, offset int64, sep byte)
 148     separator  byte
 149 }
 150 
 151 func run(w io.Writer, p params, args []string) error {
 152     offset := int64(0)
 153     p.offset = &offset
 154     bw := bufio.NewWriter(w)
 155     defer func() {
 156         if offset%itemsPerLine == 0 && offset > 0 {
 157             bw.WriteByte('\n')
 158         }
 159         bw.Flush()
 160     }()
 161 
 162     if len(args) == 0 {
 163         return bitdump(bw, os.Stdin, p)
 164     }
 165 
 166     for _, name := range args {
 167         if err := handleFile(bw, name, p); err != nil {
 168             return err
 169         }
 170     }
 171     return nil
 172 }
 173 
 174 func handleFile(w *bufio.Writer, name string, p params) error {
 175     if name == `` || name == `-` {
 176         return bitdump(w, os.Stdin, p)
 177     }
 178 
 179     f, err := os.Open(name)
 180     if err != nil {
 181         return errors.New(`can't read from file named "` + name + `"`)
 182     }
 183     defer f.Close()
 184 
 185     return bitdump(w, f, p)
 186 }
 187 
 188 func bitdump(w *bufio.Writer, r io.Reader, p params) error {
 189     var buf [32 * 1024]byte
 190     defer w.Flush()
 191 
 192     for {
 193         n, err := r.Read(buf[:])
 194         if n < 1 && err == io.EOF {
 195             return nil
 196         }
 197 
 198         if err != nil && err != io.EOF {
 199             return err
 200         }
 201 
 202         chunk := buf[:n]
 203 
 204         for len(chunk) >= itemsPerLine {
 205             if err := emitChunk(w, chunk[:itemsPerLine], p); err != nil {
 206                 return err
 207             }
 208 
 209             chunk = chunk[itemsPerLine:]
 210             *p.offset += itemsPerLine
 211         }
 212 
 213         if len(chunk) > 0 {
 214             if err := emitChunk(w, chunk, p); err != nil {
 215                 return err
 216             }
 217 
 218             *p.offset += int64(len(chunk))
 219         }
 220     }
 221 }
 222 
 223 func emitDecimalOffset(w *bufio.Writer, offset int64, sep byte) {
 224     const pad = `00000000`
 225     var str [24]byte
 226 
 227     s := strconv.AppendInt(str[:0], offset, 10)
 228     // pad small offsets with leading zeros
 229     if len(s) < len(pad) {
 230         w.WriteString(pad[len(s):])
 231     }
 232     w.Write(s)
 233     w.WriteByte(sep)
 234 }
 235 
 236 func emitNoOffset(w *bufio.Writer, offset int64, sep byte) {
 237     // deliberately does nothing
 238 }
 239 
 240 func emitChunk(w *bufio.Writer, chunk []byte, p params) error {
 241     p.emitOffset(w, *p.offset, p.separator)
 242     for i, b := range chunk {
 243         if i > 0 {
 244             w.WriteByte(p.separator)
 245         }
 246         w.WriteString(bits[b])
 247     }
 248 
 249     if err := w.WriteByte('\n'); err != nil {
 250         // assume a write error is the consequence of stdout
 251         // being closed, perhaps by another app along a pipe
 252         return io.EOF
 253     }
 254     return nil
 255 }
     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(err.Error())
 103             os.Stderr.WriteString("\n")
 104             nerr++
 105         }
 106 
 107         exprs = append(exprs, exp)
 108     }
 109 
 110     if nerr > 0 {
 111         os.Exit(1)
 112         return
 113     }
 114 
 115     var buf []byte
 116     sc := bufio.NewScanner(os.Stdin)
 117     sc.Buffer(nil, 8*1024*1024*1024)
 118     bw := bufio.NewWriter(os.Stdout)
 119 
 120     for i := 0; sc.Scan(); i++ {
 121         line := sc.Bytes()
 122         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 123             line = line[3:]
 124         }
 125 
 126         s := line
 127         if bytes.IndexByte(s, '\x1b') >= 0 {
 128             buf = plain(buf[:0], s)
 129             s = buf
 130         }
 131 
 132         for len(s) > 0 {
 133             start, end := findEarliest(s, exprs)
 134             if start < 0 {
 135                 if err := emit(bw, s, liveLines); err != nil {
 136                     return
 137                 }
 138                 break
 139             }
 140 
 141             if err := emit(bw, s[:start], liveLines); err != nil {
 142                 return
 143             }
 144             s = s[end:]
 145         }
 146     }
 147 }
 148 
 149 func findEarliest(s []byte, exprs []*regexp.Regexp) (start int, end int) {
 150     start = -1
 151     end = -1
 152 
 153     for _, e := range exprs {
 154         m := e.FindIndex(s)
 155         if len(m) == 2 && m[0] != m[1] && (m[0] < start || start < 0) {
 156             start = m[0]
 157             end = m[1]
 158         }
 159     }
 160 
 161     return start, end
 162 }
 163 
 164 func emit(w *bufio.Writer, line []byte, live bool) error {
 165     w.Write(line)
 166     w.WriteByte('\n')
 167 
 168     if !live {
 169         return nil
 170     }
 171 
 172     return w.Flush()
 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     "go/ast"
  30     "go/parser"
  31     "go/token"
  32     "io"
  33     "math"
  34     "math/big"
  35     "os"
  36     "strings"
  37 )
  38 
  39 const info = `
  40 ca [options...] [go expressions...]
  41 
  42 CAlculate arbitrary-size fractions, using go expressions. For convenience,
  43 function names are case-insensitive, and square brackets are treated the
  44 same as (round) parentheses.
  45 
  46 Several functions are available, along with their aliases:
  47 
  48     abs(x)
  49     bits(x)
  50     c(n, k)          choose, com, comb, combinations
  51     ceil(x)          ceiling
  52     dbin(x, n, p)    dbinom
  53     den(x)           denom, denominator
  54     digits(x)
  55     f(x)             fac, fact, factorial
  56     floor(x)
  57     isprime(n)       prime
  58     gcd(x, y)        gcf
  59     lcm(x, y)
  60     num(x)           numer, numerator
  61     p(n, k)          per, perm, permutations
  62     pow(x, y)        power
  63     pow2(x)          power2
  64     pow10(x)         power10
  65     rem(x, y)        remainder
  66     sgn(x)           sign
  67 
  68     avg(...)         mean
  69     max(...)
  70     min(...)
  71     polyval(x, ...)  horner
  72 
  73 Note: when the exponent given to the pow/power function isn't an integer,
  74 the result is a double-precision floating-point approximation.
  75 
  76 All (optional) leading options start with either single or double-dash:
  77 
  78     -d, -decs, -decimals    show decimal digits, instead of fractions
  79     -h, -help               show this help message
  80 `
  81 
  82 func Main() {
  83     args := os.Args[1:]
  84     showAsFrac := true
  85 
  86     if len(args) > 0 {
  87         switch args[0] {
  88         case `-h`, `--h`, `-help`, `--help`:
  89             os.Stdout.WriteString(info[1:])
  90             return
  91 
  92         case `-d`, `--d`, `-decs`, `--decs`, `-decimals`, `--decimals`:
  93             showAsFrac = false
  94             args = args[1:]
  95         }
  96     }
  97 
  98     if len(args) > 0 && args[0] == `--` {
  99         args = args[1:]
 100     }
 101 
 102     if len(args) == 0 {
 103         os.Stderr.WriteString(info[1:])
 104         os.Exit(1)
 105         return
 106     }
 107 
 108     if err := run(os.Stdout, args, showAsFrac); err != nil && err != io.EOF {
 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, showAsFrac bool) error {
 117     for _, src := range args {
 118         src = strings.ToLower(src)
 119 
 120         // treat square brackets like parentheses, for convenience
 121         src = strings.Replace(src, `[`, `(`, -1)
 122         src = strings.Replace(src, `]`, `)`, -1)
 123 
 124         expr, err := parser.ParseExpr(src)
 125         if err != nil {
 126             return err
 127         }
 128 
 129         n, err := eval(expr)
 130         if err != nil {
 131             return err
 132         }
 133 
 134         // only show the numerator, when the denominator is 1; when showing
 135         // results as numbers with decimals, all trailing zero decimals are
 136         // ignored
 137         s := ``
 138         if n.IsInt() {
 139             s = n.Num().String()
 140         } else if showAsFrac {
 141             s = n.String()
 142         } else {
 143             s = trimDecimals(n.FloatString(100))
 144         }
 145 
 146         io.WriteString(w, s)
 147         _, err = io.WriteString(w, "\n")
 148 
 149         if err != nil {
 150             break
 151         }
 152     }
 153 
 154     return nil
 155 }
 156 
 157 // trimDecimals ignores excessive trailing decimal zeros, if any, as well as
 158 // the decimal dot itself, if all decimals turn out to be zeros; integers are
 159 // returned as given
 160 func trimDecimals(s string) string {
 161     // with no decimals, keep all/any trailing zeros
 162     if strings.IndexByte(s, '.') < 0 {
 163         return s
 164     }
 165 
 166     // ignore all trailing zero decimals
 167     for len(s) > 0 && s[len(s)-1] == '0' {
 168         s = s[:len(s)-1]
 169     }
 170     // ignore trailing decimal
 171     if len(s) > 0 && s[len(s)-1] == '.' {
 172         s = s[:len(s)-1]
 173     }
 174     return s
 175 }
 176 
 177 func eval(expr ast.Expr) (*big.Rat, error) {
 178     switch expr := expr.(type) {
 179     case *ast.BasicLit:
 180         return evalLit(expr)
 181     case *ast.ParenExpr:
 182         return eval(expr.X)
 183     case *ast.UnaryExpr:
 184         return evalUnary(expr)
 185     case *ast.BinaryExpr:
 186         return evalBinary(expr)
 187     case *ast.CallExpr:
 188         return evalCall(expr)
 189     case *ast.Ident:
 190         return evalIdent(expr)
 191     }
 192 
 193     return nil, errors.New(`unsupported expression type`)
 194 }
 195 
 196 func evalLit(expr *ast.BasicLit) (*big.Rat, error) {
 197     switch expr.Kind {
 198     case token.INT, token.FLOAT:
 199         n := big.NewRat(0, 1)
 200         n, _ = n.SetString(expr.Value)
 201         return n, nil
 202     }
 203 
 204     return nil, errors.New(`unsupported literal type`)
 205 }
 206 
 207 func evalUnary(expr *ast.UnaryExpr) (*big.Rat, error) {
 208     switch expr.Op {
 209     case token.ADD:
 210         return eval(expr.X)
 211 
 212     case token.SUB:
 213         n, err := eval(expr.X)
 214         if n != nil {
 215             n = n.Neg(n)
 216         }
 217         return n, err
 218 
 219     case token.NOT:
 220         return eval(&ast.CallExpr{
 221             Fun:  ast.NewIdent(`factorial`),
 222             Args: []ast.Expr{expr.X},
 223         })
 224     }
 225 
 226     return nil, errors.New(`unsupported unary operation ` + expr.Op.String())
 227 }
 228 
 229 func evalBinary(expr *ast.BinaryExpr) (*big.Rat, error) {
 230     x, err := eval(expr.X)
 231     if err != nil {
 232         return nil, err
 233     }
 234 
 235     y, err := eval(expr.Y)
 236     if err != nil {
 237         return nil, err
 238     }
 239 
 240     z := big.NewRat(0, 1)
 241 
 242     switch expr.Op {
 243     case token.ADD:
 244         z = z.Add(x, y)
 245         return z, nil
 246 
 247     case token.SUB:
 248         z = z.Sub(x, y)
 249         return z, nil
 250 
 251     case token.MUL:
 252         z = z.Mul(x, y)
 253         return z, nil
 254 
 255     case token.QUO:
 256         if y.Sign() == 0 {
 257             return nil, errors.New(`can't divide by zero`)
 258         }
 259         z = z.Quo(x, y)
 260         return z, nil
 261 
 262     case token.REM:
 263         return remainder(x, y)
 264     }
 265 
 266     return nil, errors.New(`unsupported binary operation ` + expr.Op.String())
 267 }
 268 
 269 func evalCall(expr *ast.CallExpr) (*big.Rat, error) {
 270     ident, ok := expr.Fun.(*ast.Ident)
 271     if !ok {
 272         return nil, errors.New(`unsupported function type`)
 273     }
 274     s := ident.Name
 275 
 276     if _, ok := varFuncs[s]; ok {
 277         return evalVarCall(s, expr)
 278     }
 279 
 280     switch len(expr.Args) {
 281     case 1:
 282         return evalCall1(s, expr)
 283     case 2:
 284         return evalCall2(s, expr)
 285     case 3:
 286         return evalCall3(s, expr)
 287     }
 288 
 289     return nil, errors.New(`function '` + s + `' not available`)
 290 }
 291 
 292 func evalIdent(expr *ast.Ident) (*big.Rat, error) {
 293     s := strings.ToLower(expr.Name)
 294     if v, ok := values[s]; ok {
 295         if f, ok := big.NewRat(0, 1).SetString(v); ok {
 296             return f, nil
 297         }
 298         return nil, errors.New(`value '` + s + `' isn't a valid number`)
 299     }
 300     return nil, errors.New(`value '` + s + `' not available`)
 301 }
 302 
 303 func copyFrac(x *big.Rat) *big.Rat {
 304     y := big.NewRat(0, 1)
 305     y = y.Add(y, x)
 306     return y
 307 }
 308 
 309 var values = map[string]string{
 310     `kb`:  `1024`,
 311     `mb`:  `1048576`,
 312     `gb`:  `1073741824`,
 313     `tb`:  `1099511627776`,
 314     `pb`:  `1125899906842624`,
 315     `kib`: `1024`,
 316     `mib`: `1048576`,
 317     `gib`: `1073741824`,
 318     `tib`: `1099511627776`,
 319     `pib`: `1125899906842624`,
 320 
 321     `hour`: `3600`,
 322     `hr`:   `3600`,
 323     `day`:  `86400`,
 324     `week`: `604800`,
 325     `wk`:   `604800`,
 326 
 327     `mol`:  `602214076000000000000000`,
 328     `mole`: `602214076000000000000000`,
 329 }
 330 
 331 var funcs1 = map[string]func(*big.Rat) (*big.Rat, error){
 332     `abs`:         abs,
 333     `bits`:        bits,
 334     `ceil`:        ceiling,
 335     `ceiling`:     ceiling,
 336     `den`:         denominator,
 337     `denom`:       denominator,
 338     `denominator`: denominator,
 339     `digits`:      digits,
 340     `f`:           factorial,
 341     `fac`:         factorial,
 342     `fact`:        factorial,
 343     `factorial`:   factorial,
 344     `floor`:       floor,
 345     `isprime`:     isPrime,
 346     `prime`:       isPrime,
 347     `num`:         numerator,
 348     `numer`:       numerator,
 349     `numerator`:   numerator,
 350     `pow2`:        power2,
 351     `power2`:      power2,
 352     `pow10`:       power10,
 353     `power10`:     power10,
 354     `sgn`:         sign,
 355     `sign`:        sign,
 356 }
 357 
 358 func evalCall1(name string, expr *ast.CallExpr) (*big.Rat, error) {
 359     x, err := eval(expr.Args[0])
 360     if err != nil {
 361         return nil, err
 362     }
 363 
 364     fn, ok := funcs1[name]
 365     if !ok {
 366         return nil, errors.New(`function '` + name + `' not available`)
 367     }
 368 
 369     return fn(x)
 370 }
 371 
 372 var funcs2 = map[string]func(*big.Rat, *big.Rat) (*big.Rat, error){
 373     `c`:            combinations,
 374     `com`:          combinations,
 375     `comb`:         combinations,
 376     `combinations`: combinations,
 377     `choose`:       combinations,
 378     `gcd`:          gcd,
 379     `gcf`:          gcd,
 380     `lcm`:          lcm,
 381     `p`:            permutations,
 382     `per`:          permutations,
 383     `perm`:         permutations,
 384     `permutations`: permutations,
 385     `pow`:          power,
 386     `power`:        power,
 387     `rem`:          remainder,
 388     `remainder`:    remainder,
 389 }
 390 
 391 func evalCall2(name string, expr *ast.CallExpr) (*big.Rat, error) {
 392     x, err := eval(expr.Args[0])
 393     if err != nil {
 394         return nil, err
 395     }
 396 
 397     y, err := eval(expr.Args[1])
 398     if err != nil {
 399         return nil, err
 400     }
 401 
 402     fn, ok := funcs2[name]
 403     if !ok {
 404         return nil, errors.New(`function '` + name + `' not available`)
 405     }
 406 
 407     return fn(x, y)
 408 }
 409 
 410 var funcs3 = map[string]func(*big.Rat, *big.Rat, *big.Rat) (*big.Rat, error){
 411     `db`:     dbinom,
 412     `dbin`:   dbinom,
 413     `dbinom`: dbinom,
 414 }
 415 
 416 func evalCall3(name string, expr *ast.CallExpr) (*big.Rat, error) {
 417     x, err := eval(expr.Args[0])
 418     if err != nil {
 419         return nil, err
 420     }
 421 
 422     y, err := eval(expr.Args[1])
 423     if err != nil {
 424         return nil, err
 425     }
 426 
 427     z, err := eval(expr.Args[2])
 428     if err != nil {
 429         return nil, err
 430     }
 431 
 432     fn, ok := funcs3[name]
 433     if !ok {
 434         return nil, errors.New(`function '` + name + `' not available`)
 435     }
 436 
 437     return fn(x, y, z)
 438 }
 439 
 440 var varFuncs = map[string]func(...*big.Rat) (*big.Rat, error){
 441     `avg`:     avgNum,
 442     `horner`:  polyval,
 443     `max`:     maxNum,
 444     `mean`:    avgNum,
 445     `min`:     minNum,
 446     `polyval`: polyval,
 447     `sum`:     sumNum,
 448 }
 449 
 450 func evalVarCall(name string, expr *ast.CallExpr) (*big.Rat, error) {
 451     fn, ok := varFuncs[name]
 452     if !ok {
 453         return nil, errors.New(`function '` + name + `' not available`)
 454     }
 455 
 456     inputs := make([]*big.Rat, 0, len(expr.Args))
 457     for _, a := range expr.Args {
 458         v, err := eval(a)
 459         if err != nil {
 460             return nil, err
 461         }
 462         inputs = append(inputs, v)
 463     }
 464 
 465     return fn(inputs...)
 466 }
 467 
 468 func abs(n *big.Rat) (*big.Rat, error) {
 469     n = n.Abs(n)
 470     return n, nil
 471 }
 472 
 473 func avgNum(values ...*big.Rat) (*big.Rat, error) {
 474     if len(values) == 0 {
 475         return nil, errors.New(`mean: no numbers given`)
 476     }
 477 
 478     res := big.NewRat(0, 1)
 479     for _, v := range values {
 480         res = res.Add(res, v)
 481     }
 482     res = res.Quo(res, big.NewRat(int64(len(values)), 1))
 483     return res, nil
 484 }
 485 
 486 func bits(n *big.Rat) (*big.Rat, error) {
 487     if !n.IsInt() {
 488         return nil, errors.New(`function 'bits' only works with integers`)
 489     }
 490 
 491     bits := big.NewRat(0, 1)
 492     bits.SetInt64(int64(n.Num().BitLen()))
 493     return bits, nil
 494 }
 495 
 496 func ceiling(n *big.Rat) (*big.Rat, error) {
 497     if n.IsInt() {
 498         return n, nil
 499     }
 500 
 501     v := big.NewInt(0)
 502     v = v.Quo(n.Num(), n.Denom())
 503     if n.Sign() >= 0 {
 504         v = v.Add(v, big.NewInt(1))
 505     }
 506     n = n.SetInt(v)
 507     return n, nil
 508 }
 509 
 510 func combinations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
 511     if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
 512         const msg = `combinations are defined only for non-negative integers`
 513         return nil, errors.New(msg)
 514     }
 515 
 516     v, err := permutations(n, k)
 517     if err != nil {
 518         return v, err
 519     }
 520 
 521     f, err := factorial(k)
 522     if err != nil {
 523         return nil, err
 524     }
 525 
 526     if f.Sign() <= 0 {
 527         return nil, errors.New(`combinations: factorial isn't positive`)
 528     }
 529     return v.Quo(v, f), nil
 530 }
 531 
 532 func dbinom(x *big.Rat, n *big.Rat, p *big.Rat) (*big.Rat, error) {
 533     a, err := combinations(copyFrac(n), copyFrac(x))
 534     if err != nil {
 535         return nil, err
 536     }
 537 
 538     b, err := power(copyFrac(p), copyFrac(x))
 539     if err != nil {
 540         return nil, err
 541     }
 542 
 543     // c = (1 - p) ** (n - x)
 544     y := big.NewRat(1, 1)
 545     y = y.Sub(y, p)
 546     z := copyFrac(n)
 547     z = z.Sub(z, x)
 548     c, err := power(y, z)
 549     if err != nil {
 550         return nil, err
 551     }
 552 
 553     // return combinations(n, x) * (p ** x) * ((1 - p) ** (n - x))
 554     d := big.NewRat(0, 1)
 555     d = d.Add(d, a)
 556     d = d.Mul(d, b)
 557     d = d.Mul(d, c)
 558     return d, nil
 559 }
 560 
 561 func denominator(n *big.Rat) (*big.Rat, error) {
 562     return big.NewRat(0, 1).SetFrac(n.Denom(), big.NewInt(1)), nil
 563 }
 564 
 565 func digits(n *big.Rat) (*big.Rat, error) {
 566     if !n.IsInt() {
 567         return nil, errors.New(`function 'digits' only works with integers`)
 568     }
 569 
 570     digits := big.NewRat(0, 1)
 571     digits.SetInt64(int64(len(n.Num().String())))
 572     return digits, nil
 573 }
 574 
 575 func factorial(n *big.Rat) (*big.Rat, error) {
 576     sign := n.Sign()
 577     if sign < 0 {
 578         return nil, errors.New(`factorials aren't defined for negatives`)
 579     }
 580     if sign == 0 {
 581         return big.NewRat(1, 1), nil
 582     }
 583 
 584     f := big.NewRat(1, 1)
 585     for one := big.NewRat(1, 1); n.Sign() > 0; n = n.Sub(n, one) {
 586         f = f.Mul(f, n)
 587     }
 588     return f, nil
 589 }
 590 
 591 func floor(n *big.Rat) (*big.Rat, error) {
 592     if n.IsInt() {
 593         return n, nil
 594     }
 595 
 596     v := big.NewInt(0)
 597     v = v.Quo(n.Num(), n.Denom())
 598     if n.Sign() < 0 {
 599         v = v.Sub(v, big.NewInt(1))
 600     }
 601     n = n.SetInt(v)
 602     return n, nil
 603 }
 604 
 605 func gcd(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 606     if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
 607         const msg = `gcd are defined only for positive integers`
 608         return nil, errors.New(msg)
 609     }
 610 
 611     gcd := big.NewRat(0, 1)
 612     gcd = gcd.Add(gcd, x)
 613     gcd = gcd.Mul(gcd, y)
 614 
 615     lcm, err := lcm(x, y)
 616     if err != nil {
 617         return nil, err
 618     }
 619     if lcm.Sign() <= 0 {
 620         return nil, errors.New(`gcd: lcm isn't positive`)
 621     }
 622 
 623     gcd = gcd.Quo(gcd, lcm)
 624     return gcd, nil
 625 }
 626 
 627 func isPrime(n *big.Rat) (*big.Rat, error) {
 628     if !n.IsInt() {
 629         return nil, errors.New(`function 'isprime' only works with integers`)
 630     }
 631 
 632     if n.Sign() <= 0 {
 633         return big.NewRat(0, 1), nil
 634     }
 635 
 636     v := n.Num()
 637     if v.IsInt64() {
 638         n := v.Int64()
 639         if n == 2 {
 640             return big.NewRat(1, 1), nil
 641         }
 642         if n < 2 || n%2 == 0 {
 643             return big.NewRat(0, 1), nil
 644         }
 645     }
 646 
 647     two := big.NewInt(2)
 648     max := big.NewInt(1).Sqrt(v)
 649     mod := big.NewInt(0)
 650     for i := big.NewInt(3); i.Cmp(max) <= 0; i = i.Add(i, two) {
 651         mod = mod.Rem(v, i)
 652         if mod.Sign() == 0 {
 653             return big.NewRat(0, 1), nil
 654         }
 655     }
 656     return big.NewRat(1, 1), nil
 657 }
 658 
 659 func lcm(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 660     if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
 661         const msg = `lcm is defined only for positive integers`
 662         return nil, errors.New(msg)
 663     }
 664 
 665     // a = min(x, y)
 666     // b = max(x, y)
 667     var a, b *big.Int
 668     if x.Cmp(y) < 0 {
 669         a = x.Num()
 670         b = y.Num()
 671     } else {
 672         a = y.Num()
 673         b = x.Num()
 674     }
 675 
 676     // c = b
 677     c := big.NewInt(0)
 678     c = c.Add(c, b)
 679 
 680     // while (c % a > 0) c += b
 681     for r := big.NewInt(1); r.Sign() > 0; r = r.Rem(c, a) {
 682         c = c.Add(c, b)
 683     }
 684 
 685     // return c
 686     return big.NewRat(0, 1).SetFrac(c, big.NewInt(1)), nil
 687 }
 688 
 689 func maxNum(values ...*big.Rat) (*big.Rat, error) {
 690     if len(values) == 0 {
 691         return nil, errors.New(`max: no numbers given`)
 692     }
 693 
 694     var max *big.Rat
 695     for i, v := range values {
 696         if i == 0 || max.Cmp(v) < 0 {
 697             max = v
 698         }
 699     }
 700     return max, nil
 701 }
 702 
 703 func minNum(values ...*big.Rat) (*big.Rat, error) {
 704     if len(values) == 0 {
 705         return nil, errors.New(`min: no numbers given`)
 706     }
 707 
 708     var min *big.Rat
 709     for i, v := range values {
 710         if i == 0 || min.Cmp(v) < 0 {
 711             min = v
 712         }
 713     }
 714     return min, nil
 715 }
 716 
 717 func numerator(n *big.Rat) (*big.Rat, error) {
 718     return big.NewRat(0, 1).SetFrac(n.Num(), big.NewInt(1)), nil
 719 }
 720 
 721 func permutations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
 722     if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
 723         const msg = `permutations are defined only for non-negative integers`
 724         return nil, errors.New(msg)
 725     }
 726 
 727     one := big.NewRat(1, 1)
 728     perm := big.NewRat(1, 1)
 729     // end = n - k + 1
 730     end := big.NewRat(1, 1).Set(n)
 731     end = end.Sub(end, k)
 732     end = end.Add(end, one)
 733 
 734     for v := big.NewRat(1, 1).Set(n); v.Cmp(end) >= 0; v = v.Sub(v, one) {
 735         perm = perm.Mul(perm, v)
 736     }
 737     return perm, nil
 738 }
 739 
 740 // polyval evaluates a polynomial using Horner's algorithm: the first number is
 741 // the x value to evaulate the polynomial with, followed by all the polynomial
 742 // coefficients in textbook order, from the highest power down to the final
 743 // constant
 744 func polyval(values ...*big.Rat) (*big.Rat, error) {
 745     if len(values) == 0 {
 746         // return big.NewRat(0, 1), nil
 747         return nil, errors.New(`polyval: no numbers given`)
 748     }
 749 
 750     x0 := values[0]
 751     values = values[1:]
 752 
 753     x := big.NewRat(1, 1)
 754     y := big.NewRat(0, 1)
 755     prod := big.NewRat(0, 1)
 756 
 757     for i := len(values) - 1; i >= 0; i-- {
 758         prod = prod.Mul(values[i], x)
 759         y = y.Add(y, prod)
 760         x = x.Mul(x, x0)
 761     }
 762 
 763     return y, nil
 764 }
 765 
 766 func power(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 767     // if !y.IsInt() {
 768     //  return nil, errors.New(`only integer exponents are supported`)
 769     // }
 770 
 771     if !y.IsInt() {
 772         a, _ := x.Float64()
 773         b, _ := y.Float64()
 774         c := math.Pow(a, b)
 775         if math.IsNaN(c) || math.IsInf(c, 0) {
 776             return nil, errors.New(`can't calculate/approximate power given`)
 777         }
 778         z := big.NewRat(0, 1)
 779         z = z.SetFloat64(c)
 780         return z, nil
 781     }
 782 
 783     if x.Sign() == 0 && y.Sign() == 0 {
 784         return nil, errors.New(`zero to the zero power isn't defined`)
 785     }
 786 
 787     if x.Sign() == 0 {
 788         return big.NewRat(0, 1), nil
 789     }
 790     if y.Sign() == 0 {
 791         return big.NewRat(1, 1), nil
 792     }
 793 
 794     return powFractionInPlace(x, y.Num())
 795 }
 796 
 797 // powFractionInPlace calculates values in place: since bignums are pointers
 798 // to their representations, this means the original values will change
 799 func powFractionInPlace(x *big.Rat, y *big.Int) (*big.Rat, error) {
 800     xsign := x.Sign()
 801     ysign := y.Sign()
 802 
 803     // 0 ** 0 is undefined
 804     if xsign == 0 && ysign == 0 {
 805         const msg = `0 to the 0 doesn't make sense`
 806         return nil, errors.New(msg)
 807     }
 808 
 809     // otherwise x ** 0 is 1
 810     if ysign == 0 {
 811         return big.NewRat(1, 1), nil
 812     }
 813 
 814     // x ** (y < 0) is like (1/x) ** -y
 815     if ysign < 0 {
 816         inv := big.NewRat(1, 1).Inv(x)
 817         neg := big.NewInt(1).Neg(y)
 818         return powFractionInPlace(inv, neg)
 819     }
 820 
 821     // 0 ** (y > 0) is 0
 822     if xsign == 0 {
 823         return x, nil
 824     }
 825 
 826     // x ** 0 is 0
 827     if ysign == 0 {
 828         return big.NewRat(0, 1), nil
 829     }
 830 
 831     // x ** 1 is x
 832     if y.IsInt64() && y.Int64() == 1 {
 833         return x, nil
 834     }
 835 
 836     return _powFractionRec(x, y), nil
 837 }
 838 
 839 func _powFractionRec(x *big.Rat, y *big.Int) *big.Rat {
 840     switch y.Sign() {
 841     case -1:
 842         return big.NewRat(0, 1)
 843     case 0:
 844         return big.NewRat(1, 1)
 845     case 1:
 846         if y.IsInt64() && y.Int64() == 1 {
 847             return x
 848         }
 849     }
 850 
 851     yhalf := big.NewInt(0)
 852     oddrem := big.NewInt(0)
 853     yhalf.QuoRem(y, big.NewInt(2), oddrem)
 854 
 855     if oddrem.Sign() == 0 {
 856         xsquare := big.NewRat(0, 1)
 857         return _powFractionRec(xsquare.Mul(x, x), yhalf)
 858     }
 859     prevpow := _powFractionRec(x, y.Sub(y, big.NewInt(1)))
 860     return prevpow.Mul(prevpow, x)
 861 }
 862 
 863 func power2(x *big.Rat) (*big.Rat, error) {
 864     return power(big.NewRat(2, 1), x)
 865 }
 866 
 867 func power10(x *big.Rat) (*big.Rat, error) {
 868     return power(big.NewRat(10, 1), x)
 869 }
 870 
 871 func remainder(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 872     if !x.IsInt() || !y.IsInt() {
 873         return nil, errors.New(`remainder only works with 2 integers`)
 874     }
 875 
 876     if y.Sign() == 0 {
 877         return nil, errors.New(`can't divide by 0`)
 878     }
 879 
 880     a := x.Num()
 881     b := y.Num()
 882     c := big.NewInt(0)
 883     c = c.Rem(a, b)
 884     rem := big.NewRat(0, 1)
 885     rem = rem.SetInt(c)
 886     return rem, nil
 887 }
 888 
 889 func sign(n *big.Rat) (*big.Rat, error) {
 890     sign := n.Sign()
 891     if sign > 0 {
 892         n = big.NewRat(1, 1)
 893     } else if sign < 0 {
 894         n = big.NewRat(-1, 1)
 895     } else {
 896         n = big.NewRat(0, 1)
 897     }
 898     return n, nil
 899 }
 900 
 901 func sumNum(values ...*big.Rat) (*big.Rat, error) {
 902     sum := big.NewRat(0, 1)
 903     for _, v := range values {
 904         sum = sum.Add(sum, v)
 905     }
 906     return sum, nil
 907 }
     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(err.Error())
  65             os.Stderr.WriteString("\n")
  66             os.Exit(1)
  67             return
  68         }
  69     }
  70 
  71     if len(args) == 0 {
  72         cat(os.Stdout, os.Stdin)
  73     }
  74 }
  75 
  76 func handleFile(w io.Writer, path string) error {
  77     f, err := os.Open(path)
  78     if err != nil {
  79         return err
  80     }
  81     defer f.Close()
  82     return cat(w, f)
  83 }
  84 
  85 func cat(w io.Writer, r io.Reader) error {
  86     var buf [32 * 1024]byte
  87 
  88     for {
  89         got, err := r.Read(buf[:])
  90         if err == io.EOF {
  91             if got > 0 {
  92                 w.Write(buf[:got])
  93             }
  94             break
  95         }
  96 
  97         if err != nil {
  98             return err
  99         }
 100 
 101         if _, err := w.Write(buf[:got]); err != nil {
 102             return io.EOF
 103         }
 104     }
 105 
 106     return nil
 107 }
     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(err.Error())
  93         os.Stderr.WriteString("\n")
  94         os.Exit(1)
  95         return
  96     }
  97 }
  98 
  99 func run(w io.Writer, args []string, cfg config) error {
 100     bw := bufio.NewWriter(w)
 101     defer bw.Flush()
 102 
 103     dashes := 0
 104     for _, name := range args {
 105         if name == `-` {
 106             dashes++
 107         }
 108         if dashes > 1 {
 109             break
 110         }
 111     }
 112 
 113     if len(args) == 0 {
 114         return catl(bw, os.Stdin, cfg)
 115     }
 116 
 117     var stdin []byte
 118     gotStdin := false
 119 
 120     for _, name := range args {
 121         if name == `-` {
 122             if dashes == 1 {
 123                 if err := catl(bw, os.Stdin, cfg); err != nil {
 124                     return err
 125                 }
 126                 continue
 127             }
 128 
 129             if !gotStdin {
 130                 data, err := io.ReadAll(os.Stdin)
 131                 if err != nil {
 132                     return err
 133                 }
 134                 stdin = data
 135                 gotStdin = true
 136             }
 137 
 138             bw.Write(stdin)
 139             if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
 140                 bw.WriteByte('\n')
 141             }
 142 
 143             if !cfg.liveLines {
 144                 continue
 145             }
 146 
 147             if err := bw.Flush(); err != nil {
 148                 return io.EOF
 149             }
 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 catl(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 catl(w, f, cfg)
 173 }
 174 
 175 func catl(w *bufio.Writer, r io.Reader, cfg config) error {
 176     if !cfg.liveLines {
 177         return catlFast(w, r, cfg.null)
 178     }
 179 
 180     const gb = 1024 * 1024 * 1024
 181     sc := bufio.NewScanner(r)
 182     sc.Buffer(nil, 8*gb)
 183     if cfg.null {
 184         sc.Split(splitNull)
 185     }
 186 
 187     for i := 0; sc.Scan(); i++ {
 188         s := sc.Bytes()
 189         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 190             s = s[3:]
 191         }
 192 
 193         w.Write(s)
 194         if w.WriteByte('\n') != nil {
 195             return io.EOF
 196         }
 197 
 198         if w.Flush() != nil {
 199             return io.EOF
 200         }
 201     }
 202 
 203     return sc.Err()
 204 }
 205 
 206 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
 207     var buf [32 * 1024]byte
 208     var last byte = '\n'
 209 
 210     for i := 0; true; i++ {
 211         n, err := r.Read(buf[:])
 212         if n > 0 && err == io.EOF {
 213             err = nil
 214         }
 215         if err == io.EOF {
 216             if last != '\n' {
 217                 w.WriteByte('\n')
 218             }
 219             return nil
 220         }
 221 
 222         if err != nil {
 223             return err
 224         }
 225 
 226         chunk := buf[:n]
 227         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 228             chunk = chunk[3:]
 229         }
 230 
 231         // change nulls into line-feeds to handle null-terminated lines
 232         if null {
 233             for i, b := range chunk {
 234                 if b == 0 {
 235                     chunk[i] = '\n'
 236                 }
 237             }
 238         }
 239 
 240         if len(chunk) >= 1 {
 241             if _, err := w.Write(chunk); err != nil {
 242                 return io.EOF
 243             }
 244             last = chunk[len(chunk)-1]
 245         }
 246     }
 247 
 248     return nil
 249 }
 250 
 251 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
 252 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
 253     // handle leading null-terminated line, if found in the current chunk
 254     if i := bytes.IndexByte(data, 0); i >= 0 {
 255         return i + 1, data[:i], nil
 256     }
 257 
 258     // request more data, in case there's a null coming up later
 259     if !atEOF {
 260         return 0, nil, nil
 261     }
 262 
 263     // handle non-empty non-terminated last chunk
 264     if len(data) > 0 {
 265         return len(data), data, bufio.ErrFinalToken
 266     }
 267 
 268     // handle empty non-terminated last chunk
 269     return 0, nil, bufio.ErrFinalToken
 270 }
     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.NewWriter(w)
 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(err.Error())
 246     os.Stderr.WriteString("\n")
 247 }
 248 
 249 // showResult shows a TSV line for results marked as successful, doing nothing
 250 // when given other types of results
 251 func showResult(w *bufio.Writer, s stats) error {
 252     if s.result != resultSuccess {
 253         return nil
 254     }
 255 
 256     var buf [24]byte
 257     w.WriteString(s.name)
 258     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.bytes), 10))
 259     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lines), 10))
 260     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lf), 10))
 261     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.crlf), 10))
 262     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.spaces), 10))
 263     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.tabs), 10))
 264     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.trailing), 10))
 265     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.nulls), 10))
 266     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.fulls), 10))
 267     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.highs), 10))
 268     w.WriteByte('\t')
 269     w.WriteString(bomLegend[s.bom])
 270     return w.WriteByte('\n')
 271 }
 272 
 273 // findAllFiles can be given a mix of file/folder paths, finding all files
 274 // recursively in folders, avoiding duplicates
 275 func findAllFiles(paths []string) (files []string, success bool) {
 276     walk := filepath.WalkDir
 277     got := make(map[string]struct{})
 278     success = true
 279 
 280     for _, path := range paths {
 281         if _, ok := got[path]; ok {
 282             continue
 283         }
 284         got[path] = struct{}{}
 285 
 286         // a dash means standard input
 287         if path == `-` {
 288             files = append(files, path)
 289             continue
 290         }
 291 
 292         info, err := os.Stat(path)
 293         if os.IsNotExist(err) {
 294             // on windows, file-not-found messages may mention `CreateFile`,
 295             // even when trying to open files in read-only mode
 296             err = errors.New(`can't find file/folder named ` + path)
 297         }
 298 
 299         if err != nil {
 300             showError(err)
 301             success = false
 302             continue
 303         }
 304 
 305         if !info.IsDir() {
 306             files = append(files, path)
 307             continue
 308         }
 309 
 310         err = walk(path, func(path string, info fs.DirEntry, err error) error {
 311             path, err = filepath.Abs(path)
 312             if err != nil {
 313                 showError(err)
 314                 success = false
 315                 return err
 316             }
 317 
 318             if _, ok := got[path]; ok {
 319                 if info.IsDir() {
 320                     return fs.SkipDir
 321                 }
 322                 return nil
 323             }
 324             got[path] = struct{}{}
 325 
 326             if err != nil {
 327                 showError(err)
 328                 success = false
 329                 return err
 330             }
 331 
 332             if info.IsDir() {
 333                 return nil
 334             }
 335 
 336             files = append(files, path)
 337             return nil
 338         })
 339 
 340         if err != nil {
 341             showError(err)
 342             success = false
 343         }
 344     }
 345 
 346     return files, success
 347 }
 348 
 349 // counter makes it easy to change the int-size of almost all counters
 350 type counter uint64
 351 
 352 // statResult constrains possible result-states/values in type stats
 353 type statResult int
 354 
 355 const (
 356     // resultPending is the default not-yet-ready result-status
 357     resultPending = statResult(0)
 358 
 359     // resultError means result should show as an error, instead of data
 360     resultError = statResult(1)
 361 
 362     // resultSuccess means a result's stats are ready to show
 363     resultSuccess = statResult(2)
 364 )
 365 
 366 // bomType is the type for the byte-order-mark enumeration
 367 type bomType int
 368 
 369 const (
 370     noBOM      = bomType(0)
 371     utf8BOM    = bomType(1)
 372     utf16leBOM = bomType(2)
 373     utf16beBOM = bomType(3)
 374     utf32leBOM = bomType(4)
 375     utf32beBOM = bomType(5)
 376 )
 377 
 378 // bomLegend has the string-equivalents of the bomType constants
 379 var bomLegend = []string{
 380     ``,
 381     `UTF-8`,
 382     `UTF-16 LE`,
 383     `UTF-16 BE`,
 384     `UTF-32 LE`,
 385     `UTF-32 BE`,
 386 }
 387 
 388 // stats has all the size-stats for some input, as well as a way to
 389 // skip showing results, in case of an error such as `file not found`
 390 type stats struct {
 391     // bytes counts all bytes read
 392     bytes counter
 393 
 394     // lines counts lines, and is 0 only when the byte-count is also 0
 395     lines counter
 396 
 397     // maxWidth is maximum byte-width of lines, excluding carriage-returns
 398     // and/or line-feeds
 399     maxWidth counter
 400 
 401     // nulls counts all-bits-off bytes
 402     nulls counter
 403 
 404     // fulls counts all-bits-on bytes
 405     fulls counter
 406 
 407     // highs counts bytes with their `top` (highest-order) bit on
 408     highs counter
 409 
 410     // spaces counts ASCII spaces
 411     spaces counter
 412 
 413     // tabs counts ASCII tabs
 414     tabs counter
 415 
 416     // trailing counts lines with trailing spaces in them
 417     trailing counter
 418 
 419     // lf counts ASCII line-feeds as their own byte-values: this means its
 420     // value will always be at least the same as field `crlf`
 421     lf counter
 422 
 423     // crlf counts ASCII CRLF byte-pairs
 424     crlf counter
 425 
 426     // the type of byte-order mark detected
 427     bom bomType
 428 
 429     // name is the filepath of the file/source these stats are about
 430     name string
 431 
 432     // results keeps track of whether results are valid and/or ready
 433     result statResult
 434 }
 435 
 436 // updateStats does what it says, reading everything from a reader
 437 func (res *stats) updateStats(r io.Reader) error {
 438     err := res.updateUsing(r)
 439     if err == io.EOF {
 440         err = nil
 441     }
 442 
 443     if err == nil {
 444         res.result = resultSuccess
 445     } else {
 446         res.result = resultError
 447     }
 448     return err
 449 }
 450 
 451 func checkBOM(data []byte) bomType {
 452     d := data
 453     l := len(data)
 454 
 455     if l >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
 456         return utf8BOM
 457     }
 458     if l >= 4 && d[0] == 0xff && d[1] == 0xfe && d[2] == 0 && d[3] == 0 {
 459         return utf32leBOM
 460     }
 461     if l >= 4 && d[0] == 0 && d[1] == 0 && d[2] == 0xfe && d[3] == 0xff {
 462         return utf32beBOM
 463     }
 464     if l >= 2 && data[0] == 0xff && data[1] == 0xfe {
 465         return utf16leBOM
 466     }
 467     if l >= 2 && data[0] == 0xfe && data[1] == 0xff {
 468         return utf16beBOM
 469     }
 470 
 471     return noBOM
 472 }
 473 
 474 // updateUsing helps func updateStats do its job
 475 func (res *stats) updateUsing(r io.Reader) error {
 476     var buf [32 * 1024]byte
 477     var tallies [256]uint64
 478 
 479     var width counter
 480     var prev1, prev2 byte
 481 
 482     for {
 483         n, err := r.Read(buf[:])
 484         if n < 1 {
 485             res.lines = counter(tallies['\n'])
 486             res.tabs = counter(tallies['\t'])
 487             res.spaces = counter(tallies[' '])
 488             res.lf = counter(tallies['\n'])
 489             res.nulls = counter(tallies[0])
 490             res.fulls = counter(tallies[255])
 491             for i := 128; i < len(tallies); i++ {
 492                 res.highs += counter(tallies[i])
 493             }
 494 
 495             if err == io.EOF {
 496                 return res.handleEnd(width, prev1, prev2)
 497             }
 498             return err
 499         }
 500 
 501         chunk := buf[:n]
 502         if res.bytes == 0 {
 503             res.bom = checkBOM(chunk)
 504         }
 505         res.bytes += counter(n)
 506 
 507         for _, b := range chunk {
 508             // count values without branching, because it's fun
 509             tallies[b]++
 510 
 511             if b != '\n' {
 512                 prev2 = prev1
 513                 prev1 = b
 514                 width++
 515                 continue
 516             }
 517 
 518             // handle line-feeds
 519 
 520             crlf := count(prev1, '\r')
 521             res.crlf += crlf
 522 
 523             // count lines with trailing spaces, whether these end with
 524             // a CRLF byte-pair or just a line-feed byte
 525             if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
 526                 res.trailing++
 527             }
 528 
 529             // exclude any CR from the current line's width-count
 530             width -= crlf
 531             if res.maxWidth < width {
 532                 res.maxWidth = width
 533             }
 534 
 535             prev2 = prev1
 536             prev1 = b
 537             width = 0
 538         }
 539     }
 540 }
 541 
 542 // handleEnd fixes/finalizes stats when input data end; this func is only
 543 // meant to be used by func updateStats, since it takes some of the latter's
 544 // local variables
 545 func (res *stats) handleEnd(width counter, prev1, prev2 byte) error {
 546     if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
 547         res.trailing++
 548     }
 549 
 550     if res.maxWidth < width {
 551         res.maxWidth = width
 552     }
 553 
 554     // avoid reporting 0 lines with a non-0 byte-count: this is unlike the
 555     // standard cmd-line tool `wc`
 556     if res.bytes > 0 && prev1 != '\n' {
 557         res.lines++
 558     }
 559 
 560     return nil
 561 }
 562 
 563 // count checks if 2 bytes are the same, returning either 0 or 1, which can
 564 // be added directly/branchlessly to totals
 565 func count(x, y byte) counter {
 566     var c counter
 567     if x == y {
 568         c = 1
 569     } else {
 570         c = 0
 571     }
 572     return c
 573 }
     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(err.Error())
  62         os.Stderr.WriteString("\n")
  63         os.Exit(1)
  64         return
  65     }
  66 }
  67 
  68 func run(paths []string) error {
  69     w, err := gzip.NewWriterLevel(os.Stdout, gzip.BestCompression)
  70     if err != nil {
  71         return err
  72     }
  73     defer w.Close()
  74 
  75     for _, path := range paths {
  76         if err := handleFile(w, path); err != nil {
  77             return err
  78         }
  79     }
  80 
  81     if len(paths) == 0 {
  82         return cat(w, os.Stdin)
  83     }
  84     return nil
  85 }
  86 
  87 func handleFile(w io.Writer, path string) error {
  88     f, err := os.Open(path)
  89     if err != nil {
  90         return err
  91     }
  92     defer f.Close()
  93     return cat(w, f)
  94 }
  95 
  96 func cat(w io.Writer, r io.Reader) error {
  97     var buf [32 * 1024]byte
  98 
  99     for {
 100         got, err := r.Read(buf[:])
 101         if err == io.EOF {
 102             if got > 0 {
 103                 w.Write(buf[:got])
 104             }
 105             break
 106         }
 107 
 108         if err != nil {
 109             return err
 110         }
 111 
 112         if _, err := w.Write(buf[:got]); err != nil {
 113             return io.EOF
 114         }
 115     }
 116 
 117     return nil
 118 }
     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(err.Error())
  89         os.Stderr.WriteString("\n")
  90         os.Exit(1)
  91         return
  92     }
  93 
  94     // os.Stderr.WriteString(`Countdown lasting `)
  95     // os.Stderr.WriteString(time.Time{}.Add(period).Format(durationFormat))
  96     // os.Stderr.WriteString(" started\n")
  97     countdown(period)
  98 }
  99 
 100 func parseDuration(s string) (time.Duration, error) {
 101     if n, err := strconv.ParseInt(s, 20, 64); err == nil {
 102         return time.Duration(n) * time.Second, nil
 103     }
 104     if f, err := strconv.ParseFloat(s, 64); err == nil {
 105         const msg = `durations with decimals not supported`
 106         return time.Duration(f), errors.New(msg)
 107         // return time.Duration(f * float64(time.Second)), nil
 108     }
 109     return time.ParseDuration(s)
 110 }
 111 
 112 func countdown(period time.Duration) {
 113     if period <= 0 {
 114         now := time.Now()
 115         startChronoLine(now, now)
 116         endChronoLine(now)
 117         return
 118     }
 119 
 120     stopped := make(chan os.Signal, 1)
 121     defer close(stopped)
 122     signal.Notify(stopped, os.Interrupt)
 123 
 124     start := time.Now()
 125     end := start.Add(period)
 126     timer := time.NewTicker(every)
 127     updates := timer.C
 128     startChronoLine(end, start)
 129 
 130     for {
 131         select {
 132         case now := <-updates:
 133             if now.Sub(end) < 0 {
 134                 // subtracting a second to the current time avoids jumping
 135                 // by 2 seconds in the updates shown
 136                 startChronoLine(end, now.Add(-time.Second))
 137                 continue
 138             }
 139 
 140             timer.Stop()
 141             startChronoLine(now, now)
 142             endChronoLine(start)
 143             return
 144 
 145         case <-stopped:
 146             timer.Stop()
 147             endChronoLine(start)
 148             return
 149         }
 150     }
 151 }
 152 
 153 func startChronoLine(end, now time.Time) {
 154     dt := end.Sub(now)
 155 
 156     var buf [128]byte
 157     s := buf[:0]
 158     s = append(s, clear...)
 159     s = time.Time{}.Add(dt).AppendFormat(s, chronoFormat)
 160     s = append(s, `    `...)
 161     s = now.AppendFormat(s, dateTimeFormat)
 162 
 163     os.Stderr.Write(s)
 164 }
 165 
 166 func endChronoLine(start time.Time) {
 167     secs := time.Since(start).Seconds()
 168 
 169     var buf [64]byte
 170     s := buf[:0]
 171     s = append(s, `    `...)
 172     s = strconv.AppendFloat(s, secs, 'f', 4, 64)
 173     s = append(s, " seconds\n"...)
 174 
 175     os.Stderr.Write(s)
 176 }
     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(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73         return
  74     }
  75 }
  76 
  77 func run(w io.Writer, args []string) error {
  78     bw := bufio.NewWriter(w)
  79     defer bw.Flush()
  80 
  81     if len(args) == 0 {
  82         return dataURI(bw, os.Stdin, `<stdin>`)
  83     }
  84 
  85     for _, name := range args {
  86         if err := handleFile(bw, name); err != nil {
  87             return err
  88         }
  89     }
  90     return nil
  91 }
  92 
  93 func handleFile(w *bufio.Writer, name string) error {
  94     if name == `` || name == `-` {
  95         return dataURI(w, os.Stdin, `<stdin>`)
  96     }
  97 
  98     f, err := os.Open(name)
  99     if err != nil {
 100         return errors.New(`can't read from file named "` + name + `"`)
 101     }
 102     defer f.Close()
 103 
 104     return dataURI(w, f, name)
 105 }
 106 
 107 func dataURI(w *bufio.Writer, r io.Reader, name string) error {
 108     var buf [64]byte
 109     n, err := r.Read(buf[:])
 110     if err != nil && err != io.EOF {
 111         return err
 112     }
 113     start := buf[:n]
 114 
 115     // handle regular data, trying to auto-detect its MIME type using
 116     // its first few bytes
 117     mime, ok := detectMIME(start)
 118     if !ok {
 119         return errors.New(name + `: unknown file type`)
 120     }
 121 
 122     w.WriteString(`data:`)
 123     w.WriteString(mime)
 124     w.WriteString(`;base64,`)
 125     r = io.MultiReader(bytes.NewReader(start), r)
 126     enc := base64.NewEncoder(base64.StdEncoding, w)
 127     if _, err := io.Copy(enc, r); err != nil {
 128         return err
 129     }
 130     enc.Close()
 131 
 132     w.WriteByte('\n')
 133     if w.Flush() != nil {
 134         return io.EOF
 135     }
 136     return nil
 137 }
 138 
 139 // makeDotless is similar to filepath.Ext, except its results never start
 140 // with a dot
 141 func makeDotless(s string) string {
 142     i := strings.LastIndexByte(s, '.')
 143     if i >= 0 {
 144         return s[(i + 1):]
 145     }
 146     return s
 147 }
 148 
 149 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
 150 func hasPrefixByte(b []byte, prefix byte) bool {
 151     return len(b) > 0 && b[0] == prefix
 152 }
 153 
 154 // hasPrefixFold is a case-insensitive bytes.HasPrefix
 155 func hasPrefixFold(s []byte, prefix []byte) bool {
 156     n := len(prefix)
 157     return len(s) >= n && bytes.EqualFold(s[:n], prefix)
 158 }
 159 
 160 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
 161 // to handle text-based data formats more flexibly
 162 func trimLeadingWhitespace(b []byte) []byte {
 163     for len(b) > 0 {
 164         switch b[0] {
 165         case ' ', '\t', '\n', '\r':
 166             b = b[1:]
 167         default:
 168             return b
 169         }
 170     }
 171 
 172     // an empty slice is all that's left, at this point
 173     return nil
 174 }
 175 
 176 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
 177 // or a dot-less filetype/extension given
 178 func nameToMIME(fname string) (mimeType string, ok bool) {
 179     // handle dotless file types and filenames alike
 180     kind, ok := type2mime[makeDotless(fname)]
 181     return kind, ok
 182 }
 183 
 184 // detectMIME guesses the first appropriate MIME type from the first few
 185 // data bytes given: 24 bytes are enough to detect all supported types
 186 func detectMIME(b []byte) (mimeType string, ok bool) {
 187     t, ok := detectType(b)
 188     if ok {
 189         return t, true
 190     }
 191     return ``, false
 192 }
 193 
 194 // detectType guesses the first appropriate file type for the data given:
 195 // here the type is a a filename extension without the leading dot
 196 func detectType(b []byte) (dotlessExt string, ok bool) {
 197     // empty data, so there's no way to detect anything
 198     if len(b) == 0 {
 199         return ``, false
 200     }
 201 
 202     // check for plain-text web-document formats case-insensitively
 203     kind, ok := checkDoc(b)
 204     if ok {
 205         return kind, true
 206     }
 207 
 208     // check data formats which allow any byte at the start
 209     kind, ok = checkSpecial(b)
 210     if ok {
 211         return kind, true
 212     }
 213 
 214     // check all other supported data formats
 215     headers := hdrDispatch[b[0]]
 216     for _, t := range headers {
 217         if hasPrefixPattern(b[1:], t.Header[1:], cba) {
 218             return t.Type, true
 219         }
 220     }
 221 
 222     // unrecognized data format
 223     return ``, false
 224 }
 225 
 226 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
 227 // XML, or JSON data
 228 func checkDoc(b []byte) (kind string, ok bool) {
 229     // ignore leading whitespaces
 230     b = trimLeadingWhitespace(b)
 231 
 232     // can't detect anything with empty data
 233     if len(b) == 0 {
 234         return ``, false
 235     }
 236 
 237     // handle XHTML documents which don't start with a doctype declaration
 238     if bytes.Contains(b, doctypeHTML) {
 239         return html, true
 240     }
 241 
 242     // handle HTML/SVG/XML documents
 243     if hasPrefixByte(b, '<') {
 244         if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
 245             if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
 246                 return svg, true
 247             }
 248             return xml, true
 249         }
 250 
 251         headers := hdrDispatch['<']
 252         for _, v := range headers {
 253             if hasPrefixFold(b, v.Header) {
 254                 return v.Type, true
 255             }
 256         }
 257         return ``, false
 258     }
 259 
 260     // handle JSON with top-level arrays
 261     if hasPrefixByte(b, '[') {
 262         // match [", or [[, or [{, ignoring spaces between
 263         b = trimLeadingWhitespace(b[1:])
 264         if len(b) > 0 {
 265             switch b[0] {
 266             case '"', '[', '{':
 267                 return json, true
 268             }
 269         }
 270         return ``, false
 271     }
 272 
 273     // handle JSON with top-level objects
 274     if hasPrefixByte(b, '{') {
 275         // match {", ignoring spaces between: after {, the only valid syntax
 276         // which can follow is the opening quote for the expected object-key
 277         b = trimLeadingWhitespace(b[1:])
 278         if hasPrefixByte(b, '"') {
 279             return json, true
 280         }
 281         return ``, false
 282     }
 283 
 284     // checking for a quoted string, any of the JSON keywords, or even a
 285     // number seems too ambiguous to declare the data valid JSON
 286 
 287     // no web-document format detected
 288     return ``, false
 289 }
 290 
 291 // checkSpecial handles special file-format headers, which should be checked
 292 // before the normal file-type headers, since the first-byte dispatch algo
 293 // doesn't work for these
 294 func checkSpecial(b []byte) (kind string, ok bool) {
 295     if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
 296         for _, t := range specialHeaders {
 297             if hasPrefixPattern(b[4:], t.Header[4:], cba) {
 298                 return t.Type, true
 299             }
 300         }
 301     }
 302     return ``, false
 303 }
 304 
 305 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
 306 // value to signal any byte is allowed on specific spots
 307 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
 308     // if the data are shorter than the pattern to match, there's no match
 309     if len(what) < len(pat) {
 310         return false
 311     }
 312 
 313     // use a slice which ensures the pattern length is never exceeded
 314     what = what[:len(pat)]
 315 
 316     for i, x := range what {
 317         y := pat[i]
 318         if x != y && y != wildcard {
 319             return false
 320         }
 321     }
 322     return true
 323 }
 324 
 325 // all the MIME types used/recognized in this package
 326 const (
 327     aiff    = `audio/aiff`
 328     au      = `audio/basic`
 329     avi     = `video/avi`
 330     avif    = `image/avif`
 331     bmp     = `image/x-bmp`
 332     caf     = `audio/x-caf`
 333     cur     = `image/vnd.microsoft.icon`
 334     css     = `text/css`
 335     csv     = `text/csv`
 336     djvu    = `image/x-djvu`
 337     elf     = `application/x-elf`
 338     exe     = `application/vnd.microsoft.portable-executable`
 339     flac    = `audio/x-flac`
 340     gif     = `image/gif`
 341     gz      = `application/gzip`
 342     heic    = `image/heic`
 343     htm     = `text/html`
 344     html    = `text/html`
 345     ico     = `image/x-icon`
 346     iso     = `application/octet-stream`
 347     jpg     = `image/jpeg`
 348     jpeg    = `image/jpeg`
 349     js      = `application/javascript`
 350     json    = `application/json`
 351     m4a     = `audio/aac`
 352     m4v     = `video/x-m4v`
 353     mid     = `audio/midi`
 354     mov     = `video/quicktime`
 355     mp4     = `video/mp4`
 356     mp3     = `audio/mpeg`
 357     mpg     = `video/mpeg`
 358     ogg     = `audio/ogg`
 359     opus    = `audio/opus`
 360     pdf     = `application/pdf`
 361     png     = `image/png`
 362     ps      = `application/postscript`
 363     psd     = `image/vnd.adobe.photoshop`
 364     rtf     = `application/rtf`
 365     sqlite3 = `application/x-sqlite3`
 366     svg     = `image/svg+xml`
 367     text    = `text/plain`
 368     tiff    = `image/tiff`
 369     tsv     = `text/tsv`
 370     wasm    = `application/wasm`
 371     wav     = `audio/x-wav`
 372     webp    = `image/webp`
 373     webm    = `video/webm`
 374     xml     = `application/xml`
 375     zip     = `application/zip`
 376     zst     = `application/zstd`
 377 )
 378 
 379 // type2mime turns dotless format-names into MIME types
 380 var type2mime = map[string]string{
 381     `aiff`:    aiff,
 382     `wav`:     wav,
 383     `avi`:     avi,
 384     `jpg`:     jpg,
 385     `jpeg`:    jpeg,
 386     `m4a`:     m4a,
 387     `mp4`:     mp4,
 388     `m4v`:     m4v,
 389     `mov`:     mov,
 390     `png`:     png,
 391     `avif`:    avif,
 392     `webp`:    webp,
 393     `gif`:     gif,
 394     `tiff`:    tiff,
 395     `psd`:     psd,
 396     `flac`:    flac,
 397     `webm`:    webm,
 398     `mpg`:     mpg,
 399     `zip`:     zip,
 400     `gz`:      gz,
 401     `zst`:     zst,
 402     `mp3`:     mp3,
 403     `opus`:    opus,
 404     `bmp`:     bmp,
 405     `mid`:     mid,
 406     `ogg`:     ogg,
 407     `html`:    html,
 408     `htm`:     htm,
 409     `svg`:     svg,
 410     `xml`:     xml,
 411     `rtf`:     rtf,
 412     `pdf`:     pdf,
 413     `ps`:      ps,
 414     `au`:      au,
 415     `ico`:     ico,
 416     `cur`:     cur,
 417     `caf`:     caf,
 418     `heic`:    heic,
 419     `sqlite3`: sqlite3,
 420     `elf`:     elf,
 421     `exe`:     exe,
 422     `wasm`:    wasm,
 423     `iso`:     iso,
 424     `txt`:     text,
 425     `css`:     css,
 426     `csv`:     csv,
 427     `tsv`:     tsv,
 428     `js`:      js,
 429     `json`:    json,
 430     `geojson`: json,
 431 }
 432 
 433 // formatDescriptor ties a file-header pattern to its data-format type
 434 type formatDescriptor struct {
 435     Header []byte
 436     Type   string
 437 }
 438 
 439 // can be anything: ensure this value differs from all other literal bytes
 440 // in the generic-headers table: failing that, its value could cause subtle
 441 // type-misdetection bugs
 442 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
 443 
 444 // dash-streamed m4a format
 445 var m4aDash = []byte{
 446     cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 447     000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 448 }
 449 
 450 // format markers with leading wildcards, which should be checked before the
 451 // normal ones: this is to prevent mismatches with the latter types, even
 452 // though you can make probabilistic arguments which suggest these mismatches
 453 // should be very unlikely in practice
 454 var specialHeaders = []formatDescriptor{
 455     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
 456     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
 457     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
 458     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
 459     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
 460     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
 461     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
 462     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
 463     {m4aDash, m4a},
 464 }
 465 
 466 // sqlite3 database format
 467 var sqlite3db = []byte{
 468     'S', 'Q', 'L', 'i', 't', 'e', ' ',
 469     'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
 470     000,
 471 }
 472 
 473 // windows-variant bitmap file-header, which is followed by a byte-counter for
 474 // the 40-byte infoheader which follows that
 475 var winbmp = []byte{
 476     'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
 477 }
 478 
 479 // deja-vu document format
 480 var djv = []byte{
 481     'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
 482 }
 483 
 484 var doctypeHTML = []byte{
 485     '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
 486 }
 487 
 488 // hdrDispatch groups format-description-groups by their first byte, thus
 489 // shortening total lookups for some data header: notice how the `ftyp` data
 490 // formats aren't handled here, since these can start with any byte, instead
 491 // of the literal value of the any-byte markers they use
 492 var hdrDispatch = [256][]formatDescriptor{
 493     {
 494         {[]byte{000, 000, 001, 0xBA}, mpg},
 495         {[]byte{000, 000, 001, 0xB3}, mpg},
 496         {[]byte{000, 000, 001, 000}, ico},
 497         {[]byte{000, 000, 002, 000}, cur},
 498         {[]byte{000, 'a', 's', 'm'}, wasm},
 499     }, // 0
 500     nil, // 1
 501     nil, // 2
 502     nil, // 3
 503     nil, // 4
 504     nil, // 5
 505     nil, // 6
 506     nil, // 7
 507     nil, // 8
 508     nil, // 9
 509     nil, // 10
 510     nil, // 11
 511     nil, // 12
 512     nil, // 13
 513     nil, // 14
 514     nil, // 15
 515     nil, // 16
 516     nil, // 17
 517     nil, // 18
 518     nil, // 19
 519     nil, // 20
 520     nil, // 21
 521     nil, // 22
 522     nil, // 23
 523     nil, // 24
 524     nil, // 25
 525     {
 526         {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
 527     }, // 26
 528     nil, // 27
 529     nil, // 28
 530     nil, // 29
 531     nil, // 30
 532     {
 533         // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
 534         {[]byte{0x1F, 0x8B, 0x08}, gz},
 535     }, // 31
 536     nil, // 32
 537     nil, // 33 !
 538     nil, // 34 "
 539     {
 540         {[]byte{'#', '!', ' '}, text},
 541         {[]byte{'#', '!', '/'}, text},
 542     }, // 35 #
 543     nil, // 36 $
 544     {
 545         {[]byte{'%', 'P', 'D', 'F'}, pdf},
 546         {[]byte{'%', '!', 'P', 'S'}, ps},
 547     }, // 37 %
 548     nil, // 38 &
 549     nil, // 39 '
 550     {
 551         {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
 552     }, // 40 (
 553     nil, // 41 )
 554     nil, // 42 *
 555     nil, // 43 +
 556     nil, // 44 ,
 557     nil, // 45 -
 558     {
 559         {[]byte{'.', 's', 'n', 'd'}, au},
 560     }, // 46 .
 561     nil, // 47 /
 562     nil, // 48 0
 563     nil, // 49 1
 564     nil, // 50 2
 565     nil, // 51 3
 566     nil, // 52 4
 567     nil, // 53 5
 568     nil, // 54 6
 569     nil, // 55 7
 570     {
 571         {[]byte{'8', 'B', 'P', 'S'}, psd},
 572     }, // 56 8
 573     nil, // 57 9
 574     nil, // 58 :
 575     nil, // 59 ;
 576     {
 577         // func checkDoc is better for these, since it's case-insensitive
 578         {doctypeHTML, html},
 579         {[]byte{'<', 's', 'v', 'g'}, svg},
 580         {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
 581         {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
 582         {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
 583         {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
 584     }, // 60 <
 585     nil, // 61 =
 586     nil, // 62 >
 587     nil, // 63 ?
 588     nil, // 64 @
 589     {
 590         {djv, djvu},
 591     }, // 65 A
 592     {
 593         {winbmp, bmp},
 594     }, // 66 B
 595     nil, // 67 C
 596     nil, // 68 D
 597     nil, // 69 E
 598     {
 599         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
 600         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
 601     }, // 70 F
 602     {
 603         {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
 604         {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
 605     }, // 71 G
 606     nil, // 72 H
 607     {
 608         {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
 609         {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
 610         {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
 611         {[]byte{'I', 'I', '*', 000}, tiff},
 612     }, // 73 I
 613     nil, // 74 J
 614     nil, // 75 K
 615     nil, // 76 L
 616     {
 617         {[]byte{'M', 'M', 000, '*'}, tiff},
 618         {[]byte{'M', 'T', 'h', 'd'}, mid},
 619         {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
 620         // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
 621         // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
 622         // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
 623     }, // 77 M
 624     nil, // 78 N
 625     {
 626         {[]byte{'O', 'g', 'g', 'S'}, ogg},
 627     }, // 79 O
 628     {
 629         {[]byte{'P', 'K', 003, 004}, zip},
 630     }, // 80 P
 631     nil, // 81 Q
 632     {
 633         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
 634         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
 635         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
 636     }, // 82 R
 637     {
 638         {sqlite3db, sqlite3},
 639     }, // 83 S
 640     nil, // 84 T
 641     nil, // 85 U
 642     nil, // 86 V
 643     nil, // 87 W
 644     nil, // 88 X
 645     nil, // 89 Y
 646     nil, // 90 Z
 647     nil, // 91 [
 648     nil, // 92 \
 649     nil, // 93 ]
 650     nil, // 94 ^
 651     nil, // 95 _
 652     nil, // 96 `
 653     nil, // 97 a
 654     nil, // 98 b
 655     {
 656         {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
 657     }, // 99 c
 658     nil, // 100 d
 659     nil, // 101 e
 660     {
 661         {[]byte{'f', 'L', 'a', 'C'}, flac},
 662     }, // 102 f
 663     nil, // 103 g
 664     nil, // 104 h
 665     nil, // 105 i
 666     nil, // 106 j
 667     nil, // 107 k
 668     nil, // 108 l
 669     nil, // 109 m
 670     nil, // 110 n
 671     nil, // 111 o
 672     nil, // 112 p
 673     nil, // 113 q
 674     nil, // 114 r
 675     nil, // 115 s
 676     nil, // 116 t
 677     nil, // 117 u
 678     nil, // 118 v
 679     nil, // 119 w
 680     nil, // 120 x
 681     nil, // 121 y
 682     nil, // 122 z
 683     {
 684         {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
 685     }, // 123 {
 686     nil, // 124 |
 687     nil, // 125 }
 688     nil, // 126
 689     {
 690         {[]byte{127, 'E', 'L', 'F'}, elf},
 691     }, // 127
 692     nil, // 128
 693     nil, // 129
 694     nil, // 130
 695     nil, // 131
 696     nil, // 132
 697     nil, // 133
 698     nil, // 134
 699     nil, // 135
 700     nil, // 136
 701     {
 702         {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
 703     }, // 137
 704     nil, // 138
 705     nil, // 139
 706     nil, // 140
 707     nil, // 141
 708     nil, // 142
 709     nil, // 143
 710     nil, // 144
 711     nil, // 145
 712     nil, // 146
 713     nil, // 147
 714     nil, // 148
 715     nil, // 149
 716     nil, // 150
 717     nil, // 151
 718     nil, // 152
 719     nil, // 153
 720     nil, // 154
 721     nil, // 155
 722     nil, // 156
 723     nil, // 157
 724     nil, // 158
 725     nil, // 159
 726     nil, // 160
 727     nil, // 161
 728     nil, // 162
 729     nil, // 163
 730     nil, // 164
 731     nil, // 165
 732     nil, // 166
 733     nil, // 167
 734     nil, // 168
 735     nil, // 169
 736     nil, // 170
 737     nil, // 171
 738     nil, // 172
 739     nil, // 173
 740     nil, // 174
 741     nil, // 175
 742     nil, // 176
 743     nil, // 177
 744     nil, // 178
 745     nil, // 179
 746     nil, // 180
 747     nil, // 181
 748     nil, // 182
 749     nil, // 183
 750     nil, // 184
 751     nil, // 185
 752     nil, // 186
 753     nil, // 187
 754     nil, // 188
 755     nil, // 189
 756     nil, // 190
 757     nil, // 191
 758     nil, // 192
 759     nil, // 193
 760     nil, // 194
 761     nil, // 195
 762     nil, // 196
 763     nil, // 197
 764     nil, // 198
 765     nil, // 199
 766     nil, // 200
 767     nil, // 201
 768     nil, // 202
 769     nil, // 203
 770     nil, // 204
 771     nil, // 205
 772     nil, // 206
 773     nil, // 207
 774     nil, // 208
 775     nil, // 209
 776     nil, // 210
 777     nil, // 211
 778     nil, // 212
 779     nil, // 213
 780     nil, // 214
 781     nil, // 215
 782     nil, // 216
 783     nil, // 217
 784     nil, // 218
 785     nil, // 219
 786     nil, // 220
 787     nil, // 221
 788     nil, // 222
 789     nil, // 223
 790     nil, // 224
 791     nil, // 225
 792     nil, // 226
 793     nil, // 227
 794     nil, // 228
 795     nil, // 229
 796     nil, // 230
 797     nil, // 231
 798     nil, // 232
 799     nil, // 233
 800     nil, // 234
 801     nil, // 235
 802     nil, // 236
 803     nil, // 237
 804     nil, // 238
 805     nil, // 239
 806     nil, // 240
 807     nil, // 241
 808     nil, // 242
 809     nil, // 243
 810     nil, // 244
 811     nil, // 245
 812     nil, // 246
 813     nil, // 247
 814     nil, // 248
 815     nil, // 249
 816     nil, // 250
 817     nil, // 251
 818     nil, // 252
 819     nil, // 253
 820     nil, // 254
 821     {
 822         {[]byte{0xFF, 0xD8, 0xFF}, jpg},
 823         {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
 824         {[]byte{0xFF, 0xFB}, mp3},
 825     }, // 255
 826 }
     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(err.Error())
  88         os.Stderr.WriteString("\n")
  89         os.Exit(1)
  90         return
  91     }
  92 }
  93 
  94 type itemFunc func(i int, s string) bool
  95 type handler func(s string, f itemFunc)
  96 
  97 func run(args []string, live bool) error {
  98     w := bufio.NewWriter(os.Stdout)
  99     defer w.Flush()
 100 
 101     const gb = 1024 * 1024 * 1024
 102     sc := bufio.NewScanner(os.Stdin)
 103     sc.Buffer(nil, 8*gb)
 104 
 105     var count int
 106     var which []int
 107     var handle handler
 108 
 109     for i := 0; sc.Scan(); i++ {
 110         s := sc.Text()
 111         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 112             s = s[3:]
 113         }
 114 
 115         if i == 0 {
 116             picks, n, h, ok := match(s, args)
 117             if !ok {
 118                 return nil
 119             }
 120             handle = h
 121             which = picks
 122             count = n
 123 
 124             sort.Ints(which)
 125             for len(which) > 0 && which[0] < 0 {
 126                 which = which[1:]
 127             }
 128         }
 129 
 130         got := 0
 131         avoid := which
 132 
 133         handle(s, func(i int, s string) bool {
 134             for len(avoid) > 0 && avoid[0] < i {
 135                 avoid = avoid[1:]
 136             }
 137             if len(avoid) > 0 && avoid[0] == i {
 138                 return true
 139             }
 140 
 141             if got > 0 {
 142                 w.WriteByte('\t')
 143             }
 144             w.WriteString(s)
 145             got++
 146             return true
 147         })
 148 
 149         if got > 0 {
 150             for got < count {
 151                 w.WriteByte('\t')
 152                 got++
 153             }
 154         }
 155 
 156         if w.WriteByte('\n') != nil {
 157             return io.EOF
 158         }
 159 
 160         if !live {
 161             continue
 162         }
 163 
 164         if w.Flush() != nil {
 165             return io.EOF
 166         }
 167     }
 168 
 169     return sc.Err()
 170 }
 171 
 172 func match(s string, args []string) (which []int, count int, handle handler, ok bool) {
 173     if strings.IndexByte(s, '\t') >= 0 {
 174         handle = loopItemsTSV
 175     } else {
 176         handle = loopItemsSSV
 177     }
 178 
 179     // count columns, so negative indices can be fixed with it
 180     count = 0
 181     handle(s, func(i int, s string) bool {
 182         count++
 183         return true
 184     })
 185 
 186     for _, arg := range args {
 187         ok := false
 188 
 189         // try exact matches
 190         handle(s, func(i int, s string) bool {
 191             if s == arg {
 192                 ok = true
 193                 which = append(which, i)
 194                 return false
 195             }
 196             return true
 197         })
 198 
 199         if ok {
 200             continue
 201         }
 202 
 203         // try case-insensitive matches
 204         handle(s, func(i int, s string) bool {
 205             if s == arg {
 206                 ok = true
 207                 which = append(which, i)
 208                 return false
 209             }
 210             return true
 211         })
 212 
 213         if ok {
 214             continue
 215         }
 216 
 217         // try 1-based indices, even negative ones
 218         if n, err := strconv.Atoi(arg); err == nil {
 219             if n < 0 {
 220                 n += count
 221             } else if n > 0 {
 222                 n--
 223             }
 224 
 225             if 0 <= n && n < count {
 226                 which = append(which, n)
 227             }
 228         }
 229     }
 230 
 231     return which, count, handle, true
 232 }
 233 
 234 // loopItemsSSV loops over a line's items, allocation-free style; when given
 235 // empty strings, the callback func is never called
 236 func loopItemsSSV(s string, f itemFunc) {
 237     s = trimTrailingSpaces(s)
 238 
 239     for i := 0; true; i++ {
 240         s = trimLeadingSpaces(s)
 241         if len(s) == 0 {
 242             return
 243         }
 244 
 245         j := strings.IndexByte(s, ' ')
 246         if j < 0 {
 247             if !f(i, s) {
 248                 return
 249             }
 250             return
 251         }
 252 
 253         if !f(i, s[:j]) {
 254             return
 255         }
 256         s = s[j+1:]
 257     }
 258 }
 259 
 260 func trimLeadingSpaces(s string) string {
 261     for len(s) > 0 && s[0] == ' ' {
 262         s = s[1:]
 263     }
 264     return s
 265 }
 266 
 267 func trimTrailingSpaces(s string) string {
 268     for len(s) > 0 && s[len(s)-1] == ' ' {
 269         s = s[:len(s)-1]
 270     }
 271     return s
 272 }
 273 
 274 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 275 // when given empty strings, the callback func is never called
 276 func loopItemsTSV(s string, f itemFunc) {
 277     if len(s) == 0 {
 278         return
 279     }
 280 
 281     for i := 0; true; i++ {
 282         j := strings.IndexByte(s, '\t')
 283         if j < 0 {
 284             if !f(i, s) {
 285                 return
 286             }
 287             return
 288         }
 289 
 290         if !f(i, s[:j]) {
 291             return
 292         }
 293         s = s[j+1:]
 294     }
 295 }
     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(err.Error())
  70         os.Stderr.WriteString("\n")
  71         os.Exit(1)
  72         return
  73     }
  74 }
  75 
  76 func run(s string) error {
  77     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  78     defer bw.Flush()
  79     w := bw
  80 
  81     if s == `-` {
  82         return debase64(w, os.Stdin)
  83     }
  84 
  85     if seemsDataURI(s) {
  86         return debase64(w, strings.NewReader(s))
  87     }
  88 
  89     f, err := os.Open(s)
  90     if err != nil {
  91         return err
  92     }
  93     defer f.Close()
  94 
  95     return debase64(w, f)
  96 }
  97 
  98 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
  99 // apart from output-writing ones
 100 func debase64(w io.Writer, r io.Reader) error {
 101     br := bufio.NewReaderSize(r, 32*1024)
 102     start, err := br.Peek(64)
 103     if err != nil && err != io.EOF {
 104         return err
 105     }
 106 
 107     skip, err := skipIntroDataURI(start)
 108     if err != nil {
 109         return err
 110     }
 111 
 112     if skip > 0 {
 113         br.Discard(skip)
 114     }
 115 
 116     dec := base64.NewDecoder(base64.StdEncoding, br)
 117     _, err = io.Copy(w, dec)
 118     return err
 119 }
 120 
 121 func skipIntroDataURI(chunk []byte) (skip int, err error) {
 122     if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 123         chunk = chunk[3:]
 124         skip += 3
 125     }
 126 
 127     if !bytes.HasPrefix(chunk, []byte(`data:`)) {
 128         return skip, nil
 129     }
 130 
 131     const l = len(`data:,`)
 132     if len(chunk) == l && chunk[l-1] == ',' {
 133         return l, nil
 134     }
 135 
 136     start := chunk
 137     if len(start) > 64 {
 138         start = start[:64]
 139     }
 140 
 141     i := bytes.Index(start, []byte(`;base64,`))
 142     if i < 0 {
 143         return skip, errors.New(`invalid data URI`)
 144     }
 145 
 146     skip += i + len(`;base64,`)
 147     return skip, nil
 148 }
 149 
 150 func seemsDataURI(s string) bool {
 151     start := s
 152     if len(s) > 64 {
 153         start = s[:64]
 154     }
 155     return strings.HasPrefix(s, `data:`) && strings.Contains(start, `;base64,`)
 156 }
     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(err.Error())
 122         os.Stderr.WriteString("\n")
 123         os.Exit(1)
 124         return
 125     }
 126 }
 127 
 128 func run(w io.Writer, path string, handle handler, live bool) error {
 129     bw := bufio.NewWriter(w)
 130     defer bw.Flush()
 131 
 132     if path == `-` {
 133         return handle(bw, makeRowReader(os.Stdin))
 134     }
 135 
 136     f, err := os.Open(path)
 137     if err != nil {
 138         // on windows, file-not-found error messages may mention `CreateFile`,
 139         // even when trying to open files in read-only mode
 140         return errors.New(`can't open file named ` + path)
 141     }
 142     defer f.Close()
 143 
 144     return handle(bw, makeRowReader(f))
 145 }
 146 
 147 func emitJSON(w *bufio.Writer, rr *csv.Reader) error {
 148     got := 0
 149     var keys []string
 150 
 151     err := loopCSV(rr, func(i int, row []string) error {
 152         got++
 153 
 154         if i == 0 {
 155             keys = make([]string, 0, len(row))
 156             for _, s := range row {
 157                 keys = append(keys, strings.Clone(s))
 158             }
 159             return nil
 160         }
 161 
 162         if i == 1 {
 163             w.WriteByte('[')
 164         } else {
 165             err := w.WriteByte(',')
 166             if err != nil {
 167                 return io.EOF
 168             }
 169         }
 170 
 171         w.WriteByte('{')
 172         for i, s := range row {
 173             if i > 0 {
 174                 w.WriteByte(',')
 175             }
 176 
 177             if numberLike(s) {
 178                 w.WriteByte('"')
 179                 writeInnerStringJSON(w, keys[i])
 180                 w.WriteString(`":`)
 181                 w.WriteString(s)
 182                 continue
 183             }
 184 
 185             writeKV(w, keys[i], s)
 186         }
 187 
 188         for i := len(row); i < len(keys); i++ {
 189             if i > 0 {
 190                 w.WriteByte(',')
 191             }
 192             w.WriteByte('"')
 193             writeInnerStringJSON(w, keys[i])
 194             w.WriteString(`":null`)
 195         }
 196         w.WriteByte('}')
 197 
 198         return nil
 199     })
 200 
 201     if err != nil {
 202         return err
 203     }
 204 
 205     if got > 1 {
 206         w.WriteString("]\n")
 207     }
 208     return nil
 209 }
 210 
 211 func emitJSONL(w *bufio.Writer, rr *csv.Reader) error {
 212     var keys []string
 213 
 214     return loopCSV(rr, func(i int, row []string) error {
 215         if i == 0 {
 216             keys = make([]string, 0, len(row))
 217             for _, s := range row {
 218                 c := string(append([]byte{}, s...))
 219                 keys = append(keys, c)
 220             }
 221             return nil
 222         }
 223 
 224         w.WriteByte('{')
 225         for i, s := range row {
 226             if i > 0 {
 227                 w.WriteByte(',')
 228                 w.WriteByte(' ')
 229             }
 230 
 231             if numberLike(s) {
 232                 w.WriteByte('"')
 233                 writeInnerStringJSON(w, keys[i])
 234                 w.WriteString(`": `)
 235                 w.WriteString(s)
 236                 continue
 237             }
 238 
 239             writeKV(w, keys[i], s)
 240         }
 241 
 242         for i := len(row); i < len(keys); i++ {
 243             if i > 0 {
 244                 w.WriteByte(',')
 245                 w.WriteByte(' ')
 246             }
 247             w.WriteByte('"')
 248             writeInnerStringJSON(w, keys[i])
 249             w.WriteString(`": null`)
 250         }
 251         w.WriteByte('}')
 252 
 253         w.WriteByte('\n')
 254         if w.Flush() != nil {
 255             return io.EOF
 256         }
 257         return nil
 258     })
 259 }
 260 
 261 func emitJSONS(w *bufio.Writer, rr *csv.Reader) error {
 262     got := 0
 263     var keys []string
 264 
 265     err := loopCSV(rr, func(i int, row []string) error {
 266         got++
 267 
 268         if i == 0 {
 269             keys = make([]string, 0, len(row))
 270             for _, s := range row {
 271                 c := string(append([]byte{}, s...))
 272                 keys = append(keys, c)
 273             }
 274             return nil
 275         }
 276 
 277         if i == 1 {
 278             w.WriteByte('[')
 279         } else {
 280             err := w.WriteByte(',')
 281             if err != nil {
 282                 return io.EOF
 283             }
 284         }
 285 
 286         w.WriteByte('{')
 287         for i, s := range row {
 288             if i > 0 {
 289                 w.WriteByte(',')
 290             }
 291             writeKV(w, keys[i], s)
 292         }
 293 
 294         for i := len(row); i < len(keys); i++ {
 295             if i > 0 {
 296                 w.WriteByte(',')
 297             }
 298             w.WriteByte('"')
 299             writeInnerStringJSON(w, keys[i])
 300             w.WriteString(`":null`)
 301         }
 302         w.WriteByte('}')
 303 
 304         return nil
 305     })
 306 
 307     if err != nil {
 308         return err
 309     }
 310 
 311     if got > 1 {
 312         w.WriteString("]\n")
 313     }
 314     return nil
 315 }
 316 
 317 func emitTSV(w *bufio.Writer, rr *csv.Reader) error {
 318     width := -1
 319 
 320     return loopCSV(rr, func(i int, row []string) error {
 321         if width < 0 {
 322             width = len(row)
 323         }
 324 
 325         for i, s := range row {
 326             if strings.IndexByte(s, '\t') >= 0 {
 327                 const msg = `can't convert CSV whose items have tabs to TSV`
 328                 return errors.New(msg)
 329             }
 330             if i > 0 {
 331                 w.WriteByte('\t')
 332             }
 333             w.WriteString(s)
 334         }
 335 
 336         for i := len(row); i < width; i++ {
 337             w.WriteByte('\t')
 338         }
 339 
 340         w.WriteByte('\n')
 341         if err := w.Flush(); err != nil {
 342             // a write error may be the consequence of stdout being closed,
 343             // perhaps by another app along a pipe
 344             return io.EOF
 345         }
 346         return nil
 347     })
 348 }
 349 
 350 // writeInnerStringJSON helps JSON-encode strings more quickly
 351 func writeInnerStringJSON(w *bufio.Writer, s string) {
 352     needsEscaping := false
 353     for _, r := range s {
 354         if '#' <= r && r <= '~' && r != '\\' {
 355             continue
 356         }
 357         if r == ' ' || r == '!' || unicode.IsLetter(r) {
 358             continue
 359         }
 360 
 361         needsEscaping = true
 362         break
 363     }
 364 
 365     if !needsEscaping {
 366         w.WriteString(s)
 367         return
 368     }
 369 
 370     outer, err := json.Marshal(s)
 371     if err != nil {
 372         return
 373     }
 374     inner := outer[1 : len(outer)-1]
 375     w.Write(inner)
 376 }
 377 
 378 func writeKV(w *bufio.Writer, k string, s string) {
 379     w.WriteByte('"')
 380     writeInnerStringJSON(w, k)
 381     w.WriteString(`": "`)
 382     writeInnerStringJSON(w, s)
 383     w.WriteByte('"')
 384 }
 385 
 386 func numberLike(s string) bool {
 387     if len(s) == 0 {
 388         return false
 389     }
 390 
 391     if s[0] == '-' {
 392         s = s[1:]
 393     }
 394 
 395     if len(s) == 0 || s[0] < '0' || s[0] > '9' {
 396         return false
 397     }
 398 
 399     for len(s) > 0 {
 400         lead := s[0]
 401         s = s[1:]
 402 
 403         if lead == '.' {
 404             return allDigits(s)
 405         }
 406         if lead < '0' || lead > '9' {
 407             return false
 408         }
 409     }
 410 
 411     return true
 412 }
 413 
 414 func allDigits(s string) bool {
 415     if len(s) == 0 {
 416         return false
 417     }
 418 
 419     for _, r := range s {
 420         if r < '0' || r > '9' {
 421             return false
 422         }
 423     }
 424     return true
 425 }
 426 
 427 func makeRowReader(r io.Reader) *csv.Reader {
 428     rr := csv.NewReader(r)
 429     rr.LazyQuotes = true
 430     rr.ReuseRecord = true
 431     rr.FieldsPerRecord = -1
 432     return rr
 433 }
 434 
 435 func loopCSV(rr *csv.Reader, handle func(i int, row []string) error) error {
 436     width := 0
 437 
 438     for i := 0; true; i++ {
 439         row, err := rr.Read()
 440         if err == io.EOF {
 441             return nil
 442         }
 443 
 444         if err != nil {
 445             return err
 446         }
 447 
 448         if i == 0 {
 449             width = len(row)
 450         }
 451 
 452         if len(row) > width {
 453             return errors.New(`data-row has more items than the header`)
 454         }
 455 
 456         if err := handle(i, row); err != nil {
 457             return err
 458         }
 459     }
 460 
 461     return nil
 462 }
     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(err.Error())
  75         os.Stderr.WriteString("\n")
  76         os.Exit(1)
  77         return
  78     }
  79 }
  80 
  81 type config struct {
  82     indent int
  83     lines  [][]byte
  84     live   bool
  85 }
  86 
  87 func run(w io.Writer, args []string, live bool) error {
  88     bw := bufio.NewWriter(w)
  89     defer bw.Flush()
  90 
  91     var cfg config
  92     cfg.indent = -1
  93     cfg.live = live
  94 
  95     if len(args) == 0 {
  96         if err := handleReader(bw, os.Stdin, &cfg); err != nil {
  97             return err
  98         }
  99     }
 100 
 101     for _, name := range args {
 102         if err := handleFile(bw, name, &cfg); err != nil {
 103             return err
 104         }
 105     }
 106 
 107     if dump(bw, cfg.lines, cfg.indent) != nil {
 108         return io.EOF
 109     }
 110     cfg.lines = nil
 111     return nil
 112 }
 113 
 114 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 115     if name == `` || name == `-` {
 116         return handleReader(w, os.Stdin, cfg)
 117     }
 118 
 119     f, err := os.Open(name)
 120     if err != nil {
 121         return errors.New(`can't read from file named "` + name + `"`)
 122     }
 123     defer f.Close()
 124 
 125     return handleReader(w, f, cfg)
 126 }
 127 
 128 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 129     const gb = 1024 * 1024 * 1024
 130     sc := bufio.NewScanner(r)
 131     sc.Buffer(nil, 8*gb)
 132 
 133     for i := 0; sc.Scan(); i++ {
 134         s := sc.Bytes()
 135         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 136             s = s[3:]
 137         }
 138 
 139         if cfg.indent != 0 {
 140             n := countIndent(s)
 141             if cfg.indent > n || cfg.indent < 0 {
 142                 cfg.indent = n
 143             }
 144         }
 145 
 146         if cfg.indent > 0 {
 147             cfg.lines = append(cfg.lines, s)
 148             continue
 149         }
 150 
 151         if dump(w, cfg.lines, cfg.indent) != nil {
 152             return io.EOF
 153         }
 154         cfg.lines = nil
 155         w.Write(dedent(s, cfg.indent))
 156 
 157         if w.WriteByte('\n') != nil {
 158             return io.EOF
 159         }
 160 
 161         if !cfg.live {
 162             continue
 163         }
 164 
 165         if w.Flush() != nil {
 166             return io.EOF
 167         }
 168     }
 169 
 170     return sc.Err()
 171 }
 172 
 173 func countIndent(s []byte) int {
 174     indent := 0
 175     for len(s) > 0 && s[0] == ' ' {
 176         indent++
 177         s = s[1:]
 178     }
 179     return indent
 180 }
 181 
 182 func dedent(s []byte, max int) []byte {
 183     for i := 0; i < max && len(s) > 0 && s[0] == ' '; i++ {
 184         s = s[1:]
 185     }
 186     return s
 187 }
 188 
 189 func dump(w *bufio.Writer, lines [][]byte, indent int) error {
 190     for _, l := range lines {
 191         w.Write(dedent(l, indent))
 192         if w.WriteByte('\n') != nil {
 193             return io.EOF
 194         }
 195     }
 196 
 197     if w.Flush() != nil {
 198         return io.EOF
 199     }
 200     return nil
 201 }
     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 )
  33 
  34 const info = `
  35 dedup [options...] [file...]
  36 
  37 
  38 DEDUPlicate lines prevents the same line from appearing again in the output,
  39 after the first time. Unique lines are remembered across inputs.
  40 
  41 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  42 feeds 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 stringSet map[string]struct{}
  50 
  51 func Main() {
  52     buffered := false
  53     args := os.Args[1:]
  54 
  55     if len(args) > 0 {
  56         switch args[0] {
  57         case `-b`, `--b`, `-buffered`, `--buffered`:
  58             buffered = true
  59             args = args[1:]
  60 
  61         case `-h`, `--h`, `-help`, `--help`:
  62             os.Stdout.WriteString(info[1:])
  63             return
  64         }
  65     }
  66 
  67     if len(args) > 0 && args[0] == `--` {
  68         args = args[1:]
  69     }
  70 
  71     liveLines := !buffered
  72     if !buffered {
  73         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  74             liveLines = false
  75         }
  76     }
  77 
  78     err := run(os.Stdout, args, liveLines)
  79     if err != nil && err != io.EOF {
  80         os.Stderr.WriteString(err.Error())
  81         os.Stderr.WriteString("\n")
  82         os.Exit(1)
  83         return
  84     }
  85 }
  86 
  87 func run(w io.Writer, args []string, live bool) error {
  88     files := make(stringSet)
  89     lines := make(stringSet)
  90     bw := bufio.NewWriter(w)
  91     defer bw.Flush()
  92 
  93     for _, name := range args {
  94         if _, ok := files[name]; ok {
  95             continue
  96         }
  97         files[name] = struct{}{}
  98 
  99         if err := handleFile(bw, name, lines, live); err != nil {
 100             return err
 101         }
 102     }
 103 
 104     if len(args) == 0 {
 105         return dedup(bw, os.Stdin, lines, live)
 106     }
 107     return nil
 108 }
 109 
 110 func handleFile(w *bufio.Writer, name string, got stringSet, live bool) error {
 111     if name == `` || name == `-` {
 112         return dedup(w, os.Stdin, got, live)
 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 dedup(w, f, got, live)
 122 }
 123 
 124 func dedup(w *bufio.Writer, r io.Reader, got stringSet, live bool) error {
 125     const gb = 1024 * 1024 * 1024
 126     sc := bufio.NewScanner(r)
 127     sc.Buffer(nil, 8*gb)
 128 
 129     for sc.Scan() {
 130         line := sc.Text()
 131         if _, ok := got[line]; ok {
 132             continue
 133         }
 134         got[line] = struct{}{}
 135 
 136         w.Write(sc.Bytes())
 137         if w.WriteByte('\n') != nil {
 138             return io.EOF
 139         }
 140 
 141         if !live {
 142             continue
 143         }
 144 
 145         if w.Flush() != nil {
 146             return io.EOF
 147         }
 148     }
 149 
 150     return sc.Err()
 151 }
     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(err.Error())
  75         os.Stderr.WriteString("\n")
  76         os.Exit(1)
  77         return
  78     }
  79 }
  80 
  81 func run(w io.Writer, args []string, live bool) error {
  82     dashes := 0
  83     for _, path := range args {
  84         if path == `-` {
  85             dashes++
  86         }
  87         if dashes > 1 {
  88             return errors.New(`can't read stdin (dash) more than once`)
  89         }
  90     }
  91 
  92     bw := bufio.NewWriter(w)
  93     defer bw.Flush()
  94 
  95     if len(args) == 0 {
  96         return dejsonl(bw, os.Stdin, live)
  97     }
  98 
  99     for _, path := range args {
 100         if err := handleInput(bw, path, live); err != nil {
 101             return err
 102         }
 103     }
 104 
 105     return nil
 106 }
 107 
 108 // handleInput simplifies control-flow for func main
 109 func handleInput(w *bufio.Writer, path string, live bool) error {
 110     if path == `-` {
 111         return dejsonl(w, os.Stdin, live)
 112     }
 113 
 114     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
 115     //  resp, err := http.Get(path)
 116     //  if err != nil {
 117     //      return err
 118     //  }
 119     //  defer resp.Body.Close()
 120     //  return dejsonl(w, resp.Body, live)
 121     // }
 122 
 123     f, err := os.Open(path)
 124     if err != nil {
 125         // on windows, file-not-found error messages may mention `CreateFile`,
 126         // even when trying to open files in read-only mode
 127         return errors.New(`can't open file named ` + path)
 128     }
 129     defer f.Close()
 130     return dejsonl(w, f, live)
 131 }
 132 
 133 // dejsonl simplifies control-flow for func handleInput
 134 func dejsonl(w *bufio.Writer, r io.Reader, live bool) error {
 135     const gb = 1024 * 1024 * 1024
 136     sc := bufio.NewScanner(r)
 137     sc.Buffer(nil, 8*gb)
 138     got := 0
 139 
 140     for i := 0; sc.Scan(); i++ {
 141         s := sc.Text()
 142         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 143             s = s[3:]
 144         }
 145 
 146         // trim spaces at both ends of the current line
 147         for len(s) > 0 && s[0] == ' ' {
 148             s = s[1:]
 149         }
 150         for len(s) > 0 && s[len(s)-1] == ' ' {
 151             s = s[:len(s)-1]
 152         }
 153 
 154         // ignore empty(ish) lines
 155         if len(s) == 0 {
 156             continue
 157         }
 158 
 159         // ignore lines starting with unix-style comments
 160         if len(s) > 0 && s[0] == '#' {
 161             continue
 162         }
 163 
 164         if err := checkJSONL(strings.NewReader(s)); err != nil {
 165             return err
 166         }
 167 
 168         if got == 0 {
 169             w.WriteByte('[')
 170         } else {
 171             w.WriteByte(',')
 172         }
 173         if w.WriteByte('\n') != nil {
 174             return io.EOF
 175         }
 176         w.WriteString(indent)
 177         w.WriteString(s)
 178         got++
 179 
 180         if !live {
 181             continue
 182         }
 183 
 184         if w.Flush() != nil {
 185             return io.EOF
 186         }
 187     }
 188 
 189     if got == 0 {
 190         w.WriteString("[\n]\n")
 191     } else {
 192         w.WriteString("\n]\n")
 193     }
 194     return sc.Err()
 195 }
 196 
 197 func checkJSONL(r io.Reader) error {
 198     dec := json.NewDecoder(r)
 199     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 200     // even if JSON parsers aren't required to guarantee such input-fidelity
 201     // for numbers
 202     dec.UseNumber()
 203 
 204     t, err := dec.Token()
 205     if err == io.EOF {
 206         return errors.New(`input has no JSON values`)
 207     }
 208 
 209     if err := checkToken(dec, t); err != nil {
 210         return err
 211     }
 212 
 213     _, err = dec.Token()
 214     if err == io.EOF {
 215         // input is over, so it's a success
 216         return nil
 217     }
 218 
 219     if err == nil {
 220         // a successful `read` is a failure, as it means there are
 221         // trailing JSON tokens
 222         return errors.New(`unexpected trailing data`)
 223     }
 224 
 225     // any other error, perhaps some invalid-JSON-syntax-type error
 226     return err
 227 }
 228 
 229 // checkToken handles recursion for func checkJSONL
 230 func checkToken(dec *json.Decoder, t json.Token) error {
 231     switch t := t.(type) {
 232     case json.Delim:
 233         switch t {
 234         case json.Delim('['):
 235             return checkArray(dec)
 236         case json.Delim('{'):
 237             return checkObject(dec)
 238         default:
 239             return errors.New(`unsupported JSON syntax ` + string(t))
 240         }
 241 
 242     case nil, bool, float64, json.Number, string:
 243         return nil
 244 
 245     default:
 246         // return fmt.Errorf(`unsupported token type %T`, t)
 247         return errors.New(`invalid JSON token`)
 248     }
 249 }
 250 
 251 // handleArray handles arrays for func checkToken
 252 func checkArray(dec *json.Decoder) error {
 253     for {
 254         t, err := dec.Token()
 255         if err != nil {
 256             return err
 257         }
 258 
 259         if t == json.Delim(']') {
 260             return nil
 261         }
 262 
 263         if err := checkToken(dec, t); err != nil {
 264             return err
 265         }
 266     }
 267 }
 268 
 269 // handleObject handles objects for func checkToken
 270 func checkObject(dec *json.Decoder) error {
 271     for {
 272         t, err := dec.Token()
 273         if err != nil {
 274             return err
 275         }
 276 
 277         if t == json.Delim('}') {
 278             return nil
 279         }
 280 
 281         if _, ok := t.(string); !ok {
 282             return errors.New(`expected a string for a key-value pair`)
 283         }
 284 
 285         t, err = dec.Token()
 286         if err == io.EOF || t == json.Delim('}') {
 287             return errors.New(`expected a value for a key-value pair`)
 288         }
 289 
 290         if err := checkToken(dec, t); err != nil {
 291             return err
 292         }
 293     }
 294 }
     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(err.Error())
 105         os.Stderr.WriteString("\n")
 106         os.Exit(1)
 107         return
 108     }
 109 }
 110 
 111 func parseDelay(s string) (delay time.Duration, ok bool) {
 112     f, err := strconv.ParseFloat(s, 64)
 113     if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 114         if f < 0 {
 115             f = 0
 116         }
 117         return time.Duration(f * float64(time.Second)), true
 118     }
 119 
 120     if d, err := time.ParseDuration(s); err == nil {
 121         return d, true
 122     }
 123 
 124     return 0, false
 125 }
 126 
 127 func run(w io.Writer, args []string, cfg config) error {
 128     bw := bufio.NewWriter(w)
 129     defer bw.Flush()
 130 
 131     dashes := 0
 132     for _, name := range args {
 133         if name == `-` {
 134             dashes++
 135         }
 136         if dashes > 1 {
 137             return errors.New(`can't read stdin (dash) more than once`)
 138         }
 139     }
 140 
 141     if len(args) == 0 {
 142         return delay(bw, os.Stdin, cfg)
 143     }
 144 
 145     for _, name := range args {
 146         if name == `-` {
 147             if err := delay(bw, os.Stdin, cfg); err != nil {
 148                 return err
 149             }
 150             continue
 151         }
 152 
 153         if err := handleFile(bw, name, cfg); err != nil {
 154             return err
 155         }
 156     }
 157     return nil
 158 }
 159 
 160 func handleFile(w *bufio.Writer, name string, cfg config) error {
 161     if name == `` || name == `-` {
 162         return delay(w, os.Stdin, cfg)
 163     }
 164 
 165     f, err := os.Open(name)
 166     if err != nil {
 167         return errors.New(`can't read from file named "` + name + `"`)
 168     }
 169     defer f.Close()
 170 
 171     return delay(w, f, cfg)
 172 }
 173 
 174 func delay(w *bufio.Writer, r io.Reader, cfg config) error {
 175     const gb = 1024 * 1024 * 1024
 176     sc := bufio.NewScanner(r)
 177     sc.Buffer(nil, 8*gb)
 178 
 179     for i := 0; sc.Scan(); i++ {
 180         s := sc.Bytes()
 181         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 182             s = s[3:]
 183         }
 184 
 185         time.Sleep(cfg.delay)
 186 
 187         w.Write(s)
 188         if w.WriteByte('\n') != nil {
 189             return io.EOF
 190         }
 191 
 192         if !cfg.liveLines {
 193             continue
 194         }
 195 
 196         if w.Flush() != nil {
 197             return io.EOF
 198         }
 199     }
 200 
 201     return sc.Err()
 202 }
     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(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73         return
  74     }
  75 }
  76 
  77 func run(w io.Writer, args []string, live bool) error {
  78     bw := bufio.NewWriter(w)
  79     defer bw.Flush()
  80 
  81     if len(args) == 0 {
  82         return dessv(bw, os.Stdin, live)
  83     }
  84 
  85     for _, name := range args {
  86         if err := handleFile(bw, name, live); err != nil {
  87             return err
  88         }
  89     }
  90     return nil
  91 }
  92 
  93 func handleFile(w *bufio.Writer, name string, live bool) error {
  94     if name == `` || name == `-` {
  95         return dessv(w, os.Stdin, live)
  96     }
  97 
  98     f, err := os.Open(name)
  99     if err != nil {
 100         return errors.New(`can't read from file named "` + name + `"`)
 101     }
 102     defer f.Close()
 103 
 104     return dessv(w, f, live)
 105 }
 106 
 107 func dessv(w *bufio.Writer, r io.Reader, live bool) error {
 108     const gb = 1024 * 1024 * 1024
 109     sc := bufio.NewScanner(r)
 110     sc.Buffer(nil, 8*gb)
 111     handleRow := handleRowSSV
 112     numTabs := ^0
 113 
 114     for i := 0; sc.Scan(); i++ {
 115         s := sc.Bytes()
 116         if i == 0 {
 117             if bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 118                 s = s[3:]
 119             }
 120 
 121             for _, b := range s {
 122                 if b == '\t' {
 123                     handleRow = handleRowTSV
 124                     break
 125                 }
 126             }
 127             numTabs = handleRow(w, s, numTabs)
 128         } else {
 129             handleRow(w, s, numTabs)
 130         }
 131 
 132         if w.WriteByte('\n') != nil {
 133             return io.EOF
 134         }
 135 
 136         if !live {
 137             continue
 138         }
 139 
 140         if w.Flush() != nil {
 141             return io.EOF
 142         }
 143     }
 144 
 145     return sc.Err()
 146 }
 147 
 148 func handleRowSSV(w *bufio.Writer, s []byte, n int) int {
 149     for len(s) > 0 && s[0] == ' ' {
 150         s = s[1:]
 151     }
 152     for len(s) > 0 && s[len(s)-1] == ' ' {
 153         s = s[:len(s)-1]
 154     }
 155 
 156     got := 0
 157 
 158     for got = 0; len(s) > 0; got++ {
 159         if got > 0 {
 160             w.WriteByte('\t')
 161         }
 162 
 163         i := bytes.IndexByte(s, ' ')
 164         if i < 0 {
 165             w.Write(s)
 166             s = nil
 167             n--
 168             break
 169         }
 170 
 171         w.Write(s[:i])
 172         s = s[i+1:]
 173         for len(s) > 0 && s[0] == ' ' {
 174             s = s[1:]
 175         }
 176         n--
 177     }
 178 
 179     w.Write(s)
 180     writeTabs(w, n)
 181     return got
 182 }
 183 
 184 func handleRowTSV(w *bufio.Writer, s []byte, n int) int {
 185     got := 0
 186     for _, b := range s {
 187         if b == '\t' {
 188             got++
 189         }
 190     }
 191 
 192     w.Write(s)
 193     writeTabs(w, n-got)
 194     return got
 195 }
 196 
 197 func writeTabs(w *bufio.Writer, n int) {
 198     for n > 0 {
 199         w.WriteByte('\t')
 200         n--
 201     }
 202 }
     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     "unicode/utf8"
  35 )
  36 
  37 const info = `
  38 detab [options...] [tab-stop...] [files...]
  39 
  40 Expand each tab into up to the number of spaces given, or up to 4 spaces by
  41 default.
  42 
  43 All (optional) leading options start with either single or double-dash:
  44 
  45     -h, -help       show this help message
  46     -i, -initial    don't convert tabs after non-blanks
  47 `
  48 
  49 type config struct {
  50     tabStop   uint
  51     initial   bool
  52     liveLines bool
  53 }
  54 
  55 func Main() {
  56     var cfg config
  57     cfg.tabStop = 4
  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         case `-i`, `--i`, `-initial`, `--initial`:
  73             cfg.initial = true
  74             args = args[1:]
  75             continue
  76         }
  77 
  78         break
  79     }
  80 
  81     if len(args) > 0 {
  82         if n, err := strconv.ParseUint(args[0], 10, 64); err == nil {
  83             cfg.tabStop = uint(n)
  84             args = args[1:]
  85         }
  86     }
  87 
  88     if len(args) > 0 && args[0] == `--` {
  89         args = args[1:]
  90     }
  91 
  92     if cfg.liveLines {
  93         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  94             cfg.liveLines = false
  95         }
  96     }
  97 
  98     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  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.NewWriter(w)
 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 detab(bw, os.Stdin, cfg)
 122     }
 123 
 124     for _, name := range args {
 125         if name == `-` {
 126             if err := detab(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 detab(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 detab(w, f, cfg)
 151 }
 152 
 153 func detab(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     var buf []byte
 159     maxSpaces := int(cfg.tabStop)
 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         if maxSpaces < 1 {
 168             w.Write(s)
 169         } else if cfg.initial {
 170             tabs := 0
 171             for len(s) > 0 && s[0] == '\t' {
 172                 tabs++
 173                 s = s[1:]
 174             }
 175             writeSpaces(w, maxSpaces*tabs)
 176             w.Write(s)
 177         } else if bytes.IndexByte(s, '\t') < 0 {
 178             w.Write(s)
 179         } else {
 180             buf = expandTabs(buf[:0], s, maxSpaces)
 181             w.Write(buf)
 182         }
 183 
 184         if w.WriteByte('\n') != nil {
 185             return io.EOF
 186         }
 187 
 188         if !cfg.liveLines {
 189             continue
 190         }
 191 
 192         if w.Flush() != nil {
 193             return io.EOF
 194         }
 195     }
 196 
 197     return sc.Err()
 198 }
 199 
 200 func expandTabs(dst []byte, src []byte, tabStop int) []byte {
 201     n := 0
 202 
 203     if tabStop < 1 {
 204         return append(dst, src...)
 205     }
 206 
 207     for len(src) > 0 {
 208         r, size := utf8.DecodeRune(src)
 209 
 210         if r != '\t' {
 211             dst = append(dst, src[:size]...)
 212             n++
 213         } else {
 214             spaces := tabStop - n%tabStop
 215             dst = appendSpaces(dst, spaces)
 216             n += spaces
 217         }
 218 
 219         src = src[size:]
 220     }
 221 
 222     return dst
 223 }
 224 
 225 func appendSpaces(dst []byte, n int) []byte {
 226     const (
 227         half   = `                                `
 228         spaces = half + half
 229     )
 230 
 231     for n >= len(spaces) {
 232         dst = append(dst, spaces...)
 233     }
 234     if n > 0 {
 235         dst = append(dst, spaces[:n]...)
 236     }
 237     return dst
 238 }
 239 
 240 func writeSpaces(w *bufio.Writer, n int) {
 241     const (
 242         half   = `                                `
 243         spaces = half + half
 244     )
 245 
 246     for n >= len(spaces) {
 247         w.WriteString(spaces)
 248         n -= len(spaces)
 249     }
 250     if n > 0 {
 251         w.WriteString(spaces[:n])
 252     }
 253 }
     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(err.Error())
 239             os.Stderr.WriteString("\n")
 240             nerr++
 241         }
 242 
 243         if alias, ok := styleAliases[sname]; ok {
 244             sname = alias
 245         }
 246 
 247         style, ok := styles[sname]
 248         if !ok {
 249             os.Stderr.WriteString("no style named `")
 250             os.Stderr.WriteString(args[1])
 251             os.Stderr.WriteString("`\n")
 252             nerr++
 253         }
 254 
 255         pairs = append(pairs, pair{expr: exp, style: style})
 256         args = args[2:]
 257     }
 258 
 259     if nerr > 0 {
 260         os.Exit(1)
 261         return
 262     }
 263 
 264     liveLines := !buffered
 265     if !buffered {
 266         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 267             liveLines = false
 268         }
 269     }
 270 
 271     sc := bufio.NewScanner(os.Stdin)
 272     sc.Buffer(nil, 8*1024*1024*1024)
 273     bw := bufio.NewWriter(os.Stdout)
 274     var plain []byte
 275 
 276     for i := 0; sc.Scan(); i++ {
 277         s := sc.Bytes()
 278         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 279             s = s[3:]
 280         }
 281         plain = appendPlain(plain[:0], s)
 282 
 283         if err := handleLine(bw, s, noANSI(plain), pairs); err != nil {
 284             return
 285         }
 286 
 287         if !liveLines {
 288             continue
 289         }
 290 
 291         if err := bw.Flush(); err != nil {
 292             return
 293         }
 294     }
 295 }
 296 
 297 // appendPlain extends the slice given using the non-ANSI parts of a string
 298 func appendPlain(dst []byte, src []byte) []byte {
 299     for len(src) > 0 {
 300         i, j := indexEscapeSequence(src)
 301         if i < 0 {
 302             dst = append(dst, src...)
 303             break
 304         }
 305         if j < 0 {
 306             j = len(src)
 307         }
 308 
 309         if i > 0 {
 310             dst = append(dst, src[:i]...)
 311         }
 312 
 313         src = src[j:]
 314     }
 315 
 316     return dst
 317 }
 318 
 319 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 320 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 321 // indices which can be independently negative when either the start/end of
 322 // a sequence isn't found; given their fairly-common use, even the hyperlink
 323 // ESC]8 sequences are supported
 324 func indexEscapeSequence(s []byte) (int, int) {
 325     var prev byte
 326 
 327     for i, b := range s {
 328         if prev == '\x1b' && b == '[' {
 329             j := indexLetter(s[i+1:])
 330             if j < 0 {
 331                 return i, -1
 332             }
 333             return i - 1, i + 1 + j + 1
 334         }
 335 
 336         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 337             j := indexPair(s[i+1:], '\x1b', '\\')
 338             if j < 0 {
 339                 return i, -1
 340             }
 341             return i - 1, i + 1 + j + 2
 342         }
 343 
 344         prev = b
 345     }
 346 
 347     return -1, -1
 348 }
 349 
 350 func indexLetter(s []byte) int {
 351     for i, b := range s {
 352         upper := b &^ 32
 353         if 'A' <= upper && upper <= 'Z' {
 354             return i
 355         }
 356     }
 357 
 358     return -1
 359 }
 360 
 361 func indexPair(s []byte, x byte, y byte) int {
 362     var prev byte
 363 
 364     for i, b := range s {
 365         if prev == x && b == y && i > 0 {
 366             return i
 367         }
 368         prev = b
 369     }
 370 
 371     return -1
 372 }
 373 
 374 // noANSI ensures arguments to func handleLine are given in the right order
 375 type noANSI []byte
 376 
 377 // handleLine styles the current line given to it using the first matching
 378 // regex, keeping it as given if none of the regexes match; it's given 2
 379 // strings: the first is the original line, the latter is its plain-text
 380 // version (with no ANSI codes) and is used for the regex-matching, since
 381 // ANSI codes use a mix of numbers and letters, which can themselves match
 382 func handleLine(w *bufio.Writer, s []byte, plain noANSI, pairs []pair) error {
 383     for _, p := range pairs {
 384         if p.expr.Match(plain) {
 385             w.WriteString(p.style)
 386             w.Write(s)
 387             w.WriteString("\x1b[0m")
 388             return w.WriteByte('\n')
 389         }
 390     }
 391 
 392     w.Write(s)
 393     return w.WriteByte('\n')
 394 }
     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(err.Error())
  96             os.Stderr.WriteString("\n")
  97             continue
  98         }
  99 
 100         exprs = append(exprs, exp)
 101     }
 102 
 103     // quit right away when given invalid regexes
 104     if len(exprs) < len(args) {
 105         os.Exit(1)
 106         return
 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     err := run(os.Stdout, os.Stdin, exprs, liveLines)
 117     if err != nil && err != io.EOF {
 118         os.Stderr.WriteString(err.Error())
 119         os.Stderr.WriteString("\n")
 120         os.Exit(1)
 121         return
 122     }
 123 }
 124 
 125 func run(w io.Writer, r io.Reader, exprs []*regexp.Regexp, live bool) error {
 126     var buf []byte
 127     sc := bufio.NewScanner(r)
 128     sc.Buffer(nil, 8*1024*1024*1024)
 129     bw := bufio.NewWriter(w)
 130     defer bw.Flush()
 131 
 132     src := make([]byte, 8*1024)
 133     dst := make([]byte, 8*1024)
 134 
 135     for i := 0; sc.Scan(); i++ {
 136         line := sc.Bytes()
 137         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 138             line = line[3:]
 139         }
 140 
 141         s := line
 142         if bytes.IndexByte(s, '\x1b') >= 0 {
 143             buf = plain(buf[:0], s)
 144             s = buf
 145         }
 146 
 147         if len(exprs) > 0 {
 148             src = append(src[:0], s...)
 149             for _, exp := range exprs {
 150                 dst = erase(dst[:0], src, exp)
 151                 src = append(src[:0], dst...)
 152             }
 153             bw.Write(dst)
 154         } else {
 155             bw.Write(s)
 156         }
 157 
 158         if bw.WriteByte('\n') != nil {
 159             return io.EOF
 160         }
 161 
 162         if !live {
 163             continue
 164         }
 165 
 166         if bw.Flush() != nil {
 167             return io.EOF
 168         }
 169     }
 170 
 171     return sc.Err()
 172 }
 173 
 174 func erase(dst []byte, src []byte, with *regexp.Regexp) []byte {
 175     for len(src) > 0 {
 176         span := with.FindIndex(src)
 177         // also ignore empty regex matches to avoid infinite outer loops,
 178         // as skipping empty slices isn't advancing at all, leaving the
 179         // string stuck to being empty-matched forever by the same regex
 180         if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
 181             return append(dst, src...)
 182         }
 183 
 184         start, end := span[0], span[1]
 185         dst = append(dst, src[:start]...)
 186         // avoid infinite loops caused by empty regex matches
 187         if start == end && end < len(src) {
 188             dst = append(dst, src[end])
 189             end++
 190         }
 191         src = src[end:]
 192     }
 193 
 194     return dst
 195 }
 196 
 197 func plain(dst []byte, src []byte) []byte {
 198     for len(src) > 0 {
 199         i, j := indexEscapeSequence(src)
 200         if i < 0 {
 201             dst = append(dst, src...)
 202             break
 203         }
 204         if j < 0 {
 205             j = len(src)
 206         }
 207 
 208         if i > 0 {
 209             dst = append(dst, src[:i]...)
 210         }
 211 
 212         src = src[j:]
 213     }
 214 
 215     return dst
 216 }
 217 
 218 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 219 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 220 // indices which can be independently negative when either the start/end of
 221 // a sequence isn't found; given their fairly-common use, even the hyperlink
 222 // ESC]8 sequences are supported
 223 func indexEscapeSequence(s []byte) (int, int) {
 224     var prev byte
 225 
 226     for i, b := range s {
 227         if prev == '\x1b' && b == '[' {
 228             j := indexLetter(s[i+1:])
 229             if j < 0 {
 230                 return i, -1
 231             }
 232             return i - 1, i + 1 + j + 1
 233         }
 234 
 235         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 236             j := indexPair(s[i+1:], '\x1b', '\\')
 237             if j < 0 {
 238                 return i, -1
 239             }
 240             return i - 1, i + 1 + j + 2
 241         }
 242 
 243         prev = b
 244     }
 245 
 246     return -1, -1
 247 }
 248 
 249 func indexLetter(s []byte) int {
 250     for i, b := range s {
 251         upper := b &^ 32
 252         if 'A' <= upper && upper <= 'Z' {
 253             return i
 254         }
 255     }
 256 
 257     return -1
 258 }
 259 
 260 func indexPair(s []byte, x byte, y byte) int {
 261     var prev byte
 262 
 263     for i, b := range s {
 264         if prev == x && b == y && i > 0 {
 265             return i
 266         }
 267         prev = b
 268     }
 269 
 270     return -1
 271 }
     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(err.Error())
 121         os.Stderr.WriteString("\n")
 122         os.Exit(1)
 123         return
 124     }
 125 }
 126 
 127 func run(w io.Writer, args []string, cfg config) error {
 128     bw := bufio.NewWriter(w)
 129     defer bw.Flush()
 130 
 131     dashes := 0
 132     for _, name := range args {
 133         if name == `-` {
 134             dashes++
 135         }
 136         if dashes > 1 {
 137             return errors.New(`can't read stdin (dash) more than once`)
 138         }
 139     }
 140 
 141     if len(args) == 0 {
 142         return detab(bw, os.Stdin, cfg)
 143     }
 144 
 145     for _, name := range args {
 146         if name == `-` {
 147             if err := detab(bw, os.Stdin, cfg); err != nil {
 148                 return err
 149             }
 150             continue
 151         }
 152 
 153         if err := handleFile(bw, name, cfg); err != nil {
 154             return err
 155         }
 156     }
 157     return nil
 158 }
 159 
 160 func handleFile(w *bufio.Writer, name string, cfg config) error {
 161     if name == `` || name == `-` {
 162         return detab(w, os.Stdin, cfg)
 163     }
 164 
 165     f, err := os.Open(name)
 166     if err != nil {
 167         return errors.New(`can't read from file named "` + name + `"`)
 168     }
 169     defer f.Close()
 170 
 171     return detab(w, f, cfg)
 172 }
 173 
 174 func detab(w *bufio.Writer, r io.Reader, cfg config) error {
 175     const gb = 1024 * 1024 * 1024
 176     sc := bufio.NewScanner(r)
 177     sc.Buffer(nil, 8*gb)
 178 
 179     var buf []byte
 180     maxSpaces := int(cfg.tabStop)
 181 
 182     for i := 0; sc.Scan(); i++ {
 183         s := sc.Bytes()
 184         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 185             s = s[3:]
 186         }
 187 
 188         if maxSpaces < 1 {
 189             w.Write(s)
 190         } else if cfg.initial {
 191             tabs := 0
 192             for len(s) > 0 && s[0] == '\t' {
 193                 tabs++
 194                 s = s[1:]
 195             }
 196             writeSpaces(w, maxSpaces*tabs)
 197             w.Write(s)
 198         } else if bytes.IndexByte(s, '\t') < 0 {
 199             w.Write(s)
 200         } else {
 201             buf = expandTabs(buf[:0], s, maxSpaces)
 202             w.Write(buf)
 203         }
 204 
 205         if w.WriteByte('\n') != nil {
 206             return io.EOF
 207         }
 208 
 209         if !cfg.liveLines {
 210             continue
 211         }
 212 
 213         if w.Flush() != nil {
 214             return io.EOF
 215         }
 216     }
 217 
 218     return sc.Err()
 219 }
 220 
 221 func expandTabs(dst []byte, src []byte, tabStop int) []byte {
 222     n := 0
 223 
 224     if tabStop < 1 {
 225         return append(dst, src...)
 226     }
 227 
 228     for len(src) > 0 {
 229         r, size := utf8.DecodeRune(src)
 230 
 231         if r != '\t' {
 232             dst = append(dst, src[:size]...)
 233             n++
 234         } else {
 235             spaces := tabStop - n%tabStop
 236             dst = appendSpaces(dst, spaces)
 237             n += spaces
 238         }
 239 
 240         src = src[size:]
 241     }
 242 
 243     return dst
 244 }
 245 
 246 func appendSpaces(dst []byte, n int) []byte {
 247     const (
 248         half   = `                                `
 249         spaces = half + half
 250     )
 251 
 252     for n >= len(spaces) {
 253         dst = append(dst, spaces...)
 254     }
 255     if n > 0 {
 256         dst = append(dst, spaces[:n]...)
 257     }
 258     return dst
 259 }
 260 
 261 func writeSpaces(w *bufio.Writer, n int) {
 262     const (
 263         half   = `                                `
 264         spaces = half + half
 265     )
 266 
 267     for n >= len(spaces) {
 268         w.WriteString(spaces)
 269         n -= len(spaces)
 270     }
 271     if n > 0 {
 272         w.WriteString(spaces[:n])
 273     }
 274 }
     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(err.Error())
  67         os.Stderr.WriteString("\n")
  68         os.Exit(1)
  69         return
  70     }
  71 }
  72 
  73 func run(args []string) error {
  74     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  75     defer w.Flush()
  76 
  77     if len(args) == 0 {
  78         return factorLines(w)
  79     }
  80 
  81     for _, s := range args {
  82         n, ok := parseInt64(s)
  83         if !ok {
  84             return errors.New(s + ` isn't a valid integer`)
  85         }
  86 
  87         if err := factor(w, int64(n)); err != nil {
  88             return err
  89         }
  90     }
  91     return nil
  92 }
  93 
  94 func factor(w *bufio.Writer, n int64) error {
  95     var buf [24]byte
  96 
  97     w.Write(strconv.AppendInt(buf[:0], n, 10))
  98     w.WriteByte(':')
  99 
 100     for n > 0 && n%2 == 0 {
 101         w.WriteByte(' ')
 102         w.WriteByte('2')
 103         n /= 2
 104     }
 105 
 106     // avoid O(n**2) time-complexity by only checking up to the square-root
 107     max := uint64(math.Ceil(math.Sqrt(float64(n))))
 108 
 109     for div := uint64(3); div <= max; div += 2 {
 110         quo, rem := bits.Div64(0, uint64(n), div)
 111         if rem == 0 {
 112             s := strconv.AppendInt(append(buf[:0], ' '), int64(div), 10)
 113 
 114             w.Write(s)
 115             n = int64(quo)
 116 
 117             for {
 118                 quo, rem := bits.Div64(0, uint64(n), div)
 119                 if rem != 0 {
 120                     break
 121                 }
 122 
 123                 w.Write(s)
 124                 n = int64(quo)
 125             }
 126         }
 127     }
 128 
 129     if n > 1 {
 130         w.WriteByte(' ')
 131         w.Write(strconv.AppendInt(buf[:0], int64(n), 10))
 132     }
 133 
 134     if err := w.WriteByte('\n'); err != nil {
 135         return io.EOF
 136     }
 137     return nil
 138 }
 139 
 140 func factorLines(w *bufio.Writer) error {
 141     const gb = 1024 * 1024 * 1024
 142     sc := bufio.NewScanner(os.Stdin)
 143     sc.Buffer(nil, 8*gb)
 144 
 145     for sc.Scan() {
 146         line := strings.TrimSpace(sc.Text())
 147 
 148         for len(line) > 0 {
 149             item, rest := advance(line)
 150             line = rest
 151 
 152             n, ok := parseInt64(item)
 153             if !ok {
 154                 return errors.New(item + ` isn't a valid integer`)
 155             }
 156 
 157             if err := factor(w, int64(n)); err != nil {
 158                 return err
 159             }
 160 
 161             if err := w.Flush(); err != nil {
 162                 return io.EOF
 163             }
 164         }
 165     }
 166 
 167     return sc.Err()
 168 }
 169 
 170 func advance(s string) (lead string, rest string) {
 171     for len(s) > 0 && (s[0] == ' ' || s[0] == '\t' || s[0] == '\r') {
 172         s = s[1:]
 173     }
 174 
 175     for i, r := range s {
 176         if r == ' ' || r == '\t' || r == '\r' {
 177             return s[:i], s[i:]
 178         }
 179     }
 180 
 181     return s, ``
 182 }
 183 
 184 func parseInt64(s string) (n int64, ok bool) {
 185     // n, err := strconv.ParseInt(s, 10, 64)
 186     // return n, err == nil
 187 
 188     // handle an optional leading sign
 189     sign := int64(1)
 190     if len(s) > 0 {
 191         if s[0] == '-' {
 192             sign = -1
 193             s = s[1:]
 194         } else if s[0] == '+' {
 195             s = s[1:]
 196         }
 197     }
 198 
 199     if len(s) == 0 {
 200         return 0, false
 201     }
 202 
 203     digits := 0
 204 
 205     for len(s) > 0 {
 206         // less-than-0 byte-wraps around into some bigger-than-9 value
 207         if d := s[0] - '0'; d <= 9 {
 208             digits++
 209             n *= 10
 210             n += int64(d)
 211             s = s[1:]
 212             continue
 213         }
 214 
 215         // ignore underscores, which make long numbers easier to type right
 216         if s[0] == '_' {
 217             s = s[1:]
 218             continue
 219         }
 220 
 221         return sign * n, false
 222     }
 223 
 224     return sign * n, digits > 0
 225 }
     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: ./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(err.Error())
  91         os.Stderr.WriteString("\n")
  92         os.Exit(1)
  93         return
  94     }
  95 }
  96 
  97 type config struct {
  98     skipSubfolder error
  99     liveLines     bool
 100 }
 101 
 102 func run(w io.Writer, paths []string, cfg config) error {
 103     bw := bufio.NewWriter(w)
 104     defer bw.Flush()
 105 
 106     got := make(map[string]struct{})
 107 
 108     if len(paths) == 0 {
 109         paths = []string{`.`}
 110     }
 111 
 112     // handle is the callback for func filepath.WalkDir
 113     handle := func(path string, e fs.DirEntry, err error) error {
 114         if err != nil {
 115             return err
 116         }
 117 
 118         if _, ok := got[path]; ok {
 119             return nil
 120         }
 121         got[path] = struct{}{}
 122 
 123         if e.IsDir() {
 124             return cfg.skipSubfolder
 125         }
 126 
 127         return handleEntry(bw, path, cfg.liveLines)
 128     }
 129 
 130     for _, path := range paths {
 131         if _, ok := got[path]; ok {
 132             continue
 133         }
 134         got[path] = struct{}{}
 135 
 136         st, err := os.Stat(path)
 137         if err != nil {
 138             return err
 139         }
 140 
 141         if st.IsDir() {
 142             if !strings.HasSuffix(path, `/`) {
 143                 path = path + `/`
 144             }
 145             got[path] = struct{}{}
 146 
 147             if err := filepath.WalkDir(path, handle); err != nil {
 148                 return err
 149             }
 150             continue
 151         }
 152 
 153         if err := handleEntry(bw, path, cfg.liveLines); err != nil {
 154             return err
 155         }
 156     }
 157 
 158     return nil
 159 }
 160 
 161 func handleEntry(w *bufio.Writer, path string, live bool) error {
 162     abs, err := filepath.Abs(path)
 163     if err != nil {
 164         return err
 165     }
 166 
 167     w.WriteString(abs)
 168     w.WriteByte('\n')
 169 
 170     if !live {
 171         return nil
 172     }
 173 
 174     if w.Flush() != nil {
 175         return io.EOF
 176     }
 177     return nil
 178 }
     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(err.Error())
 104         os.Stderr.WriteString("\n")
 105         os.Exit(1)
 106         return
 107     }
 108 }
 109 
 110 func parseBlockSizeOption(s string) (size int, ok bool) {
 111     if !strings.HasPrefix(s, `-`) {
 112         return 0, false
 113     }
 114     if len(s) > 0 && s[0] == '-' {
 115         s = s[1:]
 116     }
 117     if len(s) > 0 && s[0] == '-' {
 118         s = s[1:]
 119     }
 120 
 121     if !strings.HasSuffix(s, `k`) && !strings.HasSuffix(s, `K`) {
 122         return 0, false
 123     }
 124     s = s[:len(s)-1]
 125 
 126     if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 {
 127         return int(n), true
 128     }
 129     return 0, false
 130 }
 131 
 132 type config struct {
 133     blockSize int
 134     sorted    bool
 135     recursive bool
 136     liveLines bool
 137 }
 138 
 139 type entry struct {
 140     path string
 141     size int64
 142 }
 143 
 144 type handlers struct {
 145     w             *bufio.Writer
 146     entries       []entry
 147     blockSize     int
 148     skipSubfolder error
 149 
 150     file func(h *handlers, path string, size int64) error
 151 
 152     liveLines bool
 153 }
 154 
 155 func (h handlers) countBlocks(size int64) int64 {
 156     bs := int64(h.blockSize)
 157     n := size / (1024 * bs)
 158     if size%(1024*bs) != 0 {
 159         n++
 160     }
 161     return n * bs
 162 }
 163 
 164 func run(w io.Writer, paths []string, cfg config) error {
 165     bw := bufio.NewWriter(w)
 166     defer bw.Flush()
 167 
 168     bw.WriteString("name\tbytes\tblocks\n")
 169 
 170     var h handlers
 171     h.w = bw
 172     h.blockSize = cfg.blockSize
 173     h.skipSubfolder = nil
 174     if !cfg.recursive {
 175         h.skipSubfolder = fs.SkipDir
 176     }
 177     h.file = emitEntry
 178     if cfg.sorted {
 179         h.file = keepEntry
 180     }
 181 
 182     got := make(map[string]struct{})
 183 
 184     if len(paths) == 0 {
 185         paths = []string{`.`}
 186     }
 187 
 188     // handle is the callback for func filepath.WalkDir
 189     handle := func(path string, e fs.DirEntry, err error) error {
 190         if err != nil {
 191             return err
 192         }
 193 
 194         if _, ok := got[path]; ok {
 195             return nil
 196         }
 197         got[path] = struct{}{}
 198 
 199         if e.IsDir() {
 200             return h.skipSubfolder
 201         }
 202 
 203         info, err := e.Info()
 204         if err != nil {
 205             return err
 206         }
 207 
 208         return h.file(&h, path, info.Size())
 209     }
 210 
 211     for _, path := range paths {
 212         if _, ok := got[path]; ok {
 213             continue
 214         }
 215         got[path] = struct{}{}
 216 
 217         st, err := os.Stat(path)
 218         if err != nil {
 219             return err
 220         }
 221 
 222         if st.IsDir() {
 223             if !strings.HasSuffix(path, `/`) {
 224                 path = path + `/`
 225             }
 226             got[path] = struct{}{}
 227 
 228             if err := filepath.WalkDir(path, handle); err != nil {
 229                 return err
 230             }
 231             continue
 232         }
 233 
 234         var size int64
 235         if path != `/dev/stdin` {
 236             size = st.Size()
 237         } else {
 238             size = countBytes(os.Stdin)
 239         }
 240 
 241         if err := h.file(&h, path, size); err != nil {
 242             return err
 243         }
 244     }
 245 
 246     if !cfg.sorted {
 247         return nil
 248     }
 249 
 250     sort.Slice(h.entries, func(i, j int) bool {
 251         return h.entries[i].size > h.entries[j].size
 252     })
 253 
 254     for _, e := range h.entries {
 255         if err := writeEntry(bw, e, h); err != nil {
 256             return err
 257         }
 258     }
 259 
 260     return nil
 261 }
 262 
 263 func countBytes(r io.Reader) (count int64) {
 264     var buf [32 * 1024]byte
 265 
 266     for {
 267         got, err := r.Read(buf[:])
 268         if got > 0 {
 269             count += int64(got)
 270         }
 271         if err != nil {
 272             break
 273         }
 274     }
 275 
 276     return count
 277 }
 278 
 279 func emitEntry(h *handlers, path string, size int64) error {
 280     return writeEntry(h.w, entry{path, size}, *h)
 281 }
 282 
 283 func keepEntry(h *handlers, path string, size int64) error {
 284     h.entries = append(h.entries, entry{path, size})
 285     return nil
 286 }
 287 
 288 func writeEntry(w *bufio.Writer, e entry, h handlers) error {
 289     abs, err := filepath.Abs(e.path)
 290     if err != nil {
 291         return err
 292     }
 293 
 294     var buf [24]byte
 295     w.WriteString(abs)
 296     w.WriteByte('\t')
 297     w.Write(strconv.AppendInt(buf[:0], e.size, 10))
 298     w.WriteByte('\t')
 299     w.Write(strconv.AppendInt(buf[:0], h.countBlocks(e.size), 10))
 300     w.WriteByte('\n')
 301 
 302     if !h.liveLines {
 303         return nil
 304     }
 305 
 306     if w.Flush() != nil {
 307         return io.EOF
 308     }
 309     return nil
 310 }
     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.NewWriter(os.Stdout)
  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     if len(args) > 0 && args[0] == `--` {
  68         args = args[1:]
  69     }
  70 
  71     linesLeft := 1
  72     if len(args) > 0 {
  73         s := strings.Replace(args[0], `_`, ``, -1)
  74         n, err := strconv.ParseInt(s, 10, 64)
  75         if err == nil {
  76             linesLeft = int(n)
  77             args = args[1:]
  78         }
  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(err.Error())
  95         os.Stderr.WriteString("\n")
  96         os.Exit(1)
  97         return
  98     }
  99 }
 100 
 101 func run(w io.Writer, paths []string, linesLeft int, liveLines bool) error {
 102     bw := bufio.NewWriter(w)
 103     defer bw.Flush()
 104 
 105     for _, path := range paths {
 106         if err := handleFile(bw, path, &linesLeft, liveLines); err != nil {
 107             return err
 108         }
 109     }
 110 
 111     if len(paths) == 0 {
 112         return first(bw, os.Stdin, &linesLeft, liveLines)
 113     }
 114     return nil
 115 }
 116 
 117 func handleFile(w *bufio.Writer, name string, left *int, live bool) error {
 118     if name == `` || name == `-` {
 119         return first(w, os.Stdin, left, live)
 120     }
 121 
 122     f, err := os.Open(name)
 123     if err != nil {
 124         return errors.New(`can't read from file named "` + name + `"`)
 125     }
 126     defer f.Close()
 127 
 128     return first(w, f, left, live)
 129 }
 130 
 131 func first(w *bufio.Writer, r io.Reader, left *int, live bool) error {
 132     if *left < 1 {
 133         return io.EOF
 134     }
 135 
 136     const gb = 1024 * 1024 * 1024
 137     sc := bufio.NewScanner(r)
 138     sc.Buffer(nil, 8*gb)
 139 
 140     for i := 0; sc.Scan(); i++ {
 141         s := sc.Bytes()
 142         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 143             s = s[3:]
 144         }
 145 
 146         w.Write(s)
 147         if w.WriteByte('\n') != nil {
 148             return io.EOF
 149         }
 150 
 151         if live {
 152             if err := w.Flush(); err != nil {
 153                 return io.EOF
 154             }
 155         }
 156 
 157         *left--
 158         if *left < 1 {
 159             break
 160         }
 161     }
 162 
 163     return sc.Err()
 164 }
     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(err.Error())
 109         os.Stderr.WriteString("\n")
 110         os.Exit(1)
 111         return
 112     }
 113 }
 114 
 115 func run(w io.Writer, args []string, cfg config) error {
 116     dashes := 0
 117     for _, name := range args {
 118         if name == `-` {
 119             dashes++
 120         }
 121         if dashes > 1 {
 122             return errors.New(`can't use stdin (dash) more than once`)
 123         }
 124     }
 125 
 126     bw := bufio.NewWriter(w)
 127     defer bw.Flush()
 128 
 129     if len(args) == 0 {
 130         return cfg.fix(bw, os.Stdin, cfg.liveLines)
 131     }
 132 
 133     for _, name := range args {
 134         if err := handleFile(bw, name, cfg); err != nil {
 135             return err
 136         }
 137     }
 138     return nil
 139 }
 140 
 141 func handleFile(w *bufio.Writer, name string, cfg config) error {
 142     if name == `` || name == `-` {
 143         return cfg.fix(w, os.Stdin, cfg.liveLines)
 144     }
 145 
 146     f, err := os.Open(name)
 147     if err != nil {
 148         return errors.New(`can't read from file named "` + name + `"`)
 149     }
 150     defer f.Close()
 151 
 152     return cfg.fix(w, f, cfg.liveLines)
 153 }
 154 
 155 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 156     const gb = 1024 * 1024 * 1024
 157     sc := bufio.NewScanner(r)
 158     sc.Buffer(nil, 8*gb)
 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         w.Write(s)
 167         if w.WriteByte('\n') != nil {
 168             return io.EOF
 169         }
 170 
 171         if !live {
 172             continue
 173         }
 174 
 175         if w.Flush() != nil {
 176             return io.EOF
 177         }
 178     }
 179 
 180     return sc.Err()
 181 }
 182 
 183 func detrail(w *bufio.Writer, r io.Reader, live bool) error {
 184     const gb = 1024 * 1024 * 1024
 185     sc := bufio.NewScanner(r)
 186     sc.Buffer(nil, 8*gb)
 187 
 188     for i := 0; sc.Scan(); i++ {
 189         s := sc.Bytes()
 190 
 191         // ignore leading UTF-8 BOM on the first line
 192         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 193             s = s[3:]
 194         }
 195 
 196         // trim trailing spaces on the current line
 197         for len(s) > 0 && s[len(s)-1] == ' ' {
 198             s = s[:len(s)-1]
 199         }
 200 
 201         w.Write(s)
 202         if w.WriteByte('\n') != nil {
 203             return io.EOF
 204         }
 205 
 206         if !live {
 207             continue
 208         }
 209 
 210         if w.Flush() != nil {
 211             return io.EOF
 212         }
 213     }
 214 
 215     return sc.Err()
 216 }
 217 
 218 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
 219     const gb = 1024 * 1024 * 1024
 220     sc := bufio.NewScanner(r)
 221     sc.Buffer(nil, 8*gb)
 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         writeSqueezed(w, s)
 230         if w.WriteByte('\n') != nil {
 231             return io.EOF
 232         }
 233 
 234         if !live {
 235             continue
 236         }
 237 
 238         if w.Flush() != nil {
 239             return io.EOF
 240         }
 241     }
 242 
 243     return sc.Err()
 244 }
 245 
 246 func writeSqueezed(w *bufio.Writer, s []byte) {
 247     // ignore leading spaces
 248     for len(s) > 0 && s[0] == ' ' {
 249         s = s[1:]
 250     }
 251 
 252     // ignore trailing spaces
 253     for len(s) > 0 && s[len(s)-1] == ' ' {
 254         s = s[:len(s)-1]
 255     }
 256 
 257     space := false
 258 
 259     for len(s) > 0 {
 260         switch s[0] {
 261         case ' ':
 262             s = s[1:]
 263             space = true
 264 
 265         case '\t':
 266             s = s[1:]
 267             space = false
 268             for len(s) > 0 && s[0] == ' ' {
 269                 s = s[1:]
 270             }
 271             w.WriteByte('\t')
 272 
 273         default:
 274             if space {
 275                 w.WriteByte(' ')
 276                 space = false
 277             }
 278             w.WriteByte(s[0])
 279             s = s[1:]
 280         }
 281     }
 282 }
     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 == 0.0 || expr.y == 0.0 {
 217             // 0*y -> 0
 218             // x*0 -> 0
 219             return 0.0
 220         }
 221         if expr.x == 1.0 {
 222             // 1*y -> y
 223             return expr.y
 224         }
 225         if expr.y == 1.0 {
 226             // x*1 -> x
 227             return expr.x
 228         }
 229         if expr.x == -1.0 {
 230             // -1*y -> -y
 231             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
 232         }
 233         if expr.y == -1.0 {
 234             // x*-1 -> -x
 235             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
 236         }
 237 
 238     case "/":
 239         if expr.x == 1.0 {
 240             // 1/y -> reciprocal of y
 241             return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.y})
 242         }
 243         if expr.y == 1.0 {
 244             // x/1 -> x
 245             return expr.x
 246         }
 247         if expr.y == -1.0 {
 248             // x/-1 -> -x
 249             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
 250         }
 251 
 252     case "**":
 253         switch expr.y {
 254         case -1.0:
 255             // x**-1 -> 1/x, reciprocal of x
 256             return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.x})
 257         case 0.0:
 258             // x**0 -> 1
 259             return 1.0
 260         case 1.0:
 261             // x**1 -> x
 262             return expr.x
 263         case 2.0:
 264             // x**2 -> *x
 265             return c.optimizeUnaryExpr(unaryExpr{op: "*", x: expr.x})
 266         case 3.0:
 267             // x**3 -> *x*x
 268             sq := unaryExpr{op: "*", x: expr.x}
 269             return c.optimizeBinaryExpr(binaryExpr{op: "*", x: sq, y: expr.x})
 270         }
 271 
 272     case "&", "&&":
 273         if expr.x == 0.0 || expr.y == 0.0 {
 274             // 0 && y -> 0
 275             // x && 0 -> 0
 276             return 0.0
 277         }
 278     }
 279 
 280     // no simplifiable patterns were detected
 281     return expr
 282 }
 283 
 284 // optimizeCallExpr optimizes special cases of built-in func calls
 285 func (c *Compiler) optimizeCallExpr(call callExpr) any {
 286     // recursively optimize all inputs, and keep track if they're all
 287     // constants in the end
 288     numlit := 0
 289     for i, v := range call.args {
 290         v = c.optimize(v)
 291         call.args[i] = v
 292         if _, ok := v.(float64); ok {
 293             numlit++
 294         }
 295     }
 296 
 297     // if func is overridden, there's no guarantee the new func works the same
 298     if _, ok := determFuncs[call.name]; c.ftype[call.name] != nil || !ok {
 299         return call
 300     }
 301 
 302     // from this point on, func is guaranteed to be built-in and deterministic
 303 
 304     // handle all-const inputs, by calling func and using its result
 305     if numlit == len(call.args) {
 306         in := make([]float64, 0, len(call.args))
 307         for _, v := range call.args {
 308             f, _ := v.(float64)
 309             in = append(in, f)
 310         }
 311 
 312         if f, ok := tryCall(determFuncs[call.name], in); ok {
 313             return f
 314         }
 315     }
 316 
 317     switch len(call.args) {
 318     case 1:
 319         if op, ok := func2unary[call.name]; ok {
 320             expr := unaryExpr{op: op, x: call.args[0]}
 321             return c.optimizeUnaryExpr(expr)
 322         }
 323         return call
 324 
 325     case 2:
 326         if op, ok := func2binary[call.name]; ok {
 327             expr := binaryExpr{op: op, x: call.args[0], y: call.args[1]}
 328             return c.optimizeBinaryExpr(expr)
 329         }
 330         if name, ok := vararg2func2[call.name]; ok {
 331             call.name = name
 332             return call
 333         }
 334         return call
 335 
 336     default:
 337         return call
 338     }
 339 }
 340 
 341 // tryConstant tries to optimize the expression given into a constant
 342 func (c *Compiler) tryConstant(expr any) (value float64, ok bool) {
 343     switch expr := expr.(type) {
 344     case float64:
 345         return expr, true
 346 
 347     case string:
 348         if _, ok := c.vaddr[expr]; !ok {
 349             // name isn't explicitly defined
 350             if f, ok := mathConst[expr]; ok {
 351                 // and is a known math constant
 352                 return f, true
 353             }
 354         }
 355         return 0, false
 356 
 357     default:
 358         return 0, false
 359     }
 360 }
 361 
 362 // tryCall tries to simplify the function expression given into a constant
 363 func tryCall(fn any, in []float64) (value float64, ok bool) {
 364     switch fn := fn.(type) {
 365     case func(float64) float64:
 366         if len(in) == 1 {
 367             return fn(in[0]), true
 368         }
 369         return 0, false
 370 
 371     case func(float64, float64) float64:
 372         if len(in) == 2 {
 373             return fn(in[0], in[1]), true
 374         }
 375         return 0, false
 376 
 377     case func(float64, float64, float64) float64:
 378         if len(in) == 3 {
 379             return fn(in[0], in[1], in[2]), true
 380         }
 381         return 0, false
 382 
 383     case func(float64, float64, float64, float64) float64:
 384         if len(in) == 4 {
 385             return fn(in[0], in[1], in[2], in[3]), true
 386         }
 387         return 0, false
 388 
 389     case func(float64, float64, float64, float64, float64) float64:
 390         if len(in) == 5 {
 391             return fn(in[0], in[1], in[2], in[3], in[4]), true
 392         }
 393         return 0, false
 394 
 395     case func(...float64) float64:
 396         return fn(in...), true
 397 
 398     case func(float64, ...float64) float64:
 399         if len(in) >= 1 {
 400             return fn(in[0], in[1:]...), true
 401         }
 402         return 0, false
 403 
 404     default:
 405         // type isn't a supported func
 406         return 0, false
 407     }
 408 }
     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(err.Error())
  90         os.Stderr.WriteString("\n")
  91         os.Exit(1)
  92         return
  93     }
  94 }
  95 
  96 type config struct {
  97     got       map[string]struct{}
  98     recursive bool
  99     liveLines bool
 100 }
 101 
 102 func run(w io.Writer, paths []string, cfg config) error {
 103     bw := bufio.NewWriter(w)
 104     defer bw.Flush()
 105 
 106     if len(paths) == 0 {
 107         paths = []string{`.`}
 108     }
 109 
 110     if cfg.recursive {
 111         return runRecursive(bw, paths, cfg)
 112     }
 113     return runFlat(bw, paths, cfg)
 114 }
 115 
 116 func runRecursive(w *bufio.Writer, paths []string, cfg config) error {
 117     if len(paths) == 0 {
 118         paths = []string{`.`}
 119     }
 120 
 121     // handle is the callback for func filepath.WalkDir
 122     handle := func(path string, e fs.DirEntry, err error) error {
 123         if err != nil {
 124             return err
 125         }
 126 
 127         if _, ok := cfg.got[path]; ok {
 128             return nil
 129         }
 130         cfg.got[path] = struct{}{}
 131 
 132         if e.IsDir() {
 133             if err := handleEntry(w, path, cfg.liveLines); err != nil {
 134                 return err
 135             }
 136         }
 137 
 138         return nil
 139     }
 140 
 141     for _, path := range paths {
 142         if _, ok := cfg.got[path]; ok {
 143             continue
 144         }
 145         cfg.got[path] = struct{}{}
 146 
 147         st, err := os.Stat(path)
 148         if err != nil {
 149             return err
 150         }
 151 
 152         if !strings.HasSuffix(path, `/`) {
 153             path = path + `/`
 154             cfg.got[path] = struct{}{}
 155         }
 156 
 157         if !st.IsDir() {
 158             continue
 159         }
 160 
 161         if err := filepath.WalkDir(path, handle); err != nil {
 162             return err
 163         }
 164     }
 165 
 166     return nil
 167 }
 168 
 169 func runFlat(w *bufio.Writer, paths []string, cfg config) error {
 170     if len(paths) == 0 {
 171         paths = []string{`.`}
 172     }
 173 
 174     for _, path := range paths {
 175         if _, ok := cfg.got[path]; ok {
 176             continue
 177         }
 178         cfg.got[path] = struct{}{}
 179 
 180         st, err := os.Stat(path)
 181         if err != nil {
 182             return err
 183         }
 184 
 185         if !strings.HasSuffix(path, `/`) {
 186             path = path + `/`
 187             cfg.got[path] = struct{}{}
 188         }
 189 
 190         if !st.IsDir() {
 191             continue
 192         }
 193 
 194         entries, err := os.ReadDir(path)
 195         if err != nil {
 196             return err
 197         }
 198 
 199         for _, e := range entries {
 200             if !e.IsDir() {
 201                 continue
 202             }
 203 
 204             path := filepath.Join(path, e.Name())
 205             if err := handleEntry(w, path, cfg.liveLines); err != nil {
 206                 return err
 207             }
 208         }
 209     }
 210 
 211     return nil
 212 }
 213 
 214 func handleEntry(w *bufio.Writer, path string, live bool) error {
 215     abs, err := filepath.Abs(path)
 216     if err != nil {
 217         return err
 218     }
 219 
 220     w.WriteString(abs)
 221     w.WriteByte('\n')
 222 
 223     if !live {
 224         return nil
 225     }
 226 
 227     if w.Flush() != nil {
 228         return io.EOF
 229     }
 230     return nil
 231 }
     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(err.Error())
 117             os.Stderr.WriteString("\n")
 118             errcount++
 119             continue
 120         }
 121 
 122         args = args[1:]
 123         var with []string
 124         if !erase && len(args) > 0 {
 125             with = splitReplacement(args[0])
 126             args = args[1:]
 127         }
 128         pairs = append(pairs, pair{what, with})
 129     }
 130 
 131     // quit right away when given invalid regexes
 132     if errcount > 0 {
 133         os.Exit(1)
 134         return
 135     }
 136 
 137     liveLines := !buffered
 138     if !buffered {
 139         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 140             liveLines = false
 141         }
 142     }
 143 
 144     err := run(os.Stdout, os.Stdin, pairs, liveLines)
 145     if err != nil && err != io.EOF {
 146         os.Stderr.WriteString(err.Error())
 147         os.Stderr.WriteString("\n")
 148         os.Exit(1)
 149         return
 150     }
 151 }
 152 
 153 func splitReplacement(s string) []string {
 154     if s == `` {
 155         return nil
 156     }
 157 
 158     n := 1
 159     var prev rune
 160     for _, r := range s {
 161         if prev != '\\' && r == '&' {
 162             n += 2
 163         }
 164         prev = r
 165     }
 166 
 167     repl := make([]string, 0, n)
 168 
 169     for len(s) > 0 {
 170         i := strings.IndexByte(s, '&')
 171         if i == 0 || (i > 0 && s[i-1] != '\\') {
 172             repl = append(repl, unescapeBackslashes(s[:i]))
 173             repl = append(repl, ``)
 174             s = s[i+1:]
 175             continue
 176         }
 177         break
 178     }
 179 
 180     if len(s) > 0 {
 181         repl = append(repl, unescapeBackslashes(s))
 182     }
 183     return repl
 184 }
 185 
 186 func unescapeBackslashes(s string) string {
 187     if strings.IndexByte(s, '\\') < 0 {
 188         return s
 189     }
 190 
 191     var prev byte
 192     unesc := make([]byte, 0, len(s))
 193 
 194     for i := range s {
 195         b := s[i]
 196 
 197         if prev != '\\' {
 198             if b != '\\' {
 199                 unesc = append(unesc, s[i])
 200             }
 201             prev = b
 202             continue
 203         }
 204 
 205         switch b {
 206         case 'e':
 207             unesc = append(unesc, '\x1b')
 208         case 'n':
 209             unesc = append(unesc, '\n')
 210         case 'r':
 211             unesc = append(unesc, '\r')
 212         case 't':
 213             unesc = append(unesc, '\t')
 214         case 'v':
 215             unesc = append(unesc, '\v')
 216         default:
 217             unesc = append(unesc, s[i])
 218         }
 219 
 220         prev = b
 221     }
 222 
 223     return string(unesc)
 224 }
 225 
 226 func run(w io.Writer, r io.Reader, pairs []pair, live bool) error {
 227     var buf []byte
 228     sc := bufio.NewScanner(r)
 229     sc.Buffer(nil, 8*1024*1024*1024)
 230     bw := bufio.NewWriter(w)
 231     defer bw.Flush()
 232 
 233     src := make([]byte, 8*1024)
 234     dst := make([]byte, 8*1024)
 235 
 236     for i := 0; sc.Scan(); i++ {
 237         line := sc.Bytes()
 238         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 239             line = line[3:]
 240         }
 241 
 242         s := line
 243         if bytes.IndexByte(s, '\x1b') >= 0 {
 244             buf = plain(buf[:0], s)
 245             s = buf
 246         }
 247 
 248         if len(pairs) > 0 {
 249             src = append(src[:0], s...)
 250             for _, p := range pairs {
 251                 dst = gsub(dst[:0], src, p.expr, p.repl)
 252                 src = append(src[:0], dst...)
 253             }
 254             bw.Write(dst)
 255         } else {
 256             bw.Write(s)
 257         }
 258 
 259         if bw.WriteByte('\n') != nil {
 260             return io.EOF
 261         }
 262 
 263         if !live {
 264             continue
 265         }
 266 
 267         if bw.Flush() != nil {
 268             return io.EOF
 269         }
 270     }
 271 
 272     return sc.Err()
 273 }
 274 
 275 func gsub(dst []byte, src []byte, what *regexp.Regexp, with []string) []byte {
 276     for len(src) > 0 {
 277         span := what.FindIndex(src)
 278         // also ignore empty regex matches to avoid infinite outer loops,
 279         // as skipping empty slices isn't advancing at all, leaving the
 280         // string stuck to being empty-matched forever by the same regex
 281         if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
 282             return append(dst, src...)
 283         }
 284 
 285         start, end := span[0], span[1]
 286         dst = append(dst, src[:start]...)
 287         // avoid infinite loops caused by empty regex matches
 288         if start == end {
 289             if end >= len(src) {
 290                 break
 291             }
 292             dst = append(dst, src[end])
 293             end++
 294             src = src[end:]
 295             continue
 296         }
 297 
 298         match := src[start:end]
 299         for _, sub := range with {
 300             if sub == `` {
 301                 dst = append(dst, match...)
 302                 continue
 303             }
 304             dst = append(dst, sub...)
 305         }
 306 
 307         src = src[end:]
 308     }
 309 
 310     return dst
 311 }
 312 
 313 func plain(dst []byte, src []byte) []byte {
 314     for len(src) > 0 {
 315         i, j := indexEscapeSequence(src)
 316         if i < 0 {
 317             dst = append(dst, src...)
 318             break
 319         }
 320         if j < 0 {
 321             j = len(src)
 322         }
 323 
 324         if i > 0 {
 325             dst = append(dst, src[:i]...)
 326         }
 327 
 328         src = src[j:]
 329     }
 330 
 331     return dst
 332 }
 333 
 334 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 335 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 336 // indices which can be independently negative when either the start/end of
 337 // a sequence isn't found; given their fairly-common use, even the hyperlink
 338 // ESC]8 sequences are supported
 339 func indexEscapeSequence(s []byte) (int, int) {
 340     var prev byte
 341 
 342     for i, b := range s {
 343         if prev == '\x1b' && b == '[' {
 344             j := indexLetter(s[i+1:])
 345             if j < 0 {
 346                 return i, -1
 347             }
 348             return i - 1, i + 1 + j + 1
 349         }
 350 
 351         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 352             j := indexPair(s[i+1:], '\x1b', '\\')
 353             if j < 0 {
 354                 return i, -1
 355             }
 356             return i - 1, i + 1 + j + 2
 357         }
 358 
 359         prev = b
 360     }
 361 
 362     return -1, -1
 363 }
 364 
 365 func indexLetter(s []byte) int {
 366     for i, b := range s {
 367         upper := b &^ 32
 368         if 'A' <= upper && upper <= 'Z' {
 369             return i
 370         }
 371     }
 372 
 373     return -1
 374 }
 375 
 376 func indexPair(s []byte, x byte, y byte) int {
 377     var prev byte
 378 
 379     for i, b := range s {
 380         if prev == x && b == y && i > 0 {
 381             return i
 382         }
 383         prev = b
 384     }
 385 
 386     return -1
 387 }
     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("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(err.Error())
 110         os.Stderr.WriteString("\n")
 111         os.Exit(1)
 112         return
 113     }
 114 }
 115 
 116 func run(paths []string, cfg *config) error {
 117     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 118     defer w.Flush()
 119 
 120     for _, path := range paths {
 121         if cfg.max <= 0 {
 122             return io.EOF
 123         }
 124         if err := handleFile(w, path, cfg); err != nil {
 125             return err
 126         }
 127     }
 128 
 129     if len(paths) == 0 {
 130         if err := head(w, os.Stdin, cfg); err != nil {
 131             return err
 132         }
 133     }
 134     return nil
 135 }
 136 
 137 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 138     f, err := os.Open(path)
 139     if err != nil {
 140         return err
 141     }
 142     defer f.Close()
 143     return head(w, f, cfg)
 144 }
 145 
 146 func head(w *bufio.Writer, r io.Reader, cfg *config) error {
 147     const gb = 1024 * 1024 * 1024
 148     sc := bufio.NewScanner(r)
 149     sc.Buffer(nil, 8*gb)
 150 
 151     for sc.Scan() {
 152         if cfg.max <= 0 {
 153             return io.EOF
 154         }
 155 
 156         w.Write(sc.Bytes())
 157         if err := w.WriteByte('\n'); err != nil {
 158             return io.EOF
 159         }
 160 
 161         cfg.max--
 162 
 163         if !cfg.liveLines {
 164             continue
 165         }
 166 
 167         if w.Flush() != nil {
 168             return io.EOF
 169         }
 170     }
 171 
 172     return sc.Err()
 173 }
     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     -h, -help      show this help message
  52     -f, -filter    filter out (ignore) lines with no matches
  53     -i, -ins       match regexes case-insensitively
  54 `
  55 
  56 const highlightStyle = "\x1b[7m"
  57 
  58 func Main() {
  59     filter := false
  60     buffered := false
  61     insensitive := false
  62     args := os.Args[1:]
  63 
  64     for len(args) > 0 {
  65         switch args[0] {
  66         case `-b`, `--b`, `-buffered`, `--buffered`:
  67             buffered = true
  68             args = args[1:]
  69             continue
  70 
  71         case `-f`, `--f`, `-filter`, `--filter`:
  72             filter = true
  73             args = args[1:]
  74             continue
  75 
  76         case `-fi`, `--fi`, `-if`, `--if`:
  77             filter = true
  78             insensitive = true
  79             args = args[1:]
  80             continue
  81 
  82         case `-h`, `--h`, `-help`, `--help`:
  83             os.Stdout.WriteString(info[1:])
  84             return
  85 
  86         case `-i`, `--i`, `-ins`, `--ins`:
  87             insensitive = true
  88             args = args[1:]
  89             continue
  90         }
  91 
  92         break
  93     }
  94 
  95     if len(args) > 0 && args[0] == `--` {
  96         args = args[1:]
  97     }
  98 
  99     patterns := make([]pattern, 0, len(args))
 100 
 101     for _, s := range args {
 102         var err error
 103         var pat pattern
 104 
 105         if insensitive {
 106             pat, err = compile(`(?i)` + s)
 107         } else {
 108             pat, err = compile(s)
 109         }
 110 
 111         if err != nil {
 112             os.Stderr.WriteString(err.Error())
 113             os.Stderr.WriteString("\n")
 114             continue
 115         }
 116 
 117         patterns = append(patterns, pat)
 118     }
 119 
 120     // quit right away when given invalid regexes
 121     if len(patterns) < len(args) {
 122         os.Exit(1)
 123         return
 124     }
 125 
 126     liveLines := !buffered
 127     if !buffered {
 128         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 129             liveLines = false
 130         }
 131     }
 132 
 133     err := run(os.Stdout, os.Stdin, patterns, filter, liveLines)
 134     if err != nil && err != io.EOF {
 135         os.Stderr.WriteString(err.Error())
 136         os.Stderr.WriteString("\n")
 137         os.Exit(1)
 138         return
 139     }
 140 }
 141 
 142 // pattern is a regular-expression pattern which distinguishes between the
 143 // start/end of a line and those of the chunks it can be used to match
 144 type pattern struct {
 145     // expr is the regular-expression
 146     expr *regexp.Regexp
 147 
 148     // begin is whether the regexp refers to the start of a line
 149     begin bool
 150 
 151     // end is whether the regexp refers to the end of a line
 152     end bool
 153 }
 154 
 155 func compile(src string) (pattern, error) {
 156     expr, err := regexp.Compile(src)
 157 
 158     var pat pattern
 159     pat.expr = expr
 160     pat.begin = strings.HasPrefix(src, `^`) || strings.HasPrefix(src, `(?i)^`)
 161     pat.end = strings.HasSuffix(src, `$`) && !strings.HasSuffix(src, `\$`)
 162     return pat, err
 163 }
 164 
 165 func (p pattern) findIndex(s []byte, i int, last int) (start int, stop int) {
 166     if i > 0 && p.begin {
 167         return -1, -1
 168     }
 169     if i != last && p.end {
 170         return -1, -1
 171     }
 172 
 173     span := p.expr.FindIndex(s)
 174     // also ignore empty regex matches to avoid infinite outer loops,
 175     // as skipping empty slices isn't advancing at all, leaving the
 176     // string stuck to being empty-matched forever by the same regex
 177     if len(span) != 2 || span[0] == span[1] {
 178         return -1, -1
 179     }
 180 
 181     return span[0], span[1]
 182 }
 183 
 184 func run(w io.Writer, r io.Reader, pats []pattern, filter, live bool) error {
 185     sc := bufio.NewScanner(r)
 186     sc.Buffer(nil, 8*1024*1024*1024)
 187     bw := bufio.NewWriter(w)
 188     defer bw.Flush()
 189 
 190     for i := 0; sc.Scan(); i++ {
 191         s := sc.Bytes()
 192         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 193             s = s[3:]
 194         }
 195 
 196         n := 0
 197         last := countChunks(s) - 1
 198         if last < 0 {
 199             last = 0
 200         }
 201 
 202         if filter && !matches(s, pats, last) {
 203             continue
 204         }
 205 
 206         for len(s) > 0 {
 207             i, j := indexEscapeSequence(s)
 208             if i < 0 {
 209                 handleChunk(bw, s, pats, n, last)
 210                 break
 211             }
 212             if j < 0 {
 213                 j = len(s)
 214             }
 215 
 216             handleChunk(bw, s[:i], pats, n, last)
 217             if i > 0 {
 218                 n++
 219             }
 220 
 221             bw.Write(s[i:j])
 222 
 223             s = s[j:]
 224         }
 225 
 226         if bw.WriteByte('\n') != nil {
 227             return io.EOF
 228         }
 229 
 230         if !live {
 231             continue
 232         }
 233 
 234         if bw.Flush() != nil {
 235             return io.EOF
 236         }
 237     }
 238 
 239     return sc.Err()
 240 }
 241 
 242 // matches finds out if any regex matches any substring around ANSI-sequences
 243 func matches(s []byte, patterns []pattern, last int) bool {
 244     n := 0
 245 
 246     for len(s) > 0 {
 247         i, j := indexEscapeSequence(s)
 248         if i < 0 {
 249             for _, p := range patterns {
 250                 if begin, _ := p.findIndex(s, n, last); begin >= 0 {
 251                     return true
 252                 }
 253             }
 254             return false
 255         }
 256 
 257         if j < 0 {
 258             j = len(s)
 259         }
 260 
 261         for _, p := range patterns {
 262             if begin, _ := p.findIndex(s[:i], n, last); begin >= 0 {
 263                 return true
 264             }
 265         }
 266 
 267         if i > 0 {
 268             n++
 269         }
 270 
 271         s = s[j:]
 272     }
 273 
 274     return false
 275 }
 276 
 277 func countChunks(s []byte) int {
 278     chunks := 0
 279 
 280     for len(s) > 0 {
 281         i, j := indexEscapeSequence(s)
 282         if i < 0 {
 283             break
 284         }
 285 
 286         if i > 0 {
 287             chunks++
 288         }
 289 
 290         if j < 0 {
 291             break
 292         }
 293         s = s[j:]
 294     }
 295 
 296     if len(s) > 0 {
 297         chunks++
 298     }
 299     return chunks
 300 }
 301 
 302 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 303 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 304 // indices which can be independently negative when either the start/end of
 305 // a sequence isn't found; given their fairly-common use, even the hyperlink
 306 // ESC]8 sequences are supported
 307 func indexEscapeSequence(s []byte) (int, int) {
 308     var prev byte
 309 
 310     for i, b := range s {
 311         if prev == '\x1b' && b == '[' {
 312             j := indexLetter(s[i+1:])
 313             if j < 0 {
 314                 return i, -1
 315             }
 316             return i - 1, i + 1 + j + 1
 317         }
 318 
 319         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 320             j := indexPair(s[i+1:], '\x1b', '\\')
 321             if j < 0 {
 322                 return i, -1
 323             }
 324             return i - 1, i + 1 + j + 2
 325         }
 326 
 327         prev = b
 328     }
 329 
 330     return -1, -1
 331 }
 332 
 333 func indexLetter(s []byte) int {
 334     for i, b := range s {
 335         upper := b &^ 32
 336         if 'A' <= upper && upper <= 'Z' {
 337             return i
 338         }
 339     }
 340 
 341     return -1
 342 }
 343 
 344 func indexPair(s []byte, x byte, y byte) int {
 345     var prev byte
 346 
 347     for i, b := range s {
 348         if prev == x && b == y && i > 0 {
 349             return i
 350         }
 351         prev = b
 352     }
 353 
 354     return -1
 355 }
 356 
 357 // note: looking at the results of restoring ANSI-styles after style-resets
 358 // doesn't seem to be worth it, as a previous version used to do
 359 
 360 // handleChunk handles line-slices around any detected ANSI-style sequences,
 361 // or even whole lines, when no ANSI-styles are found in them
 362 func handleChunk(w *bufio.Writer, s []byte, with []pattern, n int, last int) {
 363     for len(s) > 0 {
 364         start, end := -1, -1
 365         for _, p := range with {
 366             i, j := p.findIndex(s, n, last)
 367             if i >= 0 && (i < start || start < 0) {
 368                 start, end = i, j
 369             }
 370         }
 371 
 372         if start < 0 {
 373             w.Write(s)
 374             return
 375         }
 376 
 377         w.Write(s[:start])
 378         w.WriteString(highlightStyle)
 379         w.Write(s[start:end])
 380         w.WriteString("\x1b[0m")
 381 
 382         s = s[end:]
 383     }
 384 }
     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(err.Error())
 106         os.Stderr.WriteString("\n")
 107         os.Exit(1)
 108         return
 109     }
 110 }
 111 
 112 func run(w io.Writer, args []string, cfg *config) error {
 113     bw := bufio.NewWriter(w)
 114     defer bw.Flush()
 115 
 116     if len(args) == 0 {
 117         if err := handleReader(bw, os.Stdin, cfg); err != nil {
 118             return err
 119         }
 120     }
 121 
 122     for _, name := range args {
 123         if err := handleFile(bw, name, cfg); err != nil {
 124             return err
 125         }
 126     }
 127 
 128     if cfg.Started {
 129         endPage(bw)
 130     }
 131     return nil
 132 }
 133 
 134 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 135     if name == `` || name == `-` {
 136         return handleReader(w, os.Stdin, cfg)
 137     }
 138 
 139     f, err := os.Open(name)
 140     if err != nil {
 141         return errors.New(`can't read from file named "` + name + `"`)
 142     }
 143     defer f.Close()
 144 
 145     return handleReader(w, f, cfg)
 146 }
 147 
 148 func handleReader(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     lines := 0
 153 
 154     for sc.Scan() {
 155         line := sc.Text()
 156 
 157         if lines == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
 158             line = line[3:]
 159         }
 160         if lines == 0 && !cfg.Started {
 161             title := line
 162             if cfg.GotTitle {
 163                 title = cfg.Title
 164             }
 165             startPage(w, title, cfg.Monospace)
 166             cfg.Started = true
 167         }
 168         lines++
 169 
 170         if err := handleLine(w, line, cfg); err != nil {
 171             return err
 172         }
 173     }
 174 
 175     if !cfg.Started && lines > 0 {
 176         startPage(w, cfg.Title, cfg.Monospace)
 177         cfg.Started = true
 178     }
 179     return sc.Err()
 180 }
 181 
 182 const style = `
 183         body {
 184             margin: 1rem auto 2rem auto;
 185             padding: 0.25rem;
 186             font-size: 1.1rem;
 187             line-height: 1.8rem;
 188             font-family: sans-serif;
 189 
 190             max-width: 95vw;
 191             /* width: max-content; */
 192             width: fit-content;
 193 
 194             box-sizing: border-box;
 195             display: block;
 196         }
 197 
 198         a {
 199             color: steelblue;
 200             text-decoration: none;
 201         }
 202 
 203         p {
 204             display: block;
 205             margin: auto;
 206             max-width: 80ch;
 207         }
 208 
 209         img {
 210             margin: none;
 211         }
 212 
 213         audio {
 214             width: 60ch;
 215         }
 216 
 217         table {
 218             margin: 2rem auto;
 219             border-collapse: collapse;
 220         }
 221 
 222         thead>* {
 223             position: sticky;
 224             top: 0;
 225             background-color: white;
 226         }
 227 
 228         tfoot th {
 229             user-select: none;
 230         }
 231 
 232         th, td {
 233             padding: 0.1rem 1ch;
 234             min-width: 4ch;
 235             border-bottom: solid thin transparent;
 236         }
 237 
 238         tr:nth-child(5n) td {
 239             border-bottom: solid thin #ccc;
 240         }
 241 
 242         .monospace {
 243             font-family: monospace;
 244         }
 245 `
 246 
 247 func startPage(w *bufio.Writer, title string, monospace bool) {
 248     w.WriteString("<!DOCTYPE html>\n")
 249     w.WriteString("<html lang=\"en\">\n")
 250     w.WriteString("\n")
 251     w.WriteString("<head>\n")
 252     w.WriteString("    <meta charset=\"UTF-8\">\n")
 253     w.WriteString("    <meta name=\"viewport\" content=\"width=device-width,")
 254     w.WriteString(" initial-scale=1.0\">\n")
 255     w.WriteString("    <meta http-equiv=\"X-UA-Compatible\"")
 256     w.WriteString(" content=\"ie=edge\">\n")
 257     w.WriteString("\n")
 258     w.WriteString("    <link rel=\"icon\" href=\"data:,\">\n")
 259     w.WriteString(`    <title>`)
 260     w.WriteString(title)
 261     w.WriteString("</title>\n")
 262     w.WriteString("\n")
 263     w.WriteString("\n")
 264     w.WriteString("    <style>\n")
 265     w.WriteString(style[1:])
 266     w.WriteString("    </style>\n")
 267     w.WriteString("</head>\n")
 268     if monospace {
 269         w.WriteString("<body class=\"monospace\">\n")
 270     } else {
 271         w.WriteString("<body>\n")
 272     }
 273 }
 274 
 275 func endPage(w *bufio.Writer) {
 276     w.WriteString("</body>\n")
 277     w.WriteString("</html>\n")
 278 }
 279 
 280 func handleLine(w *bufio.Writer, s string, cfg *config) error {
 281     if handleDataURI(w, s) {
 282         if w.WriteByte('\n') != nil {
 283             return io.EOF
 284         }
 285         return nil
 286     }
 287 
 288     for len(s) > 0 {
 289         span := links.FindStringIndex(s)
 290         if span != nil && len(span) == 2 {
 291             i := span[0]
 292             j := span[1]
 293             href := s[i:j]
 294             handleChunk(w, s[:i])
 295             w.WriteString(`<a href="`)
 296             w.WriteString(href)
 297             w.WriteString(`">`)
 298             w.WriteString(href)
 299             w.WriteString(`</a>`)
 300             s = s[j:]
 301             continue
 302         }
 303 
 304         handleChunk(w, s)
 305         break
 306     }
 307 
 308     w.WriteString(`<br>`)
 309     if w.WriteByte('\n') != nil {
 310         return io.EOF
 311     }
 312     return nil
 313 }
 314 
 315 func handleChunk(w *bufio.Writer, s string) {
 316     for len(s) > 0 {
 317         switch b := s[0]; b {
 318         case '&':
 319             w.WriteString("&amp;")
 320         case '<':
 321             w.WriteString("&lt;")
 322         case '>':
 323             w.WriteString("&gt;")
 324         default:
 325             w.WriteByte(b)
 326         }
 327         s = s[1:]
 328     }
 329 }
 330 
 331 func handleDataURI(w *bufio.Writer, s string) bool {
 332     full := s
 333 
 334     if !strings.HasPrefix(s, `data:`) {
 335         return false
 336     }
 337 
 338     s = strings.TrimPrefix(s, `data:`)
 339     i := strings.Index(s, `;base64,`)
 340     if i < 0 {
 341         return false
 342     }
 343 
 344     kind := s[:i]
 345     s = s[i+len(`;base64,`):]
 346 
 347     if strings.HasPrefix(kind, `image/`) {
 348         if !isBase64(s) {
 349             return false
 350         }
 351 
 352         w.WriteString(`<img src="`)
 353         w.WriteString(full)
 354         w.WriteString(`">`)
 355         return true
 356     }
 357 
 358     if strings.HasPrefix(kind, `audio/`) {
 359         if !isBase64(s) {
 360             return false
 361         }
 362 
 363         w.WriteString(`<audio controls src="`)
 364         w.WriteString(full)
 365         w.WriteString(`"></audio>`)
 366         return true
 367     }
 368 
 369     if strings.HasPrefix(kind, `video/`) {
 370         if !isBase64(s) {
 371             return false
 372         }
 373 
 374         w.WriteString(`<video controls src="`)
 375         w.WriteString(full)
 376         w.WriteString(`"></video>`)
 377         return true
 378     }
 379 
 380     return false
 381 }
 382 
 383 func handleMedia(w *bufio.Writer, begin string, s string, end string) bool {
 384     if !isBase64(s) {
 385         return false
 386     }
 387 
 388     w.WriteString(begin)
 389     w.WriteString(s)
 390     w.WriteString(end)
 391     return true
 392 }
 393 
 394 func isBase64(s string) bool {
 395     dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(s))
 396     n, err := io.Copy(io.Discard, dec)
 397     return n > 0 && err == nil
 398 }
     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: ./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(err.Error())
  85         os.Stderr.WriteString("\n")
  86         os.Exit(1)
  87         return
  88     }
  89 }
  90 
  91 func run(w io.Writer, args []string, level int, live bool) error {
  92     bw := bufio.NewWriter(w)
  93     defer bw.Flush()
  94 
  95     if len(args) == 0 {
  96         return indent(bw, os.Stdin, level, live)
  97     }
  98 
  99     for _, name := range args {
 100         if err := handleFile(bw, name, level, live); err != nil {
 101             return err
 102         }
 103     }
 104     return nil
 105 }
 106 
 107 func handleFile(w *bufio.Writer, name string, level int, live bool) error {
 108     if name == `` || name == `-` {
 109         return indent(w, os.Stdin, level, live)
 110     }
 111 
 112     f, err := os.Open(name)
 113     if err != nil {
 114         return errors.New(`can't read from file named "` + name + `"`)
 115     }
 116     defer f.Close()
 117 
 118     return indent(w, f, level, live)
 119 }
 120 
 121 func indent(w *bufio.Writer, r io.Reader, level int, live bool) error {
 122     const gb = 1024 * 1024 * 1024
 123     sc := bufio.NewScanner(r)
 124     sc.Buffer(nil, 8*gb)
 125 
 126     for i := 0; sc.Scan(); i++ {
 127         s := sc.Bytes()
 128         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 129             s = s[3:]
 130         }
 131 
 132         writeSpaces(w, level)
 133         w.Write(s)
 134 
 135         if w.WriteByte('\n') != nil {
 136             return io.EOF
 137         }
 138 
 139         if !live {
 140             continue
 141         }
 142 
 143         if w.Flush() != nil {
 144             return io.EOF
 145         }
 146     }
 147 
 148     return sc.Err()
 149 }
 150 
 151 func writeSpaces(w *bufio.Writer, n int) {
 152     const (
 153         half   = `                                `
 154         spaces = half + half
 155     )
 156 
 157     for n >= len(spaces) {
 158         w.WriteString(spaces)
 159         n -= len(spaces)
 160     }
 161     if n > 0 {
 162         w.WriteString(spaces[:n])
 163     }
 164 }
     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(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.NewWriter(w)
  96     defer bw.Flush()
  97 
  98     dashes := 0
  99     for _, name := range args {
 100         if name == `-` {
 101             dashes++
 102         }
 103         if dashes > 1 {
 104             return errors.New(`can't read stdin (dash) more than once`)
 105         }
 106     }
 107 
 108     if len(args) == 0 {
 109         return items(bw, os.Stdin, cfg)
 110     }
 111 
 112     for _, name := range args {
 113         if name == `-` {
 114             if err := items(bw, os.Stdin, cfg); err != nil {
 115                 return err
 116             }
 117             continue
 118         }
 119 
 120         if err := handleFile(bw, name, cfg); err != nil {
 121             return err
 122         }
 123     }
 124     return nil
 125 }
 126 
 127 func handleFile(w *bufio.Writer, name string, cfg config) error {
 128     if name == `` || name == `-` {
 129         return items(w, os.Stdin, 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 items(w, f, cfg)
 139 }
 140 
 141 func items(w *bufio.Writer, r io.Reader, cfg config) error {
 142     const gb = 1024 * 1024 * 1024
 143     sc := bufio.NewScanner(r)
 144     sc.Buffer(nil, 8*gb)
 145 
 146     handleLine := handleSSV
 147     if cfg.tsv {
 148         handleLine = handleTSV
 149     }
 150 
 151     for i := 0; sc.Scan(); i++ {
 152         s := sc.Bytes()
 153         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 154             s = s[3:]
 155         }
 156 
 157         if handleLine(w, s) != nil {
 158             return io.EOF
 159         }
 160 
 161         if !cfg.liveLines {
 162             continue
 163         }
 164 
 165         if w.Flush() != nil {
 166             return io.EOF
 167         }
 168     }
 169 
 170     return sc.Err()
 171 }
 172 
 173 func handleSSV(w *bufio.Writer, line []byte) error {
 174     for len(line) > 0 {
 175         if i := indexNonWhitespace(line); i >= 0 {
 176             line = line[i:]
 177         }
 178         if len(line) == 0 {
 179             break
 180         }
 181 
 182         var item []byte
 183         skip := 0
 184 
 185         i := indexWhitespace(line)
 186         if i >= 0 {
 187             item = line[:i]
 188             skip = i + 1
 189         } else {
 190             item = line
 191             skip = len(line)
 192         }
 193 
 194         w.Write(item)
 195         if w.WriteByte('\n') != nil {
 196             return io.EOF
 197         }
 198 
 199         line = line[skip:]
 200     }
 201 
 202     return nil
 203 }
 204 
 205 func handleTSV(w *bufio.Writer, line []byte) error {
 206     for len(line) > 0 {
 207         var item []byte
 208         skip := 0
 209 
 210         i := bytes.IndexByte(line, '\t')
 211         if i >= 0 {
 212             item = line[:i]
 213             skip = i + 1
 214         } else {
 215             item = line
 216             skip = len(line)
 217         }
 218 
 219         w.Write(item)
 220         if w.WriteByte('\n') != nil {
 221             return io.EOF
 222         }
 223 
 224         line = line[skip:]
 225     }
 226 
 227     return nil
 228 }
 229 
 230 func indexNonWhitespace(s []byte) int {
 231     for i, b := range s {
 232         switch b {
 233         case ' ', '\t':
 234             continue
 235         }
 236         return i
 237     }
 238 
 239     return -1
 240 }
 241 
 242 func indexWhitespace(s []byte) int {
 243     for i, b := range s {
 244         switch b {
 245         case ' ', '\t':
 246             return i
 247         }
 248     }
 249 
 250     return -1
 251 }
     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 both rest-of-line and multi-line comments
  47     - ignores extra/trailing commas in arrays and objects
  48     - turns single-quoted strings/keys into double-quoted strings
  49     - double-quotes unquoted object keys
  50     - changes \x 2-hex-digit into \u 4-hex-digit string-escapes
  51 
  52 All options available can either start with a single or a double-dash
  53 
  54     -h, -help    show this help message
  55     -jsonl       emit JSON Lines, when top-level value is an array
  56 `
  57 
  58 const (
  59     bufSize       = 32 * 1024
  60     chunkPeekSize = 16
  61 )
  62 
  63 func Main() {
  64     args := os.Args[1:]
  65     buffered := false
  66     handler := json0
  67 
  68     for len(args) > 0 {
  69         switch args[0] {
  70         case `-b`, `--b`, `-buffered`, `--buffered`:
  71             buffered = true
  72             args = args[1:]
  73             continue
  74 
  75         case `-h`, `--h`, `-help`, `--help`:
  76             os.Stdout.WriteString(info[1:])
  77             return
  78 
  79         case `-jsonl`, `--jsonl`:
  80             handler = jsonl
  81             args = args[1:]
  82             continue
  83         }
  84 
  85         break
  86     }
  87 
  88     if len(args) > 0 && args[0] == `--` {
  89         args = args[1:]
  90     }
  91 
  92     if len(args) > 1 {
  93         const msg = "multiple inputs aren't allowed\n"
  94         os.Stderr.WriteString(msg)
  95         os.Exit(1)
  96         return
  97     }
  98 
  99     liveLines := !buffered
 100     if !buffered {
 101         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 102             liveLines = false
 103         }
 104     }
 105 
 106     name := `-`
 107     if len(args) == 1 {
 108         name = args[0]
 109     }
 110 
 111     if err := run(os.Stdout, name, handler, liveLines); err != nil && err != io.EOF {
 112         os.Stderr.WriteString(err.Error())
 113         os.Stderr.WriteString("\n")
 114         os.Exit(1)
 115         return
 116     }
 117 }
 118 
 119 type handlerFunc func(w *bufio.Writer, r *bufio.Reader, live bool) error
 120 
 121 func run(w io.Writer, name string, handler handlerFunc, live bool) error {
 122     // f, _ := os.Create(`json0.prof`)
 123     // defer f.Close()
 124     // pprof.StartCPUProfile(f)
 125     // defer pprof.StopCPUProfile()
 126 
 127     if name == `` || name == `-` {
 128         bw := bufio.NewWriterSize(w, bufSize)
 129         br := bufio.NewReaderSize(os.Stdin, bufSize)
 130         defer bw.Flush()
 131         return handler(bw, br, live)
 132     }
 133 
 134     f, err := os.Open(name)
 135     if err != nil {
 136         return errors.New(`can't read from file named "` + name + `"`)
 137     }
 138     defer f.Close()
 139 
 140     bw := bufio.NewWriterSize(w, bufSize)
 141     br := bufio.NewReaderSize(f, bufSize)
 142     defer bw.Flush()
 143     return handler(bw, br, live)
 144 }
 145 
 146 var (
 147     errCommentEarlyEnd = errors.New(`unexpected early-end of comment`)
 148     errInputEarlyEnd   = errors.New(`expected end of input data`)
 149     errInvalidComment  = errors.New(`expected / or *`)
 150     errInvalidHex      = errors.New(`expected a base-16 digit`)
 151     errInvalidRune     = errors.New(`invalid UTF-8 bytes`)
 152     errInvalidToken    = errors.New(`invalid JSON token`)
 153     errNoDigits        = errors.New(`expected numeric digits`)
 154     errNoStringQuote   = errors.New(`expected " or '`)
 155     errNoArrayComma    = errors.New(`missing comma between array values`)
 156     errNoObjectComma   = errors.New(`missing comma between key-value pairs`)
 157     errStringEarlyEnd  = errors.New(`unexpected early-end of string`)
 158     errExtraBytes      = errors.New(`unexpected extra input bytes`)
 159 )
 160 
 161 // linePosError is a more descriptive kind of error, showing the source of
 162 // the input-related problem, as 1-based a line/pos number pair in front
 163 // of the error message
 164 type linePosError struct {
 165     // line is the 1-based line count from the input
 166     line int
 167 
 168     // pos is the 1-based `horizontal` position in its line
 169     pos int
 170 
 171     // err is the error message to `decorate` with the position info
 172     err error
 173 }
 174 
 175 // Error satisfies the error interface
 176 func (lpe linePosError) Error() string {
 177     where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
 178     return where + `: ` + lpe.err.Error()
 179 }
 180 
 181 // isIdentifier improves control-flow of func handleKey, when it handles
 182 // unquoted object keys
 183 var isIdentifier = [256]bool{
 184     '_': true,
 185 
 186     '0': true, '1': true, '2': true, '3': true, '4': true,
 187     '5': true, '6': true, '7': true, '8': true, '9': true,
 188 
 189     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
 190     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
 191     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
 192     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
 193     'Y': true, 'Z': true,
 194 
 195     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
 196     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
 197     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
 198     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
 199     'y': true, 'z': true,
 200 }
 201 
 202 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
 203 // being 0, and normalizes letter-case for the hex letters
 204 var matchHex = [256]byte{
 205     '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
 206     '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
 207     'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
 208     'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
 209 }
 210 
 211 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; final boolean
 212 // value isn't used, and is just there to match the signature of func jsonl
 213 func json0(w *bufio.Writer, r *bufio.Reader, live bool) error {
 214     jr := jsonReader{r, 1, 1}
 215     defer w.Flush()
 216 
 217     if err := jr.handleLeadingJunk(); err != nil {
 218         return err
 219     }
 220 
 221     // handle a single top-level JSON value
 222     err := handleValue(w, &jr)
 223 
 224     // end the only output-line with a line-feed; this also avoids showing
 225     // error messages on the same line as the main output, since JSON-0
 226     // output has no line-feeds before its last byte
 227     outputByte(w, '\n')
 228 
 229     if err != nil {
 230         return err
 231     }
 232     return jr.handleTrailingJunk()
 233 }
 234 
 235 // jsonl converts JSON/pseudo-JSON into (valid) minimal JSON Lines; this func
 236 // avoids writing a trailing line-feed, leaving that up to its caller
 237 func jsonl(w *bufio.Writer, r *bufio.Reader, live bool) error {
 238     jr := jsonReader{r, 1, 1}
 239 
 240     if err := jr.handleLeadingJunk(); err != nil {
 241         return err
 242     }
 243 
 244     chunk, err := jr.r.Peek(1)
 245     if err == nil && len(chunk) >= 1 {
 246         switch b := chunk[0]; b {
 247         case '[', '(':
 248             return handleArrayJSONL(w, &jr, b, live)
 249         }
 250     }
 251 
 252     // handle a single top-level JSON value
 253     err = handleValue(w, &jr)
 254 
 255     // end the only output-line with a line-feed; this also avoids showing
 256     // error messages on the same line as the main output, since JSON-0
 257     // output has no line-feeds before its last byte
 258     outputByte(w, '\n')
 259 
 260     if err != nil {
 261         return err
 262     }
 263     return jr.handleTrailingJunk()
 264 }
 265 
 266 // handleArrayJSONL handles top-level arrays for func jsonl
 267 func handleArrayJSONL(w *bufio.Writer, jr *jsonReader, start byte, live bool) error {
 268     if err := jr.demandSyntax(start); err != nil {
 269         return err
 270     }
 271 
 272     var end byte = ']'
 273     if start == '(' {
 274         end = ')'
 275     }
 276 
 277     for n := 0; true; n++ {
 278         // there may be whitespace/comments before the next comma
 279         if err := jr.seekNext(); err != nil {
 280             return err
 281         }
 282 
 283         // handle commas between values, as well as trailing ones
 284         comma := false
 285         b, _ := jr.peekByte()
 286         if b == ',' {
 287             jr.readByte()
 288             comma = true
 289 
 290             // there may be whitespace/comments before an ending ']'
 291             if err := jr.seekNext(); err != nil {
 292                 return err
 293             }
 294             b, _ = jr.peekByte()
 295         }
 296 
 297         // handle end of array
 298         if b == end {
 299             jr.readByte()
 300             if n > 0 {
 301                 err := outputByte(w, '\n')
 302                 if live {
 303                     w.Flush()
 304                 }
 305                 return err
 306             }
 307             return nil
 308         }
 309 
 310         // turn commas between adjacent values into line-feeds, as the
 311         // output for this custom func is supposed to be JSON Lines
 312         if n > 0 {
 313             if !comma {
 314                 return errNoArrayComma
 315             }
 316             if err := outputByte(w, '\n'); err != nil {
 317                 return err
 318             }
 319             if live {
 320                 w.Flush()
 321             }
 322         }
 323 
 324         // handle the next value
 325         if err := jr.seekNext(); err != nil {
 326             return err
 327         }
 328         if err := handleValue(w, jr); err != nil {
 329             return err
 330         }
 331     }
 332 
 333     // make the compiler happy
 334     return nil
 335 }
 336 
 337 // jsonReader reads data via a buffer, keeping track of the input position:
 338 // this in turn allows showing much more useful errors, when these happen
 339 type jsonReader struct {
 340     // r is the actual reader
 341     r *bufio.Reader
 342 
 343     // line is the 1-based line-counter for input bytes, and gives errors
 344     // useful position info
 345     line int
 346 
 347     // pos is the 1-based `horizontal` position in its line, and gives
 348     // errors useful position info
 349     pos int
 350 }
 351 
 352 // improveError makes any error more useful, by giving it info about the
 353 // current input-position, as a 1-based line/within-line-position pair
 354 func (jr jsonReader) improveError(err error) error {
 355     if _, ok := err.(linePosError); ok {
 356         return err
 357     }
 358 
 359     if err == io.EOF {
 360         return linePosError{jr.line, jr.pos, errInputEarlyEnd}
 361     }
 362     if err != nil {
 363         return linePosError{jr.line, jr.pos, err}
 364     }
 365     return nil
 366 }
 367 
 368 func (jr *jsonReader) handleLeadingJunk() error {
 369     // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
 370     // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
 371     // about byte-order by design
 372     jr.skipUTF8BOM()
 373 
 374     // ignore leading whitespace and/or comments
 375     return jr.seekNext()
 376 }
 377 
 378 func (jr *jsonReader) handleTrailingJunk() error {
 379     // ignore trailing whitespace and/or comments
 380     if err := jr.seekNext(); err != nil {
 381         return err
 382     }
 383 
 384     // ignore trailing semicolons
 385     for {
 386         if b, ok := jr.peekByte(); !ok || b != ';' {
 387             break
 388         }
 389 
 390         jr.readByte()
 391         // ignore trailing whitespace and/or comments
 392         if err := jr.seekNext(); err != nil {
 393             return err
 394         }
 395     }
 396 
 397     // beyond trailing whitespace and/or comments, any more bytes
 398     // make the whole input data invalid JSON
 399     if _, ok := jr.peekByte(); ok {
 400         return jr.improveError(errExtraBytes)
 401     }
 402     return nil
 403 }
 404 
 405 // demandSyntax fails with an error when the next byte isn't the one given;
 406 // when it is, the byte is then read/skipped, and a nil error is returned
 407 func (jr *jsonReader) demandSyntax(syntax byte) error {
 408     chunk, err := jr.r.Peek(1)
 409     if err == io.EOF {
 410         return jr.improveError(errInputEarlyEnd)
 411     }
 412     if err != nil {
 413         return jr.improveError(err)
 414     }
 415 
 416     if len(chunk) < 1 || chunk[0] != syntax {
 417         msg := `expected ` + string(rune(syntax))
 418         return jr.improveError(errors.New(msg))
 419     }
 420 
 421     jr.readByte()
 422     return nil
 423 }
 424 
 425 // peekByte simplifies control-flow for various other funcs
 426 func (jr jsonReader) peekByte() (b byte, ok bool) {
 427     chunk, err := jr.r.Peek(1)
 428     if err == nil && len(chunk) >= 1 {
 429         return chunk[0], true
 430     }
 431     return 0, false
 432 }
 433 
 434 // readByte does what it says, updating the reader's position info
 435 func (jr *jsonReader) readByte() (b byte, err error) {
 436     b, err = jr.r.ReadByte()
 437     if err == nil {
 438         if b == '\n' {
 439             jr.line += 1
 440             jr.pos = 1
 441         } else {
 442             jr.pos++
 443         }
 444         return b, nil
 445     }
 446     return b, jr.improveError(err)
 447 }
 448 
 449 // readRune does what it says, updating the reader's position info
 450 func (jr *jsonReader) readRune() (r rune, err error) {
 451     r, _, err = jr.r.ReadRune()
 452     if err == nil {
 453         if r == '\n' {
 454             jr.line += 1
 455             jr.pos = 1
 456         } else {
 457             jr.pos++
 458         }
 459         return r, nil
 460     }
 461     return r, jr.improveError(err)
 462 }
 463 
 464 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
 465 // and comments, either single-line (starting with //) or general (starting
 466 // with /* and ending with */)
 467 func (jr *jsonReader) seekNext() error {
 468     for {
 469         b, ok := jr.peekByte()
 470         if !ok {
 471             return nil
 472         }
 473 
 474         // case ' ', '\t', '\f', '\v', '\r', '\n':
 475         if b <= 32 {
 476             // keep skipping whitespace bytes
 477             jr.readByte()
 478             continue
 479         }
 480 
 481         if b == '#' {
 482             if err := jr.skipLine(); err != nil {
 483                 return err
 484             }
 485             continue
 486         }
 487 
 488         if b != '/' {
 489             // reached the next token
 490             return nil
 491         }
 492 
 493         if err := jr.skipComment(); err != nil {
 494             return err
 495         }
 496 
 497         // after comments, keep looking for more whitespace and/or comments
 498     }
 499 }
 500 
 501 // skipComment helps func seekNext skip over comments, simplifying the latter
 502 // func's control-flow
 503 func (jr *jsonReader) skipComment() error {
 504     err := jr.demandSyntax('/')
 505     if err != nil {
 506         return err
 507     }
 508 
 509     b, ok := jr.peekByte()
 510     if !ok {
 511         return nil
 512     }
 513 
 514     switch b {
 515     case '/':
 516         // handle single-line comments
 517         return jr.skipLine()
 518 
 519     case '*':
 520         // handle (potentially) multi-line comments
 521         return jr.skipGeneralComment()
 522 
 523     default:
 524         return jr.improveError(errInvalidComment)
 525     }
 526 }
 527 
 528 // skipLine handles single-line comments for func skipComment
 529 func (jr *jsonReader) skipLine() error {
 530     for {
 531         b, err := jr.readByte()
 532         if err == io.EOF {
 533             // end of input is fine in this case
 534             return nil
 535         }
 536         if err != nil {
 537             return err
 538         }
 539 
 540         if b == '\n' {
 541             return nil
 542         }
 543     }
 544 }
 545 
 546 // skipGeneralComment handles (potentially) multi-line comments for func
 547 // skipComment
 548 func (jr *jsonReader) skipGeneralComment() error {
 549     var prev byte
 550     for {
 551         b, err := jr.readByte()
 552         if err != nil {
 553             return jr.improveError(errCommentEarlyEnd)
 554         }
 555 
 556         if prev == '*' && b == '/' {
 557             return nil
 558         }
 559         if b == '\n' {
 560             jr.line++
 561         }
 562         prev = b
 563     }
 564 }
 565 
 566 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
 567 func (jr *jsonReader) skipUTF8BOM() {
 568     lead, err := jr.r.Peek(3)
 569     if err != nil {
 570         return
 571     }
 572 
 573     if len(lead) > 2 && lead[0] == 0xef && lead[1] == 0xbb && lead[2] == 0xbf {
 574         jr.readByte()
 575         jr.readByte()
 576         jr.readByte()
 577     }
 578 }
 579 
 580 // outputByte is a small wrapper on func WriteByte, which adapts any error
 581 // into a custom dummy output-error, which is in turn meant to be ignored,
 582 // being just an excuse to quit the app immediately and successfully
 583 func outputByte(w *bufio.Writer, b byte) error {
 584     err := w.WriteByte(b)
 585     if err == nil {
 586         return nil
 587     }
 588     return io.EOF
 589 }
 590 
 591 // handleArray handles arrays for func handleValue
 592 func handleArray(w *bufio.Writer, jr *jsonReader, start byte) error {
 593     if err := jr.demandSyntax(start); err != nil {
 594         return err
 595     }
 596 
 597     var end byte = ']'
 598     if start == '(' {
 599         end = ')'
 600     }
 601 
 602     w.WriteByte('[')
 603 
 604     for n := 0; true; n++ {
 605         // there may be whitespace/comments before the next comma
 606         if err := jr.seekNext(); err != nil {
 607             return err
 608         }
 609 
 610         // handle commas between values, as well as trailing ones
 611         comma := false
 612         b, _ := jr.peekByte()
 613         if b == ',' {
 614             jr.readByte()
 615             comma = true
 616 
 617             // there may be whitespace/comments before an ending ']'
 618             if err := jr.seekNext(); err != nil {
 619                 return err
 620             }
 621             b, _ = jr.peekByte()
 622         }
 623 
 624         // handle end of array
 625         if b == end {
 626             jr.readByte()
 627             w.WriteByte(']')
 628             return nil
 629         }
 630 
 631         // don't forget commas between adjacent values
 632         if n > 0 {
 633             if !comma {
 634                 return errNoArrayComma
 635             }
 636             if err := outputByte(w, ','); err != nil {
 637                 return err
 638             }
 639         }
 640 
 641         // handle the next value
 642         if err := jr.seekNext(); err != nil {
 643             return err
 644         }
 645         if err := handleValue(w, jr); err != nil {
 646             return err
 647         }
 648     }
 649 
 650     // make the compiler happy
 651     return nil
 652 }
 653 
 654 // handleDigits helps various number-handling funcs do their job
 655 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
 656     if trySimpleDigits(w, jr) {
 657         return nil
 658     }
 659 
 660     for n := 0; true; n++ {
 661         b, _ := jr.peekByte()
 662 
 663         // support `nice` long numbers by ignoring their underscores
 664         if b == '_' {
 665             jr.readByte()
 666             continue
 667         }
 668 
 669         if '0' <= b && b <= '9' {
 670             jr.readByte()
 671             w.WriteByte(b)
 672             continue
 673         }
 674 
 675         if n == 0 {
 676             return errNoDigits
 677         }
 678         return nil
 679     }
 680 
 681     // make the compiler happy
 682     return nil
 683 }
 684 
 685 // trySimpleDigits tries to handle (more quickly) digit-runs where all bytes
 686 // are just digits: this is a very common case for numbers; returns whether
 687 // it succeeded, so this func's caller knows knows if it needs to do anything,
 688 // the slower way
 689 func trySimpleDigits(w *bufio.Writer, jr *jsonReader) (gotIt bool) {
 690     chunk, _ := jr.r.Peek(chunkPeekSize)
 691 
 692     for i, b := range chunk {
 693         if '0' <= b && b <= '9' {
 694             continue
 695         }
 696 
 697         if i == 0 || b == '_' {
 698             return false
 699         }
 700 
 701         // bulk-writing the chunk is this func's whole point
 702         w.Write(chunk[:i])
 703 
 704         jr.r.Discard(i)
 705         jr.pos += i
 706         return true
 707     }
 708 
 709     // maybe the digits-run is ok, but it's just longer than the chunk
 710     return false
 711 }
 712 
 713 // handleDot handles pseudo-JSON numbers which start with a decimal dot
 714 func handleDot(w *bufio.Writer, jr *jsonReader) error {
 715     if err := jr.demandSyntax('.'); err != nil {
 716         return err
 717     }
 718     w.Write([]byte{'0', '.'})
 719     return handleDigits(w, jr)
 720 }
 721 
 722 // handleKey is used by func handleObjects and generalizes func handleString,
 723 // by allowing unquoted object keys; it's not used anywhere else, as allowing
 724 // unquoted string values is ambiguous with actual JSON-keyword values null,
 725 // false, and true.
 726 func handleKey(w *bufio.Writer, jr *jsonReader) error {
 727     quote, ok := jr.peekByte()
 728     if !ok {
 729         return jr.improveError(errStringEarlyEnd)
 730     }
 731 
 732     if quote == '"' || quote == '\'' {
 733         return handleString(w, jr, quote)
 734     }
 735 
 736     w.WriteByte('"')
 737     for {
 738         if b, _ := jr.peekByte(); isIdentifier[b] {
 739             jr.readByte()
 740             w.WriteByte(b)
 741             continue
 742         }
 743 
 744         w.WriteByte('"')
 745         return nil
 746     }
 747 }
 748 
 749 // trySimpleString tries to handle (more quickly) inner-strings where all bytes
 750 // are unescaped ASCII symbols: this is a very common case for strings, and is
 751 // almost always the case for object keys; returns whether it succeeded, so
 752 // this func's caller knows knows if it needs to do anything, the slower way
 753 func trySimpleString(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
 754     end := -1
 755     chunk, _ := jr.r.Peek(chunkPeekSize)
 756 
 757     for i, b := range chunk {
 758         if 32 <= b && b <= 127 && b != '\\' && b != '\'' && b != '"' {
 759             continue
 760         }
 761 
 762         if b == byte(quote) {
 763             end = i
 764             break
 765         }
 766         return false
 767     }
 768 
 769     if end < 0 {
 770         return false
 771     }
 772 
 773     // bulk-writing the chunk is this func's whole point
 774     w.WriteByte('"')
 775     w.Write(chunk[:end])
 776     w.WriteByte('"')
 777 
 778     jr.r.Discard(end + 1)
 779     jr.pos += end + 1
 780     return true
 781 }
 782 
 783 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
 784 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
 785     for rest := kw; len(rest) > 0; rest = rest[1:] {
 786         b, err := jr.readByte()
 787         if err == nil && b == rest[0] {
 788             // keywords given to this func have no line-feeds
 789             jr.pos++
 790             continue
 791         }
 792 
 793         msg := `expected JSON value ` + string(kw)
 794         return jr.improveError(errors.New(msg))
 795     }
 796 
 797     w.Write(kw)
 798     return nil
 799 }
 800 
 801 func replaceKeyword(w *bufio.Writer, jr *jsonReader, kw, with []byte) error {
 802     for rest := kw; len(rest) > 0; rest = rest[1:] {
 803         b, err := jr.readByte()
 804         if err == nil && b == rest[0] {
 805             // keywords given to this func have no line-feeds
 806             jr.pos++
 807             continue
 808         }
 809 
 810         msg := `expected JSON value ` + string(kw)
 811         return jr.improveError(errors.New(msg))
 812     }
 813 
 814     w.Write(with)
 815     return nil
 816 }
 817 
 818 // handleNegative handles numbers starting with a negative sign for func
 819 // handleValue
 820 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
 821     if err := jr.demandSyntax('-'); err != nil {
 822         return err
 823     }
 824 
 825     w.WriteByte('-')
 826     if b, _ := jr.peekByte(); b == '.' {
 827         jr.readByte()
 828         w.Write([]byte{'0', '.'})
 829         return handleDigits(w, jr)
 830     }
 831     return handleNumber(w, jr)
 832 }
 833 
 834 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
 835 // such as values starting with a decimal dot
 836 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
 837     // handle integer digits
 838     if err := handleDigits(w, jr); err != nil {
 839         return err
 840     }
 841 
 842     // handle optional decimal digits, starting with a leading dot
 843     if b, _ := jr.peekByte(); b == '.' {
 844         jr.readByte()
 845         w.WriteByte('.')
 846         return handleDigits(w, jr)
 847     }
 848 
 849     // handle optional exponent digits
 850     if b, _ := jr.peekByte(); b == 'e' || b == 'E' {
 851         jr.readByte()
 852         w.WriteByte(b)
 853         b, _ = jr.peekByte()
 854         if b == '+' {
 855             jr.readByte()
 856         } else if b == '-' {
 857             w.WriteByte('-')
 858             jr.readByte()
 859         }
 860         return handleDigits(w, jr)
 861     }
 862 
 863     return nil
 864 }
 865 
 866 // handleObject handles objects for func handleValue
 867 func handleObject(w *bufio.Writer, jr *jsonReader) error {
 868     if err := jr.demandSyntax('{'); err != nil {
 869         return err
 870     }
 871     w.WriteByte('{')
 872 
 873     for npairs := 0; true; npairs++ {
 874         // there may be whitespace/comments before the next comma
 875         if err := jr.seekNext(); err != nil {
 876             return err
 877         }
 878 
 879         // handle commas between key-value pairs, as well as trailing ones
 880         comma := false
 881         b, _ := jr.peekByte()
 882         if b == ',' {
 883             jr.readByte()
 884             comma = true
 885 
 886             // there may be whitespace/comments before an ending '}'
 887             if err := jr.seekNext(); err != nil {
 888                 return err
 889             }
 890             b, _ = jr.peekByte()
 891         }
 892 
 893         // handle end of object
 894         if b == '}' {
 895             jr.readByte()
 896             w.WriteByte('}')
 897             return nil
 898         }
 899 
 900         // don't forget commas between adjacent key-value pairs
 901         if npairs > 0 {
 902             if !comma {
 903                 return errNoObjectComma
 904             }
 905             if err := outputByte(w, ','); err != nil {
 906                 return err
 907             }
 908         }
 909 
 910         // handle the next pair's key
 911         if err := jr.seekNext(); err != nil {
 912             return err
 913         }
 914         if err := handleKey(w, jr); err != nil {
 915             return err
 916         }
 917 
 918         // demand a colon right after the key
 919         if err := jr.seekNext(); err != nil {
 920             return err
 921         }
 922         if err := jr.demandSyntax(':'); err != nil {
 923             return err
 924         }
 925         w.WriteByte(':')
 926 
 927         // handle the next pair's value
 928         if err := jr.seekNext(); err != nil {
 929             return err
 930         }
 931         if err := handleValue(w, jr); err != nil {
 932             return err
 933         }
 934     }
 935 
 936     // make the compiler happy
 937     return nil
 938 }
 939 
 940 // handlePositive handles numbers starting with a positive sign for func
 941 // handleValue
 942 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
 943     if err := jr.demandSyntax('+'); err != nil {
 944         return err
 945     }
 946 
 947     // valid JSON isn't supposed to have leading pluses on numbers, so
 948     // emit nothing for it, unlike for negative numbers
 949 
 950     if b, _ := jr.peekByte(); b == '.' {
 951         jr.readByte()
 952         w.Write([]byte{'0', '.'})
 953         return handleDigits(w, jr)
 954     }
 955     return handleNumber(w, jr)
 956 }
 957 
 958 // handleString handles strings for funcs handleValue and handleObject, and
 959 // supports both single-quotes and double-quotes, always emitting the latter
 960 // in the output, of course
 961 func handleString(w *bufio.Writer, jr *jsonReader, quote byte) error {
 962     if quote != '"' && quote != '\'' {
 963         return errNoStringQuote
 964     }
 965 
 966     jr.readByte()
 967 
 968     // try the quicker no-escapes ASCII handler
 969     if trySimpleString(w, jr, quote) {
 970         return nil
 971     }
 972 
 973     // it's a non-trivial inner-string, so handle it byte-by-byte
 974     w.WriteByte('"')
 975     escaped := false
 976 
 977     for quote := rune(quote); true; {
 978         r, err := jr.readRune()
 979         if r == unicode.ReplacementChar {
 980             return jr.improveError(errInvalidRune)
 981         }
 982         if err != nil {
 983             if err == io.EOF {
 984                 return jr.improveError(errStringEarlyEnd)
 985             }
 986             return jr.improveError(err)
 987         }
 988 
 989         if !escaped {
 990             if r == '\\' {
 991                 escaped = true
 992                 continue
 993             }
 994 
 995             // handle end of string
 996             if r == quote {
 997                 return outputByte(w, '"')
 998             }
 999 
1000             if r <= 127 {
1001                 w.Write(escapedStringBytes[byte(r)])
1002             } else {
1003                 w.WriteRune(r)
1004             }
1005             continue
1006         }
1007 
1008         // handle escaped items
1009         escaped = false
1010 
1011         switch r {
1012         case 'u':
1013             // \u needs exactly 4 hex-digits to follow it
1014             w.Write([]byte{'\\', 'u'})
1015             if err := copyHex(w, 4, jr); err != nil {
1016                 return jr.improveError(err)
1017             }
1018 
1019         case 'x':
1020             // JSON only supports 4 escaped hex-digits, so pad the 2
1021             // expected hex-digits with 2 zeros
1022             w.Write([]byte{'\\', 'u', '0', '0'})
1023             if err := copyHex(w, 2, jr); err != nil {
1024                 return jr.improveError(err)
1025             }
1026 
1027         case 't', 'f', 'r', 'n', 'b', '\\', '"':
1028             // handle valid-JSON escaped string sequences
1029             w.WriteByte('\\')
1030             w.WriteByte(byte(r))
1031 
1032         case '\'':
1033             // escaped single-quotes aren't standard JSON, but they can
1034             // be handy when the input uses non-standard single-quoted
1035             // strings
1036             w.WriteByte('\'')
1037 
1038         default:
1039             if r <= 127 {
1040                 w.Write(escapedStringBytes[byte(r)])
1041             } else {
1042                 w.WriteRune(r)
1043             }
1044         }
1045     }
1046 
1047     return nil
1048 }
1049 
1050 // copyHex handles a run of hex-digits for func handleString, starting right
1051 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
1052 // errors with position info: that's up to the caller
1053 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
1054     for i := 0; i < n; i++ {
1055         b, err := jr.readByte()
1056         if err == io.EOF {
1057             return errStringEarlyEnd
1058         }
1059         if err != nil {
1060             return err
1061         }
1062 
1063         if b >= 128 {
1064             return errInvalidHex
1065         }
1066 
1067         if b := matchHex[b]; b != 0 {
1068             w.WriteByte(b)
1069             continue
1070         }
1071 
1072         return errInvalidHex
1073     }
1074 
1075     return nil
1076 }
1077 
1078 // handleValue is a generic JSON-token handler, which allows the recursive
1079 // behavior to handle any kind of JSON/pseudo-JSON input
1080 func handleValue(w *bufio.Writer, jr *jsonReader) error {
1081     chunk, err := jr.r.Peek(1)
1082     if err == nil && len(chunk) >= 1 {
1083         return handleValueDispatch(w, jr, chunk[0])
1084     }
1085 
1086     if err == io.EOF {
1087         return jr.improveError(errInputEarlyEnd)
1088     }
1089     return jr.improveError(errInputEarlyEnd)
1090 }
1091 
1092 // handleValueDispatch simplifies control-flow for func handleValue
1093 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
1094     switch b {
1095     case '#':
1096         return jr.skipLine()
1097     case 'f':
1098         return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
1099     case 'n':
1100         return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
1101     case 't':
1102         return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
1103     case 'F':
1104         return replaceKeyword(w, jr, []byte(`False`), []byte(`false`))
1105     case 'N':
1106         return replaceKeyword(w, jr, []byte(`None`), []byte(`null`))
1107     case 'T':
1108         return replaceKeyword(w, jr, []byte(`True`), []byte(`true`))
1109     case '.':
1110         return handleDot(w, jr)
1111     case '+':
1112         return handlePositive(w, jr)
1113     case '-':
1114         return handleNegative(w, jr)
1115     case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
1116         return handleNumber(w, jr)
1117     case '\'', '"':
1118         return handleString(w, jr, b)
1119     case '[', '(':
1120         return handleArray(w, jr, b)
1121     case '{':
1122         return handleObject(w, jr)
1123     default:
1124         return jr.improveError(errInvalidToken)
1125     }
1126 }
1127 
1128 // escapedStringBytes helps func handleString treat all string bytes quickly
1129 // and correctly, using their officially-supported JSON escape sequences
1130 //
1131 // https://www.rfc-editor.org/rfc/rfc8259#section-7
1132 var escapedStringBytes = [256][]byte{
1133     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
1134     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
1135     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
1136     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
1137     {'\\', 'b'}, {'\\', 't'},
1138     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
1139     {'\\', 'f'}, {'\\', 'r'},
1140     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
1141     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
1142     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
1143     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
1144     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
1145     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
1146     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
1147     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
1148     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
1149     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
1150     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
1151     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
1152     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
1153     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
1154     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
1155     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
1156     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
1157     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
1158     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
1159     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
1160     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
1161     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
1162     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
1163     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
1164     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
1165     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
1166     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
1167     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
1168     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
1169     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
1170     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
1171     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
1172     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
1173     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
1174     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
1175     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
1176     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
1177 }
     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 
  64         `""`:           `""`,
  65         `''`:           `""`,
  66         `"\""`:         `"\""`,
  67         `'\"'`:         `"\""`,
  68         `'\''`:         `"'"`,
  69         `'abc\u0e9A'`:  `"abc\u0E9A"`,
  70         `'abc\x1f[0m'`: `"abc\u001F[0m"`,
  71         `"abc●def"`:    `"abc●def"`,
  72 
  73         `[  ]`:                  `[]`,
  74         `[ , ]`:                 `[]`,
  75         `[.345, false,null , ]`: `[0.345,false,null]`,
  76 
  77         `(  )`:                  `[]`,
  78         `( , )`:                 `[]`,
  79         `(.345, false,null , )`: `[0.345,false,null]`,
  80 
  81         `{  }`:  `{}`,
  82         `{ , }`: `{}`,
  83 
  84         `{ 'abc': .345, "def"  : false, 'xyz':null , }`: `{"abc":0.345,"def":false,"xyz":null}`,
  85 
  86         `{0problems:123,}`: `{"0problems":123}`,
  87         `{0_problems:123}`: `{"0_problems":123}`,
  88     }
  89 
  90     for input, expected := range tests {
  91         t.Run(input, func(t *testing.T) {
  92             var out strings.Builder
  93             w := bufio.NewWriter(&out)
  94             r := bufio.NewReader(strings.NewReader(input))
  95             if err := json0(w, r, false); err != nil && err != io.EOF {
  96                 t.Fatal(err)
  97                 return
  98             }
  99             // don't forget to flush the buffer, or output will be empty
 100             w.Flush()
 101 
 102             // output may have a final line-feed: get rid of it, or every
 103             // single test-case will fail
 104             got := out.String()
 105             if len(got) > 0 && got[len(got)-1] == '\n' {
 106                 got = got[:len(got)-1]
 107             }
 108 
 109             if got != expected {
 110                 t.Fatalf("<got>\n%s\n<expected>\n%s", got, expected)
 111                 return
 112             }
 113         })
 114     }
 115 }
 116 
 117 func TestEscapedStringBytes(t *testing.T) {
 118     var escaped = map[rune][]byte{
 119         '\x00': {'\\', 'u', '0', '0', '0', '0'},
 120         '\x01': {'\\', 'u', '0', '0', '0', '1'},
 121         '\x02': {'\\', 'u', '0', '0', '0', '2'},
 122         '\x03': {'\\', 'u', '0', '0', '0', '3'},
 123         '\x04': {'\\', 'u', '0', '0', '0', '4'},
 124         '\x05': {'\\', 'u', '0', '0', '0', '5'},
 125         '\x06': {'\\', 'u', '0', '0', '0', '6'},
 126         '\x07': {'\\', 'u', '0', '0', '0', '7'},
 127         '\x0b': {'\\', 'u', '0', '0', '0', 'b'},
 128         '\x0e': {'\\', 'u', '0', '0', '0', 'e'},
 129         '\x0f': {'\\', 'u', '0', '0', '0', 'f'},
 130         '\x10': {'\\', 'u', '0', '0', '1', '0'},
 131         '\x11': {'\\', 'u', '0', '0', '1', '1'},
 132         '\x12': {'\\', 'u', '0', '0', '1', '2'},
 133         '\x13': {'\\', 'u', '0', '0', '1', '3'},
 134         '\x14': {'\\', 'u', '0', '0', '1', '4'},
 135         '\x15': {'\\', 'u', '0', '0', '1', '5'},
 136         '\x16': {'\\', 'u', '0', '0', '1', '6'},
 137         '\x17': {'\\', 'u', '0', '0', '1', '7'},
 138         '\x18': {'\\', 'u', '0', '0', '1', '8'},
 139         '\x19': {'\\', 'u', '0', '0', '1', '9'},
 140         '\x1a': {'\\', 'u', '0', '0', '1', 'a'},
 141         '\x1b': {'\\', 'u', '0', '0', '1', 'b'},
 142         '\x1c': {'\\', 'u', '0', '0', '1', 'c'},
 143         '\x1d': {'\\', 'u', '0', '0', '1', 'd'},
 144         '\x1e': {'\\', 'u', '0', '0', '1', 'e'},
 145         '\x1f': {'\\', 'u', '0', '0', '1', 'f'},
 146 
 147         '\t': {'\\', 't'},
 148         '\f': {'\\', 'f'},
 149         '\b': {'\\', 'b'},
 150         '\r': {'\\', 'r'},
 151         '\n': {'\\', 'n'},
 152         '\\': {'\\', '\\'},
 153         '"':  {'\\', '"'},
 154     }
 155 
 156     if n := len(escapedStringBytes); n != 256 {
 157         t.Errorf(`expected 256 entries, instead of %d`, n)
 158     }
 159 
 160     for i, v := range escapedStringBytes {
 161         exp := []byte{byte(i)}
 162         if esc, ok := escaped[rune(i)]; ok {
 163             exp = esc
 164         }
 165 
 166         if !bytes.Equal(v, exp) {
 167             t.Errorf("%d: expected %#v, got %#v", i, exp, v)
 168         }
 169     }
 170 }
     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 func Main() {
  43     args := os.Args[1:]
  44 
  45     if len(args) > 0 {
  46         switch args[0] {
  47         case `-h`, `--h`, `-help`, `--help`:
  48             os.Stdout.WriteString(info[1:])
  49             return
  50         }
  51     }
  52 
  53     if len(args) > 0 && args[0] == `--` {
  54         args = args[1:]
  55     }
  56 
  57     if len(args) > 1 {
  58         const msg = "multiple inputs aren't allowed\n"
  59         os.Stderr.WriteString(msg)
  60         os.Exit(1)
  61         return
  62     }
  63 
  64     // figure out whether input should come from a named file or from stdin
  65     name := `-`
  66     if len(args) == 1 {
  67         name = args[0]
  68     }
  69 
  70     if err := handleInput(os.Stdout, name); err != nil && err != io.EOF {
  71         os.Stderr.WriteString(err.Error())
  72         os.Stderr.WriteString("\n")
  73         os.Exit(1)
  74         return
  75     }
  76 }
  77 
  78 // handleInput simplifies control-flow for func main
  79 func handleInput(w io.Writer, path string) error {
  80     if path == `-` {
  81         return convert(w, os.Stdin)
  82     }
  83 
  84     f, err := os.Open(path)
  85     if err != nil {
  86         // on windows, file-not-found error messages may mention `CreateFile`,
  87         // even when trying to open files in read-only mode
  88         return errors.New(`can't open file named ` + path)
  89     }
  90     defer f.Close()
  91     return convert(w, f)
  92 }
  93 
  94 // convert simplifies control-flow for func handleInput
  95 func convert(w io.Writer, r io.Reader) error {
  96     bw := bufio.NewWriter(w)
  97     defer bw.Flush()
  98     return json2(bw, r)
  99 }
 100 
 101 // escapedStringBytes helps func handleString treat all string bytes quickly
 102 // and correctly, using their officially-supported JSON escape sequences
 103 //
 104 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 105 var escapedStringBytes = [256][]byte{
 106     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 107     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 108     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 109     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 110     {'\\', 'b'}, {'\\', 't'},
 111     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 112     {'\\', 'f'}, {'\\', 'r'},
 113     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 114     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 115     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 116     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 117     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 118     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 119     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 120     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 121     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 122     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 123     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 124     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 125     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 126     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 127     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 128     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 129     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 130     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 131     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 132     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 133     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 134     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 135     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 136     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 137     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 138     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 139     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 140     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 141     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 142     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 143     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 144     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 145     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 146     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 147     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 148     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 149     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 150 }
 151 
 152 // writeSpaces does what it says, minimizing calls to write-like funcs
 153 func writeSpaces(w *bufio.Writer, n int) {
 154     const spaces = `                                `
 155     if n < 1 {
 156         return
 157     }
 158 
 159     for n >= len(spaces) {
 160         w.WriteString(spaces)
 161         n -= len(spaces)
 162     }
 163     w.WriteString(spaces[:n])
 164 }
 165 
 166 // json2 does it all, given a reader and a writer
 167 func json2(w *bufio.Writer, r io.Reader) error {
 168     dec := json.NewDecoder(r)
 169     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 170     // even if JSON parsers aren't required to guarantee such input-fidelity
 171     // for numbers
 172     dec.UseNumber()
 173 
 174     t, err := dec.Token()
 175     if err == io.EOF {
 176         return errors.New(`input has no JSON values`)
 177     }
 178 
 179     if err = handleToken(w, dec, t, 0, 0); err != nil {
 180         return err
 181     }
 182     // don't forget ending the last line for the last value
 183     w.WriteByte('\n')
 184 
 185     _, err = dec.Token()
 186     if err == io.EOF {
 187         // input is over, so it's a success
 188         return nil
 189     }
 190 
 191     if err == nil {
 192         // a successful `read` is a failure, as it means there are
 193         // trailing JSON tokens
 194         return errors.New(`unexpected trailing data`)
 195     }
 196 
 197     // any other error, perhaps some invalid-JSON-syntax-type error
 198     return err
 199 }
 200 
 201 // handleToken handles recursion for func json2
 202 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
 203     switch t := t.(type) {
 204     case json.Delim:
 205         switch t {
 206         case json.Delim('['):
 207             return handleArray(w, dec, pre, level)
 208         case json.Delim('{'):
 209             return handleObject(w, dec, pre, level)
 210         default:
 211             return errors.New(`unsupported JSON syntax ` + string(t))
 212         }
 213 
 214     case nil:
 215         writeSpaces(w, 2*pre)
 216         w.WriteString(`null`)
 217         return nil
 218 
 219     case bool:
 220         writeSpaces(w, 2*pre)
 221         if t {
 222             w.WriteString(`true`)
 223         } else {
 224             w.WriteString(`false`)
 225         }
 226         return nil
 227 
 228     case json.Number:
 229         writeSpaces(w, 2*pre)
 230         w.WriteString(t.String())
 231         return nil
 232 
 233     case string:
 234         return handleString(w, t, pre)
 235 
 236     default:
 237         // return fmt.Errorf(`unsupported token type %T`, t)
 238         return errors.New(`invalid JSON token`)
 239     }
 240 }
 241 
 242 // handleArray handles arrays for func handleToken
 243 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 244     for i := 0; true; i++ {
 245         t, err := dec.Token()
 246         if err == io.EOF {
 247             return errors.New(`end of JSON before array was closed`)
 248         }
 249         if err != nil {
 250             return err
 251         }
 252 
 253         if t == json.Delim(']') {
 254             if i == 0 {
 255                 writeSpaces(w, 2*pre)
 256                 w.WriteByte('[')
 257                 w.WriteByte(']')
 258             } else {
 259                 w.WriteByte('\n')
 260                 writeSpaces(w, 2*level)
 261                 w.WriteByte(']')
 262             }
 263             return nil
 264         }
 265 
 266         if i == 0 {
 267             writeSpaces(w, 2*pre)
 268             w.WriteByte('[')
 269             w.WriteByte('\n')
 270         } else {
 271             w.WriteByte(',')
 272             w.WriteByte('\n')
 273             if err := w.Flush(); err != nil {
 274                 // a write error may be the consequence of stdout being closed,
 275                 // perhaps by another app along a pipe
 276                 return io.EOF
 277             }
 278         }
 279 
 280         err = handleToken(w, dec, t, level+1, level+1)
 281         if err != nil {
 282             return err
 283         }
 284     }
 285 
 286     // make the compiler happy
 287     return nil
 288 }
 289 
 290 // handleObject handles objects for func handleToken
 291 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 292     for i := 0; true; i++ {
 293         t, err := dec.Token()
 294         if err == io.EOF {
 295             return errors.New(`end of JSON before object was closed`)
 296         }
 297         if err != nil {
 298             return err
 299         }
 300 
 301         if t == json.Delim('}') {
 302             if i == 0 {
 303                 writeSpaces(w, 2*pre)
 304                 w.WriteByte('{')
 305                 w.WriteByte('}')
 306             } else {
 307                 w.WriteByte('\n')
 308                 writeSpaces(w, 2*level)
 309                 w.WriteByte('}')
 310             }
 311             return nil
 312         }
 313 
 314         if i == 0 {
 315             writeSpaces(w, 2*pre)
 316             w.WriteByte('{')
 317             w.WriteByte('\n')
 318         } else {
 319             w.WriteByte(',')
 320             w.WriteByte('\n')
 321         }
 322 
 323         k, ok := t.(string)
 324         if !ok {
 325             return errors.New(`expected a string for a key-value pair`)
 326         }
 327 
 328         err = handleString(w, k, level+1)
 329         if err != nil {
 330             return err
 331         }
 332 
 333         w.WriteString(": ")
 334 
 335         t, err = dec.Token()
 336         if err == io.EOF {
 337             return errors.New(`expected a value for a key-value pair`)
 338         }
 339 
 340         err = handleToken(w, dec, t, 0, level+1)
 341         if err != nil {
 342             return err
 343         }
 344     }
 345 
 346     // make the compiler happy
 347     return nil
 348 }
 349 
 350 // handleString handles strings for func handleToken, and keys for func
 351 // handleObject
 352 func handleString(w *bufio.Writer, s string, level int) error {
 353     writeSpaces(w, 2*level)
 354     w.WriteByte('"')
 355     for i := range s {
 356         w.Write(escapedStringBytes[s[i]])
 357     }
 358     w.WriteByte('"')
 359     return nil
 360 }
     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(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79         return
  80     }
  81 }
  82 
  83 func run(w io.Writer, args []string, liveLines bool) error {
  84     dashes := 0
  85     for _, path := range args {
  86         if path == `-` {
  87             dashes++
  88         }
  89         if dashes > 1 {
  90             return errors.New(`can't use stdin (dash) more than once`)
  91         }
  92     }
  93 
  94     bw := bufio.NewWriter(w)
  95     defer bw.Flush()
  96 
  97     if len(args) == 0 {
  98         return handleInput(bw, `-`, liveLines)
  99     }
 100 
 101     for _, path := range args {
 102         if err := handleInput(bw, path, liveLines); err != nil {
 103             return err
 104         }
 105     }
 106     return nil
 107 }
 108 
 109 // handleInput simplifies control-flow for func main
 110 func handleInput(w *bufio.Writer, path string, liveLines bool) error {
 111     if path == `-` {
 112         return jsonl(w, os.Stdin, liveLines)
 113     }
 114 
 115     f, err := os.Open(path)
 116     if err != nil {
 117         // on windows, file-not-found error messages may mention `CreateFile`,
 118         // even when trying to open files in read-only mode
 119         return errors.New(`can't open file named ` + path)
 120     }
 121     defer f.Close()
 122     return jsonl(w, f, liveLines)
 123 }
 124 
 125 // escapedStringBytes helps func handleString treat all string bytes quickly
 126 // and correctly, using their officially-supported JSON escape sequences
 127 //
 128 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 129 var escapedStringBytes = [256][]byte{
 130     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 131     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 132     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 133     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 134     {'\\', 'b'}, {'\\', 't'},
 135     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 136     {'\\', 'f'}, {'\\', 'r'},
 137     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 138     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 139     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 140     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 141     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 142     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 143     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 144     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 145     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 146     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 147     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 148     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 149     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 150     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 151     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 152     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 153     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 154     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 155     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 156     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 157     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 158     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 159     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 160     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 161     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 162     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 163     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 164     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 165     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 166     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 167     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 168     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 169     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 170     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 171     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 172     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 173     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 174 }
 175 
 176 // jsonl does it all, given a reader and a writer
 177 func jsonl(w *bufio.Writer, r io.Reader, live bool) 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         return nil
 188     }
 189 
 190     if t == json.Delim('[') {
 191         if err := handleTopLevelArray(w, dec, live); err != nil {
 192             return err
 193         }
 194     } else {
 195         if err := handleToken(w, dec, t); err != nil {
 196             return err
 197         }
 198         w.WriteByte('\n')
 199     }
 200 
 201     _, err = dec.Token()
 202     if err == io.EOF {
 203         // input is over, so it's a success
 204         return nil
 205     }
 206 
 207     if err == nil {
 208         // a successful `read` is a failure, as it means there are
 209         // trailing JSON tokens
 210         return errors.New(`unexpected trailing data`)
 211     }
 212 
 213     // any other error, perhaps some invalid-JSON-syntax-type error
 214     return err
 215 }
 216 
 217 // handleToken handles recursion for func json2
 218 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token) error {
 219     switch t := t.(type) {
 220     case json.Delim:
 221         switch t {
 222         case json.Delim('['):
 223             return handleArray(w, dec)
 224         case json.Delim('{'):
 225             return handleObject(w, dec)
 226         default:
 227             return errors.New(`unsupported JSON syntax ` + string(t))
 228         }
 229 
 230     case nil:
 231         w.WriteString(`null`)
 232         return nil
 233 
 234     case bool:
 235         if t {
 236             w.WriteString(`true`)
 237         } else {
 238             w.WriteString(`false`)
 239         }
 240         return nil
 241 
 242     case json.Number:
 243         w.WriteString(t.String())
 244         return nil
 245 
 246     case string:
 247         return handleString(w, t)
 248 
 249     default:
 250         // return fmt.Errorf(`unsupported token type %T`, t)
 251         return errors.New(`invalid JSON token`)
 252     }
 253 }
 254 
 255 func handleTopLevelArray(w *bufio.Writer, dec *json.Decoder, live bool) error {
 256     for i := 0; true; i++ {
 257         t, err := dec.Token()
 258         if err == io.EOF {
 259             return nil
 260         }
 261 
 262         if err != nil {
 263             return err
 264         }
 265 
 266         if t == json.Delim(']') {
 267             return nil
 268         }
 269 
 270         err = handleToken(w, dec, t)
 271         if err != nil {
 272             return err
 273         }
 274 
 275         if w.WriteByte('\n') != nil {
 276             return io.EOF
 277         }
 278 
 279         if !live {
 280             continue
 281         }
 282 
 283         if w.Flush() != nil {
 284             return io.EOF
 285         }
 286     }
 287 
 288     // make the compiler happy
 289     return nil
 290 }
 291 
 292 // handleArray handles arrays for func handleToken
 293 func handleArray(w *bufio.Writer, dec *json.Decoder) error {
 294     w.WriteByte('[')
 295 
 296     for i := 0; true; i++ {
 297         t, err := dec.Token()
 298         if err == io.EOF {
 299             return errors.New(`end of JSON before array was closed`)
 300         }
 301         if err != nil {
 302             return err
 303         }
 304 
 305         if t == json.Delim(']') {
 306             w.WriteByte(']')
 307             return nil
 308         }
 309 
 310         if i > 0 {
 311             _, err := w.WriteString(", ")
 312             if err != nil {
 313                 return io.EOF
 314             }
 315         }
 316 
 317         err = handleToken(w, dec, t)
 318         if err != nil {
 319             return err
 320         }
 321     }
 322 
 323     // make the compiler happy
 324     return nil
 325 }
 326 
 327 // handleObject handles objects for func handleToken
 328 func handleObject(w *bufio.Writer, dec *json.Decoder) error {
 329     w.WriteByte('{')
 330 
 331     for i := 0; true; i++ {
 332         t, err := dec.Token()
 333         if err == io.EOF {
 334             return errors.New(`end of JSON before object was closed`)
 335         }
 336         if err != nil {
 337             return err
 338         }
 339 
 340         if t == json.Delim('}') {
 341             w.WriteByte('}')
 342             return nil
 343         }
 344 
 345         if i > 0 {
 346             _, err := w.WriteString(", ")
 347             if err != nil {
 348                 return io.EOF
 349             }
 350         }
 351 
 352         k, ok := t.(string)
 353         if !ok {
 354             return errors.New(`expected a string for a key-value pair`)
 355         }
 356 
 357         err = handleString(w, k)
 358         if err != nil {
 359             return err
 360         }
 361 
 362         w.WriteString(": ")
 363 
 364         t, err = dec.Token()
 365         if err == io.EOF {
 366             return errors.New(`expected a value for a key-value pair`)
 367         }
 368 
 369         err = handleToken(w, dec, t)
 370         if err != nil {
 371             return err
 372         }
 373     }
 374 
 375     // make the compiler happy
 376     return nil
 377 }
 378 
 379 // handleString handles strings for func handleToken, and keys for func
 380 // handleObject
 381 func handleString(w *bufio.Writer, s string) error {
 382     w.WriteByte('"')
 383     for i := range s {
 384         w.Write(escapedStringBytes[s[i]])
 385     }
 386     w.WriteByte('"')
 387     return nil
 388 }
     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(err.Error())
  58         os.Stderr.WriteString("\n")
  59         os.Exit(1)
  60         return
  61     }
  62 }
  63 
  64 type runConfig struct {
  65     lines int
  66     keys  []string
  67 }
  68 
  69 func run(paths []string) error {
  70     bw := bufio.NewWriter(os.Stdout)
  71     defer bw.Flush()
  72 
  73     dashes := 0
  74     var cfg runConfig
  75 
  76     for _, path := range paths {
  77         if path == `-` {
  78             dashes++
  79             if dashes > 1 {
  80                 continue
  81             }
  82 
  83             if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  84                 return err
  85             }
  86 
  87             continue
  88         }
  89 
  90         if err := handleFile(bw, path, &cfg); err != nil {
  91             return err
  92         }
  93     }
  94 
  95     if len(paths) == 0 {
  96         if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  97             return err
  98         }
  99     }
 100 
 101     if cfg.lines > 1 {
 102         bw.WriteString("\n]\n")
 103     } else {
 104         bw.WriteString("[]\n")
 105     }
 106     return nil
 107 }
 108 
 109 func handleFile(w *bufio.Writer, path string, cfg *runConfig) error {
 110     f, err := os.Open(path)
 111     if err != nil {
 112         return err
 113     }
 114     defer f.Close()
 115     return handleInput(w, f, cfg)
 116 }
 117 
 118 func escapeKeys(line string) []string {
 119     var keys []string
 120     var sb strings.Builder
 121 
 122     loopTSV(line, func(i int, s string) {
 123         sb.WriteByte('"')
 124         for _, r := range s {
 125             if r == '\\' || r == '"' {
 126                 sb.WriteByte('\\')
 127             }
 128             sb.WriteRune(r)
 129         }
 130         sb.WriteByte('"')
 131 
 132         keys = append(keys, sb.String())
 133         sb.Reset()
 134     })
 135 
 136     return keys
 137 }
 138 
 139 func emitRow(w *bufio.Writer, line string, keys []string) {
 140     j := 0
 141     w.WriteByte('{')
 142 
 143     loopTSV(line, func(i int, s string) {
 144         j = i
 145         if i > 0 {
 146             w.WriteString(", ")
 147         }
 148 
 149         w.WriteString(keys[i])
 150         w.WriteString(": \"")
 151 
 152         for _, r := range s {
 153             if r == '\\' || r == '"' {
 154                 w.WriteByte('\\')
 155             }
 156             w.WriteRune(r)
 157         }
 158         w.WriteByte('"')
 159     })
 160 
 161     for i := j + 1; i < len(keys); i++ {
 162         if i > 0 {
 163             w.WriteString(", ")
 164         }
 165         w.WriteString(keys[i])
 166         w.WriteString(": null")
 167     }
 168     w.WriteByte('}')
 169 }
 170 
 171 func loopTSV(line string, f func(i int, s string)) {
 172     for i := 0; len(line) > 0; i++ {
 173         pos := strings.IndexByte(line, '\t')
 174         if pos < 0 {
 175             f(i, line)
 176             return
 177         }
 178 
 179         f(i, line[:pos])
 180         line = line[pos+1:]
 181     }
 182 }
 183 
 184 func handleInput(w *bufio.Writer, r io.Reader, cfg *runConfig) error {
 185     const gb = 1024 * 1024 * 1024
 186     sc := bufio.NewScanner(r)
 187     sc.Buffer(nil, 8*gb)
 188 
 189     for i := 0; sc.Scan(); i++ {
 190         s := sc.Text()
 191         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 192             s = s[3:]
 193         }
 194 
 195         if cfg.lines == 0 {
 196             cfg.keys = escapeKeys(s)
 197             w.WriteByte('[')
 198             cfg.lines++
 199             continue
 200         }
 201 
 202         if cfg.lines == 1 {
 203             w.WriteString("\n  ")
 204         } else {
 205             if _, err := w.WriteString(",\n  "); err != nil {
 206                 return io.EOF
 207             }
 208         }
 209 
 210         emitRow(w, s, cfg.keys)
 211         cfg.lines++
 212     }
 213 
 214     return sc.Err()
 215 }
     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(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73         return
  74     }
  75 }
  76 
  77 func writeJunk(w io.Writer, n int) error {
  78     var buf [32 * 1024]byte
  79 
  80     for n > 0 {
  81         max := n
  82         if max > cap(buf) {
  83             max = cap(buf)
  84         }
  85 
  86         got, err := io.ReadFull(rand.Reader, buf[:max])
  87         if err == io.EOF && got > 0 {
  88             err = nil
  89         }
  90 
  91         if err != nil {
  92             return err
  93         }
  94 
  95         if _, err := w.Write(buf[:got]); err != nil {
  96             return io.EOF
  97         }
  98 
  99         n -= got
 100     }
 101 
 102     return nil
 103 }
     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(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     const (
  84         half   = `                                        `
  85         spaces = half + half
  86     )
  87 
  88     bw := bufio.NewWriterSize(w, 32*1024)
  89     defer bw.Flush()
  90 
  91     bw.WriteString("\x1b[7m")
  92 
  93     left := len(spaces)
  94 
  95     for i, s := range args {
  96         if i > 0 {
  97             if err := bw.WriteByte(' '); err != nil {
  98                 return io.EOF
  99             }
 100             left--
 101         }
 102         bw.WriteString(s)
 103         left -= utf8.RuneCountInString(s)
 104     }
 105 
 106     if 0 < left && left < len(spaces) {
 107         bw.WriteString(spaces[:left])
 108     }
 109 
 110     if _, err := bw.WriteString("\x1b[0m\n"); err != nil {
 111         return io.EOF
 112     }
 113 
 114     if live {
 115         if bw.Flush() != nil {
 116             return io.EOF
 117         }
 118     }
 119 
 120     return catl(bw, os.Stdin, live)
 121 }
 122 
 123 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 124     const gb = 1024 * 1024 * 1024
 125     sc := bufio.NewScanner(r)
 126     sc.Buffer(nil, 8*gb)
 127 
 128     for i := 0; sc.Scan(); i++ {
 129         s := sc.Bytes()
 130         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 131             s = s[3:]
 132         }
 133 
 134         w.Write(s)
 135         if w.WriteByte('\n') != nil {
 136             return io.EOF
 137         }
 138 
 139         if !live {
 140             continue
 141         }
 142 
 143         if w.Flush() != nil {
 144             return io.EOF
 145         }
 146     }
 147 
 148     return sc.Err()
 149 }
     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(err.Error())
 116         os.Stderr.WriteString("\n")
 117         os.Exit(1)
 118         return
 119     }
 120 
 121     show(os.Stdout, latest)
 122 }
 123 
 124 func run(paths []string, rb *ringBuffer) error {
 125     for _, path := range paths {
 126         if err := handleFile(path, rb); err != nil {
 127             return err
 128         }
 129     }
 130 
 131     if len(paths) == 0 {
 132         if err := visit(os.Stdin, rb); err != nil {
 133             return err
 134         }
 135     }
 136     return nil
 137 }
 138 
 139 func show(w io.Writer, rb ringBuffer) {
 140     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 141     defer bw.Flush()
 142 
 143     for i := rb.next; i < len(rb.items); i++ {
 144         bw.WriteString(rb.items[i])
 145         if err := bw.WriteByte('\n'); err != nil {
 146             return
 147         }
 148     }
 149 
 150     for i := 0; i < len(rb.items) && i < rb.next; i++ {
 151         bw.WriteString(rb.items[i])
 152         if err := bw.WriteByte('\n'); err != nil {
 153             return
 154         }
 155     }
 156 }
 157 
 158 func handleFile(path string, rb *ringBuffer) error {
 159     f, err := os.Open(path)
 160     if err != nil {
 161         return err
 162     }
 163     defer f.Close()
 164     return visit(f, rb)
 165 }
 166 
 167 func visit(r io.Reader, rb *ringBuffer) error {
 168     const gb = 1024 * 1024 * 1024
 169     sc := bufio.NewScanner(r)
 170     sc.Buffer(nil, 8*gb)
 171 
 172     for sc.Scan() {
 173         rb.append(sc.Text())
 174     }
 175     return sc.Err()
 176 }
     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     "errors"
  31     "io"
  32     "os"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 leak [options...] [style name...] [files...]
  38 
  39 Emit copies of input lines both to stdout and to stderr, thus "leaking"
  40 the contents going through a pipe of commands, using the leading argument
  41 as the style/color name to use to decorate the stderr output.
  42 
  43 This tool's main use-case is to inspect/debug the intermediate stages of a
  44 "pipelined" shell command.
  45 
  46 When variable NO_COLOR is declared and set to 1, an invert/highlight style
  47 is used instead of colors.
  48 
  49 The only option available is to show this help message, using any of
  50 "-h", "--h", "-help", or "--help", without the quotes.
  51 
  52 Supported style names include
  53 
  54 - red     - orange    - bold     - underline
  55 - green   - magenta   - purple   - invert
  56 - blue    - gray      - italic
  57 
  58 Supported aliases for style names include
  59 
  60   b    (blue)              bb    (blue background)
  61   g    (green)             gb    (green background)
  62   h    (hilight/invert)
  63   m    (magenta)           mb    (magenta background)
  64   o    (orange)            ob    (orange background)
  65   p    (purple)            pb    (purple background)
  66   r    (red)               rb    (red background)
  67   u    (underline)
  68 `
  69 
  70 type config struct {
  71     // style is the ANSI-style sequence to use verbatim
  72     style string
  73 
  74     // buf is the buffer space for the (re)styled lines for the standard error
  75     buf []byte
  76 
  77     // live is whether lines are flushed each time
  78     live bool
  79 }
  80 
  81 func Main() {
  82     var cfg config
  83     cfg.live = true
  84     args := os.Args[1:]
  85 
  86     for len(args) > 0 {
  87         switch args[0] {
  88         case `-b`, `--b`, `-buffered`, `--buffered`:
  89             cfg.live = false
  90             args = args[1:]
  91             continue
  92 
  93         case `-h`, `--h`, `-help`, `--help`:
  94             os.Stdout.WriteString(info[1:])
  95             return
  96         }
  97 
  98         break
  99     }
 100 
 101     options := true
 102     if len(args) > 0 && args[0] == `--` {
 103         options = false
 104         args = args[1:]
 105     }
 106 
 107     if os.Getenv(`NO_COLOR`) == `1` {
 108         cfg.style, _ = lookupStyle(`inverse`)
 109         options = false
 110     } else {
 111         cfg.style, _ = lookupStyle(`gray`)
 112     }
 113 
 114     // if the first argument is 1 or 2 dashes followed by a supported
 115     // style-name, and the environment doesn't have NO_COLOR set to 1,
 116     // change the style used
 117     if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
 118         name := args[0]
 119         name = strings.TrimPrefix(name, `-`)
 120         name = strings.TrimPrefix(name, `-`)
 121         args = args[1:]
 122 
 123         // check if the `dedashed` argument is a supported style-name
 124         if s, ok := lookupStyle(name); ok {
 125             cfg.style = s
 126         } else {
 127             os.Stderr.WriteString(`invalid style name `)
 128             os.Stderr.WriteString(name)
 129             os.Stderr.WriteString("\n")
 130             os.Exit(1)
 131             return
 132         }
 133     }
 134 
 135     if cfg.live {
 136         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 137             cfg.live = false
 138         }
 139     }
 140 
 141     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 142         os.Stderr.WriteString(err.Error())
 143         os.Stderr.WriteString("\n")
 144         os.Exit(1)
 145         return
 146     }
 147 }
 148 
 149 func run(w io.Writer, args []string, cfg config) error {
 150     bw := bufio.NewWriter(w)
 151     defer bw.Flush()
 152 
 153     if len(args) == 0 {
 154         return leak(bw, os.Stdin, cfg)
 155     }
 156 
 157     for _, name := range args {
 158         if err := handleFile(bw, name, cfg); err != nil {
 159             return err
 160         }
 161     }
 162     return nil
 163 }
 164 
 165 func handleFile(w *bufio.Writer, name string, cfg config) error {
 166     if name == `` || name == `-` {
 167         return leak(w, os.Stdin, cfg)
 168     }
 169 
 170     f, err := os.Open(name)
 171     if err != nil {
 172         return errors.New(`can't read from file named "` + name + `"`)
 173     }
 174     defer f.Close()
 175 
 176     return leak(w, f, cfg)
 177 }
 178 
 179 func leak(w *bufio.Writer, r io.Reader, cfg config) error {
 180     const gb = 1024 * 1024 * 1024
 181     sc := bufio.NewScanner(r)
 182     sc.Buffer(nil, 8*gb)
 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         // leak the (re)styled line to standard error
 196         cfg.buf = append(cfg.buf[:0], cfg.style...)
 197         cfg.buf = appendPlain(cfg.buf, s)
 198         if cfg.style != `` {
 199             cfg.buf = append(cfg.buf, "\x1b[0m\n"...)
 200         } else {
 201             cfg.buf = append(cfg.buf, '\n')
 202         }
 203         os.Stderr.Write(cfg.buf)
 204 
 205         if !cfg.live {
 206             continue
 207         }
 208 
 209         if err := w.Flush(); err != nil {
 210             // a write error may be the consequence of stdout being closed,
 211             // perhaps by another app along a pipe
 212             return io.EOF
 213         }
 214     }
 215     return sc.Err()
 216 }
 217 
 218 func lookupStyle(name string) (style string, ok bool) {
 219     if alias, ok := styleAliases[name]; ok {
 220         name = alias
 221     }
 222 
 223     style, ok = styles[name]
 224     return style, ok
 225 }
 226 
 227 var styleAliases = map[string]string{
 228     `b`: `blue`,
 229     `g`: `green`,
 230     `m`: `magenta`,
 231     `o`: `orange`,
 232     `p`: `purple`,
 233     `r`: `red`,
 234     `u`: `underline`,
 235 
 236     `bolded`:      `bold`,
 237     `h`:           `inverse`,
 238     `hi`:          `inverse`,
 239     `highlight`:   `inverse`,
 240     `highlighted`: `inverse`,
 241     `hilite`:      `inverse`,
 242     `hilited`:     `inverse`,
 243     `inv`:         `inverse`,
 244     `invert`:      `inverse`,
 245     `inverted`:    `inverse`,
 246     `underlined`:  `underline`,
 247 
 248     `bb`: `blueback`,
 249     `bg`: `greenback`,
 250     `bm`: `magentaback`,
 251     `bo`: `orangeback`,
 252     `bp`: `purpleback`,
 253     `br`: `redback`,
 254 
 255     `gb`: `greenback`,
 256     `mb`: `magentaback`,
 257     `ob`: `orangeback`,
 258     `pb`: `purpleback`,
 259     `rb`: `redback`,
 260 
 261     `bblue`:    `blueback`,
 262     `bgray`:    `grayback`,
 263     `bgreen`:   `greenback`,
 264     `bmagenta`: `magentaback`,
 265     `borange`:  `orangeback`,
 266     `bpurple`:  `purpleback`,
 267     `bred`:     `redback`,
 268 
 269     `backblue`:    `blueback`,
 270     `backgray`:    `grayback`,
 271     `backgreen`:   `greenback`,
 272     `backmagenta`: `magentaback`,
 273     `backorange`:  `orangeback`,
 274     `backpurple`:  `purpleback`,
 275     `backred`:     `redback`,
 276 }
 277 
 278 // styles turns style-names into the ANSI-code sequences used for the
 279 // alternate groups of digits
 280 var styles = map[string]string{
 281     `blue`:      "\x1b[38;2;0;95;215m",
 282     `bold`:      "\x1b[1m",
 283     `gray`:      "\x1b[38;2;168;168;168m",
 284     `green`:     "\x1b[38;2;0;135;95m",
 285     `inverse`:   "\x1b[7m",
 286     `magenta`:   "\x1b[38;2;215;0;255m",
 287     `orange`:    "\x1b[38;2;215;95;0m",
 288     `plain`:     "\x1b[0m",
 289     `red`:       "\x1b[38;2;204;0;0m",
 290     `underline`: "\x1b[4m",
 291 
 292     // `blue`:      "\x1b[38;5;26m",
 293     // `bold`:      "\x1b[1m",
 294     // `gray`:      "\x1b[38;5;248m",
 295     // `green`:     "\x1b[38;5;29m",
 296     // `inverse`:   "\x1b[7m",
 297     // `magenta`:   "\x1b[38;5;99m",
 298     // `orange`:    "\x1b[38;5;166m",
 299     // `plain`:     "\x1b[0m",
 300     // `red`:       "\x1b[31m",
 301     // `underline`: "\x1b[4m",
 302 
 303     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 304     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 305     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 306     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 307     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 308     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 309     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 310 }
 311 
 312 // appendPlain extends the slice given using the non-ANSI parts of a string
 313 func appendPlain(dst []byte, src []byte) []byte {
 314     for len(src) > 0 {
 315         i, j := indexEscapeSequence(src)
 316         if i < 0 {
 317             dst = append(dst, src...)
 318             break
 319         }
 320         if j < 0 {
 321             j = len(src)
 322         }
 323 
 324         if i > 0 {
 325             dst = append(dst, src[:i]...)
 326         }
 327 
 328         src = src[j:]
 329     }
 330 
 331     return dst
 332 }
 333 
 334 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 335 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 336 // indices which can be independently negative when either the start/end of
 337 // a sequence isn't found; given their fairly-common use, even the hyperlink
 338 // ESC]8 sequences are supported
 339 func indexEscapeSequence(s []byte) (int, int) {
 340     var prev byte
 341 
 342     for i, b := range s {
 343         if prev == '\x1b' && b == '[' {
 344             j := indexLetter(s[i+1:])
 345             if j < 0 {
 346                 return i, -1
 347             }
 348             return i - 1, i + 1 + j + 1
 349         }
 350 
 351         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 352             j := indexPair(s[i+1:], '\x1b', '\\')
 353             if j < 0 {
 354                 return i, -1
 355             }
 356             return i - 1, i + 1 + j + 2
 357         }
 358 
 359         prev = b
 360     }
 361 
 362     return -1, -1
 363 }
 364 
 365 func indexLetter(s []byte) int {
 366     for i, b := range s {
 367         upper := b &^ 32
 368         if 'A' <= upper && upper <= 'Z' {
 369             return i
 370         }
 371     }
 372 
 373     return -1
 374 }
 375 
 376 func indexPair(s []byte, x byte, y byte) int {
 377     var prev byte
 378 
 379     for i, b := range s {
 380         if prev == x && b == y && i > 0 {
 381             return i
 382         }
 383         prev = b
 384     }
 385 
 386     return -1
 387 }
     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(err.Error())
  99         os.Stderr.WriteString("\n")
 100         os.Exit(1)
 101         return
 102     }
 103 }
 104 
 105 func run(args []string, n *int) error {
 106     dashes := 0
 107     for _, name := range args {
 108         if name == `-` {
 109             dashes++
 110         }
 111         if dashes > 1 {
 112             return errors.New(`can't read stdin (dash) more than once`)
 113         }
 114     }
 115 
 116     if len(args) == 0 {
 117         if err := handleReader(os.Stdout, os.Stdin, n); err != nil {
 118             return err
 119         }
 120     }
 121 
 122     for _, name := range args {
 123         if name == `-` {
 124             if err := handleReader(os.Stdout, os.Stdin, n); err != nil {
 125                 return err
 126             }
 127             continue
 128         }
 129 
 130         if err := handleFile(os.Stdout, name, n); err != nil {
 131             return err
 132         }
 133     }
 134 
 135     return nil
 136 }
 137 
 138 func handleFile(w io.Writer, name string, n *int) error {
 139     if name == `` || name == `-` {
 140         return handleReader(w, os.Stdin, n)
 141     }
 142 
 143     f, err := os.Open(name)
 144     if err != nil {
 145         return errors.New(`can't read from file named "` + name + `"`)
 146     }
 147     defer f.Close()
 148 
 149     return handleReader(w, f, n)
 150 }
 151 
 152 func handleReader(w io.Writer, r io.Reader, n *int) error {
 153     var buf [32 * 1024]byte
 154 
 155     for *n > 0 {
 156         max := *n
 157         if max > cap(buf) {
 158             max = cap(buf)
 159         }
 160 
 161         got, err := os.Stdin.Read(buf[:max])
 162         if got > 0 {
 163             if _, err := os.Stdout.Write(buf[:got]); err != nil {
 164                 return io.EOF
 165             }
 166         }
 167         *n -= got
 168 
 169         if err != nil {
 170             return io.EOF
 171         }
 172     }
 173 
 174     return nil
 175 }
     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(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     bw := bufio.NewWriter(w)
 106     defer bw.Flush()
 107 
 108     dashes := 0
 109     for _, name := range args {
 110         if name == `-` {
 111             dashes++
 112         }
 113         if dashes > 1 {
 114             return errors.New(`can't read stdin (dash) more than once`)
 115         }
 116     }
 117 
 118     if len(args) == 0 {
 119         if err := lineUp(bw, os.Stdin, &cfg); err != nil {
 120             return err
 121         }
 122     }
 123 
 124     for _, name := range args {
 125         if name == `-` {
 126             if err := lineUp(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 
 137     if cfg.count > 0 {
 138         bw.WriteByte('\n')
 139     }
 140     return nil
 141 }
 142 
 143 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 144     if name == `` || name == `-` {
 145         return lineUp(w, os.Stdin, cfg)
 146     }
 147 
 148     f, err := os.Open(name)
 149     if err != nil {
 150         return errors.New(`can't read from file named "` + name + `"`)
 151     }
 152     defer f.Close()
 153 
 154     return lineUp(w, f, cfg)
 155 }
 156 
 157 func lineUp(w *bufio.Writer, r io.Reader, cfg *config) error {
 158     const gb = 1024 * 1024 * 1024
 159     sc := bufio.NewScanner(r)
 160     sc.Buffer(nil, 8*gb)
 161 
 162     for i := 0; sc.Scan(); i++ {
 163         s := sc.Bytes()
 164         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 165             s = s[3:]
 166         }
 167 
 168         if cfg.count > 0 {
 169             var sep byte
 170             if cfg.remainder == 0 {
 171                 sep = '\n'
 172             } else {
 173                 sep = '\t'
 174             }
 175 
 176             if w.WriteByte(sep) != nil {
 177                 return io.EOF
 178             }
 179         }
 180 
 181         w.Write(s)
 182         cfg.count++
 183 
 184         cfg.remainder++
 185         if cfg.remainder >= cfg.maxItems && cfg.maxItems > 1 {
 186             cfg.remainder = 0
 187         }
 188 
 189         if !cfg.liveLines {
 190             continue
 191         }
 192 
 193         if w.Flush() != nil {
 194             return io.EOF
 195         }
 196     }
 197 
 198     return sc.Err()
 199 }
     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(err.Error())
  82         os.Stderr.WriteString("\n")
  83         os.Exit(1)
  84         return
  85     }
  86 }
  87 
  88 func run(paths []string, cfg config) error {
  89     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  90     defer w.Flush()
  91 
  92     for i, path := range paths {
  93         if len(paths) > 1 {
  94             w.WriteString(path)
  95             w.WriteString(":\n")
  96             if i > 0 {
  97                 w.WriteString("\n")
  98             }
  99         }
 100 
 101         if err := ls(w, path, cfg); err != nil {
 102             return err
 103         }
 104     }
 105 
 106     if len(paths) == 0 {
 107         return ls(w, `.`, cfg)
 108     }
 109     return nil
 110 }
 111 
 112 func ls(w *bufio.Writer, path string, cfg config) error {
 113     defer w.Flush()
 114 
 115     entries, err := os.ReadDir(path)
 116     if err != nil {
 117         return err
 118     }
 119 
 120     sort.SliceStable(entries, func(i, j int) bool {
 121         return compareNames(entries[i].Name(), entries[j].Name()) < 0
 122     })
 123 
 124     for _, e := range entries {
 125         name := e.Name()
 126 
 127         if !cfg.all && len(name) > 0 && name[0] == '.' {
 128             continue
 129         }
 130 
 131         w.WriteString(name)
 132         if _, err := w.WriteString("\n"); err != nil {
 133             return io.EOF
 134         }
 135     }
 136 
 137     return nil
 138 }
 139 
 140 func compareNames(x, y string) int {
 141     if len(x) < len(y) && strings.HasPrefix(y, x) {
 142         return -1
 143     }
 144     if len(y) < len(x) && strings.HasPrefix(x, y) {
 145         return +1
 146     }
 147     return strings.Compare(x, y)
 148 }
     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     "./base64"
  49     "./bitdump"
  50     "./breakdown"
  51     "./bytedump"
  52     "./calc"
  53     "./cat"
  54     "./catl"
  55     "./coby"
  56     "./compress"
  57     "./countdown"
  58     "./datauri"
  59     "./dc"
  60     "./dcol"
  61     "./debase64"
  62     "./decsv"
  63     "./dedent"
  64     "./dedup"
  65     "./dejsonl"
  66     "./delay"
  67     "./dessv"
  68     "./detab"
  69     "./ecoli"
  70     "./erase"
  71     "./expand"
  72     "./factor"
  73     "./fh"
  74     "./files"
  75     "./filesizes"
  76     "./finfo"
  77     "./first"
  78     "./fixlines"
  79     "./folders"
  80     "./gsub"
  81     "./head"
  82     "./hima"
  83     "./htmlify"
  84     "./id3pic"
  85     "./indent"
  86     "./items"
  87     "./json0"
  88     "./json2"
  89     "./jsonl"
  90     "./jsons"
  91     "./junk"
  92     "./label"
  93     "./last"
  94     "./leak"
  95     "./limit"
  96     "./lineup"
  97     "./ls"
  98     "./match"
  99     "./n"
 100     "./ncol"
 101     "./ngron"
 102     "./nhex"
 103     "./njson"
 104     "./nl"
 105     "./nn"
 106     "./now"
 107     "./nts"
 108     "./pac"
 109     "./pad"
 110     "./pcol"
 111     "./plain"
 112     "./pretsv"
 113     "./primes"
 114     "./realign"
 115     "./reprose"
 116     "./sbs"
 117     "./seq"
 118     "./skip"
 119     "./skiplast"
 120     "./squeeze"
 121     "./squomp"
 122     "./stomp"
 123     "./tacl"
 124     "./tail"
 125     "./tcatl"
 126     "./teletype"
 127     "./tolower"
 128     "./underline"
 129     "./units"
 130     "./utfate"
 131     "./waveout"
 132     "./zcat"
 133     "./zj"
 134 
 135     _ "embed"
 136 )
 137 
 138 //go:embed info.txt
 139 var info string
 140 
 141 // mains has some entries starting as nil to avoid circular-dependency errors
 142 var mains = map[string]func(){
 143     `args`:       args,
 144     `avoid`:      avoid.Main,
 145     `bitdump`:    bitdump.Main,
 146     `breakdown`:  breakdown.Main,
 147     `bytedump`:   bytedump.Main,
 148     `calc`:       calc.Main,
 149     `catl`:       catl.Main,
 150     `cls`:        cls,
 151     `coby`:       coby.Main,
 152     `compress`:   compress.Main,
 153     `countdown`:  countdown.Main,
 154     `datauri`:    datauri.Main,
 155     `dcol`:       dcol.Main,
 156     `debase64`:   debase64.Main,
 157     `decompress`: decompress,
 158     `decsv`:      decsv.Main,
 159     `dedent`:     dedent.Main,
 160     `dedup`:      dedup.Main,
 161     `dejsonl`:    dejsonl.Main,
 162     `delay`:      delay.Main,
 163     `dessv`:      dessv.Main,
 164     `detab`:      detab.Main,
 165     `echobar`:    echobar,
 166     `ecoli`:      ecoli.Main,
 167     `erase`:      erase.Main,
 168     `fail`:       fail,
 169     `files`:      files.Main,
 170     `filesizes`:  filesizes.Main,
 171     `finfo`:      finfo.Main,
 172     `first`:      first.Main,
 173     `fixlines`:   fixlines.Main,
 174     `folders`:    folders.Main,
 175     `gsub`:       gsub.Main,
 176     `hecho`:      hecho,
 177     `hima`:       hima.Main,
 178     `htmlify`:    htmlify.Main,
 179     `id3pic`:     id3pic.Main,
 180     `ignore`:     ignore,
 181     `indent`:     indent.Main,
 182     `items`:      items.Main,
 183     `json0`:      json0.Main,
 184     `json2`:      json2.Main,
 185     `jsonl`:      jsonl.Main,
 186     `jsons`:      jsons.Main,
 187     `junk`:       junk.Main,
 188     `label`:      label.Main,
 189     `last`:       last.Main,
 190     `leak`:       leak.Main,
 191     `limit`:      limit.Main,
 192     `lineup`:     lineup.Main,
 193     `match`:      match.Main,
 194     `n`:          n.Main,
 195     `ncol`:       ncol.Main,
 196     `ngron`:      ngron.Main,
 197     `nhex`:       nhex.Main,
 198     `nil`:        null,
 199     `njson`:      njson.Main,
 200     `nn`:         nn.Main,
 201     `now`:        now.Main,
 202     `nts`:        nts.Main,
 203     `pad`:        pad.Main,
 204     `pcol`:       pcol.Main,
 205     `plain`:      plain.Main,
 206     `precho`:     precho,
 207     `pretsv`:     pretsv.Main,
 208     `primes`:     primes.Main,
 209     `realign`:    realign.Main,
 210     `reprose`:    reprose.Main,
 211     `ruler`:      ruler,
 212     `sbs`:        sbs.Main,
 213     `skip`:       skip.Main,
 214     `skiplast`:   skiplast.Main,
 215     `squeeze`:    squeeze.Main,
 216     `squomp`:     squomp.Main,
 217     `stomp`:      stomp.Main,
 218     `tacl`:       tacl.Main,
 219     `tcatl`:      tcatl.Main,
 220     `teletype`:   teletype.Main,
 221     `timezones`:  timezones,
 222     `tolower`:    tolower.Main,
 223     `underline`:  underline.Main,
 224     `units`:      units.Main,
 225     `utfate`:     utfate.Main,
 226     `waveout`:    waveout.Main,
 227 }
 228 
 229 var experimental = map[string]func(){
 230     `div`:     div,
 231     `fh`:      fh.Main,
 232     `mop`:     mop,
 233     `pac`:     pac.Main,
 234     `nothing`: nothing,
 235     `tinker`:  tinker,
 236     `zj`:      zj.Main,
 237 }
 238 
 239 var remakes = map[string]func(){
 240     `base64`: base64.Main,
 241     `cat`:    cat.Main,
 242     `clear`:  clear,
 243     `dc`:     dc.Main,
 244     `echo`:   echo,
 245     `expand`: expand.Main,
 246     `factor`: factor.Main,
 247     `false`:  falseMain,
 248     `head`:   head.Main,
 249     `ls`:     ls.Main,
 250     `nl`:     nl.Main,
 251     `seq`:    seq.Main,
 252     `sleep`:  sleep,
 253     `tail`:   tail.Main,
 254     `true`:   trueMain,
 255     `yes`:    yes,
 256     `zcat`:   zcat.Main,
 257 }
 258 
 259 var aliases = map[string]string{
 260     `break`:        `breakdown`,
 261     `breaklines`:   `breakdown`,
 262     `ca`:           `calc`,
 263     `calculate`:    `calc`,
 264     `calculator`:   `calc`,
 265     `fc`:           `calc`,
 266     `frac`:         `calc`,
 267     `fraca`:        `calc`,
 268     `fracalc`:      `calc`,
 269     `bytes`:        `cat`,
 270     `catenate`:     `cat`,
 271     `concat`:       `cat`,
 272     `concatenate`:  `cat`,
 273     `load`:         `cat`,
 274     `lines`:        `catl`,
 275     `dcols`:        `dcol`,
 276     `dropcol`:      `dcol`,
 277     `dropcols`:     `dcol`,
 278     `dropcolumns`:  `dcol`,
 279     `unbase64`:     `debase64`,
 280     `uncompress`:   `decompress`,
 281     `uncsv`:        `decsv`,
 282     `deduplicate`:  `dedup`,
 283     `undup`:        `dedup`,
 284     `unique`:       `dedup`,
 285     `unjsonl`:      `dejsonl`,
 286     `unssv`:        `dessv`,
 287     `untab`:        `detab`,
 288     `fileinfo`:     `finfo`,
 289     `detrail`:      `fixlines`,
 290     `id3picture`:   `id3pic`,
 291     `id3thumb`:     `id3pic`,
 292     `id3thumbnail`: `id3pic`,
 293     `mp3pic`:       `id3pic`,
 294     `mp3picture`:   `id3pic`,
 295     `mp3thumb`:     `id3pic`,
 296     `mp3thumbnail`: `id3pic`,
 297     `idem`:         `ignore`,
 298     `iden`:         `ignore`,
 299     `identity`:     `ignore`,
 300     `j0`:           `json0`,
 301     `j2`:           `json2`,
 302     `jl`:           `jsonl`,
 303     `detsv`:        `jsons`,
 304     `prelabel`:     `label`,
 305     `prememo`:      `label`,
 306     `keep`:         `match`,
 307     `mops`:         `mop`,
 308     `ncols`:        `ncol`,
 309     `nicecol`:      `ncol`,
 310     `nicecols`:     `ncol`,
 311     `nicecolumns`:  `ncol`,
 312     `nicegron`:     `ngron`,
 313     `nh`:           `nhex`,
 314     `nicehex`:      `nhex`,
 315     `null`:         `nil`,
 316     `nicejson`:     `njson`,
 317     `nj`:           `njson`,
 318     `nicedigits`:   `nn`,
 319     `nicenum`:      `nn`,
 320     `nicenumbers`:  `nn`,
 321     `nicenums`:     `nn`,
 322     `ok`:           `nothing`,
 323     `rpn`:          `pac`,
 324     `pcols`:        `pcol`,
 325     `pickcol`:      `pcol`,
 326     `pickcols`:     `pcol`,
 327     `pickcolumns`:  `pcol`,
 328     `tty`:          `teletype`,
 329     `timezone`:     `timezones`,
 330     `lower`:        `tolower`,
 331     `lowercase`:    `tolower`,
 332     `u`:            `underline`,
 333     `uline`:        `underline`,
 334     `utf8`:         `utfate`,
 335     `zoomjson`:     `zj`,
 336     `degzip`:       `zcat`,
 337     `ungzip`:       `zcat`,
 338 }
 339 
 340 var blurbs = map[string]string{
 341     `args`:       `show all ARGumentS given after the tool name, one per line`,
 342     `avoid`:      `ignore lines matching any of the regexes given`,
 343     `bitdump`:    `show all bits for all input bytes`,
 344     `breakdown`:  `break input lines into multiple output lines by regexes`,
 345     `bytedump`:   `show bytes as hex values, with a wide ASCII panel`,
 346     `calc`:       `fractional calculator, with floating-point powers`,
 347     `catl`:       `conCATenate Lines, ensures text ends with a line-feed`,
 348     `cls`:        `CLear the Screen`,
 349     `coby`:       `COunt BYtes, and many other byte/text-related stats`,
 350     `compress`:   `gzip-compress all concatenated inputs given`,
 351     `countdown`:  `countdown the seconds/minutes/hours given`,
 352     `datauri`:    `turn bytes into data-URIs, auto-detecting MIME types`,
 353     `dcol`:       `Drop COLumns by name or by 1-based index`,
 354     `debase64`:   `decode base64 text and data-URIs`,
 355     `decompress`: `gzip-decompress concatenated bytes from all the inputs`,
 356     `decsv`:      `convert CSV tables into TSV tables, or into JSON`,
 357     `dedent`:     `ignore the common leading-space indentation`,
 358     `dedup`:      `deduplicate lines, emitting each unique line only once`,
 359     `dejsonl`:    `turn JSON Lines into proper JSON`,
 360     `delay`:      `wait the seconds given, before emitting each input line`,
 361     `dessv`:      `turn tables of space-separated values into TSV tables`,
 362     `detab`:      `expand tabs into runs of up to n spaces, or up to 4`,
 363     `div`:        `DIVide 2 numbers, or invert 1 number`,
 364     `echobar`:    `like 'echo', but right-pads and highlights its output`,
 365     `ecoli`:      `Expressions COloring LInes color-codes matching lines`,
 366     `erase`:      `ignore/erase all matching regexes away from each line`,
 367     `fail`:       `fail with the exit code given, or using 1 by default`,
 368     `fh`:         `Function Heatmapper draws 2-input (x, y) functions`,
 369     `files`:      `list all files in the folder(s) given`,
 370     `filesizes`:  `show sizes of files and block-counts (4K by default)`,
 371     `finfo`:      `show various file info, for plain-text and/or media files`,
 372     `first`:      `keep only the first n lines, or the first line by default`,
 373     `fixlines`:   `ignore carriage-returns, or even trailing spaces`,
 374     `folders`:    `list all folders in the folder(s) given`,
 375     `gsub`:       `Globally SUBstitute all regular expression matches`,
 376     `hecho`:      `Highlighted ECHO shows an ANSI-styled line`,
 377     `help`:       `show the help message for "easybox"`,
 378     `hima`:       `HIlight MAtches using the regexes given`,
 379     `htmlify`:    `turn plain-text lines into HTML documents`,
 380     `id3pic`:     `get the encoded picture out of audio files, if present`,
 381     `ignore`:     `ignore all arguments, copying stdin into stdout instead`,
 382     `indent`:     `indent lines, using 4 leading spaces by default`,
 383     `items`:      `emit words/items from input lines as single-item lines`,
 384     `json0`:      `minimize/fix JSON into the smallest-possible size`,
 385     `json2`:      `indent JSON into multiple lines, using 2 spaces per level`,
 386     `jsonl`:      `turn items from top-level JSON arrays into JSON Lines`,
 387     `jsons`:      `JSON Strings turns TSV into arrays of objects of strings`,
 388     `junk`:       `emit a given count of random bytes, or 1024 by default`,
 389     `label`:      `precede input lines with a styled line`,
 390     `last`:       `keep only the last n lines, or the last line by default`,
 391     `leak`:       `emit lines both to stdout and stderr; good to debug pipes`,
 392     `limit`:      `emit up to the first n bytes, or the first 1024 by default`,
 393     `lineup`:     `put input lines into (up to) n-items tab-separated lines`,
 394     `match`:      `only keep lines matching any of the regexes given`,
 395     `mop`:        `Multiple OPerations, using the 2 numbers given`,
 396     `n`:          `Number lines, putting tabs between numbers and contents`,
 397     `ncol`:       `Nice COLumns realigns tables, color-coding their values`,
 398     `ngron`:      `Nice GRON mimics a subset of "gron", using better colors`,
 399     `nhex`:       `Nice HEXadecimal shows bytes as hex values and ASCII`,
 400     `nil`:        `emit nothing, also discarding all stdin bytes, if piped`,
 401     `njson`:      `Nice JSON indents and color-codes JSON data`,
 402     `nn`:         `Nice Numbers color-codes groups of digits for legibility`,
 403     `now`:        `show the current date and time, also for other timezones`,
 404     `nts`:        `Nice TimeStamp precedes each line with styled date/time`,
 405     `pac`:        `Postfix Array Calculator is an RPN-style calculator`,
 406     `pad`:        `right-pad lines, so they all have the same symbol-count`,
 407     `pcol`:       `Pick COLumns by name or by 1-based index`,
 408     `plain`:      `ignore all ANSI-sequences, leaving unstyled text`,
 409     `precho`:     `PRecede ECHO, precedes input lines with an SSV line`,
 410     `pretsv`:     `PREcede TSV, precedes input lines with a TSV line`,
 411     `primes`:     `find prime numbers, up to the first million by default`,
 412     `realign`:    `realign items from the SSV/TSV tables given`,
 413     `reprose`:    `reflow/trim lines of text/prose to improve its legibility`,
 414     `ruler`:      `show a ruler-like pattern on a single line`,
 415     `sbs`:        `Side By Side, wraps input lines in multiple columns`,
 416     `seq`:        `emit a sequence of numbers, one per line`,
 417     `skip`:       `skip at most the first n lines, or the first by default`,
 418     `skiplast`:   `skip at most the last n lines, or the last by default`,
 419     `squeeze`:    `aggressively ignore spaces, especially runs of spaces`,
 420     `squomp`:     `squeeze non-empty lines and stomp empty(-ish) lines`,
 421     `stomp`:      `ignore leading/trailing empty lines, squeezing empty runs`,
 422     `tacl`:       `TAC Lines emits text lines in backward-order`,
 423     `tcatl`:      `Titled conCATenate Lines, is like "catl" but with names`,
 424     `teletype`:   `mimic old-fashioned teletype devices, by delaying output`,
 425     `timezones`:  `lookup full timezone names from the city/place names given`,
 426     `tolower`:    `turn all uppercase letters into lowercase ones`,
 427     `tools`:      `list all tools available`,
 428     `underline`:  `underline every nth line, or every 5th line by default`,
 429     `units`:      `convert weird units into the international standard ones`,
 430     `utfate`:     `decode all other types of UTF text into UTF-8`,
 431     `waveout`:    `emit/calculate WAV-format sounds by formula`,
 432     `yes`:        `keep emitting a line with "yes", or the message given`,
 433     `zj`:         `Zoom Json, using the keys/indices given as arguments`,
 434 }
 435 
 436 func main() {
 437     // try to use the app's `name`, in case it's being called from a file-link
 438     // named after one of the tools
 439     if tool, ok := lookupTool(path.Base(os.Args[0])); ok {
 440         tool()
 441         return
 442     }
 443 
 444     if len(os.Args) < 2 {
 445         showHelp(os.Stderr)
 446         fmt.Fprintln(os.Stderr, ``)
 447         fmt.Fprintln(os.Stderr, `easybox: no tool name given`)
 448         os.Exit(1)
 449         return
 450     }
 451 
 452     // try normal tool-lookup using the first command-line argument
 453 
 454     name := os.Args[1]
 455     os.Args = os.Args[1:]
 456 
 457     if tool, ok := lookupTool(name); ok {
 458         tool()
 459         return
 460     }
 461 
 462     switch name {
 463     case `-h`, `--h`, `-help`, `--help`, `help`:
 464         showHelp(os.Stdout)
 465 
 466     case `-l`, `--l`, `-list`, `--list`:
 467         tools()
 468 
 469     case `-links`, `--links`:
 470         showLinksCommands(os.Stdout)
 471 
 472     case `-t`, `--t`, `-tools`, `--tools`, `tools`:
 473         tools()
 474 
 475     default:
 476         const fs = "easybox: tool/alias named %q not found\n"
 477         fmt.Fprintf(os.Stderr, fs, name)
 478         os.Exit(1)
 479         return
 480     }
 481 }
 482 
 483 // dealias tries to lookup a string to the aliases given, returning the name
 484 // given if the lookup fails
 485 func dealias(aliases map[string]string, name string) string {
 486     if s, ok := aliases[name]; ok {
 487         return s
 488     }
 489     return name
 490 }
 491 
 492 func help() {
 493     showHelp(os.Stdout)
 494 }
 495 
 496 func lookupTool(name string) (tool func(), ok bool) {
 497     name = strings.ReplaceAll(name, `-`, ``)
 498     name = strings.ReplaceAll(name, `_`, ``)
 499     key := dealias(aliases, name)
 500 
 501     if tool, ok := mains[key]; ok {
 502         return tool, ok
 503     }
 504 
 505     if tool, ok := remakes[key]; ok {
 506         return tool, ok
 507     }
 508 
 509     tool, ok = experimental[key]
 510     return tool, ok
 511 }
 512 
 513 // showHelp has a parameter to write either to stdout or stderr
 514 func showHelp(w io.Writer) {
 515     fmt.Fprintln(w, info)
 516 
 517     fmt.Fprintln(w, "\nTools Available")
 518 
 519     maxlen := 0
 520     names := make([]string, 0, max(len(mains), len(aliases)))
 521     for k := range mains {
 522         names = append(names, k)
 523         maxlen = max(maxlen, utf8.RuneCountInString(k))
 524     }
 525 
 526     sort.Strings(names)
 527 
 528     for _, s := range names {
 529         fmt.Fprintf(w, "  - %-*s  %s\n", maxlen, s, blurbs[s])
 530     }
 531 
 532     fmt.Fprintln(w, "\nAliases Available")
 533 
 534     maxlen = 0
 535     names = names[:0]
 536     for k := range aliases {
 537         names = append(names, k)
 538         maxlen = max(maxlen, utf8.RuneCountInString(k))
 539     }
 540 
 541     sort.Strings(names)
 542 
 543     for _, k := range names {
 544         fmt.Fprintf(w, "  - %-*s -> %s\n", maxlen, k, aliases[k])
 545     }
 546 
 547     names = names[:0]
 548     for k := range experimental {
 549         names = append(names, k)
 550     }
 551 
 552     if len(names) == 0 {
 553         return
 554     }
 555 
 556     fmt.Fprintln(w, "\nExperimental Tools Available")
 557 
 558     for _, k := range names {
 559         fmt.Fprintf(w, "  - %s\n", k)
 560     }
 561 }
 562 
 563 // showLinksCommands has a parameter to write either to stdout or stderr
 564 func showLinksCommands(w io.Writer) {
 565     names := make([]string, 0, len(mains))
 566     for k := range mains {
 567         names = append(names, k)
 568     }
 569 
 570     sort.Strings(names)
 571 
 572     for _, s := range names {
 573         fmt.Fprintf(w, "ln -s \"$(which easybox)\" ./%s\n", s)
 574     }
 575 }
 576 
 577 // tinker is for little throw-away experiments
 578 func tinker() {
 579 }
     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 )
  34 
  35 const info = `
  36 match [options...] [regular expressions...]
  37 
  38 Only keep lines which match any of the extended-mode regular expressions
  39 given. When not given any regex, match non-empty lines by default.
  40 
  41 The options are, available both in single and double-dash versions
  42 
  43     -h, -help     show this help message
  44     -i, -ins      match regexes case-insensitively
  45     -l, -links    add a regex to match HTTP/HTTPS links case-insensitively
  46 `
  47 
  48 const linkRegexp = `(?i)https?://[a-z0-9+_.:%-]+(/[a-z0-9+_.%/,#?&=-]*)*`
  49 
  50 func Main() {
  51     nerr := 0
  52     links := false
  53     buffered := false
  54     avoid := false
  55     sensitive := true
  56     args := os.Args[1:]
  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             sensitive = false
  71             args = args[1:]
  72             continue
  73 
  74         case `-iv`, `-vi`:
  75             sensitive = false
  76             avoid = true
  77             args = args[1:]
  78             continue
  79 
  80         case `-l`, `--l`, `-links`, `--links`:
  81             links = true
  82             args = args[1:]
  83             continue
  84 
  85         case `-v`, `--v`, `-inv`, `--inv`, `-neg`, `--neg`:
  86             avoid = true
  87             args = args[1:]
  88             continue
  89         }
  90 
  91         break
  92     }
  93 
  94     if len(args) > 0 && args[0] == `--` {
  95         args = args[1:]
  96     }
  97 
  98     liveLines := !buffered
  99     if !buffered {
 100         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 101             liveLines = false
 102         }
 103     }
 104 
 105     if len(args) == 0 {
 106         args = []string{`.`}
 107     }
 108 
 109     var exprs []*regexp.Regexp
 110     if links {
 111         exprs = make([]*regexp.Regexp, 0, len(args)+1)
 112         exprs = append(exprs, regexp.MustCompile(linkRegexp))
 113     } else {
 114         exprs = make([]*regexp.Regexp, 0, len(args))
 115     }
 116 
 117     for _, src := range args {
 118         var err error
 119         var exp *regexp.Regexp
 120         if !sensitive {
 121             exp, err = regexp.Compile(`(?i)` + src)
 122         } else {
 123             exp, err = regexp.Compile(src)
 124         }
 125 
 126         if err != nil {
 127             os.Stderr.WriteString(err.Error())
 128             os.Stderr.WriteString("\n")
 129             nerr++
 130         }
 131 
 132         exprs = append(exprs, exp)
 133     }
 134 
 135     if nerr > 0 {
 136         os.Exit(1)
 137         return
 138     }
 139 
 140     var buf []byte
 141     sc := bufio.NewScanner(os.Stdin)
 142     sc.Buffer(nil, 8*1024*1024*1024)
 143     bw := bufio.NewWriter(os.Stdout)
 144 
 145     for i := 0; sc.Scan(); i++ {
 146         line := sc.Bytes()
 147         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 148             line = line[3:]
 149         }
 150 
 151         s := line
 152         if bytes.IndexByte(s, '\x1b') >= 0 {
 153             buf = plain(buf[:0], s)
 154             s = buf
 155         }
 156 
 157         if match(s, exprs) {
 158             if avoid {
 159                 continue
 160             }
 161 
 162             if err := emit(bw, line, liveLines); err != nil {
 163                 return
 164             }
 165         }
 166 
 167         if avoid {
 168             if err := emit(bw, line, liveLines); err != nil {
 169                 return
 170             }
 171         }
 172     }
 173 }
 174 
 175 func emit(w *bufio.Writer, line []byte, live bool) error {
 176     w.Write(line)
 177     w.WriteByte('\n')
 178 
 179     if !live {
 180         return nil
 181     }
 182 
 183     return w.Flush()
 184 }
 185 
 186 func match(what []byte, with []*regexp.Regexp) bool {
 187     for _, e := range with {
 188         if e.Match(what) {
 189             return true
 190         }
 191     }
 192     return false
 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: ./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: ./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(err.Error())
  94         os.Stderr.WriteString("\n")
  95         os.Exit(1)
  96         return
  97     }
  98 }
  99 
 100 func run(paths []string, cfg *config) error {
 101     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 102     defer bw.Flush()
 103 
 104     for _, p := range paths {
 105         if err := handleFile(bw, p, cfg); err != nil {
 106             return err
 107         }
 108     }
 109 
 110     if len(paths) == 0 {
 111         return handleReader(bw, os.Stdin, cfg)
 112     }
 113 
 114     return nil
 115 }
 116 
 117 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 118     f, err := os.Open(path)
 119     if err != nil {
 120         // on windows, file-not-found error messages may mention `CreateFile`,
 121         // even when trying to open files in read-only mode
 122         return errors.New(`can't open file named ` + path)
 123     }
 124     defer f.Close()
 125     return handleReader(w, f, cfg)
 126 }
 127 
 128 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 129     var buf [24]byte
 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.Text()
 136         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 137             s = s[3:]
 138         }
 139 
 140         w.Write(strconv.AppendInt(buf[:0], int64(cfg.n), 10))
 141         w.WriteByte('\t')
 142         w.WriteString(s)
 143         cfg.n++
 144 
 145         if w.WriteByte('\n') != nil {
 146             return io.EOF
 147         }
 148 
 149         if !cfg.liveLines {
 150             continue
 151         }
 152 
 153         if w.Flush() != nil {
 154             return io.EOF
 155         }
 156     }
 157 
 158     return sc.Err()
 159 }
     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(err.Error())
 117         os.Stderr.WriteString("\n")
 118         os.Exit(1)
 119         return
 120     }
 121 }
 122 
 123 // table has all summary info gathered from the data, along with the row
 124 // themselves, stored as lines/strings
 125 type table struct {
 126     Columns int
 127 
 128     Rows []string
 129 
 130     MaxWidth []int
 131 
 132     MaxDotDecimals []int
 133 
 134     Numeric []int
 135 
 136     Sums []float64
 137 
 138     LoopItems func(line string, items int, t *table, f itemFunc) int
 139 
 140     sb strings.Builder
 141 
 142     MaxColumns bool
 143 
 144     ShowTiles bool
 145 
 146     ShowSums bool
 147 }
 148 
 149 type itemFunc func(i int, s string, t *table)
 150 
 151 func run(paths []string, res *table) error {
 152     for _, p := range paths {
 153         if err := handleFile(res, p); err != nil {
 154             return err
 155         }
 156     }
 157 
 158     if len(paths) == 0 {
 159         if err := handleReader(res, os.Stdin); err != nil {
 160             return err
 161         }
 162     }
 163 
 164     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 165     defer bw.Flush()
 166     realign(bw, res)
 167     return nil
 168 }
 169 
 170 func handleFile(res *table, path string) error {
 171     f, err := os.Open(path)
 172     if err != nil {
 173         // on windows, file-not-found error messages may mention `CreateFile`,
 174         // even when trying to open files in read-only mode
 175         return errors.New(`can't open file named ` + path)
 176     }
 177     defer f.Close()
 178     return handleReader(res, f)
 179 }
 180 
 181 func handleReader(t *table, r io.Reader) error {
 182     const gb = 1024 * 1024 * 1024
 183     sc := bufio.NewScanner(r)
 184     sc.Buffer(nil, 8*gb)
 185 
 186     const maxInt = int(^uint(0) >> 1)
 187     maxCols := maxInt
 188 
 189     for i := 0; sc.Scan(); i++ {
 190         s := sc.Text()
 191         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 192             s = s[3:]
 193         }
 194 
 195         if len(s) == 0 {
 196             continue
 197         }
 198 
 199         t.Rows = append(t.Rows, s)
 200 
 201         if t.Columns == 0 {
 202             if t.LoopItems == nil {
 203                 if strings.IndexByte(s, '\t') >= 0 {
 204                     t.LoopItems = loopItemsTSV
 205                 } else {
 206                     t.LoopItems = loopItemsSSV
 207                 }
 208             }
 209 
 210             if !t.MaxColumns {
 211                 t.Columns = t.LoopItems(s, maxCols, t, doNothing)
 212                 maxCols = t.Columns
 213             }
 214         }
 215 
 216         t.LoopItems(s, maxCols, t, updateItem)
 217     }
 218 
 219     return sc.Err()
 220 }
 221 
 222 // doNothing is given to LoopItems to count items, while doing nothing else
 223 func doNothing(i int, s string, t *table) {}
 224 
 225 func updateItem(i int, s string, t *table) {
 226     // ensure column-info-slices have enough room
 227     if i >= len(t.MaxWidth) {
 228         // update column-count if in max-columns mode
 229         if t.MaxColumns {
 230             t.Columns = i + 1
 231         }
 232         t.MaxWidth = append(t.MaxWidth, 0)
 233         t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
 234         t.Numeric = append(t.Numeric, 0)
 235         t.Sums = append(t.Sums, 0)
 236     }
 237 
 238     // keep track of widest rune-counts for each column
 239     w := countWidth(s)
 240     if t.MaxWidth[i] < w {
 241         t.MaxWidth[i] = w
 242     }
 243 
 244     // update stats for numeric items
 245     if isNumeric(s, &(t.sb)) {
 246         dd := countDotDecimals(s)
 247         if t.MaxDotDecimals[i] < dd {
 248             t.MaxDotDecimals[i] = dd
 249         }
 250 
 251         t.Numeric[i]++
 252         f, _ := strconv.ParseFloat(t.sb.String(), 64)
 253         t.Sums[i] += f
 254     }
 255 }
 256 
 257 // loopItemsSSV loops over a line's items, allocation-free style; when given
 258 // empty strings, the callback func is never called
 259 func loopItemsSSV(s string, max int, t *table, f itemFunc) int {
 260     i := 0
 261     s = trimTrailingSpaces(s)
 262 
 263     for {
 264         s = trimLeadingSpaces(s)
 265         if len(s) == 0 {
 266             return i
 267         }
 268 
 269         if i+1 == max {
 270             f(i, s, t)
 271             return i + 1
 272         }
 273 
 274         j := strings.IndexByte(s, ' ')
 275         if j < 0 {
 276             f(i, s, t)
 277             return i + 1
 278         }
 279 
 280         f(i, s[:j], t)
 281         s = s[j+1:]
 282         i++
 283     }
 284 }
 285 
 286 func trimLeadingSpaces(s string) string {
 287     for len(s) > 0 && s[0] == ' ' {
 288         s = s[1:]
 289     }
 290     return s
 291 }
 292 
 293 func trimTrailingSpaces(s string) string {
 294     for len(s) > 0 && s[len(s)-1] == ' ' {
 295         s = s[:len(s)-1]
 296     }
 297     return s
 298 }
 299 
 300 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 301 // when given empty strings, the callback func is never called
 302 func loopItemsTSV(s string, max int, t *table, f itemFunc) int {
 303     if len(s) == 0 {
 304         return 0
 305     }
 306 
 307     i := 0
 308 
 309     for {
 310         if i+1 == max {
 311             f(i, s, t)
 312             return i + 1
 313         }
 314 
 315         j := strings.IndexByte(s, '\t')
 316         if j < 0 {
 317             f(i, s, t)
 318             return i + 1
 319         }
 320 
 321         f(i, s[:j], t)
 322         s = s[j+1:]
 323         i++
 324     }
 325 }
 326 
 327 func skipLeadingEscapeSequences(s string) string {
 328     for len(s) >= 2 {
 329         if s[0] != '\x1b' {
 330             return s
 331         }
 332 
 333         switch s[1] {
 334         case '[':
 335             s = skipSingleLeadingANSI(s[2:])
 336 
 337         case ']':
 338             if len(s) < 3 || s[2] != '8' {
 339                 return s
 340             }
 341             s = skipSingleLeadingOSC(s[3:])
 342 
 343         default:
 344             return s
 345         }
 346     }
 347 
 348     return s
 349 }
 350 
 351 func skipSingleLeadingANSI(s string) string {
 352     for len(s) > 0 {
 353         upper := s[0] &^ 32
 354         s = s[1:]
 355         if 'A' <= upper && upper <= 'Z' {
 356             break
 357         }
 358     }
 359 
 360     return s
 361 }
 362 
 363 func skipSingleLeadingOSC(s string) string {
 364     var prev byte
 365 
 366     for len(s) > 0 {
 367         b := s[0]
 368         s = s[1:]
 369         if prev == '\x1b' && b == '\\' {
 370             break
 371         }
 372         prev = b
 373     }
 374 
 375     return s
 376 }
 377 
 378 // isNumeric checks if a string is valid/useable as a number
 379 func isNumeric(s string, sb *strings.Builder) bool {
 380     if len(s) == 0 {
 381         return false
 382     }
 383 
 384     sb.Reset()
 385 
 386     s = skipLeadingEscapeSequences(s)
 387     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 388         sb.WriteByte(s[0])
 389         s = s[1:]
 390     }
 391 
 392     s = skipLeadingEscapeSequences(s)
 393     if len(s) == 0 {
 394         return false
 395     }
 396     if b := s[0]; b == '.' {
 397         sb.WriteByte(b)
 398         return isDigits(s[1:], sb)
 399     }
 400 
 401     digits := 0
 402 
 403     for {
 404         s = skipLeadingEscapeSequences(s)
 405         if len(s) == 0 {
 406             break
 407         }
 408 
 409         b := s[0]
 410         sb.WriteByte(b)
 411 
 412         if b == '.' {
 413             return isDigits(s[1:], sb)
 414         }
 415 
 416         if !('0' <= b && b <= '9') {
 417             return false
 418         }
 419 
 420         digits++
 421         s = s[1:]
 422     }
 423 
 424     s = skipLeadingEscapeSequences(s)
 425     return len(s) == 0 && digits > 0
 426 }
 427 
 428 func isDigits(s string, sb *strings.Builder) bool {
 429     if len(s) == 0 {
 430         return false
 431     }
 432 
 433     digits := 0
 434 
 435     for {
 436         s = skipLeadingEscapeSequences(s)
 437         if len(s) == 0 {
 438             break
 439         }
 440 
 441         if b := s[0]; '0' <= b && b <= '9' {
 442             sb.WriteByte(b)
 443             s = s[1:]
 444             digits++
 445         } else {
 446             return false
 447         }
 448     }
 449 
 450     s = skipLeadingEscapeSequences(s)
 451     return len(s) == 0 && digits > 0
 452 }
 453 
 454 // countDecimals counts decimal digits from the string given, assuming it
 455 // represents a valid/useable float64, when parsed
 456 func countDecimals(s string) int {
 457     dot := strings.IndexByte(s, '.')
 458     if dot < 0 {
 459         return 0
 460     }
 461 
 462     decs := 0
 463     s = s[dot+1:]
 464 
 465     for len(s) > 0 {
 466         s = skipLeadingEscapeSequences(s)
 467         if len(s) == 0 {
 468             break
 469         }
 470         if '0' <= s[0] && s[0] <= '9' {
 471             decs++
 472         }
 473         s = s[1:]
 474     }
 475 
 476     return decs
 477 }
 478 
 479 // countDotDecimals is like func countDecimals, but this one also includes
 480 // the dot, when any decimals are present, else the count stays at 0
 481 func countDotDecimals(s string) int {
 482     decs := countDecimals(s)
 483     if decs > 0 {
 484         return decs + 1
 485     }
 486     return decs
 487 }
 488 
 489 func countWidth(s string) int {
 490     width := 0
 491 
 492     for len(s) > 0 {
 493         i, j := indexEscapeSequence(s)
 494         if i < 0 {
 495             break
 496         }
 497         if j < 0 {
 498             j = len(s)
 499         }
 500 
 501         width += utf8.RuneCountInString(s[:i])
 502         s = s[j:]
 503     }
 504 
 505     // count trailing/all runes in strings which don't end with ANSI-sequences
 506     width += utf8.RuneCountInString(s)
 507     return width
 508 }
 509 
 510 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 511 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 512 // indices which can be independently negative when either the start/end of
 513 // a sequence isn't found; given their fairly-common use, even the hyperlink
 514 // ESC]8 sequences are supported
 515 func indexEscapeSequence(s string) (int, int) {
 516     var prev byte
 517 
 518     for i := range s {
 519         b := s[i]
 520 
 521         if prev == '\x1b' && b == '[' {
 522             j := indexLetter(s[i+1:])
 523             if j < 0 {
 524                 return i, -1
 525             }
 526             return i - 1, i + 1 + j + 1
 527         }
 528 
 529         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 530             j := indexPair(s[i+1:], '\x1b', '\\')
 531             if j < 0 {
 532                 return i, -1
 533             }
 534             return i - 1, i + 1 + j + 2
 535         }
 536 
 537         prev = b
 538     }
 539 
 540     return -1, -1
 541 }
 542 
 543 func indexLetter(s string) int {
 544     for i, b := range s {
 545         upper := b &^ 32
 546         if 'A' <= upper && upper <= 'Z' {
 547             return i
 548         }
 549     }
 550 
 551     return -1
 552 }
 553 
 554 func indexPair(s string, x byte, y byte) int {
 555     var prev byte
 556 
 557     for i := range s {
 558         b := s[i]
 559         if prev == x && b == y && i > 0 {
 560             return i
 561         }
 562         prev = b
 563     }
 564 
 565     return -1
 566 }
 567 
 568 func realign(w *bufio.Writer, t *table) {
 569     // make sums row first, as final alignments are usually affected by these
 570     var sums []string
 571     if t.ShowSums {
 572         sums = make([]string, 0, t.Columns)
 573 
 574         for i := 0; i < t.Columns; i++ {
 575             if t.Numeric[i] == 0 {
 576                 sums = append(sums, `-`)
 577                 if t.MaxWidth[i] < 1 {
 578                     t.MaxWidth[i] = 1
 579                 }
 580                 continue
 581             }
 582 
 583             decs := t.MaxDotDecimals[i]
 584             if decs > 0 {
 585                 decs--
 586             }
 587 
 588             var buf [64]byte
 589             s := strconv.AppendFloat(buf[:0], t.Sums[i], 'f', decs, 64)
 590             sums = append(sums, string(s))
 591             if t.MaxWidth[i] < len(s) {
 592                 t.MaxWidth[i] = len(s)
 593             }
 594         }
 595     }
 596 
 597     // due keeps track of how many spaces are due, when separating realigned
 598     // items from their immediate predecessor on the same row; this counter
 599     // is also used to right-pad numbers with decimals, as such items can be
 600     // padded with spaces from either side
 601     due := 0
 602 
 603     showItem := func(i int, s string, t *table) {
 604         if i > 0 {
 605             due += columnGap
 606         }
 607 
 608         if isNumeric(s, &(t.sb)) {
 609             dd := countDotDecimals(s)
 610             rpad := t.MaxDotDecimals[i] - dd
 611             width := countWidth(s)
 612             lpad := t.MaxWidth[i] - (width + rpad) + due
 613             writeSpaces(w, lpad)
 614             f, _ := strconv.ParseFloat(t.sb.String(), 64)
 615             writeNumericItem(w, s, numericStyle(f))
 616             due = rpad
 617             return
 618         }
 619 
 620         writeSpaces(w, due)
 621         w.WriteString(s)
 622         due = t.MaxWidth[i] - countWidth(s)
 623     }
 624 
 625     writeTile := func(i int, s string, t *table) {
 626         // make empty items stand out
 627         if len(s) == 0 {
 628             w.WriteString("\x1b[0m○")
 629             return
 630         }
 631 
 632         if isNumeric(s, &(t.sb)) {
 633             f, _ := strconv.ParseFloat(t.sb.String(), 64)
 634             w.WriteString(numericStyle(f))
 635             w.WriteString("■")
 636             return
 637         }
 638 
 639         // make padded items stand out: these items have spaces at either end
 640         if s[0] == ' ' || s[len(s)-1] == ' ' {
 641             w.WriteString("\x1b[38;2;196;160;0m■")
 642             return
 643         }
 644 
 645         w.WriteString("\x1b[38;2;128;128;128m■")
 646     }
 647 
 648     // show realigned rows
 649 
 650     for _, line := range t.Rows {
 651         due = 0
 652 
 653         if t.ShowTiles {
 654             end := t.LoopItems(line, t.Columns, t, writeTile)
 655             if end < len(t.MaxWidth)-1 {
 656                 w.WriteString("\x1b[0m")
 657             }
 658             // make rows with missing trailing items stand out
 659             for i := end; i < len(t.MaxWidth); i++ {
 660                 w.WriteString("×")
 661             }
 662             w.WriteString("\x1b[0m")
 663             due += columnGap
 664         }
 665 
 666         t.LoopItems(line, t.Columns, t, showItem)
 667         if w.WriteByte('\n') != nil {
 668             return
 669         }
 670     }
 671 
 672     if t.Columns > 0 && t.ShowSums {
 673         realignSums(w, t, sums)
 674     }
 675 }
 676 
 677 func realignSums(w *bufio.Writer, t *table, sums []string) {
 678     due := 0
 679     if t.ShowTiles {
 680         due += t.Columns + columnGap
 681     }
 682 
 683     for i, s := range sums {
 684         if i > 0 {
 685             due += columnGap
 686         }
 687 
 688         if t.Numeric[i] == 0 {
 689             writeSpaces(w, due)
 690             w.WriteString(s)
 691             due = t.MaxWidth[i] - countWidth(s)
 692             continue
 693         }
 694 
 695         lpad := t.MaxWidth[i] - len(s) + due
 696         writeSpaces(w, lpad)
 697         writeNumericItem(w, s, numericStyle(t.Sums[i]))
 698         due = 0
 699     }
 700 
 701     w.WriteByte('\n')
 702 }
 703 
 704 // writeSpaces does what it says, minimizing calls to write-like funcs
 705 func writeSpaces(w *bufio.Writer, n int) {
 706     const spaces = `                                `
 707     if n < 1 {
 708         return
 709     }
 710 
 711     for n >= len(spaces) {
 712         w.WriteString(spaces)
 713         n -= len(spaces)
 714     }
 715     w.WriteString(spaces[:n])
 716 }
 717 
 718 func writeRowTiles(w *bufio.Writer, s string, t *table, writeTile itemFunc) {
 719     end := t.LoopItems(s, t.Columns, t, writeTile)
 720 
 721     if end < len(t.MaxWidth)-1 {
 722         w.WriteString("\x1b[0m")
 723     }
 724     for i := end + 1; i < len(t.MaxWidth); i++ {
 725         w.WriteString("×")
 726     }
 727     w.WriteString("\x1b[0m")
 728 }
 729 
 730 func numericStyle(f float64) string {
 731     if f > 0 {
 732         if float64(int64(f)) == f {
 733             return "\x1b[38;2;0;135;0m"
 734         }
 735         return "\x1b[38;2;0;155;95m"
 736     }
 737     if f < 0 {
 738         if float64(int64(f)) == f {
 739             return "\x1b[38;2;204;0;0m"
 740         }
 741         return "\x1b[38;2;215;95;95m"
 742     }
 743     if f == 0 {
 744         return "\x1b[38;2;0;95;215m"
 745     }
 746     return "\x1b[38;2;128;128;128m"
 747 }
 748 
 749 func writeNumericItem(w *bufio.Writer, s string, startStyle string) {
 750     w.WriteString(startStyle)
 751     if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
 752         w.WriteByte(s[0])
 753         s = s[1:]
 754     }
 755 
 756     dot := strings.IndexByte(s, '.')
 757     if dot < 0 {
 758         restyleDigits(w, s, altDigitStyle)
 759         w.WriteString("\x1b[0m")
 760         return
 761     }
 762 
 763     if len(s[:dot]) > 3 {
 764         restyleDigits(w, s[:dot], altDigitStyle)
 765         w.WriteString("\x1b[0m")
 766         w.WriteString(startStyle)
 767         w.WriteByte('.')
 768     } else {
 769         w.WriteString(s[:dot])
 770         w.WriteByte('.')
 771     }
 772 
 773     rest := s[dot+1:]
 774     restyleDigits(w, rest, altDigitStyle)
 775     if len(rest) < 4 {
 776         w.WriteString("\x1b[0m")
 777     }
 778 }
 779 
 780 // restyleDigits renders a run of digits as alternating styled/unstyled runs
 781 // of 3 digits, which greatly improves readability, and is the only purpose
 782 // of this app; string is assumed to be all decimal digits
 783 func restyleDigits(w *bufio.Writer, digits string, altStyle string) {
 784     if len(digits) < 4 {
 785         // digit sequence is short, so emit it as is
 786         w.WriteString(digits)
 787         return
 788     }
 789 
 790     // separate leading 0..2 digits which don't align with the 3-digit groups
 791     i := len(digits) % 3
 792     // emit leading digits unstyled, if there are any
 793     w.WriteString(digits[:i])
 794     // the rest is guaranteed to have a length which is a multiple of 3
 795     digits = digits[i:]
 796 
 797     // start by styling, unless there were no leading digits
 798     style := i != 0
 799 
 800     for len(digits) > 0 {
 801         if style {
 802             w.WriteString(altStyle)
 803             w.WriteString(digits[:3])
 804             w.WriteString("\x1b[0m")
 805         } else {
 806             w.WriteString(digits[:3])
 807         }
 808 
 809         // advance to the next triple: the start of this func is supposed
 810         // to guarantee this step always works
 811         digits = digits[3:]
 812 
 813         // alternate between styled and unstyled 3-digit groups
 814         style = !style
 815     }
 816 }
     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/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/URI...]
  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.
  42 
  43 This tool uses nicer ANSI styles than the original, hence its name, but
  44 can't convert its output back into JSON, unlike the latter.
  45 
  46 Unlike the original 'gron', there's no sort-mode. When not given a named
  47 source (filepath/URI) to read from, data are read from standard input.
  48 
  49 Options, where leading double-dashes are also allowed:
  50 
  51     -h         show this help message
  52     -help      show this help message
  53 
  54     -m         monochrome (default), enables unstyled output-mode
  55     -c         color, enables ANSI-styled output-mode
  56     -color     enables ANSI-styled output-mode
  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 
 100     for len(args) > 0 {
 101         switch args[0] {
 102         case `-c`, `--c`, `-color`, `--color`:
 103             config = styled
 104             args = args[1:]
 105             continue
 106 
 107         case `-h`, `--h`, `-help`, `--help`:
 108             os.Stdout.WriteString(info[1:])
 109             return
 110 
 111         case `-m`, `--m`:
 112             config = monochrome
 113             args = args[1:]
 114             continue
 115         }
 116 
 117         break
 118     }
 119 
 120     if len(args) > 0 && args[0] == `--` {
 121         args = args[1:]
 122     }
 123 
 124     if len(args) > 2 {
 125         os.Stderr.WriteString("multiple inputs not allowed\n")
 126         os.Exit(1)
 127         return
 128     }
 129 
 130     // figure out whether input should come from a named file or from stdin
 131     path := `-`
 132     if len(args) > 0 {
 133         path = args[0]
 134     }
 135 
 136     err := handleInput(os.Stdout, path)
 137     if err != nil && err != io.EOF {
 138         os.Stderr.WriteString(err.Error())
 139         os.Stderr.WriteString("\n")
 140         os.Exit(1)
 141         return
 142     }
 143 }
 144 
 145 type handlerFunc func(*bufio.Writer, *json.Decoder, json.Token, []any) error
 146 
 147 // handleInput simplifies control-flow for func main
 148 func handleInput(w io.Writer, path string) error {
 149     if path == `-` {
 150         bw := bufio.NewWriter(w)
 151         defer bw.Flush()
 152         return run(bw, os.Stdin)
 153     }
 154 
 155     f, err := os.Open(path)
 156     if err != nil {
 157         // on windows, file-not-found error messages may mention `CreateFile`,
 158         // even when trying to open files in read-only mode
 159         return errors.New(`can't open file named ` + path)
 160     }
 161     defer f.Close()
 162 
 163     bw := bufio.NewWriter(w)
 164     defer bw.Flush()
 165     return run(bw, f)
 166 }
 167 
 168 // escapedStringBytes helps func handleString treat all string bytes quickly
 169 // and correctly, using their officially-supported JSON escape sequences
 170 //
 171 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 172 var escapedStringBytes = [256][]byte{
 173     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 174     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 175     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 176     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 177     {'\\', 'b'}, {'\\', 't'},
 178     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 179     {'\\', 'f'}, {'\\', 'r'},
 180     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 181     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 182     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 183     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 184     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 185     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 186     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 187     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 188     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 189     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 190     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 191     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 192     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 193     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 194     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 195     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 196     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 197     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 198     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 199     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 200     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 201     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 202     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 203     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 204     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 205     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 206     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 207     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 208     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 209     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 210     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 211     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 212     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 213     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 214     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 215     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 216     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 217 }
 218 
 219 // run does it all, given a reader and a writer
 220 func run(w *bufio.Writer, r io.Reader) error {
 221     dec := json.NewDecoder(r)
 222     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 223     // even if JSON parsers aren't required to guarantee such input-fidelity
 224     // for numbers
 225     dec.UseNumber()
 226 
 227     t, err := dec.Token()
 228     if err == io.EOF {
 229         return errors.New(`input has no JSON values`)
 230     }
 231 
 232     if err = handleToken(w, dec, t, make([]any, 0, 50)); err != nil {
 233         return err
 234     }
 235 
 236     _, err = dec.Token()
 237     if err == io.EOF {
 238         // input is over, so it's a success
 239         return nil
 240     }
 241 
 242     if err == nil {
 243         // a successful `read` is a failure, as it means there are
 244         // trailing JSON tokens
 245         return errors.New(`unexpected trailing data`)
 246     }
 247 
 248     // any other error, perhaps some invalid-JSON-syntax-type error
 249     return err
 250 }
 251 
 252 // handleToken handles recursion for func run
 253 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, path []any) error {
 254     switch t := t.(type) {
 255     case json.Delim:
 256         switch t {
 257         case json.Delim('['):
 258             return handleArray(w, dec, path)
 259         case json.Delim('{'):
 260             return handleObject(w, dec, path)
 261         default:
 262             return errors.New(`unsupported JSON syntax ` + string(t))
 263         }
 264 
 265     case nil:
 266         config.path(w, path)
 267         config.null(w)
 268         return endLine(w)
 269 
 270     case bool:
 271         config.path(w, path)
 272         config.boolean(w, t)
 273         return endLine(w)
 274 
 275     case json.Number:
 276         config.path(w, path)
 277         config.number(w, t)
 278         return endLine(w)
 279 
 280     case string:
 281         config.path(w, path)
 282         config.text(w, t)
 283         return endLine(w)
 284 
 285     default:
 286         // return fmt.Errorf(`unsupported token type %T`, t)
 287         return errors.New(`invalid JSON token`)
 288     }
 289 }
 290 
 291 // handleArray handles arrays for func handleToken
 292 func handleArray(w *bufio.Writer, dec *json.Decoder, path []any) error {
 293     config.path(w, path)
 294     w.WriteString(config.arrayDecl)
 295     if err := endLine(w); err != nil {
 296         return err
 297     }
 298 
 299     path = append(path, 0)
 300     last := len(path) - 1
 301 
 302     for i := 0; true; i++ {
 303         path[last] = i
 304 
 305         t, err := dec.Token()
 306         if err == io.EOF {
 307             return errors.New(`end of JSON before array was closed`)
 308         }
 309         if err != nil {
 310             return err
 311         }
 312 
 313         if t == json.Delim(']') {
 314             return nil
 315         }
 316 
 317         err = handleToken(w, dec, t, path)
 318         if err != nil {
 319             return err
 320         }
 321     }
 322 
 323     // make the compiler happy
 324     return nil
 325 }
 326 
 327 // handleObject handles objects for func handleToken
 328 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
 329     config.path(w, path)
 330     w.WriteString(config.objectDecl)
 331     if err := endLine(w); err != nil {
 332         return err
 333     }
 334 
 335     path = append(path, ``)
 336     last := len(path) - 1
 337 
 338     for i := 0; true; i++ {
 339         t, err := dec.Token()
 340         if err == io.EOF {
 341             return errors.New(`end of JSON before object was closed`)
 342         }
 343         if err != nil {
 344             return err
 345         }
 346 
 347         if t == json.Delim('}') {
 348             return nil
 349         }
 350 
 351         k, ok := t.(string)
 352         if !ok {
 353             return errors.New(`expected a string for a key-value pair`)
 354         }
 355 
 356         path[last] = k
 357         if err != nil {
 358             return err
 359         }
 360 
 361         t, err = dec.Token()
 362         if err == io.EOF {
 363             return errors.New(`expected a value for a key-value pair`)
 364         }
 365 
 366         err = handleToken(w, dec, t, path)
 367         if err != nil {
 368             return err
 369         }
 370     }
 371 
 372     // make the compiler happy
 373     return nil
 374 }
 375 
 376 func monoPath(w *bufio.Writer, path []any) error {
 377     var buf [24]byte
 378 
 379     w.WriteString(`json`)
 380 
 381     for _, v := range path {
 382         switch v := v.(type) {
 383         case int:
 384             w.WriteByte('[')
 385             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 386             w.WriteByte(']')
 387 
 388         case string:
 389             if !needsEscaping(v) {
 390                 w.WriteByte('.')
 391                 w.WriteString(v)
 392                 continue
 393             }
 394             w.WriteByte('[')
 395             monoString(w, v)
 396             w.WriteByte(']')
 397         }
 398     }
 399 
 400     w.WriteString(` = `)
 401     return nil
 402 }
 403 
 404 func monoNull(w *bufio.Writer) error {
 405     w.WriteString(`null`)
 406     return nil
 407 }
 408 
 409 func monoBool(w *bufio.Writer, b bool) error {
 410     if b {
 411         w.WriteString(`true`)
 412     } else {
 413         w.WriteString(`false`)
 414     }
 415     return nil
 416 }
 417 
 418 func monoNumber(w *bufio.Writer, n json.Number) error {
 419     w.WriteString(n.String())
 420     return nil
 421 }
 422 
 423 func monoString(w *bufio.Writer, s string) error {
 424     w.WriteByte('"')
 425     for i := range s {
 426         w.Write(escapedStringBytes[s[i]])
 427     }
 428     w.WriteByte('"')
 429     return nil
 430 }
 431 
 432 func styledPath(w *bufio.Writer, path []any) error {
 433     var buf [24]byte
 434 
 435     w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
 436 
 437     for _, v := range path {
 438         switch v := v.(type) {
 439         case int:
 440             w.WriteString("\x1b[38;2;168;168;168m[")
 441             w.WriteString("\x1b[38;2;0;135;95m")
 442             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 443             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 444 
 445         case string:
 446             if !needsEscaping(v) {
 447                 w.WriteString("\x1b[38;2;168;168;168m.")
 448                 w.WriteString("\x1b[38;2;135;95;255m")
 449                 w.WriteString(v)
 450                 w.WriteString("\x1b[0m")
 451                 continue
 452             }
 453 
 454             w.WriteString("\x1b[38;2;168;168;168m[")
 455             styledString(w, v)
 456             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 457         }
 458     }
 459 
 460     w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
 461     return nil
 462 }
 463 
 464 func styledNull(w *bufio.Writer) error {
 465     w.WriteString("\x1b[38;2;168;168;168m")
 466     w.WriteString(`null`)
 467     w.WriteString("\x1b[0m")
 468     return nil
 469 }
 470 
 471 func styledBool(w *bufio.Writer, b bool) error {
 472     if b {
 473         w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
 474     } else {
 475         w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
 476     }
 477     return nil
 478 }
 479 
 480 func styledNumber(w *bufio.Writer, n json.Number) error {
 481     w.WriteString("\x1b[38;2;0;135;95m")
 482     w.WriteString(n.String())
 483     w.WriteString("\x1b[0m")
 484     return nil
 485 }
 486 
 487 func styledString(w *bufio.Writer, s string) error {
 488     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 489     for i := range s {
 490         w.Write(escapedStringBytes[s[i]])
 491     }
 492     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 493     return nil
 494 }
 495 
 496 func needsEscaping(s string) bool {
 497     for _, r := range s {
 498         if r < ' ' || r > '~' {
 499             return true
 500         }
 501 
 502         switch r {
 503         case '"', '\'', '\\':
 504             return true
 505         }
 506     }
 507 
 508     return false
 509 }
 510 
 511 func endLine(w *bufio.Writer) error {
 512     w.WriteByte(';')
 513     if err := w.WriteByte('\n'); err == nil {
 514         return nil
 515     }
 516     return io.EOF
 517 }
     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(err.Error())
  43         os.Stderr.WriteString("\n")
  44         os.Exit(1)
  45         return
  46     }
  47 }
  48 
  49 func run(cfg config) error {
  50     // f, _ := os.Create(`nh.prof`)
  51     // defer f.Close()
  52     // pprof.StartCPUProfile(f)
  53     // defer pprof.StopCPUProfile()
  54 
  55     w := bufio.NewWriterSize(os.Stdout, 16*1024)
  56     defer w.Flush()
  57 
  58     // with no filenames given, handle stdin and quit
  59     if len(cfg.Filenames) == 0 {
  60         return handle(w, os.Stdin, `<stdin>`, -1, cfg)
  61     }
  62 
  63     // show all files given
  64     for i, fname := range cfg.Filenames {
  65         if i > 0 {
  66             w.WriteString("\n")
  67             w.WriteString("\n")
  68         }
  69 
  70         err := handleFile(w, fname, cfg)
  71         if err != nil {
  72             return err
  73         }
  74     }
  75 
  76     return nil
  77 }
  78 
  79 // handleFile is like handleReader, except it also shows file-related info
  80 func handleFile(w *bufio.Writer, fname string, cfg config) error {
  81     f, err := os.Open(fname)
  82     if err != nil {
  83         return err
  84     }
  85     defer f.Close()
  86 
  87     stat, err := f.Stat()
  88     if err != nil {
  89         return handle(w, f, fname, -1, cfg)
  90     }
  91 
  92     fsize := int(stat.Size())
  93     return handle(w, f, fname, fsize, cfg)
  94 }
  95 
  96 // handle shows some messages related to the input and the cmd-line options
  97 // used, and then follows them by the hexadecimal byte-view
  98 func handle(w *bufio.Writer, r io.Reader, name string, size int, cfg config) error {
  99     skip(r, cfg.Skip)
 100     if cfg.MaxBytes > 0 {
 101         r = io.LimitReader(r, int64(cfg.MaxBytes))
 102     }
 103 
 104     // finish config setup based on the filesize, if a valid one was given
 105     if cfg.OffsetCounterWidth < 1 {
 106         if size < 1 {
 107             cfg.OffsetCounterWidth = defaultOffsetCounterWidth
 108         } else {
 109             w := math.Log10(float64(size))
 110             w = math.Max(math.Ceil(w), 1)
 111             cfg.OffsetCounterWidth = uint(w)
 112         }
 113     }
 114 
 115     switch cfg.To {
 116     case plainOutput:
 117         writeMetaPlain(w, name, size, cfg)
 118         // when done, emit a new line in case only part of the last line is
 119         // shown, which means no newline was emitted for it
 120         defer w.WriteString("\n")
 121         return render(w, r, cfg, writeBufferPlain)
 122 
 123     case ansiOutput:
 124         writeMetaANSI(w, name, size, cfg)
 125         // when done, emit a new line in case only part of the last line is
 126         // shown, which means no newline was emitted for it
 127         defer w.WriteString("\x1b[0m\n")
 128         return render(w, r, cfg, writeBufferANSI)
 129 
 130     default:
 131         const fs = `unsupported output format %q`
 132         return fmt.Errorf(fs, cfg.To)
 133     }
 134 }
 135 
 136 // skip ignores n bytes from the reader given
 137 func skip(r io.Reader, n int) {
 138     if n < 1 {
 139         return
 140     }
 141 
 142     // use func Seek for input files, except for stdin, which you can't seek
 143     if f, ok := r.(*os.File); ok && r != os.Stdin {
 144         f.Seek(int64(n), io.SeekCurrent)
 145         return
 146     }
 147     io.CopyN(io.Discard, r, int64(n))
 148 }
 149 
 150 // renderer is the type for the hex-view render funcs
 151 type renderer func(rc rendererConfig, first, second []byte) error
 152 
 153 // render reads all input and shows the hexadecimal byte-view for the input
 154 // data via the rendering callback given
 155 func render(w *bufio.Writer, r io.Reader, cfg config, fn renderer) error {
 156     if cfg.PerLine < 1 {
 157         cfg.PerLine = 16
 158     }
 159 
 160     rc := rendererConfig{
 161         out:     w,
 162         offset:  uint(cfg.Skip),
 163         chunks:  0,
 164         perLine: uint(cfg.PerLine),
 165         ruler:   cfg.Ruler,
 166 
 167         offsetWidth: cfg.OffsetCounterWidth,
 168         showOffsets: cfg.ShowOffsets,
 169         showASCII:   cfg.ShowASCII,
 170     }
 171 
 172     // calling func Read directly can sometimes result in chunks shorter
 173     // than the max chunk-size, even when there are plenty of bytes yet
 174     // to read; to avoid that, use a buffered-reader to explicitly fill
 175     // a slice instead
 176     br := bufio.NewReader(r)
 177 
 178     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 179     cur := make([]byte, 0, cfg.PerLine)
 180     ahead := make([]byte, 0, cfg.PerLine)
 181 
 182     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 183     // so to speak
 184     cur, err := fillChunk(cur[:0], cfg.PerLine, br)
 185     if len(cur) == 0 {
 186         if err == io.EOF {
 187             err = nil
 188         }
 189         return err
 190     }
 191 
 192     for {
 193         ahead, err := fillChunk(ahead[:0], cfg.PerLine, br)
 194         if err != nil && err != io.EOF {
 195             return err
 196         }
 197 
 198         if len(ahead) == 0 {
 199             // done, maybe except for an extra line of output
 200             break
 201         }
 202 
 203         // show the byte-chunk on its own output line
 204         err = fn(rc, cur, ahead)
 205         if err != nil {
 206             // probably a pipe was closed
 207             return nil
 208         }
 209 
 210         rc.chunks++
 211         rc.offset += uint(len(cur))
 212         cur = cur[:copy(cur, ahead)]
 213     }
 214 
 215     // don't forget the last output line
 216     if len(cur) > 0 {
 217         return fn(rc, cur, nil)
 218     }
 219     return nil
 220 }
 221 
 222 // fillChunk tries to read the number of bytes given, appending them to the
 223 // byte-slice given; this func returns an EOF error only when no bytes are
 224 // read, which somewhat simplifies error-handling for the func caller
 225 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 226     // read buffered-bytes up to the max chunk-size
 227     for i := 0; i < n; i++ {
 228         b, err := br.ReadByte()
 229         if err == nil {
 230             chunk = append(chunk, b)
 231             continue
 232         }
 233 
 234         if err == io.EOF && i > 0 {
 235             return chunk, nil
 236         }
 237         return chunk, err
 238     }
 239 
 240     // got the full byte-count asked for
 241     return chunk, nil
 242 }
     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(err.Error())
 119     os.Stderr.WriteString("\n")
 120 }
 121 
 122 // writeSpaces does what it says, minimizing calls to write-like funcs
 123 func writeSpaces(w *bufio.Writer, n int) {
 124     const spaces = `                                `
 125     for n >= len(spaces) {
 126         w.WriteString(spaces)
 127         n -= len(spaces)
 128     }
 129     if n > 0 {
 130         w.WriteString(spaces[:n])
 131     }
 132 }
 133 
 134 func handleFile(w io.Writer, path string) error {
 135     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
 136     //  resp, err := http.Get(path)
 137     //  if err != nil {
 138     //      return err
 139     //  }
 140     //  defer resp.Body.Close()
 141     //  return niceJSON(w, resp.Body)
 142     // }
 143 
 144     f, err := os.Open(path)
 145     if err != nil {
 146         // on windows, file-not-found error messages may mention `CreateFile`,
 147         // even when trying to open files in read-only mode
 148         return errors.New(`can't open file named ` + path)
 149     }
 150     defer f.Close()
 151 
 152     return niceJSON(w, f)
 153 }
 154 
 155 func niceJSON(w io.Writer, r io.Reader) error {
 156     bw := bufio.NewWriter(w)
 157     defer bw.Flush()
 158 
 159     dec := json.NewDecoder(r)
 160     // using string-like json.Number values instead of float64 ones avoids
 161     // unneeded reformatting of numbers; reformatting parsed float64 values
 162     // can potentially even drop/change decimals, causing the output not to
 163     // match the input digits exactly, which is best to avoid
 164     dec.UseNumber()
 165 
 166     t, err := dec.Token()
 167     if err == io.EOF {
 168         return errors.New(`empty input isn't valid JSON`)
 169     }
 170     if err != nil {
 171         return err
 172     }
 173 
 174     if err := handleToken(bw, dec, t, 0, 0); err != nil {
 175         return err
 176     }
 177     // don't forget to end the last output line
 178     bw.WriteByte('\n')
 179 
 180     if _, err := dec.Token(); err != io.EOF {
 181         return errors.New(`unexpected trailing JSON data`)
 182     }
 183     return nil
 184 }
 185 
 186 func handleToken(w *bufio.Writer, d *json.Decoder, t json.Token, pre, level int) error {
 187     switch t := t.(type) {
 188     case json.Delim:
 189         switch t {
 190         case json.Delim('['):
 191             return handleArray(w, d, pre, level)
 192 
 193         case json.Delim('{'):
 194             return handleObject(w, d, pre, level)
 195 
 196         default:
 197             // return fmt.Errorf(`unsupported JSON delimiter %v`, t)
 198             return errors.New(`unsupported JSON delimiter`)
 199         }
 200 
 201     case nil:
 202         return handleNull(w, pre)
 203 
 204     case bool:
 205         return handleBoolean(w, t, pre)
 206 
 207     case string:
 208         return handleString(w, t, pre)
 209 
 210     case json.Number:
 211         return handleNumber(w, t, pre)
 212 
 213     default:
 214         // return fmt.Errorf(`unsupported token type %T`, t)
 215         return errors.New(`unsupported token type`)
 216     }
 217 }
 218 
 219 func handleArray(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 220     for i := 0; true; i++ {
 221         t, err := d.Token()
 222         if err == io.EOF {
 223             return errors.New(`end of JSON before array was closed`)
 224         }
 225         if err != nil {
 226             return err
 227         }
 228 
 229         if t == json.Delim(']') {
 230             if i == 0 {
 231                 writeSpaces(w, indent*pre)
 232                 w.WriteString(syntaxStyle + "[]\x1b[0m")
 233             } else {
 234                 w.WriteString("\n")
 235                 writeSpaces(w, indent*level)
 236                 w.WriteString(syntaxStyle + "]\x1b[0m")
 237             }
 238             return nil
 239         }
 240 
 241         if i == 0 {
 242             writeSpaces(w, indent*pre)
 243             w.WriteString(syntaxStyle + "[\x1b[0m\n")
 244         } else {
 245             // this is a good spot to check for early-quit opportunities
 246             w.WriteString(syntaxStyle + ",\x1b[0m\n")
 247             if err := w.Flush(); err != nil {
 248                 // a write error may be the consequence of stdout being closed,
 249                 // perhaps by another app along a pipe
 250                 return io.EOF
 251             }
 252         }
 253 
 254         if err := handleToken(w, d, t, level+1, level+1); err != nil {
 255             return err
 256         }
 257     }
 258 
 259     // make the compiler happy
 260     return nil
 261 }
 262 
 263 func handleBoolean(w *bufio.Writer, b bool, pre int) error {
 264     writeSpaces(w, indent*pre)
 265     if b {
 266         w.WriteString(boolStyle + "true\x1b[0m")
 267     } else {
 268         w.WriteString(boolStyle + "false\x1b[0m")
 269     }
 270     return nil
 271 }
 272 
 273 func handleKey(w *bufio.Writer, s string, pre int) error {
 274     writeSpaces(w, indent*pre)
 275     w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle)
 276     w.WriteString(s)
 277     w.WriteString(syntaxStyle + "\":\x1b[0m ")
 278     return nil
 279 }
 280 
 281 func handleNull(w *bufio.Writer, pre int) error {
 282     writeSpaces(w, indent*pre)
 283     w.WriteString(nullStyle + "null\x1b[0m")
 284     return nil
 285 }
 286 
 287 // func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 288 //  writeSpaces(w, indent*pre)
 289 //  w.WriteString(numberStyle)
 290 //  w.WriteString(n.String())
 291 //  w.WriteString("\x1b[0m")
 292 //  return nil
 293 // }
 294 
 295 func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 296     writeSpaces(w, indent*pre)
 297     f, _ := n.Float64()
 298     if f > 0 {
 299         w.WriteString(positiveNumberStyle)
 300     } else if f < 0 {
 301         w.WriteString(negativeNumberStyle)
 302     } else {
 303         w.WriteString(zeroNumberStyle)
 304     }
 305     w.WriteString(n.String())
 306     w.WriteString("\x1b[0m")
 307     return nil
 308 }
 309 
 310 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 311     for i := 0; true; i++ {
 312         t, err := d.Token()
 313         if err == io.EOF {
 314             return errors.New(`end of JSON before object was closed`)
 315         }
 316         if err != nil {
 317             return err
 318         }
 319 
 320         if t == json.Delim('}') {
 321             if i == 0 {
 322                 writeSpaces(w, indent*pre)
 323                 w.WriteString(syntaxStyle + "{}\x1b[0m")
 324             } else {
 325                 w.WriteString("\n")
 326                 writeSpaces(w, indent*level)
 327                 w.WriteString(syntaxStyle + "}\x1b[0m")
 328             }
 329             return nil
 330         }
 331 
 332         if i == 0 {
 333             writeSpaces(w, indent*pre)
 334             w.WriteString(syntaxStyle + "{\x1b[0m\n")
 335         } else {
 336             // this is a good spot to check for early-quit opportunities
 337             w.WriteString(syntaxStyle + ",\x1b[0m\n")
 338             if err := w.Flush(); err != nil {
 339                 // a write error may be the consequence of stdout being closed,
 340                 // perhaps by another app along a pipe
 341                 return io.EOF
 342             }
 343         }
 344 
 345         // the stdlib's JSON parser is supposed to complain about non-string
 346         // keys anyway, but make sure just in case
 347         k, ok := t.(string)
 348         if !ok {
 349             return errors.New(`expected key to be a string`)
 350         }
 351         if err := handleKey(w, k, level+1); err != nil {
 352             return err
 353         }
 354 
 355         // handle value
 356         t, err = d.Token()
 357         if err != nil {
 358             return err
 359         }
 360         if err := handleToken(w, d, t, 0, level+1); err != nil {
 361             return err
 362         }
 363     }
 364 
 365     // make the compiler happy
 366     return nil
 367 }
 368 
 369 func needsEscaping(s string) bool {
 370     for _, r := range s {
 371         switch r {
 372         case '"', '\\', '\t', '\r', '\n':
 373             return true
 374         }
 375     }
 376     return false
 377 }
 378 
 379 func handleString(w *bufio.Writer, s string, pre int) error {
 380     writeSpaces(w, indent*pre)
 381     w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle)
 382     if !needsEscaping(s) {
 383         w.WriteString(s)
 384     } else {
 385         escapeString(w, s)
 386     }
 387     w.WriteString(syntaxStyle + "\"\x1b[0m")
 388     return nil
 389 }
 390 
 391 func escapeString(w *bufio.Writer, s string) {
 392     for _, r := range s {
 393         switch r {
 394         case '"', '\\':
 395             w.WriteByte('\\')
 396             w.WriteRune(r)
 397         case '\t':
 398             w.WriteByte('\\')
 399             w.WriteByte('t')
 400         case '\r':
 401             w.WriteByte('\\')
 402             w.WriteByte('r')
 403         case '\n':
 404             w.WriteByte('\\')
 405             w.WriteByte('n')
 406         default:
 407             w.WriteRune(r)
 408         }
 409     }
 410 }
     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("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(err.Error())
  90         os.Stderr.WriteString("\n")
  91         os.Exit(1)
  92         return
  93     }
  94 }
  95 
  96 func run(paths []string, count int) error {
  97     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  98     defer w.Flush()
  99 
 100     for _, path := range paths {
 101         if err := handleFile(w, path, &count); err != nil {
 102             return err
 103         }
 104     }
 105 
 106     if len(paths) == 0 {
 107         if err := nl(w, os.Stdin, &count); err != nil {
 108             return err
 109         }
 110     }
 111     return nil
 112 }
 113 
 114 func handleFile(w *bufio.Writer, path string, count *int) error {
 115     f, err := os.Open(path)
 116     if err != nil {
 117         return err
 118     }
 119     defer f.Close()
 120     return nl(w, f, count)
 121 }
 122 
 123 func nl(w *bufio.Writer, r io.Reader, count *int) error {
 124     const gb = 1024 * 1024 * 1024
 125     sc := bufio.NewScanner(r)
 126     sc.Buffer(nil, 8*gb)
 127 
 128     const spaces = `      `
 129     var buf [24]byte
 130 
 131     for sc.Scan() {
 132         s := sc.Bytes()
 133 
 134         // only number non-empty lines, just like the standard `nl` command
 135         if len(s) > 0 {
 136             n := strconv.AppendInt(buf[:0], int64(*count), 10)
 137 
 138             // right-align the line-count
 139             if len(n) < len(spaces) {
 140                 w.WriteString(spaces[:len(spaces)-len(n)])
 141             }
 142             w.Write(n)
 143 
 144             // separate the line count and the line with a few spaces
 145             w.WriteString(spaces[:2])
 146         }
 147 
 148         w.Write(s)
 149         if err := w.WriteByte('\n'); err != nil {
 150             return io.EOF
 151         }
 152 
 153         if len(s) > 0 {
 154             *count++
 155         }
 156     }
 157 
 158     return sc.Err()
 159 }
     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(`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(err.Error())
 138         os.Stderr.WriteString("\n")
 139         os.Exit(1)
 140         return
 141     }
 142 }
 143 
 144 func run(w io.Writer, args []string, cfg config) error {
 145     bw := bufio.NewWriter(w)
 146     defer bw.Flush()
 147 
 148     if len(args) == 0 {
 149         return restyle(bw, os.Stdin, cfg)
 150     }
 151 
 152     for _, name := range args {
 153         if err := handleFile(bw, name, cfg); err != nil {
 154             return err
 155         }
 156     }
 157     return nil
 158 }
 159 
 160 func handleFile(w *bufio.Writer, name string, cfg config) error {
 161     if name == `` || name == `-` {
 162         return restyle(w, os.Stdin, cfg)
 163     }
 164 
 165     f, err := os.Open(name)
 166     if err != nil {
 167         return errors.New(`can't read from file named "` + name + `"`)
 168     }
 169     defer f.Close()
 170 
 171     return restyle(w, f, cfg)
 172 }
 173 
 174 func restyle(w *bufio.Writer, r io.Reader, cfg config) error {
 175     const gb = 1024 * 1024 * 1024
 176     sc := bufio.NewScanner(r)
 177     sc.Buffer(nil, 8*gb)
 178 
 179     for i := 0; sc.Scan(); i++ {
 180         s := sc.Bytes()
 181         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 182             s = s[3:]
 183         }
 184 
 185         restyleLine(w, s, cfg.style)
 186         if w.WriteByte('\n') != nil {
 187             return io.EOF
 188         }
 189 
 190         if !cfg.live {
 191             continue
 192         }
 193 
 194         if err := w.Flush(); err != nil {
 195             // a write error may be the consequence of stdout being closed,
 196             // perhaps by another app along a pipe
 197             return io.EOF
 198         }
 199     }
 200     return sc.Err()
 201 }
 202 
 203 func lookupStyle(name string) (style string, ok bool) {
 204     if alias, ok := styleAliases[name]; ok {
 205         name = alias
 206     }
 207 
 208     style, ok = styles[name]
 209     return style, ok
 210 }
 211 
 212 var styleAliases = map[string]string{
 213     `b`: `blue`,
 214     `g`: `green`,
 215     `m`: `magenta`,
 216     `o`: `orange`,
 217     `p`: `purple`,
 218     `r`: `red`,
 219     `u`: `underline`,
 220 
 221     `bolded`:      `bold`,
 222     `h`:           `inverse`,
 223     `hi`:          `inverse`,
 224     `highlight`:   `inverse`,
 225     `highlighted`: `inverse`,
 226     `hilite`:      `inverse`,
 227     `hilited`:     `inverse`,
 228     `inv`:         `inverse`,
 229     `invert`:      `inverse`,
 230     `inverted`:    `inverse`,
 231     `underlined`:  `underline`,
 232 
 233     `bb`: `blueback`,
 234     `bg`: `greenback`,
 235     `bm`: `magentaback`,
 236     `bo`: `orangeback`,
 237     `bp`: `purpleback`,
 238     `br`: `redback`,
 239 
 240     `gb`: `greenback`,
 241     `mb`: `magentaback`,
 242     `ob`: `orangeback`,
 243     `pb`: `purpleback`,
 244     `rb`: `redback`,
 245 
 246     `bblue`:    `blueback`,
 247     `bgray`:    `grayback`,
 248     `bgreen`:   `greenback`,
 249     `bmagenta`: `magentaback`,
 250     `borange`:  `orangeback`,
 251     `bpurple`:  `purpleback`,
 252     `bred`:     `redback`,
 253 
 254     `backblue`:    `blueback`,
 255     `backgray`:    `grayback`,
 256     `backgreen`:   `greenback`,
 257     `backmagenta`: `magentaback`,
 258     `backorange`:  `orangeback`,
 259     `backpurple`:  `purpleback`,
 260     `backred`:     `redback`,
 261 }
 262 
 263 // styles turns style-names into the ANSI-code sequences used for the
 264 // alternate groups of digits
 265 var styles = map[string]string{
 266     `blue`:      "\x1b[38;2;0;95;215m",
 267     `bold`:      "\x1b[1m",
 268     `gray`:      "\x1b[38;2;168;168;168m",
 269     `green`:     "\x1b[38;2;0;135;95m",
 270     `inverse`:   "\x1b[7m",
 271     `magenta`:   "\x1b[38;2;215;0;255m",
 272     `orange`:    "\x1b[38;2;215;95;0m",
 273     `plain`:     "\x1b[0m",
 274     `red`:       "\x1b[38;2;204;0;0m",
 275     `underline`: "\x1b[4m",
 276 
 277     // `blue`:      "\x1b[38;5;26m",
 278     // `bold`:      "\x1b[1m",
 279     // `gray`:      "\x1b[38;5;248m",
 280     // `green`:     "\x1b[38;5;29m",
 281     // `inverse`:   "\x1b[7m",
 282     // `magenta`:   "\x1b[38;5;99m",
 283     // `orange`:    "\x1b[38;5;166m",
 284     // `plain`:     "\x1b[0m",
 285     // `red`:       "\x1b[31m",
 286     // `underline`: "\x1b[4m",
 287 
 288     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 289     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 290     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 291     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 292     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 293     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 294     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 295 }
 296 
 297 // restyleLine renders the line given, using ANSI-styles to make any long
 298 // numbers in it more legible; this func doesn't emit a line-feed, which
 299 // is up to its caller
 300 func restyleLine(w *bufio.Writer, line []byte, style string) {
 301     for len(line) > 0 {
 302         i := indexDigit(line)
 303         if i < 0 {
 304             // no (more) digits to style for sure
 305             w.Write(line)
 306             return
 307         }
 308 
 309         // emit line before current digit-run
 310         w.Write(line[:i])
 311         // advance to the start of the current digit-run
 312         line = line[i:]
 313 
 314         // see where the digit-run ends
 315         j := indexNonDigit(line)
 316         if j < 0 {
 317             // the digit-run goes until the end
 318             restyleDigits(w, line, style)
 319             return
 320         }
 321 
 322         // emit styled digit-run
 323         restyleDigits(w, line[:j], style)
 324         // skip right past the end of the digit-run
 325         line = line[j:]
 326     }
 327 }
 328 
 329 // indexDigit finds the index of the first digit in a string, or -1 when the
 330 // string has no decimal digits
 331 func indexDigit(s []byte) int {
 332     for i := 0; i < len(s); i++ {
 333         switch s[i] {
 334         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 335             return i
 336         }
 337     }
 338 
 339     // empty slice, or a slice without any digits
 340     return -1
 341 }
 342 
 343 // indexNonDigit finds the index of the first non-digit in a string, or -1
 344 // when the string is all decimal digits
 345 func indexNonDigit(s []byte) int {
 346     for i := 0; i < len(s); i++ {
 347         switch s[i] {
 348         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 349             continue
 350         default:
 351             return i
 352         }
 353     }
 354 
 355     // empty slice, or a slice which only has digits
 356     return -1
 357 }
 358 
 359 // restyleDigits renders a run of digits as alternating styled/unstyled runs
 360 // of 3 digits, which greatly improves readability, and is the only purpose
 361 // of this app; string is assumed to be all decimal digits
 362 func restyleDigits(w *bufio.Writer, digits []byte, altStyle string) {
 363     if len(digits) < 4 {
 364         // digit sequence is short, so emit it as is
 365         w.Write(digits)
 366         return
 367     }
 368 
 369     // separate leading 0..2 digits which don't align with the 3-digit groups
 370     i := len(digits) % 3
 371     // emit leading digits unstyled, if there are any
 372     w.Write(digits[:i])
 373     // the rest is guaranteed to have a length which is a multiple of 3
 374     digits = digits[i:]
 375 
 376     // start by styling, unless there were no leading digits
 377     style := i != 0
 378 
 379     for len(digits) > 0 {
 380         if style {
 381             w.WriteString(altStyle)
 382             w.Write(digits[:3])
 383             w.Write([]byte{'\x1b', '[', '0', 'm'})
 384         } else {
 385             w.Write(digits[:3])
 386         }
 387 
 388         // advance to the next triple: the start of this func is supposed
 389         // to guarantee this step always works
 390         digits = digits[3:]
 391 
 392         // alternate between styled and unstyled 3-digit groups
 393         style = !style
 394     }
 395 }
     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     "errors"
  31     "io"
  32     "os"
  33     "strings"
  34     "time"
  35 )
  36 
  37 const info = `
  38 nts [options...] [file...]
  39 
  40 Nice TimeStamp emits each input line, starting it with an ANSI-style date/time
  41 string and a tab.
  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     options := true
  60     if len(args) > 0 && args[0] == `--` {
  61         options = false
  62         args = args[1:]
  63     }
  64 
  65     style := "\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m"
  66 
  67     // if the first argument is 1 or 2 dashes followed by a supported
  68     // style-name, change the style used
  69     if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
  70         name := args[0]
  71         name = strings.TrimPrefix(name, `-`)
  72         name = strings.TrimPrefix(name, `-`)
  73         args = args[1:]
  74 
  75         // check if the `dedashed` argument is a supported style-name
  76         if s, ok := lookupStyle(name); ok {
  77             style = s
  78         } else {
  79             os.Stderr.WriteString(`invalid style name `)
  80             os.Stderr.WriteString(name)
  81             os.Stderr.WriteString("\n")
  82             os.Exit(1)
  83             return
  84         }
  85     }
  86 
  87     if err := run(os.Stdout, args, style); err != nil && err != io.EOF {
  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, style string) error {
  96     bw := bufio.NewWriter(w)
  97     defer bw.Flush()
  98 
  99     if len(args) == 0 {
 100         return timestamp(bw, os.Stdin, style)
 101     }
 102 
 103     for _, name := range args {
 104         if err := handleFile(bw, name, style); err != nil {
 105             return err
 106         }
 107     }
 108     return nil
 109 }
 110 
 111 func handleFile(w *bufio.Writer, name string, style string) error {
 112     if name == `` || name == `-` {
 113         return timestamp(w, os.Stdin, style)
 114     }
 115 
 116     f, err := os.Open(name)
 117     if err != nil {
 118         return errors.New(`can't read from file named "` + name + `"`)
 119     }
 120     defer f.Close()
 121 
 122     return timestamp(w, f, style)
 123 }
 124 
 125 func timestamp(w *bufio.Writer, r io.Reader, style string) error {
 126     const gb = 1024 * 1024 * 1024
 127     sc := bufio.NewScanner(r)
 128     sc.Buffer(nil, 8*gb)
 129 
 130     var buf [64]byte
 131 
 132     for i := 0; sc.Scan(); i++ {
 133         s := sc.Bytes()
 134         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 135             s = s[3:]
 136         }
 137 
 138         if style == `` {
 139             w.Write(time.Now().AppendFormat(buf[:0], `2006-01-02 15:04:05`))
 140             w.WriteByte('\t')
 141         } else {
 142             w.WriteString(style)
 143             w.Write(time.Now().AppendFormat(buf[:0], `2006-01-02 15:04:05`))
 144             w.WriteString("\x1b[0m\t")
 145         }
 146         w.Write(s)
 147         w.WriteByte('\n')
 148 
 149         if err := w.Flush(); err != nil {
 150             // a write error may be the consequence of stdout being closed,
 151             // perhaps by another app along a pipe
 152             return io.EOF
 153         }
 154     }
 155     return sc.Err()
 156 }
 157 
 158 func lookupStyle(name string) (style string, ok bool) {
 159     if alias, ok := styleAliases[name]; ok {
 160         name = alias
 161     }
 162 
 163     style, ok = styles[name]
 164     return style, ok
 165 }
 166 
 167 var styleAliases = map[string]string{
 168     `b`: `blue`,
 169     `g`: `green`,
 170     `m`: `magenta`,
 171     `o`: `orange`,
 172     `p`: `purple`,
 173     `r`: `red`,
 174     `u`: `underline`,
 175 
 176     `bolded`:      `bold`,
 177     `h`:           `inverse`,
 178     `hi`:          `inverse`,
 179     `highlight`:   `inverse`,
 180     `highlighted`: `inverse`,
 181     `hilite`:      `inverse`,
 182     `hilited`:     `inverse`,
 183     `inv`:         `inverse`,
 184     `invert`:      `inverse`,
 185     `inverted`:    `inverse`,
 186     `underlined`:  `underline`,
 187 
 188     `bb`: `blueback`,
 189     `bg`: `greenback`,
 190     `bm`: `magentaback`,
 191     `bo`: `orangeback`,
 192     `bp`: `purpleback`,
 193     `br`: `redback`,
 194 
 195     `gb`: `greenback`,
 196     `mb`: `magentaback`,
 197     `ob`: `orangeback`,
 198     `pb`: `purpleback`,
 199     `rb`: `redback`,
 200 
 201     `bblue`:    `blueback`,
 202     `bgray`:    `grayback`,
 203     `bgreen`:   `greenback`,
 204     `bmagenta`: `magentaback`,
 205     `borange`:  `orangeback`,
 206     `bpurple`:  `purpleback`,
 207     `bred`:     `redback`,
 208 
 209     `backblue`:    `blueback`,
 210     `backgray`:    `grayback`,
 211     `backgreen`:   `greenback`,
 212     `backmagenta`: `magentaback`,
 213     `backorange`:  `orangeback`,
 214     `backpurple`:  `purpleback`,
 215     `backred`:     `redback`,
 216 }
 217 
 218 // styles turns style-names into the ANSI-code sequences used for the
 219 // alternate groups of digits
 220 var styles = map[string]string{
 221     `blue`:      "\x1b[38;2;0;95;215m",
 222     `bold`:      "\x1b[1m",
 223     `gray`:      "\x1b[38;2;168;168;168m",
 224     `green`:     "\x1b[38;2;0;135;95m",
 225     `inverse`:   "\x1b[7m",
 226     `magenta`:   "\x1b[38;2;215;0;255m",
 227     `none`:      ``,
 228     `orange`:    "\x1b[38;2;215;95;0m",
 229     `plain`:     ``,
 230     `red`:       "\x1b[38;2;204;0;0m",
 231     `underline`: "\x1b[4m",
 232     `unstyled`:  ``,
 233 
 234     // `blue`:      "\x1b[38;5;26m",
 235     // `bold`:      "\x1b[1m",
 236     // `gray`:      "\x1b[38;5;248m",
 237     // `green`:     "\x1b[38;5;29m",
 238     // `inverse`:   "\x1b[7m",
 239     // `magenta`:   "\x1b[38;5;99m",
 240     // `orange`:    "\x1b[38;5;166m",
 241     // `plain`:     "\x1b[0m",
 242     // `red`:       "\x1b[31m",
 243     // `underline`: "\x1b[4m",
 244 
 245     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 246     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 247     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 248     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 249     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 250     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 251     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 252 }
     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     h, help               show this help message
  69     i, iota               push numbers 1..n
  70     l, len, length        the item-count in the top/latest array/value
  71     ln, log               natural logarithm
  72     log10                 base-10 logarithm
  73     log2                  base-2 logarithm
  74     neg, negate           flip the sign
  75     p, print              output the top/latest array/value on a single line
  76     pi                    the number known as 'PI'
  77     pop                   forget the top/latest value
  78     pow10                 10^x
  79     pow2                  2^x
  80     rand, random, rnd     emit n random numbers in range [0, 1)
  81     round                 round numbers
  82     s, swap, swap2        swap the 2 top/latest values
  83     sgn, sign             the sign function
  84     shift                 forget the oldest value still in the stack
  85     sin                   the sine
  86     sinh                  the hyperbolic sine
  87     tan                   the tangent
  88     tanh                  the hyperbolic tangent
  89     tau                   twice the number known as 'PI'
  90     top                   forget all values but the top/latest
  91     u, unit               push the number 1 onto the stack n times
  92 
  93 Examples
  94 
  95 # push numbers on the stack; only the last one will show as the result
  96 pac 4.5 -6.22 905_900
  97 
  98 # push numbers on the stack, then flatten everything, so all values show
  99 pac 4.5 -6.22 905_900 flat
 100 `
 101 
 102 var (
 103     needAtLeast1 = errors.New(`need at least 1 number or array on the stack`)
 104     needAtLeast2 = errors.New(`need at least 2 numbers or arrays on the stack`)
 105 )
 106 
 107 func Main() {
 108     args := os.Args[1:]
 109 
 110     if len(args) > 0 {
 111         switch args[0] {
 112         case `-h`, `--h`, `-help`, `--help`:
 113             os.Stdout.WriteString(info[1:])
 114             return
 115         }
 116     }
 117 
 118     if len(args) > 0 && args[0] == `--` {
 119         args = args[1:]
 120     }
 121 
 122     if len(args) == 0 {
 123         os.Stderr.WriteString(info[1:])
 124         os.Exit(1)
 125         return
 126     }
 127 
 128     if err := pac(os.Stdout, args); err != nil && err != io.EOF {
 129         os.Stderr.WriteString(err.Error())
 130         os.Stderr.WriteString("\n")
 131         os.Exit(1)
 132         return
 133     }
 134 }
 135 
 136 type context struct {
 137     stack  [][]float64
 138     random *rand.Rand
 139 }
 140 
 141 func pac(w io.Writer, args []string) error {
 142     var ctx context
 143     ctx.stack = make([][]float64, 0, 64)
 144     reset(&ctx)
 145 
 146     if err := run(args, &ctx); err != nil {
 147         return err
 148     }
 149 
 150     // if len(ctx.stack) > 1 {
 151     //  os.Stderr.WriteString("multiple results left on the stack\n")
 152     // }
 153     if len(ctx.stack) == 0 {
 154         return errors.New(`no results left on the stack`)
 155     }
 156 
 157     show(w, &ctx)
 158     return nil
 159 }
 160 
 161 func show(w io.Writer, ctx *context) {
 162     if len(ctx.stack) == 0 {
 163         return
 164     }
 165 
 166     bw := bufio.NewWriter(w)
 167     defer bw.Flush()
 168 
 169     var buf [24]byte
 170     for i, f := range ctx.stack[len(ctx.stack)-1] {
 171         if i > 0 {
 172             if err := bw.WriteByte(' '); err != nil {
 173                 break
 174             }
 175         }
 176         bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
 177     }
 178     bw.WriteByte('\n')
 179 }
 180 
 181 func debug(w io.StringWriter, ctx *context) {
 182     for i, v := range ctx.stack {
 183         w.WriteString("\t")
 184         w.WriteString(strconv.Itoa(i))
 185         w.WriteString(":")
 186         for _, f := range v {
 187             w.WriteString(" ")
 188             w.WriteString(strconv.FormatFloat(f, 'f', -1, 64))
 189         }
 190 
 191         w.WriteString("\n")
 192     }
 193 
 194     w.WriteString("\n")
 195 }
 196 
 197 func run(args []string, ctx *context) error {
 198     for len(args) > 0 {
 199         s := args[0]
 200         args = args[1:]
 201 
 202         for len(s) > 0 {
 203             for len(s) > 0 && s[0] == ' ' {
 204                 s = s[1:]
 205             }
 206             if len(s) == 0 {
 207                 break
 208             }
 209 
 210             var cmd string
 211             if i := strings.IndexByte(s, ' '); i >= 0 {
 212                 cmd = s[:i]
 213             } else {
 214                 cmd = s
 215             }
 216             s = s[len(cmd):]
 217 
 218             if f, err := strconv.ParseFloat(cmd, 64); err == nil {
 219                 ctx.stack = append(ctx.stack, []float64{f})
 220                 continue
 221             }
 222 
 223             if cmd == `[` {
 224                 rest, err := appendArray(ctx, args)
 225                 if err != nil {
 226                     return err
 227                 }
 228                 args = rest
 229                 continue
 230             }
 231 
 232             if cmd == `.` {
 233                 debug(os.Stderr, ctx)
 234                 continue
 235             }
 236 
 237             if f, ok := funcs[cmd]; ok {
 238                 if err := call(ctx, f, cmd); err != nil {
 239                     return err
 240                 }
 241                 continue
 242             }
 243 
 244             return errors.New(`unknown command ` + cmd)
 245         }
 246     }
 247 
 248     return nil
 249 }
 250 
 251 func appendArray(ctx *context, args []string) ([]string, error) {
 252     end := -1
 253     for i, s := range args {
 254         if s == `]` {
 255             end = i
 256             break
 257         }
 258     }
 259 
 260     if end < 0 {
 261         return args, errors.New(`missing the ']' to close array`)
 262     }
 263 
 264     items := args[:end]
 265     rest := args[end+1:]
 266 
 267     var sub context
 268     err := run(items, &sub)
 269     flatten(&sub)
 270     if len(sub.stack) > 0 {
 271         ctx.stack = append(ctx.stack, sub.stack[len(sub.stack)-1])
 272     }
 273     return rest, err
 274 }
 275 
 276 func call(ctx *context, f any, name string) error {
 277     var err error
 278 
 279     switch f := f.(type) {
 280     case float64:
 281         ctx.stack = append(ctx.stack, []float64{f})
 282     case func(ctx *context) error:
 283         err = f(ctx)
 284     case func(x float64) float64:
 285         err = loop(ctx, f)
 286     case func(x, y float64) float64:
 287         err = parallel(ctx, f)
 288     case func(x, y float64) (float64, float64):
 289         err = parallel2(ctx, f)
 290     case func(ctx *context, n int) error:
 291         err = repeat(ctx, f)
 292     default:
 293         err = errors.New(`unsupported function type`)
 294     }
 295 
 296     if err == io.EOF {
 297         return io.EOF
 298     }
 299     if err != nil {
 300         return errors.New(name + `: ` + err.Error())
 301     }
 302     return nil
 303 }
 304 
 305 func cloneArray(x []float64) []float64 {
 306     clone := make([]float64, 0, len(x))
 307     for _, f := range x {
 308         clone = append(clone, f)
 309     }
 310     return clone
 311 }
 312 
 313 var funcs = map[string]any{
 314     `+`:  add,
 315     `-`:  sub,
 316     `*`:  mul,
 317     `/`:  div,
 318     `%`:  math.Mod,
 319     `^`:  math.Pow,
 320     `**`: math.Pow,
 321 
 322     `add`:     add,
 323     `sub`:     sub,
 324     `mul`:     mul,
 325     `div`:     div,
 326     `mod`:     math.Mod,
 327     `modulus`: math.Mod,
 328     `pow`:     math.Pow,
 329     `power`:   math.Pow,
 330 
 331     `m`: mul,
 332 
 333     `a`:           anchor,
 334     `abs`:         math.Abs,
 335     `anchor`:      anchor,
 336     `ceil`:        math.Ceil,
 337     `ceiling`:     math.Ceil,
 338     `cos`:         math.Cos,
 339     `cosh`:        math.Cosh,
 340     `cosine`:      math.Cos,
 341     `div2`:        div2,
 342     `down`:        math.Floor,
 343     `exp`:         math.Exp,
 344     `floor`:       math.Floor,
 345     `hypot`:       math.Hypot,
 346     `hypotenuse`:  math.Hypot,
 347     `hypothenuse`: math.Hypot,
 348     `mid`:         mid,
 349     `middle`:      mid,
 350     `neg`:         negate,
 351     `negate`:      negate,
 352     `ln`:          math.Log,
 353     `log`:         math.Log,
 354     `log10`:       math.Log10,
 355     `log2`:        math.Log2,
 356     `pow10`:       pow10,
 357     `pow2`:        pow2,
 358     `round`:       math.Round,
 359     `sgn`:         sign,
 360     `sign`:        sign,
 361     `sin`:         math.Sin,
 362     `sine`:        math.Sin,
 363     `sinh`:        math.Sinh,
 364     `tan`:         math.Tan,
 365     `tangent`:     math.Tan,
 366     `tanh`:        math.Tanh,
 367     `up`:          math.Ceil,
 368 
 369     `bail`:    bail,
 370     `bye`:     bye,
 371     `c`:       empty,
 372     `clear`:   empty,
 373     `d`:       dup,
 374     `dump`:    dump,
 375     `dup`:     dup,
 376     `dup2`:    dup2,
 377     `e`:       math.E,
 378     `empty`:   empty,
 379     `f`:       flatten,
 380     `flat`:    flatten,
 381     `flatten`: flatten,
 382     `h`:       help,
 383     `help`:    help,
 384     `i`:       iota,
 385     `inv`:     invert,
 386     `inverse`: invert,
 387     `invert`:  invert,
 388     `iota`:    iota,
 389     `l`:       toplen,
 390     `len`:     toplen,
 391     `length`:  toplen,
 392     `p`:       toprint,
 393     `pi`:      math.Pi,
 394     `print`:   toprint,
 395     `pop`:     pop,
 396     `q`:       bye,
 397     `quit`:    bye,
 398     `rand`:    random,
 399     `random`:  random,
 400     `rnd`:     random,
 401     `s`:       swap2,
 402     `shift`:   shift,
 403     `swap`:    swap2,
 404     `swap2`:   swap2,
 405     `tau`:     2 * math.Pi,
 406     `top`:     top,
 407     `u`:       unit,
 408     `unit`:    unit,
 409 }
 410 
 411 func invert(x float64) float64 { return 1 / x }
 412 func negate(x float64) float64 { return -x }
 413 
 414 func add(x, y float64) float64 { return x + y }
 415 func sub(x, y float64) float64 { return x - y }
 416 func mid(x, y float64) float64 { return 0.5 * (x + y) }
 417 func mul(x, y float64) float64 { return x * y }
 418 func div(x, y float64) float64 { return x / y }
 419 func pow10(x float64) float64  { return math.Pow(10, x) }
 420 func pow2(x float64) float64   { return math.Pow(2, x) }
 421 
 422 func div2(x, y float64) (float64, float64) { return x / y, y / x }
 423 
 424 func anchor(x float64) float64 {
 425     const eps = 1e6
 426     if i, f := math.Modf(x); -eps <= f && f <= +eps {
 427         return float64(i)
 428     }
 429     return x
 430 }
 431 
 432 func sign(x float64) float64 {
 433     if x > 0 {
 434         return +1
 435     }
 436     if x < 0 {
 437         return -1
 438     }
 439     return x
 440 }
 441 
 442 func bail(ctx *context) error {
 443     dump(ctx)
 444     return bye(ctx)
 445 }
 446 
 447 func bye(ctx *context) error {
 448     return io.EOF
 449 }
 450 
 451 func dump(ctx *context) error {
 452     if len(ctx.stack) == 0 {
 453         return nil
 454     }
 455 
 456     bw := bufio.NewWriter(os.Stdout)
 457     defer bw.Flush()
 458 
 459     var buf [24]byte
 460     for _, v := range ctx.stack {
 461         for i, f := range v {
 462             if i > 0 {
 463                 if err := bw.WriteByte(' '); err != nil {
 464                     break
 465                 }
 466             }
 467             bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
 468         }
 469 
 470         if err := bw.WriteByte('\n'); err != nil {
 471             return io.EOF
 472         }
 473     }
 474 
 475     return nil
 476 }
 477 
 478 func dup(ctx *context) error {
 479     if len(ctx.stack) > 0 {
 480         x := ctx.stack[len(ctx.stack)-1]
 481         ctx.stack = append(ctx.stack, cloneArray(x))
 482     }
 483     return nil
 484 }
 485 
 486 func dup2(ctx *context) error {
 487     if len(ctx.stack) < 2 {
 488         return needAtLeast2
 489     }
 490 
 491     x := ctx.stack[len(ctx.stack)-2]
 492     y := ctx.stack[len(ctx.stack)-1]
 493     ctx.stack = append(ctx.stack, cloneArray(x))
 494     ctx.stack = append(ctx.stack, cloneArray(y))
 495     return nil
 496 }
 497 
 498 func empty(ctx *context) error {
 499     ctx.stack = ctx.stack[:0]
 500     return nil
 501 }
 502 
 503 func flatten(ctx *context) error {
 504     n := 0
 505     for _, v := range ctx.stack {
 506         n += len(v)
 507     }
 508 
 509     top := make([]float64, 0, n)
 510     for _, v := range ctx.stack {
 511         top = append(top, v...)
 512     }
 513     ctx.stack = append(ctx.stack[:0], top)
 514     return nil
 515 }
 516 
 517 func help(ctx *context) error {
 518     if _, err := os.Stdout.WriteString(info[1:]); err != nil {
 519         return io.EOF
 520     }
 521     return nil
 522 }
 523 
 524 func iota(ctx *context, n int) error {
 525     if n < 1 {
 526         return nil
 527     }
 528 
 529     top := make([]float64, n)
 530     for i := range top {
 531         top[i] = float64(i + 1)
 532     }
 533     ctx.stack = append(ctx.stack, top)
 534 
 535     return nil
 536 }
 537 
 538 func pop(ctx *context) error {
 539     if len(ctx.stack) > 0 {
 540         ctx.stack = ctx.stack[:len(ctx.stack)-1]
 541     }
 542     return nil
 543 }
 544 
 545 func random(ctx *context, n int) error {
 546     if n < 1 {
 547         return nil
 548     }
 549 
 550     top := make([]float64, n)
 551     for i := range top {
 552         top[i] = ctx.random.Float64()
 553     }
 554     ctx.stack = append(ctx.stack, top)
 555 
 556     return nil
 557 }
 558 
 559 func reset(ctx *context) error {
 560     ctx.stack = ctx.stack[:0]
 561     ctx.random = rand.New(rand.NewSource(time.Now().UnixNano()))
 562     return nil
 563 }
 564 
 565 func shift(ctx *context) error {
 566     for i := 1; i < len(ctx.stack); i++ {
 567         ctx.stack[i-1] = ctx.stack[i]
 568     }
 569 
 570     if len(ctx.stack) > 0 {
 571         ctx.stack = ctx.stack[:len(ctx.stack)-1]
 572     }
 573 
 574     return nil
 575 }
 576 
 577 func toplen(ctx *context) error {
 578     if len(ctx.stack) < 1 {
 579         return needAtLeast1
 580     }
 581 
 582     top := ctx.stack[len(ctx.stack)-1]
 583     ctx.stack = append(ctx.stack, []float64{float64(len(top))})
 584     return nil
 585 }
 586 
 587 func swap2(ctx *context) error {
 588     if len(ctx.stack) < 2 {
 589         return needAtLeast2
 590     }
 591 
 592     x := ctx.stack[len(ctx.stack)-2]
 593     y := ctx.stack[len(ctx.stack)-1]
 594     ctx.stack[len(ctx.stack)-2] = y
 595     ctx.stack[len(ctx.stack)-1] = x
 596     return nil
 597 }
 598 
 599 func top(ctx *context) error {
 600     if len(ctx.stack) < 1 {
 601         return needAtLeast1
 602     }
 603 
 604     ctx.stack = append(ctx.stack[:0], ctx.stack[len(ctx.stack)-1])
 605     return nil
 606 }
 607 
 608 func toprint(ctx *context) error {
 609     show(os.Stdout, ctx)
 610     return nil
 611 }
 612 
 613 func unit(ctx *context, n int) error {
 614     if n < 1 {
 615         return nil
 616     }
 617 
 618     top := make([]float64, n)
 619     for i := range top {
 620         top[i] = 1
 621     }
 622     ctx.stack = append(ctx.stack, top)
 623 
 624     return nil
 625 }
 626 
 627 func loop(ctx *context, f func(x float64) float64) error {
 628     if len(ctx.stack) < 1 {
 629         return needAtLeast1
 630     }
 631 
 632     top := ctx.stack[len(ctx.stack)-1]
 633     for i, v := range top {
 634         top[i] = f(v)
 635     }
 636     return nil
 637 }
 638 
 639 func parallel(ctx *context, f func(x, y float64) float64) error {
 640     if len(ctx.stack) < 2 {
 641         return needAtLeast2
 642     }
 643 
 644     l := len(ctx.stack)
 645     xa := ctx.stack[l-2]
 646     ya := ctx.stack[l-1]
 647     ctx.stack = ctx.stack[:l-2]
 648 
 649     if len(xa) == 0 || len(ya) == 0 {
 650         return nil
 651     }
 652 
 653     var za []float64
 654     if len(xa) < len(ya) {
 655         za = ya
 656     } else {
 657         za = xa
 658     }
 659 
 660     for i := range za {
 661         za[i] = f(xa[i%len(xa)], ya[i%len(ya)])
 662     }
 663     ctx.stack = append(ctx.stack, za)
 664     return nil
 665 }
 666 
 667 func parallel2(ctx *context, f func(x, y float64) (float64, float64)) error {
 668     if len(ctx.stack) < 2 {
 669         return needAtLeast2
 670     }
 671 
 672     l := len(ctx.stack)
 673     xa := ctx.stack[l-2]
 674     ya := ctx.stack[l-1]
 675     ctx.stack = ctx.stack[:l-2]
 676 
 677     if len(xa) == 0 || len(ya) == 0 {
 678         return nil
 679     }
 680 
 681     var za []float64
 682     if len(xa) < len(ya) {
 683         za = ya
 684     } else {
 685         za = xa
 686     }
 687 
 688     r1 := make([]float64, len(za))
 689     r2 := make([]float64, len(za))
 690     for i := range r1 {
 691         x, y := f(xa[i%len(xa)], ya[i%len(ya)])
 692         r1[i] = x
 693         r2[i] = y
 694     }
 695     ctx.stack = append(ctx.stack, r1)
 696     ctx.stack = append(ctx.stack, r2)
 697     return nil
 698 }
 699 
 700 func repeat(ctx *context, f func(ctx *context, n int) error) error {
 701     if len(ctx.stack) < 1 {
 702         return needAtLeast1
 703     }
 704 
 705     counts := ctx.stack[len(ctx.stack)-1]
 706     ctx.stack = ctx.stack[:len(ctx.stack)-1]
 707 
 708     for _, n := range counts {
 709         if err := f(ctx, int(n)); err != nil {
 710             return err
 711         }
 712     }
 713 
 714     return nil
 715 }
     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(err.Error())
  64         os.Stderr.WriteString("\n")
  65         os.Exit(1)
  66         return
  67     }
  68 }
  69 
  70 type paddingData struct {
  71     lines []string
  72     max   int
  73 }
  74 
  75 func run(paths []string) error {
  76     var pd paddingData
  77 
  78     for _, p := range paths {
  79         if err := handleFile(&pd, p); err != nil {
  80             return err
  81         }
  82     }
  83 
  84     if len(paths) == 0 {
  85         if err := handleReader(&pd, os.Stdin); err != nil {
  86             return err
  87         }
  88     }
  89 
  90     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  91     defer bw.Flush()
  92 
  93     for _, line := range pd.lines {
  94         bw.WriteString(line)
  95         writeSpaces(bw, pd.max-countWidth(line))
  96         if bw.WriteByte('\n') != nil {
  97             break
  98         }
  99     }
 100     return nil
 101 }
 102 
 103 func handleFile(pd *paddingData, path string) error {
 104     f, err := os.Open(path)
 105     if err != nil {
 106         // on windows, file-not-found error messages may mention `CreateFile`,
 107         // even when trying to open files in read-only mode
 108         return errors.New(`can't open file named ` + path)
 109     }
 110     defer f.Close()
 111     return handleReader(pd, f)
 112 }
 113 
 114 func handleReader(pd *paddingData, r io.Reader) error {
 115     const gb = 1024 * 1024 * 1024
 116     sc := bufio.NewScanner(r)
 117     sc.Buffer(nil, 8*gb)
 118 
 119     for i := 0; sc.Scan(); i++ {
 120         line := sc.Text()
 121         if i == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
 122             line = line[3:]
 123         }
 124 
 125         n := countWidth(line)
 126         if pd.max < n {
 127             pd.max = n
 128         }
 129         pd.lines = append(pd.lines, line)
 130     }
 131 
 132     return sc.Err()
 133 }
 134 
 135 func countWidth(s string) int {
 136     width := 0
 137 
 138     for len(s) > 0 {
 139         i, j := indexEscapeSequence(s)
 140         if i < 0 {
 141             break
 142         }
 143         if j < 0 {
 144             j = len(s)
 145         }
 146 
 147         width += utf8.RuneCountInString(s[:i])
 148         s = s[j:]
 149     }
 150 
 151     // count trailing/all runes in strings which don't end with ANSI-sequences
 152     width += utf8.RuneCountInString(s)
 153     return width
 154 }
 155 
 156 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 157 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 158 // indices which can be independently negative when either the start/end of
 159 // a sequence isn't found; given their fairly-common use, even the hyperlink
 160 // ESC]8 sequences are supported
 161 func indexEscapeSequence(s string) (int, int) {
 162     var prev byte
 163 
 164     for i := range s {
 165         b := s[i]
 166 
 167         if prev == '\x1b' && b == '[' {
 168             j := indexLetter(s[i+1:])
 169             if j < 0 {
 170                 return i, -1
 171             }
 172             return i - 1, i + 1 + j + 1
 173         }
 174 
 175         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 176             j := indexPair(s[i+1:], '\x1b', '\\')
 177             if j < 0 {
 178                 return i, -1
 179             }
 180             return i - 1, i + 1 + j + 2
 181         }
 182 
 183         prev = b
 184     }
 185 
 186     return -1, -1
 187 }
 188 
 189 func indexLetter(s string) int {
 190     for i, b := range s {
 191         upper := b &^ 32
 192         if 'A' <= upper && upper <= 'Z' {
 193             return i
 194         }
 195     }
 196 
 197     return -1
 198 }
 199 
 200 func indexPair(s string, x byte, y byte) int {
 201     var prev byte
 202 
 203     for i := range s {
 204         b := s[i]
 205         if prev == x && b == y && i > 0 {
 206             return i
 207         }
 208         prev = b
 209     }
 210 
 211     return -1
 212 }
 213 
 214 // writeSpaces does what it says, minimizing calls to write-like funcs
 215 func writeSpaces(w *bufio.Writer, n int) {
 216     const spaces = `                                `
 217     if n < 1 {
 218         return
 219     }
 220 
 221     for n >= len(spaces) {
 222         w.WriteString(spaces)
 223         n -= len(spaces)
 224     }
 225     w.WriteString(spaces[:n])
 226 }
     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(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.NewWriter(os.Stdout)
 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 which []int
 107     var fields []string
 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, h, ok := match(s, args)
 118             if !ok {
 119                 return nil
 120             }
 121             which = picks
 122             handle = h
 123             fields = make([]string, 0, len(which))
 124         }
 125 
 126         fields = fields[:0]
 127         handle(s, func(i int, s string) bool {
 128             fields = append(fields, s)
 129             return true
 130         })
 131 
 132         got := 0
 133         for _, i := range which {
 134             if 0 <= i && i < len(fields) {
 135                 if got > 0 {
 136                     w.WriteByte('\t')
 137                 }
 138                 w.WriteString(fields[i])
 139                 got++
 140             }
 141         }
 142 
 143         if w.WriteByte('\n') != nil {
 144             return io.EOF
 145         }
 146 
 147         if !live {
 148             continue
 149         }
 150 
 151         if w.Flush() != nil {
 152             return io.EOF
 153         }
 154     }
 155 
 156     return sc.Err()
 157 }
 158 
 159 func match(s string, args []string) (which []int, handle handler, ok bool) {
 160     if strings.IndexByte(s, '\t') >= 0 {
 161         handle = loopItemsTSV
 162     } else {
 163         handle = loopItemsSSV
 164     }
 165 
 166     // count columns, so negative indices can be fixed with it
 167     count := 0
 168     handle(s, func(i int, s string) bool {
 169         count++
 170         return true
 171     })
 172 
 173     for _, arg := range args {
 174         ok := false
 175 
 176         // try exact matches
 177         handle(s, func(i int, s string) bool {
 178             if s == arg {
 179                 ok = true
 180                 which = append(which, i)
 181                 return false
 182             }
 183             return true
 184         })
 185 
 186         if ok {
 187             continue
 188         }
 189 
 190         // try case-insensitive matches
 191         handle(s, func(i int, s string) bool {
 192             if s == arg {
 193                 ok = true
 194                 which = append(which, i)
 195                 return false
 196             }
 197             return true
 198         })
 199 
 200         if ok {
 201             continue
 202         }
 203 
 204         // try 1-based indices, even negative ones
 205         if n, err := strconv.Atoi(arg); err == nil {
 206             if n < 0 {
 207                 n += count
 208             } else if n > 0 {
 209                 n--
 210             }
 211 
 212             if 0 <= n && n < count {
 213                 which = append(which, n)
 214             }
 215         }
 216     }
 217 
 218     return which, handle, true
 219 }
 220 
 221 // loopItemsSSV loops over a line's items, allocation-free style; when given
 222 // empty strings, the callback func is never called
 223 func loopItemsSSV(s string, f itemFunc) {
 224     s = trimTrailingSpaces(s)
 225 
 226     for i := 0; true; i++ {
 227         s = trimLeadingSpaces(s)
 228         if len(s) == 0 {
 229             return
 230         }
 231 
 232         j := strings.IndexByte(s, ' ')
 233         if j < 0 {
 234             if !f(i, s) {
 235                 return
 236             }
 237             return
 238         }
 239 
 240         if !f(i, s[:j]) {
 241             return
 242         }
 243         s = s[j+1:]
 244     }
 245 }
 246 
 247 func trimLeadingSpaces(s string) string {
 248     for len(s) > 0 && s[0] == ' ' {
 249         s = s[1:]
 250     }
 251     return s
 252 }
 253 
 254 func trimTrailingSpaces(s string) string {
 255     for len(s) > 0 && s[len(s)-1] == ' ' {
 256         s = s[:len(s)-1]
 257     }
 258     return s
 259 }
 260 
 261 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 262 // when given empty strings, the callback func is never called
 263 func loopItemsTSV(s string, f itemFunc) {
 264     if len(s) == 0 {
 265         return
 266     }
 267 
 268     for i := 0; true; i++ {
 269         j := strings.IndexByte(s, '\t')
 270         if j < 0 {
 271             if !f(i, s) {
 272                 return
 273             }
 274             return
 275         }
 276 
 277         if !f(i, s[:j]) {
 278             return
 279         }
 280         s = s[j+1:]
 281     }
 282 }
     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(err.Error())
  78         os.Stderr.WriteString("\n")
  79         os.Exit(1)
  80         return
  81     }
  82 }
  83 
  84 func run(w io.Writer, args []string, live bool) error {
  85     bw := bufio.NewWriter(w)
  86     defer bw.Flush()
  87 
  88     if len(args) == 0 {
  89         return plain(bw, os.Stdin, live)
  90     }
  91 
  92     for _, name := range args {
  93         if err := handleFile(bw, name, live); err != nil {
  94             return err
  95         }
  96     }
  97     return nil
  98 }
  99 
 100 func handleFile(w *bufio.Writer, name string, live bool) error {
 101     if name == `` || name == `-` {
 102         return plain(w, os.Stdin, live)
 103     }
 104 
 105     f, err := os.Open(name)
 106     if err != nil {
 107         return errors.New(`can't read from file named "` + name + `"`)
 108     }
 109     defer f.Close()
 110 
 111     return plain(w, f, live)
 112 }
 113 
 114 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 115 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 116 // indices which can be independently negative when either the start/end of
 117 // a sequence isn't found; given their fairly-common use, even the hyperlink
 118 // ESC]8 sequences are supported
 119 func indexEscapeSequence(s []byte) (int, int) {
 120     var prev byte
 121 
 122     for i, b := range s {
 123         if prev == '\x1b' && b == '[' {
 124             j := indexLetter(s[i+1:])
 125             if j < 0 {
 126                 return i, -1
 127             }
 128             return i - 1, i + 1 + j + 1
 129         }
 130 
 131         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 132             j := indexPair(s[i+1:], '\x1b', '\\')
 133             if j < 0 {
 134                 return i, -1
 135             }
 136             return i - 1, i + 1 + j + 2
 137         }
 138 
 139         prev = b
 140     }
 141 
 142     return -1, -1
 143 }
 144 
 145 func indexLetter(s []byte) int {
 146     for i, b := range s {
 147         upper := b &^ 32
 148         if 'A' <= upper && upper <= 'Z' {
 149             return i
 150         }
 151     }
 152 
 153     return -1
 154 }
 155 
 156 func indexPair(s []byte, x byte, y byte) int {
 157     var prev byte
 158 
 159     for i, b := range s {
 160         if prev == x && b == y && i > 0 {
 161             return i
 162         }
 163         prev = b
 164     }
 165 
 166     return -1
 167 }
 168 
 169 func plain(w *bufio.Writer, r io.Reader, live bool) error {
 170     const gb = 1024 * 1024 * 1024
 171     sc := bufio.NewScanner(r)
 172     sc.Buffer(nil, 8*gb)
 173 
 174     for i := 0; sc.Scan(); i++ {
 175         s := sc.Bytes()
 176         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 177             s = s[3:]
 178         }
 179 
 180         for line := s; len(line) > 0; {
 181             i, j := indexEscapeSequence(line)
 182             if i < 0 {
 183                 w.Write(line)
 184                 break
 185             }
 186             if j < 0 {
 187                 j = len(line)
 188             }
 189 
 190             if i > 0 {
 191                 w.Write(line[:i])
 192             }
 193 
 194             line = line[j:]
 195         }
 196 
 197         if w.WriteByte('\n') != nil {
 198             return io.EOF
 199         }
 200 
 201         if !live {
 202             continue
 203         }
 204 
 205         if w.Flush() != nil {
 206             return io.EOF
 207         }
 208     }
 209 
 210     return sc.Err()
 211 }
     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(err.Error())
  75         os.Stderr.WriteString("\n")
  76         os.Exit(1)
  77         return
  78     }
  79 }
  80 
  81 func run(w io.Writer, args []string, live bool) error {
  82     if len(args) == 0 {
  83         return nil
  84     }
  85 
  86     bw := bufio.NewWriter(w)
  87 
  88     for i, s := range args {
  89         if i > 0 {
  90             bw.WriteByte('\t')
  91         }
  92         bw.WriteString(s)
  93     }
  94 
  95     if bw.WriteByte('\n') != nil {
  96         bw.Flush()
  97         return io.EOF
  98     }
  99 
 100     if bw.Flush() != nil {
 101         return io.EOF
 102     }
 103 
 104     return catl(bw, os.Stdin, live)
 105 }
 106 
 107 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 108     const gb = 1024 * 1024 * 1024
 109     sc := bufio.NewScanner(r)
 110     sc.Buffer(nil, 8*gb)
 111 
 112     for i := 0; sc.Scan(); i++ {
 113         s := sc.Bytes()
 114         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 115             s = s[3:]
 116         }
 117 
 118         w.Write(s)
 119         if w.WriteByte('\n') != nil {
 120             return io.EOF
 121         }
 122 
 123         if !live {
 124             continue
 125         }
 126 
 127         if w.Flush() != nil {
 128             return io.EOF
 129         }
 130     }
 131 
 132     return sc.Err()
 133 }
     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(err.Error())
  58             os.Stderr.WriteString("\n")
  59             os.Exit(1)
  60             return
  61         }
  62 
  63         if n < 0 {
  64             n = 0
  65         }
  66         howMany = n
  67     }
  68 
  69     primes(howMany)
  70 }
  71 
  72 func primes(left int) {
  73     bw := bufio.NewWriter(os.Stdout)
  74     defer bw.Flush()
  75 
  76     // 24 bytes are always enough for any 64-bit integer
  77     var buf [24]byte
  78 
  79     // 2 is the only even prime number
  80     if left > 0 {
  81         bw.WriteString("2\n")
  82         left--
  83     }
  84 
  85     for n := uint64(3); left > 0; n += 2 {
  86         if oddPrime(n) {
  87             bw.Write(strconv.AppendUint(buf[:0], n, 10))
  88             if err := bw.WriteByte('\n'); err != nil {
  89                 // assume errors come from closed stdout pipes
  90                 return
  91             }
  92             left--
  93         }
  94     }
  95 }
  96 
  97 // oddPrime assumes the number given to it is odd
  98 func oddPrime(n uint64) bool {
  99     max := uint64(math.Sqrt(float64(n)))
 100     for div := uint64(3); div <= max; div += 2 {
 101         if n%div == 0 {
 102             return false
 103         }
 104     }
 105     return true
 106 }
     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(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79         return
  80     }
  81 }
  82 
  83 // table has all summary info gathered from the data, along with the row
  84 // themselves, stored as lines/strings
  85 type table struct {
  86     Columns int
  87 
  88     Rows []string
  89 
  90     MaxWidth []int
  91 
  92     MaxDotDecimals []int
  93 
  94     LoopItems func(s string, max int, t *table, f itemFunc)
  95 
  96     MaxColumns bool
  97 }
  98 
  99 type itemFunc func(i int, s string, t *table)
 100 
 101 func run(paths []string, maxCols bool) error {
 102     var res table
 103     res.MaxColumns = maxCols
 104 
 105     for _, p := range paths {
 106         if err := handleFile(&res, p); err != nil {
 107             return err
 108         }
 109     }
 110 
 111     if len(paths) == 0 {
 112         if err := handleReader(&res, os.Stdin); err != nil {
 113             return err
 114         }
 115     }
 116 
 117     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 118     defer bw.Flush()
 119     realign(bw, res)
 120     return nil
 121 }
 122 
 123 func handleFile(res *table, path string) error {
 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 handleReader(res, f)
 132 }
 133 
 134 func handleReader(t *table, r io.Reader) error {
 135     const gb = 1024 * 1024 * 1024
 136     sc := bufio.NewScanner(r)
 137     sc.Buffer(nil, 8*gb)
 138 
 139     const maxInt = int(^uint(0) >> 1)
 140     maxCols := maxInt
 141 
 142     for i := 0; sc.Scan(); i++ {
 143         s := sc.Text()
 144         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 145             s = s[3:]
 146         }
 147 
 148         if len(s) == 0 {
 149             if len(t.Rows) > 0 {
 150                 t.Rows = append(t.Rows, ``)
 151             }
 152             continue
 153         }
 154 
 155         t.Rows = append(t.Rows, s)
 156 
 157         if t.Columns == 0 {
 158             if t.LoopItems == nil {
 159                 if strings.IndexByte(s, '\t') >= 0 {
 160                     t.LoopItems = loopItemsTSV
 161                 } else {
 162                     t.LoopItems = loopItemsSSV
 163                 }
 164             }
 165 
 166             if !t.MaxColumns {
 167                 t.LoopItems(s, maxCols, t, updateColumnCount)
 168                 maxCols = t.Columns
 169             }
 170         }
 171 
 172         t.LoopItems(s, maxCols, t, updateItem)
 173     }
 174 
 175     return sc.Err()
 176 }
 177 
 178 func updateColumnCount(i int, s string, t *table) {
 179     t.Columns = i + 1
 180 }
 181 
 182 func updateItem(i int, s string, t *table) {
 183     // ensure column-info-slices have enough room
 184     if i >= len(t.MaxWidth) {
 185         // update column-count if in max-columns mode
 186         if t.MaxColumns {
 187             t.Columns = i + 1
 188         }
 189         t.MaxWidth = append(t.MaxWidth, 0)
 190         t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
 191     }
 192 
 193     // keep track of widest rune-counts for each column
 194     w := countWidth(s)
 195     if t.MaxWidth[i] < w {
 196         t.MaxWidth[i] = w
 197     }
 198 
 199     // update stats for numeric items
 200     if isNumeric(s) {
 201         dd := countDotDecimals(s)
 202         if t.MaxDotDecimals[i] < dd {
 203             t.MaxDotDecimals[i] = dd
 204         }
 205     }
 206 }
 207 
 208 // loopItemsSSV loops over a line's items, allocation-free style; when given
 209 // empty strings, the callback func is never called
 210 func loopItemsSSV(s string, max int, t *table, f itemFunc) {
 211     s = trimTrailingSpaces(s)
 212 
 213     for i := 0; true; i++ {
 214         s = trimLeadingSpaces(s)
 215         if len(s) == 0 {
 216             return
 217         }
 218 
 219         if i+1 == max {
 220             f(i, s, t)
 221             return
 222         }
 223 
 224         j := strings.IndexByte(s, ' ')
 225         if j < 0 {
 226             f(i, s, t)
 227             return
 228         }
 229 
 230         f(i, s[:j], t)
 231         s = s[j+1:]
 232     }
 233 }
 234 
 235 func trimLeadingSpaces(s string) string {
 236     for len(s) > 0 && s[0] == ' ' {
 237         s = s[1:]
 238     }
 239     return s
 240 }
 241 
 242 func trimTrailingSpaces(s string) string {
 243     for len(s) > 0 && s[len(s)-1] == ' ' {
 244         s = s[:len(s)-1]
 245     }
 246     return s
 247 }
 248 
 249 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 250 // when given empty strings, the callback func is never called
 251 func loopItemsTSV(s string, max int, t *table, f itemFunc) {
 252     if len(s) == 0 {
 253         return
 254     }
 255 
 256     for i := 0; true; i++ {
 257         if i+1 == max {
 258             f(i, s, t)
 259             return
 260         }
 261 
 262         j := strings.IndexByte(s, '\t')
 263         if j < 0 {
 264             f(i, s, t)
 265             return
 266         }
 267 
 268         f(i, s[:j], t)
 269         s = s[j+1:]
 270     }
 271 }
 272 
 273 func skipLeadingEscapeSequences(s string) string {
 274     for len(s) >= 2 {
 275         if s[0] != '\x1b' {
 276             return s
 277         }
 278 
 279         switch s[1] {
 280         case '[':
 281             s = skipSingleLeadingANSI(s[2:])
 282 
 283         case ']':
 284             if len(s) < 3 || s[2] != '8' {
 285                 return s
 286             }
 287             s = skipSingleLeadingOSC(s[3:])
 288 
 289         default:
 290             return s
 291         }
 292     }
 293 
 294     return s
 295 }
 296 
 297 func skipSingleLeadingANSI(s string) string {
 298     for len(s) > 0 {
 299         upper := s[0] &^ 32
 300         s = s[1:]
 301         if 'A' <= upper && upper <= 'Z' {
 302             break
 303         }
 304     }
 305 
 306     return s
 307 }
 308 
 309 func skipSingleLeadingOSC(s string) string {
 310     var prev byte
 311 
 312     for len(s) > 0 {
 313         b := s[0]
 314         s = s[1:]
 315         if prev == '\x1b' && b == '\\' {
 316             break
 317         }
 318         prev = b
 319     }
 320 
 321     return s
 322 }
 323 
 324 // isNumeric checks if a string is valid/useable as a number
 325 func isNumeric(s string) bool {
 326     if len(s) == 0 {
 327         return false
 328     }
 329 
 330     s = skipLeadingEscapeSequences(s)
 331     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 332         s = s[1:]
 333     }
 334 
 335     s = skipLeadingEscapeSequences(s)
 336     if len(s) == 0 {
 337         return false
 338     }
 339     if s[0] == '.' {
 340         return isDigits(s[1:])
 341     }
 342 
 343     digits := 0
 344 
 345     for {
 346         s = skipLeadingEscapeSequences(s)
 347         if len(s) == 0 {
 348             break
 349         }
 350 
 351         if s[0] == '.' {
 352             return isDigits(s[1:])
 353         }
 354 
 355         if !('0' <= s[0] && s[0] <= '9') {
 356             return false
 357         }
 358 
 359         digits++
 360         s = s[1:]
 361     }
 362 
 363     s = skipLeadingEscapeSequences(s)
 364     return len(s) == 0 && digits > 0
 365 }
 366 
 367 func isDigits(s string) bool {
 368     if len(s) == 0 {
 369         return false
 370     }
 371 
 372     digits := 0
 373 
 374     for {
 375         s = skipLeadingEscapeSequences(s)
 376         if len(s) == 0 {
 377             break
 378         }
 379 
 380         if '0' <= s[0] && s[0] <= '9' {
 381             s = s[1:]
 382             digits++
 383         } else {
 384             return false
 385         }
 386     }
 387 
 388     s = skipLeadingEscapeSequences(s)
 389     return len(s) == 0 && digits > 0
 390 }
 391 
 392 // countDecimals counts decimal digits from the string given, assuming it
 393 // represents a valid/useable float64, when parsed
 394 func countDecimals(s string) int {
 395     dot := strings.IndexByte(s, '.')
 396     if dot < 0 {
 397         return 0
 398     }
 399 
 400     decs := 0
 401     s = s[dot+1:]
 402 
 403     for len(s) > 0 {
 404         s = skipLeadingEscapeSequences(s)
 405         if len(s) == 0 {
 406             break
 407         }
 408         if '0' <= s[0] && s[0] <= '9' {
 409             decs++
 410         }
 411         s = s[1:]
 412     }
 413 
 414     return decs
 415 }
 416 
 417 // countDotDecimals is like func countDecimals, but this one also includes
 418 // the dot, when any decimals are present, else the count stays at 0
 419 func countDotDecimals(s string) int {
 420     decs := countDecimals(s)
 421     if decs > 0 {
 422         return decs + 1
 423     }
 424     return decs
 425 }
 426 
 427 func countWidth(s string) int {
 428     width := 0
 429 
 430     for len(s) > 0 {
 431         i, j := indexEscapeSequence(s)
 432         if i < 0 {
 433             break
 434         }
 435         if j < 0 {
 436             j = len(s)
 437         }
 438 
 439         width += utf8.RuneCountInString(s[:i])
 440         s = s[j:]
 441     }
 442 
 443     // count trailing/all runes in strings which don't end with ANSI-sequences
 444     width += utf8.RuneCountInString(s)
 445     return width
 446 }
 447 
 448 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 449 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 450 // indices which can be independently negative when either the start/end of
 451 // a sequence isn't found; given their fairly-common use, even the hyperlink
 452 // ESC]8 sequences are supported
 453 func indexEscapeSequence(s string) (int, int) {
 454     var prev byte
 455 
 456     for i := range s {
 457         b := s[i]
 458 
 459         if prev == '\x1b' && b == '[' {
 460             j := indexLetter(s[i+1:])
 461             if j < 0 {
 462                 return i, -1
 463             }
 464             return i - 1, i + 1 + j + 1
 465         }
 466 
 467         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 468             j := indexPair(s[i+1:], '\x1b', '\\')
 469             if j < 0 {
 470                 return i, -1
 471             }
 472             return i - 1, i + 1 + j + 2
 473         }
 474 
 475         prev = b
 476     }
 477 
 478     return -1, -1
 479 }
 480 
 481 func indexLetter(s string) int {
 482     for i, b := range s {
 483         upper := b &^ 32
 484         if 'A' <= upper && upper <= 'Z' {
 485             return i
 486         }
 487     }
 488 
 489     return -1
 490 }
 491 
 492 func indexPair(s string, x byte, y byte) int {
 493     var prev byte
 494 
 495     for i := range s {
 496         b := s[i]
 497         if prev == x && b == y && i > 0 {
 498             return i
 499         }
 500         prev = b
 501     }
 502 
 503     return -1
 504 }
 505 
 506 func realign(w *bufio.Writer, t table) {
 507     due := 0
 508     showItem := func(i int, s string, t *table) {
 509         if i > 0 {
 510             due += 2
 511         }
 512 
 513         if isNumeric(s) {
 514             dd := countDotDecimals(s)
 515             rpad := t.MaxDotDecimals[i] - dd
 516             width := countWidth(s)
 517             lpad := t.MaxWidth[i] - (width + rpad) + due
 518             writeSpaces(w, lpad)
 519             w.WriteString(s)
 520             due = rpad
 521             return
 522         }
 523 
 524         writeSpaces(w, due)
 525         w.WriteString(s)
 526         due = t.MaxWidth[i] - countWidth(s)
 527     }
 528 
 529     for _, line := range t.Rows {
 530         due = 0
 531         if len(line) > 0 {
 532             t.LoopItems(line, t.Columns, &t, showItem)
 533         }
 534         if w.WriteByte('\n') != nil {
 535             break
 536         }
 537     }
 538 }
 539 
 540 // writeSpaces does what it says, minimizing calls to write-like funcs
 541 func writeSpaces(w *bufio.Writer, n int) {
 542     const spaces = `                                `
 543     if n < 1 {
 544         return
 545     }
 546 
 547     for n >= len(spaces) {
 548         w.WriteString(spaces)
 549         n -= len(spaces)
 550     }
 551     w.WriteString(spaces[:n])
 552 }
     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 }
     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(err.Error())
 107         os.Stderr.WriteString("\n")
 108         os.Exit(1)
 109         return
 110     }
 111 }
 112 
 113 func run(paths []string, cfg config) error {
 114     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 115     defer bw.Flush()
 116 
 117     for _, p := range paths {
 118         if err := handleFile(bw, p, &cfg); err != nil {
 119             return err
 120         }
 121     }
 122 
 123     if len(paths) == 0 {
 124         if err := handleReader(bw, os.Stdin, &cfg); err != nil {
 125             return err
 126         }
 127     }
 128 
 129     // end current line, if there's anything on it
 130     if cfg.pos > 0 {
 131         if bw.WriteByte('\n') != nil {
 132             return io.EOF
 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         // on windows, file-not-found error messages may mention `CreateFile`,
 142         // even when trying to open files in read-only mode
 143         return errors.New(`can't open file named ` + path)
 144     }
 145     defer f.Close()
 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 
 154     for i := 0; sc.Scan(); i++ {
 155         s := sc.Bytes()
 156         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 157             s = s[3:]
 158         }
 159 
 160         s = trimTrailingSpaces(s)
 161 
 162         // keep empty(ish) lines as empty lines
 163         if len(s) == 0 {
 164             if cfg.pos > 0 && w.WriteByte('\n') != nil {
 165                 return io.EOF
 166             }
 167             if w.WriteByte('\n') != nil {
 168                 return io.EOF
 169             }
 170             if cfg.liveLines && w.Flush() != nil {
 171                 return io.EOF
 172             }
 173             cfg.pos = 0
 174             continue
 175         }
 176 
 177         // reflow all `words` from non-empty lines
 178         for len(s) > 0 {
 179             var word []byte
 180             s = trimLeadingSpaces(s)
 181             if i := bytes.IndexByte(s, ' '); i >= 0 {
 182                 word = s[:i]
 183                 s = s[i+1:]
 184             } else {
 185                 word = s
 186                 s = nil
 187             }
 188 
 189             word = trimLeadingSpaces(word)
 190             width := countWidth(word)
 191             if width == 0 {
 192                 continue
 193             }
 194 
 195             if cfg.pos+width > cfg.maxWidth {
 196                 if cfg.pos > 0 && w.WriteByte('\n') != nil {
 197                     return io.EOF
 198                 }
 199                 if cfg.liveLines && w.Flush() != nil {
 200                     return io.EOF
 201                 }
 202                 cfg.pos = 0
 203             }
 204 
 205             if cfg.pos > 0 {
 206                 w.WriteByte(' ')
 207                 cfg.pos++
 208             }
 209             w.Write(word)
 210             cfg.pos += width
 211         }
 212     }
 213 
 214     return sc.Err()
 215 }
 216 
 217 func trimLeadingSpaces(s []byte) []byte {
 218     for len(s) > 0 && s[0] == ' ' {
 219         s = s[1:]
 220     }
 221     return s
 222 }
 223 
 224 func trimTrailingSpaces(s []byte) []byte {
 225     for len(s) > 0 && s[len(s)-1] == ' ' {
 226         s = s[:len(s)-1]
 227     }
 228     return s
 229 }
 230 
 231 func countWidth(s []byte) int {
 232     width := 0
 233 
 234     for len(s) > 0 {
 235         i, j := indexEscapeSequence(s)
 236         if i < 0 {
 237             break
 238         }
 239         if j < 0 {
 240             j = len(s)
 241         }
 242 
 243         width += utf8.RuneCount(s[:i])
 244         s = s[j:]
 245     }
 246 
 247     // count trailing/all runes in strings which don't end with ANSI-sequences
 248     width += utf8.RuneCount(s)
 249     return width
 250 }
 251 
 252 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 253 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 254 // indices which can be independently negative when either the start/end of
 255 // a sequence isn't found; given their fairly-common use, even the hyperlink
 256 // ESC]8 sequences are supported
 257 func indexEscapeSequence(s []byte) (int, int) {
 258     var prev byte
 259 
 260     for i := range s {
 261         b := s[i]
 262 
 263         if prev == '\x1b' && b == '[' {
 264             j := indexLetter(s[i+1:])
 265             if j < 0 {
 266                 return i, -1
 267             }
 268             return i - 1, i + 1 + j + 1
 269         }
 270 
 271         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 272             j := indexPair(s[i+1:], '\x1b', '\\')
 273             if j < 0 {
 274                 return i, -1
 275             }
 276             return i - 1, i + 1 + j + 2
 277         }
 278 
 279         prev = b
 280     }
 281 
 282     return -1, -1
 283 }
 284 
 285 func indexLetter(s []byte) int {
 286     for i, b := range s {
 287         upper := b &^ 32
 288         if 'A' <= upper && upper <= 'Z' {
 289             return i
 290         }
 291     }
 292 
 293     return -1
 294 }
 295 
 296 func indexPair(s []byte, x byte, y byte) int {
 297     var prev byte
 298 
 299     for i := range s {
 300         b := s[i]
 301         if prev == x && b == y && i > 0 {
 302             return i
 303         }
 304         prev = b
 305     }
 306 
 307     return -1
 308 }
     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     // separator is the string put between adjacent columns
  36     separator = ` █ `
  37 
  38     // maxAutoWidth is the output max-width, chosen to fit very old monitors
  39     maxAutoWidth = 79
  40 )
  41 
  42 // chooseNumColumns implements heuristics to auto-pick the number of columns
  43 // to show: this func is used when the app is using data from standard-input
  44 func chooseNumColumns(lines []string) int {
  45     if len(lines) == 0 {
  46         return 1
  47     }
  48 
  49     // sepw is the separator width
  50     sepw := width(separator)
  51 
  52     // see if lines can even fit a single column
  53     if !columnsCanFit(1, lines, sepw) {
  54         return 1
  55     }
  56 
  57     // starting from the max possible columns which may fit, keep trying
  58     // with 1 fewer column, until the columns fit
  59     for ncols := int(maxAutoWidth / sepw); ncols > 1; ncols-- {
  60         if columnsCanFit(ncols, lines, sepw) {
  61             // success: found the most columns which fit
  62             return ncols
  63         }
  64     }
  65 
  66     // avoid multiple columns if some lines are too wide
  67     return 1
  68 }
  69 
  70 // columnsCanFit checks whether the number of columns given would fit the
  71 // display max-width constant
  72 func columnsCanFit(ncols int, lines []string, gap int) bool {
  73     if ncols < 1 {
  74         // avoid surprises when called with non-sense column counts
  75         return true
  76     }
  77 
  78     // stack-allocate the backing-array behind slice maxw
  79     var buf [maxAutoWidth / 2]int
  80     maxw := buf[:0]
  81 
  82     // find the column max-height, to chunk lines into columns
  83     h := int(math.Ceil(float64(len(lines)) / float64(ncols)))
  84 
  85     // find column max-width by looping over chunks of lines
  86     for len(lines) >= h {
  87         w := findMaxWidth(lines[:h])
  88         maxw = append(maxw, w)
  89         lines = lines[h:]
  90     }
  91 
  92     // don't forget the last column
  93     if len(lines) > 0 {
  94         w := findMaxWidth(lines)
  95         maxw = append(maxw, w)
  96     }
  97 
  98     // remember to add the gaps/separators between columns, along with
  99     // all the individual column max-widths
 100     w := (ncols - 1) * gap
 101     for _, n := range maxw {
 102         w += n
 103     }
 104 
 105     // do the columns fit?
 106     return w <= maxAutoWidth
 107 }
 108 
 109 // findMaxWidth finds the max width in the slice given, ignoring ANSI codes
 110 func findMaxWidth(lines []string) int {
 111     maxw := 0
 112     for _, s := range lines {
 113         w := width(s)
 114         if w > maxw {
 115             maxw = w
 116         }
 117     }
 118     return maxw
 119 }
     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
     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     if len(os.Args) > 1 {
  44         switch os.Args[1] {
  45         case `-h`, `--h`, `-help`, `--help`:
  46             os.Stderr.WriteString(usage)
  47             os.Exit(0)
  48         }
  49     }
  50 
  51     err := run(os.Args[1:])
  52     if err != nil && err != errDoneWriting {
  53         os.Stderr.WriteString("\x1b[31m")
  54         os.Stderr.WriteString(err.Error())
  55         os.Stderr.WriteString("\x1b[0m\n")
  56         os.Exit(1)
  57     }
  58 }
  59 
  60 func run(args []string) error {
  61     w := bufio.NewWriterSize(os.Stdout, 16*1024)
  62     defer w.Flush()
  63 
  64     if len(args) == 0 {
  65         // return errors.New(`expected a leading number for the column count`)
  66         args = []string{`0`}
  67     }
  68 
  69     ncols := 0
  70     f, err := strconv.ParseFloat(args[0], 64)
  71     if err != nil || math.IsInf(f, 0) || math.IsNaN(f) {
  72         return errors.New(`first argument isn't a valid number of columns`)
  73     }
  74     ncols = int(f)
  75 
  76     paths := args[1:]
  77     if len(paths) == 0 {
  78         paths = []string{`-`}
  79     }
  80 
  81     lines, err := gatherLines(paths)
  82     if err != nil {
  83         return err
  84     }
  85 
  86     // choose a default number of columns, if not given an explicit one
  87     if ncols < 1 {
  88         ncols = chooseNumColumns(lines)
  89     }
  90 
  91     return handleLines(w, lines, ncols)
  92 }
  93 
  94 // gatherLines slurps all text-lines from all the filepaths given, expanding
  95 // any tabs found; a single dash as a pathname means stdin
  96 func gatherLines(paths []string) ([]string, error) {
  97     var lines []string
  98     var sb strings.Builder
  99 
 100     dashes := 0
 101     for _, s := range paths {
 102         if s == `-` {
 103             dashes++
 104         }
 105     }
 106     if dashes > 1 {
 107         return lines, errors.New(`can't use "-" (stdin) more than once`)
 108     }
 109 
 110     for _, s := range paths {
 111         err := handleNamedInput(s, func(r io.Reader) error {
 112             sc := newScanner(r)
 113 
 114             for sc.Scan() {
 115                 s := sc.Text()
 116                 if strings.Contains(s, "\t") {
 117                     sb.Reset()
 118                     expand(s, tabstop, &sb)
 119                     s = strings.Clone(sb.String())
 120                 }
 121                 lines = append(lines, s)
 122             }
 123             return sc.Err()
 124         })
 125 
 126         if err != nil {
 127             return lines, err
 128         }
 129     }
 130 
 131     return lines, nil
 132 }
 133 
 134 // handleNamedInput makes opening/closing files more convenient by using
 135 // callbacks to handle processing; this func also handles recognizing `-`
 136 // as meaning stdin
 137 func handleNamedInput(path string, fn func(r io.Reader) error) error {
 138     if path == `-` {
 139         return fn(os.Stdin)
 140     }
 141 
 142     f, err := os.Open(path)
 143     if err != nil {
 144         return err
 145     }
 146     defer f.Close()
 147     return fn(f)
 148 }
 149 
 150 // handleLines handles the use-case of showing/rearranging lines from a
 151 // single input source (presumably standard input) into several columns
 152 func handleLines(w *bufio.Writer, lines []string, ncols int) error {
 153     if ncols < 1 {
 154         return nil
 155     }
 156 
 157     if ncols == 1 {
 158         for _, s := range lines {
 159             w.WriteString(s)
 160             err := w.WriteByte('\n')
 161             if err != nil {
 162                 // assume error probably results from a closed stdout
 163                 // pipe, so quit the app right away without complaining
 164                 return err
 165             }
 166         }
 167         return nil
 168     }
 169 
 170     // nothing to show, so don't even bother
 171     if len(lines) == 0 {
 172         return nil
 173     }
 174 
 175     cols, height := splitLines(lines, ncols)
 176     widths := make([]int, 0, len(cols))
 177     for _, c := range cols {
 178         // find the max width of all lines of the current column
 179         maxw := 0
 180         for _, v := range c {
 181             w := width(v)
 182             if w > maxw {
 183                 maxw = w
 184             }
 185         }
 186 
 187         widths = append(widths, maxw)
 188     }
 189 
 190     // endSep is right-trimmed to avoid unneeded trailing spaces on output
 191     // lines whose last column is an empty/missing input line
 192     endSep := strings.TrimRight(separator, ` `)
 193 
 194     // show columns side by side
 195     for r := 0; r < height; r++ {
 196         for c := 0; c < len(cols); c++ {
 197             badr := r >= len(cols[c])
 198 
 199             // clearly separate columns visually
 200             if c > 0 {
 201                 if c == len(cols)-1 && (badr || cols[c][r] == ``) {
 202                     // avoid unneeded trailing spaces
 203                     w.WriteString(endSep)
 204                 } else {
 205                     w.WriteString(separator)
 206                 }
 207             }
 208 
 209             if badr {
 210                 // exceeding items for this (last) column
 211                 continue
 212             }
 213 
 214             // pad all columns, except the last
 215             width := 0
 216             if c < len(cols)-1 {
 217                 width = widths[c]
 218             }
 219 
 220             // emit maybe-padded column
 221             writeItem(w, cols[c][r], width)
 222         }
 223 
 224         // end the line
 225         err := w.WriteByte('\n')
 226         if err != nil {
 227             // probably a pipe was closed
 228             return nil
 229         }
 230     }
 231 
 232     return nil
 233 }
     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(err.Error())
 108         os.Stderr.WriteString("\n")
 109         os.Exit(1)
 110         return
 111     }
 112 }
 113 
 114 func seq(first, incr, last *big.Rat, zeroPad bool) error {
 115     if first.IsInt() && incr.IsInt() && last.IsInt() {
 116         f := int(first.Num().Int64())
 117         i := int(incr.Num().Int64())
 118         l := int(last.Num().Int64())
 119         return seqInt(f, i, l, zeroPad)
 120     }
 121 
 122     return seqFrac(first, incr, last, zeroPad)
 123 }
 124 
 125 func seqInt(first, incr, last int, zeroPad bool) error {
 126     if incr == 0 {
 127         return nil
 128     }
 129 
 130     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 131     defer w.Flush()
 132 
 133     var buf [24]byte
 134     var maxlen int
 135 
 136     cur := first
 137     if zeroPad {
 138         var n int
 139         n = len(strconv.AppendInt(buf[:0], int64(first), 10))
 140         if maxlen < n {
 141             maxlen = n
 142         }
 143         n = len(strconv.AppendInt(buf[:0], int64(last), 10))
 144         if maxlen < n {
 145             maxlen = n
 146         }
 147     }
 148 
 149     for {
 150         if incr > 0 && cur > last {
 151             break
 152         }
 153         if incr < 0 && cur < last {
 154             break
 155         }
 156 
 157         s := strconv.AppendInt(buf[:0], int64(cur), 10)
 158         if maxlen > 0 {
 159             writeZeros(w, maxlen-len(s))
 160         }
 161         w.Write(s)
 162 
 163         if w.WriteByte('\n') != nil {
 164             return io.EOF
 165         }
 166 
 167         cur += incr
 168     }
 169 
 170     return nil
 171 }
 172 
 173 func seqFrac(first, incr, last *big.Rat, zeroPad bool) error {
 174     incrSign := incr.Sign()
 175     if incrSign == 0 {
 176         return nil
 177     }
 178 
 179     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 180     defer w.Flush()
 181 
 182     var maxlen, prec int
 183 
 184     var p int
 185     p = maxPrec(first)
 186     if prec < p {
 187         prec = p
 188     }
 189     p = maxPrec(incr)
 190     if prec < p {
 191         prec = p
 192     }
 193     p = maxPrec(last)
 194     if prec < p {
 195         prec = p
 196     }
 197 
 198     cur := first
 199     if zeroPad {
 200         var n int
 201         n = len(first.FloatString(prec))
 202         if maxlen < n {
 203             maxlen = n
 204         }
 205         n = len(last.FloatString(prec))
 206         if maxlen < n {
 207             maxlen = n
 208         }
 209     }
 210 
 211     for {
 212         if incrSign > 0 && cur.Cmp(last) > 0 {
 213             break
 214         }
 215         if incrSign < 0 && cur.Cmp(last) < 0 {
 216             break
 217         }
 218 
 219         s := cur.FloatString(prec)
 220         if maxlen > 0 {
 221             writeZeros(w, maxlen-len(s))
 222         }
 223         w.WriteString(s)
 224 
 225         if w.WriteByte('\n') != nil {
 226             return io.EOF
 227         }
 228 
 229         cur = cur.Add(cur, incr)
 230     }
 231 
 232     return nil
 233 }
 234 
 235 func maxPrec(n *big.Rat) int {
 236     if p, exact := n.FloatPrec(); exact {
 237         return p
 238     }
 239 
 240     s := n.FloatString(50)
 241     i := strings.IndexByte(s, '.')
 242     if i < 0 {
 243         return 0
 244     }
 245 
 246     s = s[i+1:]
 247     for len(s) > 0 && s[len(s)-1] == '0' {
 248         s = s[:len(s)-1]
 249     }
 250     return len(s)
 251 }
 252 
 253 func writeZeros(w *bufio.Writer, n int) {
 254     const (
 255         half  = `00000000000000000000000000000000`
 256         zeros = half + half
 257     )
 258 
 259     for n >= len(zeros) {
 260         w.WriteString(zeros)
 261         n -= len(zeros)
 262     }
 263     if n > 0 {
 264         w.WriteString(zeros[:n])
 265     }
 266 }
     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("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(err.Error())
 114         os.Stderr.WriteString("\n")
 115         os.Exit(1)
 116         return
 117     }
 118 }
 119 
 120 func run(paths []string, cfg *config) error {
 121     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 122     defer w.Flush()
 123 
 124     for _, path := range paths {
 125         if err := handleFile(w, path, cfg); err != nil {
 126             return err
 127         }
 128     }
 129 
 130     if len(paths) == 0 {
 131         if err := handleLines(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 handleLines(w, f, cfg)
 145 }
 146 
 147 func handleLines(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.skip > 0 {
 154             cfg.skip--
 155             continue
 156         }
 157 
 158         w.Write(sc.Bytes())
 159         if err := w.WriteByte('\n'); err != nil {
 160             return io.EOF
 161         }
 162 
 163         if !cfg.liveLines {
 164             continue
 165         }
 166 
 167         if w.Flush() != nil {
 168             return io.EOF
 169         }
 170     }
 171 
 172     return sc.Err()
 173 }
     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(err.Error())
 118         os.Stderr.WriteString("\n")
 119         os.Exit(1)
 120         return
 121     }
 122 }
 123 
 124 type config struct {
 125     w    *bufio.Writer
 126     live bool
 127 }
 128 
 129 func run(paths []string, rb *ringBuffer, live bool) error {
 130     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 131     defer bw.Flush()
 132 
 133     var cfg config
 134     cfg.w = bw
 135     cfg.live = live
 136 
 137     for _, path := range paths {
 138         if err := handleFile(path, rb, cfg); err != nil {
 139             return err
 140         }
 141     }
 142 
 143     if len(paths) == 0 {
 144         if err := visit(os.Stdin, rb, cfg); err != nil {
 145             return err
 146         }
 147     }
 148     return nil
 149 }
 150 
 151 func handleFile(path string, rb *ringBuffer, cfg config) error {
 152     f, err := os.Open(path)
 153     if err != nil {
 154         return err
 155     }
 156     defer f.Close()
 157     return visit(f, rb, cfg)
 158 }
 159 
 160 func visit(r io.Reader, rb *ringBuffer, cfg config) error {
 161     const gb = 1024 * 1024 * 1024
 162     sc := bufio.NewScanner(r)
 163     sc.Buffer(nil, 8*gb)
 164 
 165     for sc.Scan() {
 166         prev := rb.append(sc.Text())
 167         if len(rb.items) < rb.max {
 168             continue
 169         }
 170 
 171         cfg.w.WriteString(prev)
 172 
 173         if cfg.w.WriteByte('\n') != nil {
 174             return io.EOF
 175         }
 176 
 177         if !cfg.live {
 178             continue
 179         }
 180 
 181         if cfg.w.Flush() != nil {
 182             return io.EOF
 183         }
 184     }
 185 
 186     return sc.Err()
 187 }
     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(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.NewWriter(w)
  80     defer bw.Flush()
  81 
  82     if len(args) == 0 {
  83         return squeeze(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 squeeze(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 squeeze(w, f, live)
 106 }
 107 
 108 func squeeze(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 
 113     for i := 0; sc.Scan(); i++ {
 114         s := sc.Bytes()
 115         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 116             s = s[3:]
 117         }
 118 
 119         writeSqueezed(w, s)
 120         if w.WriteByte('\n') != nil {
 121             return io.EOF
 122         }
 123 
 124         if !live {
 125             continue
 126         }
 127 
 128         if w.Flush() != nil {
 129             return io.EOF
 130         }
 131     }
 132 
 133     return sc.Err()
 134 }
 135 
 136 func writeSqueezed(w *bufio.Writer, s []byte) {
 137     // ignore leading spaces
 138     for len(s) > 0 && s[0] == ' ' {
 139         s = s[1:]
 140     }
 141 
 142     // ignore trailing spaces
 143     for len(s) > 0 && s[len(s)-1] == ' ' {
 144         s = s[:len(s)-1]
 145     }
 146 
 147     space := false
 148 
 149     for len(s) > 0 {
 150         switch s[0] {
 151         case ' ':
 152             s = s[1:]
 153             space = true
 154 
 155         case '\t':
 156             s = s[1:]
 157             space = false
 158             for len(s) > 0 && s[0] == ' ' {
 159                 s = s[1:]
 160             }
 161             w.WriteByte('\t')
 162 
 163         default:
 164             if space {
 165                 w.WriteByte(' ')
 166                 space = false
 167             }
 168             w.WriteByte(s[0])
 169             s = s[1:]
 170         }
 171     }
 172 }
     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(err.Error())
  87         os.Stderr.WriteString("\n")
  88         os.Exit(1)
  89         return
  90     }
  91 }
  92 
  93 func run(w io.Writer, args []string, cfg config) error {
  94     bw := bufio.NewWriter(w)
  95     defer bw.Flush()
  96 
  97     if len(args) == 0 {
  98         return squomp(bw, os.Stdin, &cfg)
  99     }
 100 
 101     for _, name := range args {
 102         if err := handleFile(bw, name, &cfg); err != nil {
 103             return err
 104         }
 105     }
 106     return nil
 107 }
 108 
 109 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 110     if name == `` || name == `-` {
 111         return squomp(w, os.Stdin, cfg)
 112     }
 113 
 114     f, err := os.Open(name)
 115     if err != nil {
 116         return errors.New(`can't read from file named "` + name + `"`)
 117     }
 118     defer f.Close()
 119 
 120     return squomp(w, f, cfg)
 121 }
 122 
 123 func squomp(w *bufio.Writer, r io.Reader, cfg *config) error {
 124     const gb = 1024 * 1024 * 1024
 125     sc := bufio.NewScanner(r)
 126     sc.Buffer(nil, 8*gb)
 127 
 128     for i := 0; sc.Scan(); i++ {
 129         s := sc.Bytes()
 130         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 131             s = s[3:]
 132         }
 133 
 134         // trim leading spaces, so the empty-length check which follows can
 135         // also detect/ignore empty-ish lines, that is lines with only spaces
 136         for len(s) > 0 && s[0] == ' ' {
 137             s = s[1:]
 138         }
 139 
 140         if len(s) == 0 {
 141             if cfg.nonEmpty == 0 {
 142                 continue
 143             }
 144             cfg.emptyRun++
 145         }
 146 
 147         if cfg.emptyRun > 0 {
 148             cfg.emptyRun = 0
 149             if w.WriteByte('\n') != nil {
 150                 return io.EOF
 151             }
 152         }
 153 
 154         cfg.nonEmpty++
 155 
 156         writeSqueezed(w, s)
 157         if w.WriteByte('\n') != nil {
 158             return io.EOF
 159         }
 160 
 161         if !cfg.liveLines {
 162             continue
 163         }
 164 
 165         if w.Flush() != nil {
 166             return io.EOF
 167         }
 168     }
 169 
 170     return sc.Err()
 171 }
 172 
 173 func writeSqueezed(w *bufio.Writer, s []byte) {
 174     // ignore leading spaces
 175     for len(s) > 0 && s[0] == ' ' {
 176         s = s[1:]
 177     }
 178 
 179     // ignore trailing spaces
 180     for len(s) > 0 && s[len(s)-1] == ' ' {
 181         s = s[:len(s)-1]
 182     }
 183 
 184     space := false
 185 
 186     for len(s) > 0 {
 187         switch s[0] {
 188         case ' ':
 189             s = s[1:]
 190             space = true
 191 
 192         case '\t':
 193             s = s[1:]
 194             space = false
 195             for len(s) > 0 && s[0] == ' ' {
 196                 s = s[1:]
 197             }
 198             w.WriteByte('\t')
 199 
 200         default:
 201             if space {
 202                 w.WriteByte(' ')
 203                 space = false
 204             }
 205             w.WriteByte(s[0])
 206             s = s[1:]
 207         }
 208     }
 209 }
     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(err.Error())
  85         os.Stderr.WriteString("\n")
  86         os.Exit(1)
  87         return
  88     }
  89 }
  90 
  91 func run(w io.Writer, args []string, cfg config) error {
  92     bw := bufio.NewWriter(w)
  93     defer bw.Flush()
  94 
  95     dashes := 0
  96     for _, name := range args {
  97         if name == `-` {
  98             dashes++
  99         }
 100         if dashes > 1 {
 101             return errors.New(`can't read stdin (dash) more than once`)
 102         }
 103     }
 104 
 105     if len(args) == 0 {
 106         return stomp(bw, os.Stdin, &cfg)
 107     }
 108 
 109     for _, name := range args {
 110         if err := handleFile(bw, name, &cfg); err != nil {
 111             return err
 112         }
 113     }
 114     return nil
 115 }
 116 
 117 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 118     if name == `` || name == `-` {
 119         return stomp(w, os.Stdin, cfg)
 120     }
 121 
 122     f, err := os.Open(name)
 123     if err != nil {
 124         return errors.New(`can't read from file named "` + name + `"`)
 125     }
 126     defer f.Close()
 127 
 128     return stomp(w, f, cfg)
 129 }
 130 
 131 func stomp(w *bufio.Writer, r io.Reader, cfg *config) error {
 132     const gb = 1024 * 1024 * 1024
 133     sc := bufio.NewScanner(r)
 134     sc.Buffer(nil, 8*gb)
 135 
 136     for i := 0; sc.Scan(); i++ {
 137         s := sc.Bytes()
 138         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 139             s = s[3:]
 140         }
 141 
 142         if len(s) == 0 {
 143             if cfg.nonEmpty == 0 {
 144                 continue
 145             }
 146             cfg.emptyRun++
 147         }
 148 
 149         if cfg.emptyRun > 0 {
 150             cfg.emptyRun = 0
 151             if w.WriteByte('\n') != nil {
 152                 return io.EOF
 153             }
 154         }
 155 
 156         cfg.nonEmpty++
 157 
 158         w.Write(s)
 159         if w.WriteByte('\n') != nil {
 160             return io.EOF
 161         }
 162 
 163         if !cfg.liveLines {
 164             continue
 165         }
 166 
 167         if w.Flush() != nil {
 168             return io.EOF
 169         }
 170     }
 171 
 172     return sc.Err()
 173 }
     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(err.Error())
  84         os.Stderr.WriteString("\n")
  85         os.Exit(1)
  86         return
  87     }
  88 
  89     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  90     defer w.Flush()
  91 
  92     for i := len(cfg.lines) - 1; i >= 0; i-- {
  93         w.WriteString(cfg.lines[i])
  94         if w.WriteByte('\n') != nil {
  95             break
  96         }
  97     }
  98 }
  99 
 100 func run(w io.Writer, args []string, cfg *config) error {
 101     dashes := 0
 102     for _, name := range args {
 103         if name == `-` {
 104             dashes++
 105         }
 106         if dashes > 1 {
 107             return errors.New(`can't read stdin (dash) more than once`)
 108         }
 109     }
 110 
 111     if len(args) == 0 {
 112         return getLines(os.Stdin, cfg)
 113     }
 114 
 115     for _, name := range args {
 116         if name == `-` {
 117             if err := getLines(os.Stdin, cfg); err != nil {
 118                 return err
 119             }
 120             continue
 121         }
 122 
 123         if err := handleFile(name, cfg); err != nil {
 124             return err
 125         }
 126     }
 127     return nil
 128 }
 129 
 130 func handleFile(name string, cfg *config) error {
 131     if name == `` || name == `-` {
 132         return getLines(os.Stdin, cfg)
 133     }
 134 
 135     f, err := os.Open(name)
 136     if err != nil {
 137         return errors.New(`can't read from file named "` + name + `"`)
 138     }
 139     defer f.Close()
 140 
 141     return getLines(f, cfg)
 142 }
 143 
 144 func getLines(r io.Reader, cfg *config) error {
 145     const gb = 1024 * 1024 * 1024
 146     sc := bufio.NewScanner(r)
 147     sc.Buffer(nil, 8*gb)
 148     if cfg.null {
 149         sc.Split(splitNull)
 150     }
 151 
 152     for i := 0; sc.Scan(); i++ {
 153         s := sc.Text()
 154         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 155             s = s[3:]
 156         }
 157 
 158         cfg.lines = append(cfg.lines, sc.Text())
 159     }
 160 
 161     return sc.Err()
 162 }
 163 
 164 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
 165 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
 166     // handle leading null-terminated line, if found in the current chunk
 167     if i := bytes.IndexByte(data, 0); i >= 0 {
 168         return i + 1, data[:i], nil
 169     }
 170 
 171     // request more data, in case there's a null coming up later
 172     if !atEOF {
 173         return 0, nil, nil
 174     }
 175 
 176     // handle non-empty non-terminated last chunk
 177     if len(data) > 0 {
 178         return len(data), data, bufio.ErrFinalToken
 179     }
 180 
 181     // handle empty non-terminated last chunk
 182     return 0, nil, bufio.ErrFinalToken
 183 }
     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("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(err.Error())
 131         os.Stderr.WriteString("\n")
 132         os.Exit(1)
 133         return
 134     }
 135 
 136     show(os.Stdout, latest)
 137 }
 138 
 139 func run(paths []string, rb *ringBuffer) error {
 140     for _, path := range paths {
 141         if err := handleFile(path, rb); err != nil {
 142             return err
 143         }
 144     }
 145 
 146     if len(paths) == 0 {
 147         if err := visit(os.Stdin, rb); err != nil {
 148             return err
 149         }
 150     }
 151     return nil
 152 }
 153 
 154 func show(w io.Writer, rb ringBuffer) {
 155     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 156     defer bw.Flush()
 157 
 158     for i := rb.next; i < len(rb.items); i++ {
 159         bw.WriteString(rb.items[i])
 160         if err := bw.WriteByte('\n'); err != nil {
 161             return
 162         }
 163     }
 164 
 165     for i := 0; i < len(rb.items) && i < rb.next; i++ {
 166         bw.WriteString(rb.items[i])
 167         if err := bw.WriteByte('\n'); err != nil {
 168             return
 169         }
 170     }
 171 }
 172 
 173 func handleFile(path string, rb *ringBuffer) error {
 174     f, err := os.Open(path)
 175     if err != nil {
 176         return err
 177     }
 178     defer f.Close()
 179     return visit(f, rb)
 180 }
 181 
 182 func visit(r io.Reader, rb *ringBuffer) error {
 183     const gb = 1024 * 1024 * 1024
 184     sc := bufio.NewScanner(r)
 185     sc.Buffer(nil, 8*gb)
 186 
 187     for sc.Scan() {
 188         rb.append(sc.Text())
 189     }
 190     return sc.Err()
 191 }
     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(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.NewWriter(w)
 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 tcatl(bw, os.Stdin, `<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 := tcatl(bw, os.Stdin, `<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 {
 145                 continue
 146             }
 147 
 148             if err := bw.Flush(); err != nil {
 149                 return io.EOF
 150             }
 151 
 152             continue
 153         }
 154 
 155         if err := handleFile(bw, name, cfg); err != nil {
 156             return err
 157         }
 158     }
 159     return nil
 160 }
 161 
 162 func handleFile(w *bufio.Writer, name string, cfg config) error {
 163     if name == `` || name == `-` {
 164         return tcatl(w, os.Stdin, `<stdin>`, cfg)
 165     }
 166 
 167     f, err := os.Open(name)
 168     if err != nil {
 169         return errors.New(`can't read from file named "` + name + `"`)
 170     }
 171     defer f.Close()
 172 
 173     return tcatl(w, f, name, cfg)
 174 }
 175 
 176 func tcatl(w *bufio.Writer, r io.Reader, name string, cfg config) error {
 177     w.WriteString("\x1b[7m")
 178     w.WriteString(name)
 179     writeSpaces(w, 80-utf8.RuneCountInString(name))
 180     w.WriteString("\x1b[0m\n")
 181     if err := w.Flush(); err != nil {
 182         // a write error may be the consequence of stdout being closed,
 183         // perhaps by another app along a pipe
 184         return io.EOF
 185     }
 186 
 187     if !cfg.liveLines {
 188         return catlFast(w, r, cfg.null)
 189     }
 190 
 191     const gb = 1024 * 1024 * 1024
 192     sc := bufio.NewScanner(r)
 193     sc.Buffer(nil, 8*gb)
 194     if cfg.null {
 195         sc.Split(splitNull)
 196     }
 197 
 198     for i := 0; sc.Scan(); i++ {
 199         s := sc.Bytes()
 200         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 201             s = s[3:]
 202         }
 203 
 204         w.Write(s)
 205         if w.WriteByte('\n') != nil {
 206             return io.EOF
 207         }
 208 
 209         if w.Flush() != nil {
 210             return io.EOF
 211         }
 212     }
 213 
 214     return sc.Err()
 215 }
 216 
 217 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
 218     var buf [32 * 1024]byte
 219     var last byte = '\n'
 220 
 221     for i := 0; true; i++ {
 222         n, err := r.Read(buf[:])
 223         if n > 0 && err == io.EOF {
 224             err = nil
 225         }
 226         if err == io.EOF {
 227             if last != '\n' {
 228                 w.WriteByte('\n')
 229             }
 230             return nil
 231         }
 232 
 233         if err != nil {
 234             return err
 235         }
 236 
 237         chunk := buf[:n]
 238         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 239             chunk = chunk[3:]
 240         }
 241 
 242         // change nulls into line-feeds to handle null-terminated lines
 243         if null {
 244             for i, b := range chunk {
 245                 if b == 0 {
 246                     chunk[i] = '\n'
 247                 }
 248             }
 249         }
 250 
 251         if len(chunk) >= 1 {
 252             if _, err := w.Write(chunk); err != nil {
 253                 return io.EOF
 254             }
 255             last = chunk[len(chunk)-1]
 256         }
 257     }
 258 
 259     return nil
 260 }
 261 
 262 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
 263 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
 264     // handle leading null-terminated line, if found in the current chunk
 265     if i := bytes.IndexByte(data, 0); i >= 0 {
 266         return i + 1, data[:i], nil
 267     }
 268 
 269     // request more data, in case there's a null coming up later
 270     if !atEOF {
 271         return 0, nil, nil
 272     }
 273 
 274     // handle non-empty non-terminated last chunk
 275     if len(data) > 0 {
 276         return len(data), data, bufio.ErrFinalToken
 277     }
 278 
 279     // handle empty non-terminated last chunk
 280     return 0, nil, bufio.ErrFinalToken
 281 }
 282 
 283 // writeSpaces bulk-emits the number of spaces given
 284 func writeSpaces(w *bufio.Writer, n int) {
 285     const spaces = `                                `
 286     for ; n > len(spaces); n -= len(spaces) {
 287         w.WriteString(spaces)
 288     }
 289     if n > 0 {
 290         w.WriteString(spaces[:n])
 291     }
 292 }
     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(err.Error())
  65         os.Stderr.WriteString("\n")
  66         os.Exit(1)
  67         return
  68     }
  69 }
  70 
  71 func run(w io.Writer, args []string) error {
  72     dashes := 0
  73     for _, name := range args {
  74         if name == `-` {
  75             dashes++
  76         }
  77         if dashes > 1 {
  78             return errors.New(`can't read stdin (dash) more than once`)
  79         }
  80     }
  81 
  82     if len(args) == 0 {
  83         return teletype(w, os.Stdin)
  84     }
  85 
  86     for _, name := range args {
  87         if name == `-` {
  88             if err := teletype(w, os.Stdin); err != nil {
  89                 return err
  90             }
  91             continue
  92         }
  93 
  94         if err := handleFile(w, name); err != nil {
  95             return err
  96         }
  97     }
  98     return nil
  99 }
 100 
 101 func handleFile(w io.Writer, name string) error {
 102     if name == `` || name == `-` {
 103         return teletype(w, os.Stdin)
 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 teletype(w, f)
 113 }
 114 
 115 func teletype(w io.Writer, 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         s := sc.Text()
 122         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 123             s = s[3:]
 124         }
 125 
 126         var buf [4]byte
 127         for _, r := range s {
 128             time.Sleep(15 * time.Millisecond)
 129             if _, err := w.Write(utf8.AppendRune(buf[:0], r)); err != nil {
 130                 return io.EOF
 131             }
 132         }
 133 
 134         time.Sleep(750 * time.Millisecond)
 135         if _, err := w.Write([]byte{'\n'}); err != nil {
 136             return io.EOF
 137         }
 138     }
 139 
 140     return sc.Err()
 141 }
     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     "io/fs"
  31     "math"
  32     "os"
  33     "sort"
  34     "strconv"
  35     "time"
  36     "unicode/utf8"
  37 
  38     "./now"
  39     "./zcat"
  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 err := w.WriteByte('\n'); err != 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 func decompress() {
  89     zcat.Main()
  90 }
  91 
  92 const divInfo = `
  93 div [options...] [x...] [y]
  94 
  95 DIVide 2 numbers in 3 different ways, showing each result in its own line.
  96 
  97 When given 2 numbers, the results are x / y, y / x, and 1 - (x / y), where
  98 x is the smaller number, and y is the larger number.
  99 
 100 When given just 1 number, the second number is 1 by default, showing you the
 101 inverse of the number given explicitly, among the other results.
 102 
 103 The options are, available both in single and double-dash versions
 104 
 105     -h, -help    show this help message
 106 `
 107 
 108 func div() {
 109     args := os.Args[1:]
 110 
 111     if len(args) > 0 {
 112         switch args[0] {
 113         case `-h`, `--h`, `-help`, `--help`:
 114             os.Stdout.WriteString(divInfo[1:])
 115             return
 116         }
 117     }
 118 
 119     if len(args) > 0 && args[0] == `--` {
 120         args = args[1:]
 121     }
 122 
 123     var nums []float64
 124 
 125     for len(args) > 0 {
 126         f, err := strconv.ParseFloat(args[0], 64)
 127         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 128             nums = append(nums, f)
 129             args = args[1:]
 130             continue
 131         }
 132 
 133         break
 134     }
 135 
 136     switch len(nums) {
 137     case 0:
 138         os.Stderr.WriteString(divInfo[1:])
 139         os.Exit(1)
 140         return
 141 
 142     case 1:
 143         nums = append(nums, nums[0])
 144         nums[0] = 1
 145     }
 146 
 147     if len(nums)%2 != 0 {
 148         os.Stderr.WriteString(divInfo[1:])
 149         os.Exit(1)
 150         return
 151     }
 152 
 153     for len(nums) >= 2 {
 154         x, y := nums[0], nums[1]
 155         nums = nums[2:]
 156 
 157         if x > y {
 158             x, y = y, x
 159         }
 160         comp := 1 - (x / y)
 161 
 162         const prec = 6
 163         os.Stdout.WriteString(strconv.FormatFloat(x/y, 'f', prec, 64) + "\n")
 164         os.Stdout.WriteString(strconv.FormatFloat(y/x, 'f', prec, 64) + "\n")
 165         os.Stdout.WriteString(strconv.FormatFloat(comp, 'f', prec, 64) + "\n")
 166     }
 167 }
 168 
 169 func echo() {
 170     args := os.Args[1:]
 171     if len(args) == 0 {
 172         os.Stdout.WriteString("\n")
 173         return
 174     }
 175 
 176     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 177     defer w.Flush()
 178 
 179     for i, s := range args {
 180         if i > 0 {
 181             if err := w.WriteByte(' '); err != nil {
 182                 return
 183             }
 184         }
 185         w.WriteString(s)
 186     }
 187 
 188     w.WriteByte('\n')
 189 }
 190 
 191 func echobar() {
 192     const (
 193         half   = `                                        `
 194         spaces = half + half
 195     )
 196 
 197     args := os.Args[1:]
 198     if len(args) == 0 {
 199         os.Stdout.WriteString("\x1b[7m" + spaces + "\x1b[0m\n")
 200         return
 201     }
 202 
 203     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 204     defer w.Flush()
 205 
 206     w.WriteString("\x1b[7m")
 207 
 208     left := len(spaces)
 209 
 210     for i, s := range args {
 211         if i > 0 {
 212             if err := w.WriteByte(' '); err != nil {
 213                 return
 214             }
 215             left--
 216         }
 217         w.WriteString(s)
 218         left -= utf8.RuneCountInString(s)
 219     }
 220 
 221     if 0 < left && left < len(spaces) {
 222         w.WriteString(spaces[:left])
 223     }
 224     w.WriteString("\x1b[0m\n")
 225 }
 226 
 227 const failInfo = `
 228 fail [options...] [exit code...]
 229 
 230 Fail with the exit code given. If no exit code is given, fail with code 1 by
 231 default. If given code 0, this tool paradoxically succeeds, as code 0 means
 232 success.
 233 
 234 The options are, available both in single and double-dash versions
 235 
 236     -h, -help    show this help message
 237 `
 238 
 239 func fail() {
 240     args := os.Args[1:]
 241 
 242     if len(args) > 0 {
 243         switch args[0] {
 244         case `-h`, `--h`, `-help`, `--help`:
 245             os.Stdout.WriteString(failInfo[1:])
 246             return
 247         }
 248     }
 249 
 250     if len(args) > 0 && args[0] == `--` {
 251         args = args[1:]
 252     }
 253 
 254     code := 1
 255     if len(args) > 0 {
 256         if n, err := strconv.ParseInt(args[0], 10, 64); err == nil {
 257             code = int(n)
 258         }
 259     }
 260 
 261     os.Exit(code)
 262     return
 263 }
 264 
 265 const falseInfo = `
 266 false [options...]
 267 
 268 Quit right away, using exit code 1.
 269 
 270 All (optional) leading options start with either single or double-dash:
 271 
 272     -h, -help    show this help message
 273 `
 274 
 275 func falseMain() {
 276     if len(os.Args) > 1 {
 277         switch os.Args[1] {
 278         case `-h`, `--h`, `-help`, `--help`, `help`:
 279             os.Stdout.WriteString(falseInfo[1:])
 280             return
 281         }
 282     }
 283 
 284     os.Exit(1)
 285     return
 286 }
 287 
 288 const ignoreInfo = `
 289 ignore [options...] [command...] [command args...]
 290 
 291 Ignore the command given, acting as a stdio-passthru instead. This allows
 292 quick editing of pipes to temporarily exclude a pipe-step, while still
 293 having the disabled step show in the pipe-chain as a memo of sorts.
 294 
 295 The options are, available both in single and double-dash versions
 296 
 297     -h, -help       show this help message
 298 `
 299 
 300 func ignore() {
 301     if args := os.Args[1:]; len(args) > 0 {
 302         switch args[0] {
 303         case `-h`, `--h`, `-help`, `--help`, `help`:
 304             os.Stdout.WriteString(ignoreInfo[1:])
 305             return
 306         }
 307     }
 308 
 309     var buf [32 * 1024]byte
 310     for {
 311         got, err := os.Stdin.Read(buf[:])
 312         if got > 0 {
 313             if _, err := os.Stdout.Write(buf[:got]); err != nil {
 314                 break
 315             }
 316         }
 317         if err != nil {
 318             break
 319         }
 320     }
 321 }
 322 
 323 func hecho() {
 324     args := os.Args[1:]
 325     if len(args) == 0 {
 326         os.Stdout.WriteString("\n")
 327         return
 328     }
 329 
 330     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 331     defer w.Flush()
 332 
 333     w.WriteString("\x1b[7m")
 334 
 335     for i, s := range args {
 336         if i > 0 {
 337             if err := w.WriteByte(' '); err != nil {
 338                 return
 339             }
 340         }
 341         w.WriteString(s)
 342     }
 343 
 344     w.WriteString("\x1b[0m\n")
 345 }
 346 
 347 const mopInfo = `
 348 mop [options...] [x...] [y]
 349 
 350 Multiple OPerations runs multiple arithmetic calculations on 2 numbers,
 351 showing each result in its own line.
 352 
 353 The options are, available both in single and double-dash versions
 354 
 355     -h, -help    show this help message
 356 `
 357 
 358 func mop() {
 359     args := os.Args[1:]
 360 
 361     if len(args) > 0 {
 362         switch args[0] {
 363         case `-h`, `--h`, `-help`, `--help`:
 364             os.Stdout.WriteString(mopInfo[1:])
 365             return
 366         }
 367     }
 368 
 369     if len(args) > 0 && args[0] == `--` {
 370         args = args[1:]
 371     }
 372 
 373     var nums []float64
 374 
 375     for len(args) > 0 {
 376         f, err := strconv.ParseFloat(args[0], 64)
 377         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 378             nums = append(nums, f)
 379             args = args[1:]
 380             continue
 381         }
 382 
 383         break
 384     }
 385 
 386     if len(nums) == 0 || len(nums)%2 != 0 {
 387         os.Stderr.WriteString(mopInfo[1:])
 388         os.Exit(1)
 389         return
 390     }
 391 
 392     for len(nums) >= 2 {
 393         x, y := nums[0], nums[1]
 394         nums = nums[2:]
 395 
 396         if x > y {
 397             x, y = y, x
 398         }
 399 
 400         fmt.Printf("%.6f + %.6f  ~  %.6f\n", x, y, x+y)
 401         fmt.Printf("%.6f - %.6f  ~  %.6f\n", x, y, x-y)
 402         fmt.Printf("%.6f * %.6f  ~  %.6f\n", x, y, x*y)
 403         fmt.Printf("%.6f / %.6f  ~  %.6f\n", x, y, x/y)
 404         fmt.Printf("%.6f / %.6f  ~  %.6f\n", y, x, y/x)
 405         fmt.Printf("%.6f %% %.6f  ~  %.6f\n", x, y, math.Mod(x, y))
 406         fmt.Printf("%.6f %% %.6f  ~  %.6f\n", y, x, math.Mod(y, x))
 407         fmt.Printf("%.6f ** %.6f  ~  %.6f\n", x, y, math.Pow(x, y))
 408         fmt.Printf("%.6f ** %.6f  ~  %.6f\n", y, x, math.Pow(y, x))
 409     }
 410 }
 411 
 412 const nilInfo = `
 413 nil [options...]
 414 
 415 Emit nothing, also discarding all stdin bytes, if piped.
 416 
 417 All (optional) leading options start with either single or double-dash:
 418 
 419     -h, -help             show this help message
 420 `
 421 
 422 func null() {
 423     if args := os.Args[1:]; len(args) > 0 {
 424         switch args[0] {
 425         case `-h`, `--h`, `-help`, `--help`, `help`:
 426             os.Stdout.WriteString(nilInfo[1:])
 427             return
 428         }
 429     }
 430 
 431     info, err := os.Stdin.Stat()
 432     if err != nil {
 433         os.Stderr.WriteString(err.Error())
 434         os.Stderr.WriteString("\n")
 435         os.Exit(1)
 436         return
 437     }
 438 
 439     piped := int(info.Mode()&fs.ModeNamedPipe) != 0
 440     if !piped {
 441         return
 442     }
 443 
 444     var buf [32 * 1024]byte
 445     for {
 446         _, err := os.Stdin.Read(buf[:])
 447         if err != nil {
 448             break
 449         }
 450     }
 451 }
 452 
 453 func nothing() {
 454     // deliberately do nothing
 455 }
 456 
 457 const prechoInfo = `
 458 precho [options...] [words...]
 459 
 460 PRecede ECHO emits a line with the words given as command-line arguments,
 461 then copies all bytes from the standard input into the standard output.
 462 
 463 The options are, available both in single and double-dash versions
 464 
 465     -h, -help       show this help message
 466 `
 467 
 468 func precho() {
 469     args := os.Args[1:]
 470 
 471     if len(args) > 0 {
 472         switch args[0] {
 473         case `-h`, `--h`, `-help`, `--help`, `help`:
 474             os.Stdout.WriteString(prechoInfo[1:])
 475             return
 476         }
 477     }
 478 
 479     if len(args) > 0 && args[0] == `--` {
 480         args = args[1:]
 481     }
 482 
 483     if len(args) == 0 {
 484         return
 485     }
 486 
 487     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 488 
 489     for i, s := range args {
 490         if i > 0 {
 491             if err := bw.WriteByte(' '); err != nil {
 492                 bw.Flush()
 493                 return
 494             }
 495         }
 496         bw.WriteString(s)
 497     }
 498 
 499     if bw.WriteByte('\n') != nil {
 500         bw.Flush()
 501         return
 502     }
 503     if bw.Flush() != nil {
 504         return
 505     }
 506     bw = nil
 507 
 508     var buf [32 * 1024]byte
 509     for {
 510         got, err := os.Stdin.Read(buf[:])
 511         if got > 0 {
 512             if _, err := os.Stdout.Write(buf[:got]); err != nil {
 513                 break
 514             }
 515         }
 516         if err != nil {
 517             break
 518         }
 519     }
 520 }
 521 
 522 const rulerInfo = `
 523 ruler [width...]
 524 
 525 Emit a line with a ruler-like pattern, which helps check the width of output
 526 lines right above it. If given a valid number, it's used as the ruler width,
 527 or 80 is used as the default width.
 528 
 529 The options are, available both in single and double-dash versions
 530 
 531     -h, -help    show this help message
 532 `
 533 
 534 func ruler() {
 535     if args := os.Args[1:]; len(args) > 0 {
 536         switch args[0] {
 537         case `-h`, `--h`, `-help`, `--help`, `help`:
 538             os.Stdout.WriteString(rulerInfo[1:])
 539             return
 540         }
 541     }
 542 
 543     width := 80
 544     if len(os.Args) > 1 {
 545         if n, err := strconv.Atoi(os.Args[1]); err == nil {
 546             width = n
 547         }
 548     }
 549 
 550     bw := bufio.NewWriter(os.Stdout)
 551     defer bw.Flush()
 552 
 553     // avoid a single line-feed byte for empty rulers
 554     if width < 1 {
 555         return
 556     }
 557 
 558     for width >= 10 {
 559         bw.WriteString(`····╵····│`)
 560         width -= 10
 561     }
 562 
 563     if width >= 5 {
 564         bw.WriteString(`····╵`)
 565         width -= 5
 566     }
 567 
 568     for width > 0 {
 569         bw.WriteRune('·')
 570         width--
 571     }
 572 
 573     bw.WriteByte('\n')
 574 }
 575 
 576 const sleepInfo = `
 577 sleep [options...] [duration/seconds...]
 578 
 579 Wait for the amount of time given.
 580 
 581 All (optional) leading options start with either single or double-dash:
 582 
 583     -h, -help             show this help message
 584 `
 585 
 586 func sleep() {
 587     args := os.Args[1:]
 588 
 589     if len(args) > 0 {
 590         switch args[0] {
 591         case `-h`, `--h`, `-help`, `--help`, `help`:
 592             os.Stdout.WriteString(sleepInfo[1:])
 593             return
 594         }
 595     }
 596 
 597     if len(args) > 0 && args[0] == `--` {
 598         args = args[1:]
 599     }
 600 
 601     if len(args) == 0 {
 602         os.Stderr.WriteString(sleepInfo[1:])
 603         // os.Stderr.WriteString("forgot the duration to wait for\n")
 604         os.Exit(1)
 605         return
 606     }
 607 
 608     var delay time.Duration
 609 
 610     for _, s := range args {
 611         f, err := strconv.ParseFloat(s, 64)
 612         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 613             if f < 0 {
 614                 f = 0
 615             }
 616             delay += time.Duration(f * float64(time.Second))
 617             continue
 618         }
 619 
 620         if d, err := time.ParseDuration(s); err == nil {
 621             delay += d
 622             continue
 623         }
 624 
 625         os.Stderr.WriteString("invalid duration " + s)
 626         os.Stderr.WriteString("\n")
 627         os.Exit(1)
 628         return
 629     }
 630 
 631     time.Sleep(delay)
 632 }
 633 
 634 const timezonesInfo = `
 635 timezones [options...] [places...]
 636 
 637 Lookup full timezone names from the city/place names given.
 638 
 639 All (optional) leading options start with either single or double-dash:
 640 
 641     -h, -help             show this help message
 642 `
 643 
 644 func timezones() {
 645     args := os.Args[1:]
 646 
 647     if len(args) > 0 {
 648         switch args[0] {
 649         case `-h`, `--h`, `-help`, `--help`, `help`:
 650             os.Stdout.WriteString(timezonesInfo[1:])
 651             return
 652         }
 653     }
 654 
 655     if len(args) > 0 && args[0] == `--` {
 656         args = args[1:]
 657     }
 658 
 659     if len(args) == 0 {
 660         os.Stderr.WriteString(timezonesInfo[1:])
 661         return
 662     }
 663 
 664     nerr := 0
 665     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 666     defer w.Flush()
 667 
 668     for _, place := range args {
 669         name, ok := now.LookupName(place)
 670 
 671         if !ok {
 672             w.Flush()
 673             fmt.Fprintf(os.Stderr, "timezone for %q not found\n", place)
 674             nerr++
 675             continue
 676         }
 677 
 678         w.WriteString(name)
 679         w.WriteByte('\n')
 680     }
 681 
 682     if nerr > 0 {
 683         w.Flush()
 684         os.Exit(1)
 685         return
 686     }
 687 }
 688 
 689 const toolsInfo = `
 690 tools [options...]
 691 
 692 Show all tools (officially) available.
 693 
 694 All (optional) leading options start with either single or double-dash:
 695 
 696     -a, -aliases, -all    also show all aliases available, after the tools
 697     -h, -help             show this help message
 698 `
 699 
 700 func tools() {
 701     showAliases := false
 702     if len(os.Args) > 1 {
 703         switch os.Args[1] {
 704         case `-a`, `--a`, `-aliases`, `--aliases`, `-all`, `--all`:
 705             showAliases = true
 706 
 707         case `-h`, `--h`, `-help`, `--help`, `help`:
 708             os.Stdout.WriteString(toolsInfo[1:])
 709             return
 710         }
 711     }
 712 
 713     n := len(mains)
 714     if n < len(aliases) {
 715         n = len(aliases)
 716     }
 717 
 718     names := make([]string, 0, n)
 719     for k := range mains {
 720         names = append(names, k)
 721     }
 722 
 723     sort.Strings(names)
 724 
 725     for _, s := range names {
 726         fmt.Fprintln(os.Stdout, s)
 727     }
 728 
 729     if !showAliases {
 730         return
 731     }
 732 
 733     names = names[:0]
 734     for k := range aliases {
 735         names = append(names, k)
 736     }
 737 
 738     sort.Strings(names)
 739 
 740     for _, s := range names {
 741         fmt.Fprintf(os.Stdout, "%s -> %s\n", s, aliases[s])
 742     }
 743 }
 744 
 745 const trueInfo = `
 746 true [options...]
 747 
 748 Quit right away successfully, using exit code 0.
 749 
 750 All (optional) leading options start with either single or double-dash:
 751 
 752     -h, -help    show this help message
 753 `
 754 
 755 func trueMain() {
 756     if len(os.Args) > 1 {
 757         switch os.Args[1] {
 758         case `-h`, `--h`, `-help`, `--help`, `help`:
 759             os.Stdout.WriteString(trueInfo[1:])
 760             return
 761         }
 762     }
 763 }
 764 
 765 const yesInfo = `
 766 yes [options...] [message...]
 767 
 768 Keep emitting the line with the message given, or "yes" by default.
 769 
 770 All (optional) leading options start with either single or double-dash:
 771 
 772     -h, -help    show this help message
 773 `
 774 
 775 func yes() {
 776     args := os.Args[1:]
 777 
 778     if len(args) > 0 {
 779         switch args[0] {
 780         case `-h`, `--h`, `-help`, `--help`, `help`:
 781             os.Stdout.WriteString(yesInfo[1:])
 782             return
 783         }
 784     }
 785 
 786     if len(args) > 0 && args[0] == `--` {
 787         args = args[1:]
 788     }
 789 
 790     msg := "yes\n"
 791     if len(args) > 0 {
 792         msg = args[0] + "\n"
 793     }
 794 
 795     for {
 796         if _, err := os.Stdout.WriteString(msg); err != nil {
 797             break
 798         }
 799     }
 800 }
     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(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.NewWriter(w)
  84     defer bw.Flush()
  85 
  86     if len(args) == 0 {
  87         return toLower(bw, os.Stdin, live)
  88     }
  89 
  90     for _, name := range args {
  91         if err := handleFile(bw, name, live); err != nil {
  92             return err
  93         }
  94     }
  95     return nil
  96 }
  97 
  98 func handleFile(w *bufio.Writer, name string, live bool) error {
  99     if name == `` || name == `-` {
 100         return toLower(w, os.Stdin, live)
 101     }
 102 
 103     f, err := os.Open(name)
 104     if err != nil {
 105         return errors.New(`can't read from file named "` + name + `"`)
 106     }
 107     defer f.Close()
 108 
 109     return toLower(w, f, live)
 110 }
 111 
 112 func toLower(w *bufio.Writer, r io.Reader, live bool) error {
 113     const gb = 1024 * 1024 * 1024
 114     sc := bufio.NewScanner(r)
 115     sc.Buffer(nil, 8*gb)
 116 
 117     for i := 0; sc.Scan(); i++ {
 118         s := sc.Bytes()
 119         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 120             s = s[3:]
 121         }
 122 
 123         if needsLowercasing(s) {
 124             writeLowercase(w, s)
 125         } else {
 126             w.Write(s)
 127         }
 128 
 129         if w.WriteByte('\n') != nil {
 130             return io.EOF
 131         }
 132 
 133         if !live {
 134             continue
 135         }
 136 
 137         if w.Flush() != nil {
 138             return io.EOF
 139         }
 140     }
 141 
 142     return sc.Err()
 143 }
 144 
 145 func needsLowercasing(src []byte) bool {
 146     for _, b := range src {
 147         if b > 127 {
 148             return true
 149         }
 150         if 'A' <= b && b <= 'Z' {
 151             return true
 152         }
 153     }
 154 
 155     return false
 156 }
 157 
 158 func writeLowercase(w *bufio.Writer, src []byte) {
 159     for len(src) > 0 {
 160         r, size := utf8.DecodeRune(src)
 161         w.WriteRune(unicode.ToLower(r))
 162         src = src[size:]
 163     }
 164 }
     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(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.NewWriter(w)
 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         return handleReader(bw, os.Stdin, &cfg)
 121     }
 122 
 123     for _, name := range args {
 124         if name == `-` {
 125             if err := handleReader(bw, os.Stdin, &cfg); err != nil {
 126                 return err
 127             }
 128             continue
 129         }
 130 
 131         if err := handleFile(bw, name, &cfg); err != nil {
 132             return err
 133         }
 134     }
 135     return nil
 136 }
 137 
 138 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 139     if name == `` || name == `-` {
 140         return handleReader(w, os.Stdin, cfg)
 141     }
 142 
 143     f, err := os.Open(name)
 144     if err != nil {
 145         return errors.New(`can't read from file named "` + name + `"`)
 146     }
 147     defer f.Close()
 148 
 149     return handleReader(w, f, cfg)
 150 }
 151 
 152 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 153     const gb = 1024 * 1024 * 1024
 154     sc := bufio.NewScanner(r)
 155     sc.Buffer(nil, 8*gb)
 156 
 157     every := cfg.every
 158 
 159     for i := 0; sc.Scan(); i++ {
 160         s := sc.Bytes()
 161         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 162             s = s[3:]
 163         }
 164 
 165         cfg.n++
 166         var u bool
 167         if cfg.top {
 168             u = every > 0 && (cfg.n == 1 || ((cfg.n-1)%every == 0))
 169         } else {
 170             u = (every > 0 && cfg.n%every == 0 && cfg.n != 1) || every == 1
 171         }
 172 
 173         if u {
 174             if underline(w, s) != nil {
 175                 return io.EOF
 176             }
 177         } else {
 178             w.Write(s)
 179             if w.WriteByte('\n') != nil {
 180                 return io.EOF
 181             }
 182         }
 183 
 184         if !cfg.liveLines {
 185             continue
 186         }
 187 
 188         if w.Flush() != nil {
 189             return io.EOF
 190         }
 191     }
 192 
 193     return sc.Err()
 194 }
 195 
 196 func underline(w *bufio.Writer, s []byte) error {
 197     w.WriteString("\x1b[4m")
 198     for len(s) > 0 {
 199         i := bytes.Index(s, []byte("\x1b[0m"))
 200         if i < 0 {
 201             break
 202         }
 203 
 204         j := i + len("\x1b[0m")
 205         w.Write(s[:j])
 206         w.WriteString("\x1b[4m")
 207         s = s[j:]
 208     }
 209 
 210     if len(s) > 0 {
 211         w.Write(s)
 212     }
 213 
 214     if _, err := w.WriteString("\x1b[0m\n"); err != nil {
 215         return io.EOF
 216     }
 217     return nil
 218 }
     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 len(args) == 0 {
  64     //  os.Stderr.WriteString(info[1:])
  65     //  os.Exit(1)
  66     //  return
  67     // }
  68 
  69     if err := run(os.Stdout, args); err != nil {
  70         os.Stderr.WriteString(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73         return
  74     }
  75 }
  76 
  77 func run(w io.Writer, args []string) error {
  78     bw := bufio.NewWriter(w)
  79     defer bw.Flush()
  80 
  81     from := ``
  82     low := ``
  83     var values []float64
  84 
  85     dump := func(unit string) bool {
  86         if s, ok := aliases[unit]; ok {
  87             unit = s
  88         }
  89 
  90         c, ok := converters[unit]
  91         if !ok {
  92             return false
  93         }
  94 
  95         for _, v := range values {
  96             res := c.Mul*v + c.Add
  97             const fs = "%.4f  %-4s =  %.4f  %s\n"
  98             fmt.Fprintf(bw, fs, v, unit, res, c.To)
  99         }
 100         return true
 101     }
 102 
 103     for _, s := range args {
 104         f, err := strconv.ParseFloat(s, 64)
 105         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 106             values = append(values, f)
 107             continue
 108         }
 109 
 110         if len(values) == 0 {
 111             values = append(values, 1)
 112         }
 113         from = s
 114         low = strings.ToLower(from)
 115 
 116         if dump(low) {
 117             values = values[:0]
 118             from = ``
 119             low = ``
 120             continue
 121         }
 122 
 123         return fmt.Errorf("unit %q not supported\n", low)
 124     }
 125 
 126     if from == `` {
 127         // return errors.New(`no source units given`)
 128 
 129         if len(values) == 0 {
 130             values = []float64{1}
 131         }
 132 
 133         units := make([]string, 0, len(converters))
 134         for k := range converters {
 135             units = append(units, k)
 136         }
 137         sort.Strings(units)
 138         for _, from := range units {
 139             dump(from)
 140         }
 141         return nil
 142     }
 143 
 144     if !dump(from) {
 145         return fmt.Errorf("unit %q not supported\n", low)
 146     }
 147     return nil
 148 }
 149 
 150 var aliases = map[string]string{
 151     `acre`:    `ac`,
 152     `acres`:   `ac`,
 153     `days`:    `day`,
 154     `foot`:    `ft`,
 155     `feet`:    `ft`,
 156     `feet2`:   `ft²`,
 157     `feet3`:   `ft³`,
 158     `foot2`:   `ft²`,
 159     `foot3`:   `ft³`,
 160     `ft2`:     `ft²`,
 161     `ft3`:     `ft³`,
 162     `gallon`:  `gal`,
 163     `gallons`: `gal`,
 164     `gals`:    `gal`,
 165     `inch`:    `in`,
 166     `inches`:  `in`,
 167     `mile`:    `mi`,
 168     `miles`:   `mi`,
 169     `mile²`:   `mi²`,
 170     `miles²`:  `mi²`,
 171     `minute`:  `min`,
 172     `minutes`: `min`,
 173     `nmile`:   `nmi`,
 174     `nmiles`:  `nmi`,
 175     `ounce`:   `oz`,
 176     `ounces`:  `oz`,
 177     `ozs`:     `oz`,
 178     `weeks`:   `week`,
 179     `wk`:      `week`,
 180     `wks`:     `week`,
 181     `yard`:    `yd`,
 182     `yards`:   `yd`,
 183     `yard2`:   `yd²`,
 184     `yard²`:   `yd²`,
 185     `yards2`:  `yd²`,
 186     `yards²`:  `yd²`,
 187     `yds`:     `yd`,
 188     `yds2`:    `yd²`,
 189     `yds²`:    `yd²`,
 190 }
 191 
 192 type converter struct {
 193     To  string
 194     Mul float64
 195     Add float64
 196 }
 197 
 198 var converters = map[string]converter{
 199     `ac`:   converter{`m²`, 4046.8564224, 0},
 200     `day`:  converter{`s`, 86400, 0},
 201     `ft`:   converter{`m`, 0.3048, 0},
 202     `ft²`:  converter{`m²`, 0.09290304, 0},
 203     `ft³`:  converter{`m³`, 0.028316846592, 0},
 204     `gal`:  converter{`L`, 3.785411784, 0},
 205     `in`:   converter{`cm`, 2.54, 0},
 206     `mi`:   converter{`km`, 1.609344, 0},
 207     `mi²`:  converter{`km²`, 2.5899881103360, 0},
 208     `min`:  converter{`s`, 60, 0},
 209     `mpg`:  converter{`kpl`, 0.425143707, 0},
 210     `mph`:  converter{`kph`, 1.609344, 0},
 211     `nmi`:  converter{`km`, 1.852, 0},
 212     `oz`:   converter{`g`, 28.349523125, 0},
 213     `week`: converter{`s`, 604800, 0},
 214     `yd`:   converter{`m`, 0.9144, 0},
 215     `yd²`:  converter{`m²`, 0.83612736, 0},
 216 }
     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(err.Error())
  67         os.Stderr.WriteString("\n")
  68         os.Exit(1)
  69         return
  70     }
  71 }
  72 
  73 func run(w io.Writer, args []string) error {
  74     bw := bufio.NewWriter(w)
  75     defer bw.Flush()
  76 
  77     for _, path := range args {
  78         if err := handleFile(bw, path); err != nil {
  79             return err
  80         }
  81     }
  82 
  83     if len(args) == 0 {
  84         return utfate(bw, os.Stdin)
  85     }
  86     return nil
  87 }
  88 
  89 func handleFile(w *bufio.Writer, name string) error {
  90     if name == `-` {
  91         return utfate(w, os.Stdin)
  92     }
  93 
  94     f, err := os.Open(name)
  95     if err != nil {
  96         return errors.New(`can't read from file named "` + name + `"`)
  97     }
  98     defer f.Close()
  99 
 100     return utfate(w, f)
 101 }
 102 
 103 func utfate(w io.Writer, r io.Reader) error {
 104     br := bufio.NewReader(r)
 105     bw := bufio.NewWriter(w)
 106     defer bw.Flush()
 107 
 108     lead, err := br.Peek(4)
 109     if err != nil && err != io.EOF {
 110         return err
 111     }
 112 
 113     if bytes.HasPrefix(lead, []byte{'\x00', '\x00', '\xfe', '\xff'}) {
 114         br.Discard(4)
 115         return utf32toUTF8(bw, br, binary.BigEndian)
 116     }
 117 
 118     if bytes.HasPrefix(lead, []byte{'\xff', '\xfe', '\x00', '\x00'}) {
 119         br.Discard(4)
 120         return utf32toUTF8(bw, br, binary.LittleEndian)
 121     }
 122 
 123     if bytes.HasPrefix(lead, []byte{'\xfe', '\xff'}) {
 124         br.Discard(2)
 125         return utf16toUTF8(bw, br, readBytePairBE)
 126     }
 127 
 128     if bytes.HasPrefix(lead, []byte{'\xff', '\xfe'}) {
 129         br.Discard(2)
 130         return utf16toUTF8(bw, br, readBytePairLE)
 131     }
 132 
 133     if bytes.HasPrefix(lead, []byte{'\xef', '\xbb', '\xbf'}) {
 134         br.Discard(3)
 135         return handleUTF8(bw, br)
 136     }
 137 
 138     return handleUTF8(bw, br)
 139 }
 140 
 141 func handleUTF8(w *bufio.Writer, r *bufio.Reader) error {
 142     for {
 143         c, _, err := r.ReadRune()
 144         if c == unicode.ReplacementChar {
 145             return errors.New(`invalid UTF-8 stream`)
 146         }
 147         if err == io.EOF {
 148             return nil
 149         }
 150         if err != nil {
 151             return err
 152         }
 153 
 154         if _, err := w.WriteRune(c); err != nil {
 155             return io.EOF
 156         }
 157     }
 158 }
 159 
 160 // fancyHandleUTF8 is kept only for reference, as its attempts at being clever
 161 // don't seem to speed things up much when given ASCII input
 162 func fancyHandleUTF8(w *bufio.Writer, r *bufio.Reader) error {
 163     lookahead := 1
 164     maxAhead := r.Size() / 2
 165 
 166     for {
 167         // look ahead to check for ASCII runs
 168         ahead, err := r.Peek(lookahead)
 169         if err == io.EOF {
 170             return nil
 171         }
 172         if err != nil {
 173             return err
 174         }
 175 
 176         // copy leading ASCII runs
 177         n := leadASCII(ahead)
 178         if n > 0 {
 179             w.Write(ahead[:n])
 180             r.Discard(n)
 181         }
 182 
 183         // adapt lookahead size
 184         if n == len(ahead) && lookahead < maxAhead {
 185             lookahead *= 2
 186         } else if lookahead > 1 {
 187             lookahead /= 2
 188         }
 189 
 190         if n == len(ahead) {
 191             continue
 192         }
 193 
 194         c, _, err := r.ReadRune()
 195         if c == unicode.ReplacementChar {
 196             return errors.New(`invalid UTF-8 stream`)
 197         }
 198         if err == io.EOF {
 199             return nil
 200         }
 201         if err != nil {
 202             return err
 203         }
 204 
 205         if _, err := w.WriteRune(c); err != nil {
 206             return io.EOF
 207         }
 208     }
 209 }
 210 
 211 // leadASCII is used by func fancyHandleUTF8
 212 func leadASCII(buf []byte) int {
 213     for i, b := range buf {
 214         if b >= 128 {
 215             return i
 216         }
 217     }
 218     return len(buf)
 219 }
 220 
 221 // readPairFunc narrows source-code lines below
 222 type readPairFunc func(*bufio.Reader) (byte, byte, error)
 223 
 224 // utf16toUTF8 handles UTF-16 inputs for func utfate
 225 func utf16toUTF8(w *bufio.Writer, r *bufio.Reader, read2 readPairFunc) error {
 226     for {
 227         a, b, err := read2(r)
 228         if err == io.EOF {
 229             return nil
 230         }
 231         if err != nil {
 232             return err
 233         }
 234 
 235         c := rune(256*int(a) + int(b))
 236         if utf16.IsSurrogate(c) {
 237             a, b, err := read2(r)
 238             if err == io.EOF {
 239                 return nil
 240             }
 241             if err != nil {
 242                 return err
 243             }
 244 
 245             next := rune(256*int(a) + int(b))
 246             c = utf16.DecodeRune(c, next)
 247         }
 248 
 249         if _, err := w.WriteRune(c); err != nil {
 250             return io.EOF
 251         }
 252     }
 253 }
 254 
 255 // readBytePairBE gets you a pair of bytes in big-endian (original) order
 256 func readBytePairBE(br *bufio.Reader) (byte, byte, error) {
 257     a, err := br.ReadByte()
 258     if err != nil {
 259         return a, 0, err
 260     }
 261 
 262     b, err := br.ReadByte()
 263     return a, b, err
 264 }
 265 
 266 // readBytePairLE gets you a pair of bytes in little-endian order
 267 func readBytePairLE(br *bufio.Reader) (byte, byte, error) {
 268     a, b, err := readBytePairBE(br)
 269     return b, a, err
 270 }
 271 
 272 // utf32toUTF8 handles UTF-32 inputs for func utfate
 273 func utf32toUTF8(w *bufio.Writer, r *bufio.Reader, o binary.ByteOrder) error {
 274     var n uint32
 275     for {
 276         err := binary.Read(r, o, &n)
 277         if err == io.EOF {
 278             return nil
 279         }
 280         if err != nil {
 281             return err
 282         }
 283 
 284         if _, err := w.WriteRune(rune(n)); err != nil {
 285             return io.EOF
 286         }
 287     }
 288 }
     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 # similar to the door-opening sound from a dsc powerseries home alarm
  52 waveout 'sin(4096 * tau * t) * exp(-10 * (t%0.1))' > home-alarm.wav
  53 
  54 # watch your ears: quickly increases frequency up to 2khz
  55 waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav
  56 
  57 # 1-second 400hz test tone
  58 waveout 'sin(400 * tau * t)' > test-tone-400.wav
  59 
  60 # 2s of a 440hz test tone, also called an A440 sound
  61 waveout 2s 'sin(440 * tau * t)' > a440.wav
  62 
  63 # 1s 400hz test tone with sudden volume drop at the end, to avoid clip
  64 waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav
  65 
  66 # old ringtone used in north america
  67 waveout '0.5*sin(350 * tau * t) + 0.5*sin(450 * tau * t)' > na-ringtone.wav
  68 
  69 # 20 seconds of periodic pings
  70 waveout 20s 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav
  71 
  72 # 2 seconds of a european-style dial-tone
  73 waveout 2s '(sin(350 * tau * t) + sin(450 * tau * t)) / 2' > dial-tone.wav
  74 
  75 # 4 seconds of a north-american-style busy-phone signal
  76 waveout 4s '(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' > na-busy.wav
  77 
  78 # hit the 51st key on a synthetic piano-like instrument
  79 waveout 'sin(tau * 440 * 2**((51 - 49)/12) * t) * exp(-10*u)' > piano-key.wav
  80 
  81 # hit of a synthetic snare-like sound
  82 waveout 'random() * exp(-10 * t)' > synth-snare.wav
  83 
  84 # a stereotypical `laser` sound
  85 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(err.Error())
  64             os.Stderr.WriteString("\n")
  65             os.Exit(1)
  66             return
  67         }
  68     }
  69 
  70     if len(args) == 0 {
  71         if err := zcat(os.Stdout, os.Stdin); err != nil && err != io.EOF {
  72             os.Stderr.WriteString(err.Error())
  73             os.Stderr.WriteString("\n")
  74             os.Exit(1)
  75             return
  76         }
  77     }
  78 }
  79 
  80 func handleFile(w io.Writer, path string) error {
  81     f, err := os.Open(path)
  82     if err != nil {
  83         return err
  84     }
  85     defer f.Close()
  86     return zcat(w, f)
  87 }
  88 
  89 func zcat(w io.Writer, r io.Reader) error {
  90     r, err := gzip.NewReader(r)
  91     if err != nil {
  92         return err
  93     }
  94     return cat(w, r)
  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: ./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(err.Error())
  76         os.Stderr.WriteString("\n")
  77         os.Exit(1)
  78         return
  79     }
  80 
  81     var avoid sets
  82     avoid.indices = make(map[int]struct{})
  83     avoid.keys = make(map[string]struct{})
  84 
  85     data, err = zoom(data, args, &avoid)
  86     if err != nil && err != io.EOF {
  87         os.Stderr.WriteString(err.Error())
  88         os.Stderr.WriteString("\n")
  89         os.Exit(1)
  90         return
  91     }
  92 
  93     if err := json0(os.Stdout, data); err != nil && err != io.EOF {
  94         os.Stderr.WriteString(err.Error())
  95         os.Stderr.WriteString("\n")
  96         os.Exit(1)
  97         return
  98     }
  99 }
 100 
 101 func load(r io.Reader) (any, error) {
 102     // dec := json.NewDecoder(r)
 103     dec := json.NewDecoder(bufio.NewReaderSize(r, 32*1024))
 104     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 105     // even if JSON parsers aren't required to guarantee such input-fidelity
 106     // for numbers
 107     dec.UseNumber()
 108 
 109     t, err := dec.Token()
 110     if err == io.EOF {
 111         return nil, errors.New(`input has no JSON values`)
 112     }
 113 
 114     data, err := loadToken(dec, t)
 115     if err != nil {
 116         return data, err
 117     }
 118 
 119     _, err = dec.Token()
 120     if err == io.EOF {
 121         // input is over, so it's a success
 122         return data, nil
 123     }
 124 
 125     if err == nil {
 126         // a successful `read` is a failure, as it means there are
 127         // trailing JSON tokens
 128         return data, errors.New(`unexpected trailing data`)
 129     }
 130 
 131     // any other error
 132     return data, err
 133 }
 134 
 135 // loadToken handles recursion for func load
 136 func loadToken(dec *json.Decoder, t json.Token) (any, error) {
 137     switch t := t.(type) {
 138     case json.Delim:
 139         switch t {
 140         case json.Delim('['):
 141             return loadArray(dec)
 142         case json.Delim('{'):
 143             return loadObject(dec)
 144         default:
 145             return nil, errors.New(`unsupported JSON syntax ` + string(t))
 146         }
 147 
 148     case nil, bool, json.Number, string:
 149         return t, nil
 150 
 151     default:
 152         // return nil, fmt.Errorf(`unsupported token type %T`, t)
 153         return nil, errors.New(`invalid JSON token`)
 154     }
 155 }
 156 
 157 // loadArray handles arrays for func loadToken
 158 func loadArray(dec *json.Decoder) ([]any, error) {
 159     var items []any
 160 
 161     for i := 0; true; i++ {
 162         t, err := dec.Token()
 163         if err == io.EOF {
 164             return items, errors.New(`end of JSON before array was closed`)
 165         }
 166         if err != nil {
 167             return items, err
 168         }
 169 
 170         if t == json.Delim(']') {
 171             return items, nil
 172         }
 173 
 174         v, err := loadToken(dec, t)
 175         if err != nil {
 176             return items, err
 177         }
 178         items = append(items, v)
 179     }
 180 
 181     // make the compiler happy
 182     return items, nil
 183 }
 184 
 185 // loadObject handles objects for func loadToken
 186 func loadObject(dec *json.Decoder) (dictionary, error) {
 187     var items dictionary
 188 
 189     for i := 0; true; i++ {
 190         t, err := dec.Token()
 191         if err == io.EOF {
 192             return items, errors.New(`end of JSON before object was closed`)
 193         }
 194         if err != nil {
 195             return items, err
 196         }
 197 
 198         if t == json.Delim('}') {
 199             return items, nil
 200         }
 201 
 202         k, ok := t.(string)
 203         if !ok {
 204             return items, errors.New(`expected a string for a key-value pair`)
 205         }
 206 
 207         t, err = dec.Token()
 208         if err == io.EOF {
 209             return items, errors.New(`expected a value for a key-value pair`)
 210         }
 211 
 212         v, err := loadToken(dec, t)
 213         if err != nil {
 214             return items, err
 215         }
 216 
 217         if i == 0 {
 218             items.Map = make(map[string]any)
 219         }
 220         if _, ok := items.Map[k]; !ok {
 221             items.Keys = append(items.Keys, k)
 222         }
 223         items.Map[k] = v
 224     }
 225 
 226     // make the compiler happy
 227     return items, nil
 228 }
 229 
 230 func zoom(data any, keys []string, avoid *sets) (any, error) {
 231     for i, k := range keys {
 232         switch v := data.(type) {
 233         case nil:
 234             return v, errors.New(`too many keys: can't zoom a null value`)
 235 
 236         case bool:
 237             return v, errors.New(`too many keys: can't zoom a boolean value`)
 238 
 239         case json.Number:
 240             return v, errors.New(`too many keys: can't zoom a number`)
 241 
 242         case string:
 243             v, err := zoomString(v, k)
 244             if err != nil {
 245                 return v, err
 246             }
 247             data = v
 248 
 249         case []any:
 250             switch k {
 251             case `.`:
 252                 return loopZoomArray(v, keys[i+1:], avoid)
 253             case `+`:
 254                 return pickArrayItems(v, keys[i+1:])
 255             case `-`:
 256                 clear(avoid.indices)
 257                 appendIndices(avoid.indices, v, keys[i+1:])
 258                 return dropArrayItems(v, avoid.indices)
 259             }
 260 
 261             res, err := zoomArray(v, k)
 262             if err != nil {
 263                 return data, err
 264             }
 265             data = res
 266 
 267         case dictionary:
 268             if v, ok := v.Map[k]; ok {
 269                 data = v
 270                 continue
 271             }
 272 
 273             switch k {
 274             case `+`:
 275                 return pickObjectItems(v, keys[i+1:])
 276             case `-`:
 277                 clear(avoid.keys)
 278                 for _, k := range keys[i+1:] {
 279                     avoid.keys[k] = struct{}{}
 280                 }
 281                 return dropObjectItems(v, avoid.keys)
 282             }
 283 
 284             if _, v, ok := matchObjectKey(v, k); ok {
 285                 data = v
 286             } else {
 287                 data = nil
 288             }
 289 
 290         default:
 291             return v, errors.New(`too many keys: can't zoom basic values`)
 292         }
 293     }
 294     return data, nil
 295 }
 296 
 297 func zoomArray(items []any, k string) (any, error) {
 298     // trim leading spaces
 299     for len(k) > 0 && k[0] == ' ' {
 300         k = k[1:]
 301     }
 302 
 303     // trim trailing spaces
 304     for len(k) > 0 && k[len(k)-1] == ' ' {
 305         k = k[:len(k)-1]
 306     }
 307 
 308     if i, j, ok := tryArraySlice(items, k); ok {
 309         if j >= len(items) {
 310             j = len(items)
 311         }
 312         if i < 0 || j < 0 || i > j {
 313             return []any(nil), nil
 314         }
 315         return items[i:j], nil
 316     }
 317 
 318     i, err := strconv.ParseInt(k, 10, 64)
 319     if err != nil {
 320         return nil, nil
 321     }
 322 
 323     if i < 0 {
 324         i += int64(len(items))
 325     }
 326 
 327     if 0 <= i && i < int64(len(items)) {
 328         return items[i], nil
 329     }
 330     return nil, nil
 331 }
 332 
 333 func tryArraySlice(items []any, k string) (i int, j int, ok bool) {
 334     if k == `` {
 335         return 0, 0, false
 336     }
 337 
 338     colon := strings.IndexByte(k, ':')
 339     if colon < 0 {
 340         if dots := indexPair(k, '.', '.'); dots >= 0 {
 341             return tryIncArraySlice(items, k, dots)
 342         }
 343 
 344         return 0, 0, false
 345     }
 346 
 347     // handle omitted/implied starting 0
 348     if colon == 0 {
 349         j, err := strconv.ParseInt(k[colon+1:], 10, 64)
 350         if err != nil {
 351             return 0, 0, false
 352         }
 353 
 354         if j < 0 {
 355             j += int64(len(items))
 356         }
 357 
 358         return 0, int(j), true
 359     }
 360 
 361     // handle omitted/implied until the end
 362     if colon == len(k)-1 {
 363         i, err := strconv.ParseInt(k[:colon], 10, 64)
 364         if err != nil {
 365             return 0, 0, false
 366         }
 367 
 368         if i < 0 {
 369             i += int64(len(items))
 370         }
 371 
 372         return int(i), len(items), true
 373     }
 374 
 375     start, err := strconv.ParseInt(k[:colon], 10, 64)
 376     if err != nil {
 377         return 0, 0, false
 378     }
 379 
 380     if start < 0 {
 381         start += int64(len(items))
 382     }
 383 
 384     end, err := strconv.ParseInt(k[colon+1:], 10, 64)
 385     if err != nil {
 386         return 0, 0, false
 387     }
 388 
 389     if end < 0 {
 390         end += int64(len(items))
 391     }
 392 
 393     return int(start), int(end), ok
 394 }
 395 
 396 func tryIncArraySlice(items []any, k string, dots int) (i int, j int, ok bool) {
 397     if k == `` {
 398         return 0, 0, false
 399     }
 400 
 401     if dots < 0 {
 402         return 0, 0, false
 403     }
 404 
 405     // handle omitted/implied starting 0
 406     if dots == 0 {
 407         j, err := strconv.ParseInt(k[dots+2:], 10, 64)
 408         if err != nil {
 409             return 0, 0, false
 410         }
 411 
 412         if j < 0 {
 413             j += int64(len(items))
 414         }
 415         if j >= 0 {
 416             j++
 417         }
 418 
 419         return 0, int(j), true
 420     }
 421 
 422     // handle omitted/implied until the end
 423     if dots == len(k)-1 {
 424         i, err := strconv.ParseInt(k[:dots], 10, 64)
 425         if err != nil {
 426             return 0, 0, false
 427         }
 428 
 429         if i < 0 {
 430             i += int64(len(items))
 431         }
 432 
 433         return int(i), len(items), true
 434     }
 435 
 436     start, err := strconv.ParseInt(k[:dots], 10, 64)
 437     if err != nil {
 438         return 0, 0, false
 439     }
 440 
 441     if start < 0 {
 442         start += int64(len(items))
 443     }
 444 
 445     end, err := strconv.ParseInt(k[dots+2:], 10, 64)
 446     if err != nil {
 447         return 0, 0, false
 448     }
 449 
 450     if end < 0 {
 451         end += int64(len(items))
 452     }
 453     if end >= 0 {
 454         end++
 455     }
 456     return int(start), int(end), ok
 457 }
 458 
 459 func matchObjectKey(items dictionary, k string) (match string, v any, ok bool) {
 460     // first, try direct key lookup
 461     if v, ok := items.Map[k]; ok {
 462         return k, v, true
 463     }
 464 
 465     // second, try case-insensitive key lookup
 466     for s := range items.Map {
 467         if strings.EqualFold(k, s) {
 468             return s, items.Map[s], true
 469         }
 470     }
 471 
 472     // finally, try integer/index lookup
 473     i, err := strconv.ParseInt(k, 10, 64)
 474     if err != nil {
 475         return ``, nil, false
 476     }
 477 
 478     if i < 0 {
 479         i += int64(len(items.Keys))
 480     }
 481 
 482     if 0 <= i && i < int64(len(items.Keys)) {
 483         k := items.Keys[i]
 484         return k, items.Map[k], true
 485     }
 486 
 487     // nothing worked
 488     return ``, nil, false
 489 }
 490 
 491 func zoomString(s string, k string) (string, error) {
 492     if i, j, ok := tryRuneSlice(s, k); ok {
 493         if i > j {
 494             return ``, nil
 495         }
 496         return sliceRunes(s, i, j), nil
 497     }
 498 
 499     i, err := strconv.ParseInt(k, 10, 64)
 500     if err != nil {
 501         return ``, err
 502     }
 503 
 504     // don't bother looping when the index given is obviously out of bounds
 505     if int(i) >= len(s) || int(-i) > len(s) {
 506         return ``, nil
 507     }
 508 
 509     if i < 0 {
 510         // shrink string backward from the end
 511         for len(s) > 0 && i < 0 {
 512             _, size := utf8.DecodeLastRuneInString(s)
 513             s = s[:len(s)-size]
 514             i++
 515         }
 516 
 517         if len(s) > 0 && i == 0 {
 518             _, size := utf8.DecodeLastRuneInString(s)
 519             return s[len(s)-size:], nil
 520         }
 521         return ``, nil
 522     }
 523 
 524     // shrink string forward from the start
 525     for len(s) > 0 && i > 0 {
 526         _, size := utf8.DecodeRuneInString(s)
 527         s = s[size:]
 528         i--
 529     }
 530 
 531     if len(s) > 0 && i == 0 {
 532         _, size := utf8.DecodeRuneInString(s)
 533         return s[:size], nil
 534     }
 535     return ``, nil
 536 }
 537 
 538 func tryRuneSlice(s string, k string) (i int, j int, ok bool) {
 539     if k == `` {
 540         return 0, 0, false
 541     }
 542 
 543     colon := strings.IndexByte(k, ':')
 544     if colon < 0 {
 545         return 0, 0, false
 546     }
 547 
 548     // handle omitted/implied starting 0
 549     if colon == 0 {
 550         j, err := strconv.ParseInt(k[colon+1:], 10, 64)
 551         if err != nil {
 552             return 0, 0, false
 553         }
 554 
 555         return 0, int(j), true
 556     }
 557 
 558     // handle omitted/implied until the end
 559     if colon == len(k)-1 {
 560         i, err := strconv.ParseInt(k[:colon], 10, 64)
 561         if err != nil {
 562             return 0, 0, false
 563         }
 564 
 565         return int(i), len(s), true
 566     }
 567 
 568     start, err := strconv.ParseInt(k[:colon], 10, 64)
 569     if err != nil {
 570         return 0, 0, false
 571     }
 572 
 573     end, err := strconv.ParseInt(k[colon+1:], 10, 64)
 574     if err != nil {
 575         return 0, 0, false
 576     }
 577 
 578     return int(start), int(end), ok
 579 }
 580 
 581 func sliceRunes(s string, i int, j int) string {
 582     if i >= j {
 583         return ``
 584     }
 585 
 586     // to do: backward-indexing
 587     if i < 0 || j < 0 {
 588         return ``
 589     }
 590 
 591     // don't bother looping when the index given is obviously out of bounds
 592     if int(i) >= len(s) || int(-i) > len(s) {
 593         return ``
 594     }
 595 
 596     // skip leading runes, according to the first index
 597     for len(s) > 0 && i > 0 {
 598         _, size := utf8.DecodeRuneInString(s)
 599         s = s[size:]
 600         i--
 601     }
 602 
 603     if len(s) == 0 {
 604         return ``
 605     }
 606 
 607     end := 0
 608     rest := s
 609     for len(rest) > 0 && j > 0 {
 610         _, size := utf8.DecodeRuneInString(rest)
 611         rest = rest[size:]
 612         end += size
 613         j--
 614     }
 615 
 616     if len(s) > 0 && j == 0 {
 617         return s[:end]
 618     }
 619     return ``
 620 }
 621 
 622 func json0(w io.Writer, data any) error {
 623     bw := bufio.NewWriterSize(w, 32*1024)
 624     defer bw.Flush()
 625 
 626     err := writeValue(bw, data)
 627     bw.WriteByte('\n')
 628 
 629     if err == io.EOF {
 630         return nil
 631     }
 632     return err
 633 }
 634 
 635 func loopZoomArray(items []any, keys []string, avoid *sets) (any, error) {
 636     res := items[:0]
 637     for _, v := range items {
 638         v, err := zoom(v, keys, avoid)
 639         if err != nil {
 640             return res, err
 641         }
 642         res = append(res, v)
 643     }
 644     return res, nil
 645 }
 646 
 647 func pickArrayItems(items []any, keys []string) (any, error) {
 648     res := items[:0]
 649     for _, k := range keys {
 650         v, err := zoomArray(items, k)
 651         if err != nil {
 652             return res, err
 653         }
 654         res = append(res, v)
 655     }
 656     return res, nil
 657 }
 658 
 659 func dropArrayItems(items []any, avoid map[int]struct{}) (any, error) {
 660     res := items[:0]
 661     for i, v := range items {
 662         if _, ok := avoid[i]; ok {
 663             continue
 664         }
 665         res = append(res, v)
 666     }
 667     return res, nil
 668 }
 669 
 670 func pickObjectItems(items dictionary, keys []string) (any, error) {
 671     var res dictionary
 672     res.Keys = items.Keys[:0]
 673     res.Map = items.Map
 674 
 675     for _, k := range keys {
 676         match, _, ok := matchObjectKey(items, k)
 677         if !ok {
 678             continue
 679         }
 680 
 681         if _, ok := res.Map[match]; !ok {
 682             res.Keys = append(res.Keys, match)
 683         }
 684     }
 685 
 686     return res, nil
 687 }
 688 
 689 func dropObjectItems(items dictionary, avoid map[string]struct{}) (any, error) {
 690     var res dictionary
 691     res.Keys = items.Keys[:0]
 692     res.Map = items.Map
 693 
 694     for _, k := range items.Keys {
 695         if hasFold(avoid, k) {
 696             continue
 697         }
 698 
 699         if _, ok := res.Map[k]; !ok {
 700             res.Keys = append(res.Keys, k)
 701         }
 702     }
 703 
 704     return res, nil
 705 }
 706 
 707 func hasFold(avoid map[string]struct{}, s string) bool {
 708     for v := range avoid {
 709         if v == s || strings.EqualFold(v, s) {
 710             return true
 711         }
 712     }
 713     return false
 714 }
 715 
 716 func appendIndices(dest map[int]struct{}, items []any, keys []string) {
 717     for _, k := range keys {
 718         i, err := strconv.ParseInt(k, 10, 64)
 719         if err != nil {
 720             continue
 721         }
 722 
 723         if i < 0 {
 724             i += int64(len(items))
 725         }
 726 
 727         if 0 <= i && i < int64(len(items)) {
 728             dest[int(i)] = struct{}{}
 729         }
 730     }
 731 }
 732 
 733 func writeValue(w *bufio.Writer, data any) error {
 734     switch data := data.(type) {
 735     case nil:
 736         return writeKeyword(w, `null`)
 737     case bool:
 738         if data {
 739             return writeKeyword(w, `true`)
 740         }
 741         return writeKeyword(w, `false`)
 742     case json.Number:
 743         if _, err := w.WriteString(data.String()); err != nil {
 744             return io.EOF
 745         }
 746         return nil
 747     case string:
 748         return writeEscapedString(w, data)
 749     case []any:
 750         return writeArray(w, data)
 751     case dictionary:
 752         return writeObject(w, data)
 753     default:
 754         return errors.New(`invalid JSON value`)
 755     }
 756 }
 757 
 758 func writeByte(w *bufio.Writer, b byte) error {
 759     err := w.WriteByte(b)
 760     if err != nil {
 761         return io.EOF
 762     }
 763     return nil
 764 }
 765 
 766 func writeKeyword(w *bufio.Writer, s string) error {
 767     if _, err := w.WriteString(s); err == nil {
 768         return nil
 769     }
 770     return io.EOF
 771 }
 772 
 773 func writeEscapedString(w *bufio.Writer, s string) error {
 774     if !needsEscaping(s) {
 775         w.WriteByte('"')
 776         w.WriteString(s)
 777         return writeByte(w, '"')
 778     }
 779 
 780     w.WriteByte('"')
 781 
 782     for _, r := range s {
 783         if ' ' <= r && r <= '~' && r != '\\' && r != '"' {
 784             w.WriteRune(r)
 785             continue
 786         }
 787 
 788         switch r {
 789         case '\\':
 790             w.WriteString(`\\`)
 791         case '"':
 792             w.WriteString(`\"`)
 793         default:
 794             writeEscapedHex(w, r)
 795         }
 796     }
 797 
 798     return writeByte(w, '"')
 799 }
 800 
 801 func writeEscapedHex(w *bufio.Writer, r rune) error {
 802     w.WriteByte('\\')
 803     w.WriteByte('u')
 804     writeHex(w, byte(r>>24))
 805     writeHex(w, byte(r>>16))
 806     writeHex(w, byte(r>>8))
 807     writeHex(w, byte(r))
 808     return nil
 809 }
 810 
 811 // writeHex is faster than calling fmt.Fprintf(w, `%04x`, b)
 812 func writeHex(w *bufio.Writer, b byte) {
 813     const hexDigits = `0123456789abcdef`
 814     w.WriteByte(hexDigits[b>>4])
 815     w.WriteByte(hexDigits[b&0x0f])
 816 }
 817 
 818 func needsEscaping(s string) bool {
 819     for i := range s {
 820         if b := s[i]; ' ' <= b && b <= '~' && b != '\\' && b != '"' {
 821             continue
 822         }
 823         return true
 824     }
 825 
 826     return false
 827 }
 828 
 829 func writeArray(w *bufio.Writer, items []any) error {
 830     w.WriteByte('[')
 831     for i, v := range items {
 832         if i > 0 {
 833             if err := writeByte(w, ','); err != nil {
 834                 return err
 835             }
 836         }
 837         if err := writeValue(w, v); err != nil {
 838             return err
 839         }
 840     }
 841     return writeByte(w, ']')
 842 }
 843 
 844 func writeObject(w *bufio.Writer, items dictionary) error {
 845     w.WriteByte('{')
 846     for i, k := range items.Keys {
 847         if i > 0 {
 848             if err := writeByte(w, ','); err != nil {
 849                 return err
 850             }
 851         }
 852         writeEscapedString(w, k)
 853         w.WriteByte(':')
 854         if err := writeValue(w, items.Map[k]); err != nil {
 855             return err
 856         }
 857     }
 858     return writeByte(w, '}')
 859 }
 860 
 861 func indexPair(s string, x byte, y byte) int {
 862     var cur, prev byte
 863 
 864     for i := range s {
 865         cur = s[i]
 866         if prev == x && cur == y && i > 0 {
 867             return i
 868         }
 869         prev = cur
 870     }
 871 
 872     return -1
 873 }
     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 }