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     }
 111 
 112     var buf []byte
 113     sc := bufio.NewScanner(os.Stdin)
 114     sc.Buffer(nil, 8*1024*1024*1024)
 115     bw := bufio.NewWriter(os.Stdout)
 116 
 117     for i := 0; sc.Scan(); i++ {
 118         line := sc.Bytes()
 119         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 120             line = line[3:]
 121         }
 122 
 123         s := line
 124         if bytes.IndexByte(s, '\x1b') >= 0 {
 125             buf = plain(buf[:0], s)
 126             s = buf
 127         }
 128 
 129         if !match(s, exprs) {
 130             bw.Write(line)
 131             bw.WriteByte('\n')
 132 
 133             if !liveLines {
 134                 continue
 135             }
 136 
 137             if err := bw.Flush(); err != nil {
 138                 return
 139             }
 140         }
 141     }
 142 }
 143 
 144 func match(what []byte, with []*regexp.Regexp) bool {
 145     for _, e := range with {
 146         if e.Match(what) {
 147             return true
 148         }
 149     }
 150     return false
 151 }
 152 
 153 func plain(dst []byte, src []byte) []byte {
 154     for len(src) > 0 {
 155         i, j := indexEscapeSequence(src)
 156         if i < 0 {
 157             dst = append(dst, src...)
 158             break
 159         }
 160         if j < 0 {
 161             j = len(src)
 162         }
 163 
 164         if i > 0 {
 165             dst = append(dst, src[:i]...)
 166         }
 167 
 168         src = src[j:]
 169     }
 170 
 171     return dst
 172 }
 173 
 174 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 175 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 176 // indices which can be independently negative when either the start/end of
 177 // a sequence isn't found; given their fairly-common use, even the hyperlink
 178 // ESC]8 sequences are supported
 179 func indexEscapeSequence(s []byte) (int, int) {
 180     var prev byte
 181 
 182     for i, b := range s {
 183         if prev == '\x1b' && b == '[' {
 184             j := indexLetter(s[i+1:])
 185             if j < 0 {
 186                 return i, -1
 187             }
 188             return i - 1, i + 1 + j + 1
 189         }
 190 
 191         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 192             j := indexPair(s[i+1:], '\x1b', '\\')
 193             if j < 0 {
 194                 return i, -1
 195             }
 196             return i - 1, i + 1 + j + 2
 197         }
 198 
 199         prev = b
 200     }
 201 
 202     return -1, -1
 203 }
 204 
 205 func indexLetter(s []byte) int {
 206     for i, b := range s {
 207         upper := b &^ 32
 208         if 'A' <= upper && upper <= 'Z' {
 209             return i
 210         }
 211     }
 212 
 213     return -1
 214 }
 215 
 216 func indexPair(s []byte, x byte, y byte) int {
 217     var prev byte
 218 
 219     for i, b := range s {
 220         if prev == x && b == y && i > 0 {
 221             return i
 222         }
 223         prev = b
 224     }
 225 
 226     return -1
 227 }
     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     }
 113 }
 114 
 115 func run(args []string) error {
 116     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 117     defer w.Flush()
 118 
 119     // with no filenames given, handle stdin and quit
 120     if len(args) == 0 {
 121         return handle(w, os.Stdin, `<stdin>`, -1)
 122     }
 123 
 124     for i, fname := range args {
 125         if i > 0 {
 126             w.WriteString("\n")
 127             w.WriteString("\n")
 128         }
 129 
 130         if err := handleFile(w, fname); err != nil {
 131             return err
 132         }
 133     }
 134 
 135     return nil
 136 }
 137 
 138 func handleFile(w *bufio.Writer, fname string) error {
 139     f, err := os.Open(fname)
 140     if err != nil {
 141         return err
 142     }
 143     defer f.Close()
 144 
 145     stat, err := f.Stat()
 146     if err != nil {
 147         return handle(w, f, fname, -1)
 148     }
 149 
 150     fsize := int(stat.Size())
 151     return handle(w, f, fname, fsize)
 152 }
 153 
 154 // handle shows some messages related to the input and the cmd-line options
 155 // used, and then follows them by the hexadecimal byte-view
 156 func handle(w *bufio.Writer, r io.Reader, name string, size int) error {
 157     owidth10 := -1
 158     owidth16 := -1
 159     if size > 0 {
 160         w10 := math.Log10(float64(size))
 161         w10 = math.Max(math.Ceil(w10), 1)
 162         w16 := math.Log2(float64(size)) / 4
 163         w16 = math.Max(math.Ceil(w16), 1)
 164         owidth10 = int(w10)
 165         owidth16 = int(w16)
 166     }
 167 
 168     if owidth10 < 0 {
 169         owidth10 = 8
 170     }
 171     if owidth16 < 0 {
 172         owidth16 = 8
 173     }
 174 
 175     rc := rendererConfig{
 176         out:           w,
 177         offsetWidth10: max(owidth10, 8),
 178         offsetWidth16: max(owidth16, 8),
 179     }
 180 
 181     if size < 0 {
 182         fmt.Fprintf(w, "• %s\n", name)
 183     } else {
 184         const fs = "• %s  (%s bytes)\n"
 185         fmt.Fprintf(w, fs, name, sprintCommas(size))
 186     }
 187     w.WriteByte('\n')
 188 
 189     // calling func Read directly can sometimes result in chunks shorter
 190     // than the max chunk-size, even when there are plenty of bytes yet
 191     // to read; to avoid that, use a buffered-reader to explicitly fill
 192     // a slice instead
 193     br := bufio.NewReader(r)
 194 
 195     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 196     cur := make([]byte, 0, perLine)
 197     ahead := make([]byte, 0, perLine)
 198 
 199     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 200     // so to speak
 201     cur, err := fillChunk(cur[:0], perLine, br)
 202     if len(cur) == 0 {
 203         if err == io.EOF {
 204             err = nil
 205         }
 206         return err
 207     }
 208 
 209     for {
 210         ahead, err := fillChunk(ahead[:0], perLine, br)
 211         if err != nil && err != io.EOF {
 212             return err
 213         }
 214 
 215         if len(ahead) == 0 {
 216             // done, maybe except for an extra line of output
 217             break
 218         }
 219 
 220         // show the byte-chunk on its own output line
 221         if err := writeChunk(rc, cur, ahead); err != nil {
 222             return io.EOF
 223         }
 224 
 225         rc.offset += uint(len(cur))
 226         cur = cur[:copy(cur, ahead)]
 227     }
 228 
 229     // don't forget the last output line
 230     if len(cur) > 0 {
 231         return writeChunk(rc, cur, nil)
 232     }
 233     return nil
 234 }
 235 
 236 // fillChunk tries to read the number of bytes given, appending them to the
 237 // byte-slice given; this func returns an EOF error only when no bytes are
 238 // read, which somewhat simplifies error-handling for the func caller
 239 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 240     // read buffered-bytes up to the max chunk-size
 241     for i := 0; i < n; i++ {
 242         b, err := br.ReadByte()
 243         if err == nil {
 244             chunk = append(chunk, b)
 245             continue
 246         }
 247 
 248         if err == io.EOF && i > 0 {
 249             return chunk, nil
 250         }
 251         return chunk, err
 252     }
 253 
 254     // got the full byte-count asked for
 255     return chunk, nil
 256 }
 257 
 258 // rendererConfig groups several arguments given to any of the rendering funcs
 259 type rendererConfig struct {
 260     // out is writer to send all output to
 261     out *bufio.Writer
 262 
 263     // offset is the byte-offset of the first byte shown on the current output
 264     // line: if shown at all, it's shown at the start the line
 265     offset uint
 266 
 267     // offsetWidth10 is the max string-width for the base-10 byte-offsets
 268     // shown at the start of output lines, and determines those values'
 269     // left-padding
 270     offsetWidth10 int
 271 
 272     // offsetWidth16 is the max string-width for the base-16 byte-offsets
 273     // shown at the start of output lines, and determines those values'
 274     // left-padding
 275     offsetWidth16 int
 276 }
 277 
 278 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
 279 // handles negatives, even though this app only uses it with non-negatives.
 280 func loopThousandsGroups(n int, fn func(i, n int)) {
 281     // 0 doesn't have a log10
 282     if n == 0 {
 283         fn(0, 0)
 284         return
 285     }
 286 
 287     sign := +1
 288     if n < 0 {
 289         n = -n
 290         sign = -1
 291     }
 292 
 293     intLog1000 := int(math.Log10(float64(n)) / 3)
 294     remBase := int(math.Pow10(3 * intLog1000))
 295 
 296     for i := 0; remBase > 0; i++ {
 297         group := (1000 * n) / remBase / 1000
 298         fn(i, sign*group)
 299         // if original number was negative, ensure only first
 300         // group gives a negative input to the callback
 301         sign = +1
 302 
 303         n %= remBase
 304         remBase /= 1000
 305     }
 306 }
 307 
 308 // sprintCommas turns the non-negative number given into a readable string,
 309 // where digits are grouped-separated by commas
 310 func sprintCommas(n int) string {
 311     var sb strings.Builder
 312     loopThousandsGroups(n, func(i, n int) {
 313         if i == 0 {
 314             var buf [4]byte
 315             sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
 316             return
 317         }
 318         sb.WriteByte(',')
 319         writePad0Sub1000Counter(&sb, uint(n))
 320     })
 321     return sb.String()
 322 }
 323 
 324 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
 325 func writePad0Sub1000Counter(w io.Writer, n uint) {
 326     // precondition is 0...999
 327     if n > 999 {
 328         w.Write([]byte(`???`))
 329         return
 330     }
 331 
 332     var buf [3]byte
 333     buf[0] = byte(n/100) + '0'
 334     n %= 100
 335     buf[1] = byte(n/10) + '0'
 336     buf[2] = byte(n%10) + '0'
 337     w.Write(buf[:])
 338 }
 339 
 340 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
 341 // matters because it's called for every byte of input which isn't
 342 // all 0s or all 1s
 343 func writeHex(w *bufio.Writer, b byte) {
 344     const hexDigits = `0123456789abcdef`
 345     w.WriteByte(hexDigits[b>>4])
 346     w.WriteByte(hexDigits[b&0x0f])
 347 }
 348 
 349 // padding is the padding/spacing emitted across each output line
 350 const padding = 2
 351 
 352 func writeChunk(rc rendererConfig, first, second []byte) error {
 353     w := rc.out
 354 
 355     // start each line with the byte-offset for the 1st item shown on it
 356     // writeDecimalCounter(w, rc.offsetWidth10, rc.offset)
 357     // w.WriteByte(' ')
 358 
 359     // start each line with the byte-offset for the 1st item shown on it
 360     writeHexadecimalCounter(w, rc.offsetWidth16, rc.offset)
 361     w.WriteByte(' ')
 362 
 363     for _, b := range first {
 364         // fmt.Fprintf(w, ` %02x`, b)
 365         //
 366         // the commented part above was a performance bottleneck, since
 367         // the slow/generic fmt.Fprintf was called for each input byte
 368         w.WriteByte(' ')
 369         writeHex(w, b)
 370     }
 371 
 372     writeASCII(w, first, second, perLine)
 373     return w.WriteByte('\n')
 374 }
 375 
 376 // writeDecimalCounter just emits a left-padded number
 377 func writeDecimalCounter(w *bufio.Writer, width int, n uint) {
 378     var buf [24]byte
 379     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 380     writeSpaces(w, width-len(str))
 381     w.Write(str)
 382 }
 383 
 384 // writeHexadecimalCounter just emits a zero-padded base-16 number
 385 func writeHexadecimalCounter(w *bufio.Writer, width int, n uint) {
 386     var buf [24]byte
 387     str := strconv.AppendUint(buf[:0], uint64(n), 16)
 388     // writeSpaces(w, width-len(str))
 389     for i := 0; i < width-len(str); i++ {
 390         w.WriteByte('0')
 391     }
 392     w.Write(str)
 393 }
 394 
 395 // writeSpaces bulk-emits the number of spaces given
 396 func writeSpaces(w *bufio.Writer, n int) {
 397     const spaces = `                                `
 398     for ; n > len(spaces); n -= len(spaces) {
 399         w.WriteString(spaces)
 400     }
 401     if n > 0 {
 402         w.WriteString(spaces[:n])
 403     }
 404 }
 405 
 406 // writeASCII emits the side-panel showing all ASCII runs for each line
 407 func writeASCII(w *bufio.Writer, first, second []byte, perline int) {
 408     spaces := padding + 3*(perline-len(first))
 409 
 410     for _, b := range first {
 411         if 32 < b && b < 127 {
 412             writeSpaces(w, spaces)
 413             w.WriteByte(b)
 414             spaces = 0
 415         } else {
 416             spaces++
 417         }
 418     }
 419 
 420     for _, b := range second {
 421         if 32 < b && b < 127 {
 422             writeSpaces(w, spaces)
 423             w.WriteByte(b)
 424             spaces = 0
 425         } else {
 426             spaces++
 427         }
 428     }
 429 }
     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     }
 106 
 107     if err := run(os.Stdout, args, showAsFrac); err != nil && err != io.EOF {
 108         os.Stderr.WriteString(err.Error())
 109         os.Stderr.WriteString("\n")
 110         os.Exit(1)
 111     }
 112 }
 113 
 114 func run(w io.Writer, args []string, showAsFrac bool) error {
 115     for _, src := range args {
 116         src = strings.ToLower(src)
 117 
 118         // treat square brackets like parentheses, for convenience
 119         src = strings.Replace(src, `[`, `(`, -1)
 120         src = strings.Replace(src, `]`, `)`, -1)
 121 
 122         expr, err := parser.ParseExpr(src)
 123         if err != nil {
 124             return err
 125         }
 126 
 127         n, err := eval(expr)
 128         if err != nil {
 129             return err
 130         }
 131 
 132         // only show the numerator, when the denominator is 1; when showing
 133         // results as numbers with decimals, all trailing zero decimals are
 134         // ignored
 135         s := ``
 136         if n.IsInt() {
 137             s = n.Num().String()
 138         } else if showAsFrac {
 139             s = n.String()
 140         } else {
 141             s = trimDecimals(n.FloatString(100))
 142         }
 143 
 144         io.WriteString(w, s)
 145         _, err = io.WriteString(w, "\n")
 146 
 147         if err != nil {
 148             break
 149         }
 150     }
 151 
 152     return nil
 153 }
 154 
 155 // trimDecimals ignores excessive trailing decimal zeros, if any, as well as
 156 // the decimal dot itself, if all decimals turn out to be zeros; integers are
 157 // returned as given
 158 func trimDecimals(s string) string {
 159     // with no decimals, keep all/any trailing zeros
 160     if strings.IndexByte(s, '.') < 0 {
 161         return s
 162     }
 163 
 164     // ignore all trailing zero decimals
 165     for len(s) > 0 && s[len(s)-1] == '0' {
 166         s = s[:len(s)-1]
 167     }
 168     // ignore trailing decimal
 169     if len(s) > 0 && s[len(s)-1] == '.' {
 170         s = s[:len(s)-1]
 171     }
 172     return s
 173 }
 174 
 175 func eval(expr ast.Expr) (*big.Rat, error) {
 176     switch expr := expr.(type) {
 177     case *ast.BasicLit:
 178         return evalLit(expr)
 179     case *ast.ParenExpr:
 180         return eval(expr.X)
 181     case *ast.UnaryExpr:
 182         return evalUnary(expr)
 183     case *ast.BinaryExpr:
 184         return evalBinary(expr)
 185     case *ast.CallExpr:
 186         return evalCall(expr)
 187     case *ast.Ident:
 188         return evalIdent(expr)
 189     }
 190 
 191     return nil, errors.New(`unsupported expression type`)
 192 }
 193 
 194 func evalLit(expr *ast.BasicLit) (*big.Rat, error) {
 195     switch expr.Kind {
 196     case token.INT, token.FLOAT:
 197         n := big.NewRat(0, 1)
 198         n, _ = n.SetString(expr.Value)
 199         return n, nil
 200     }
 201 
 202     return nil, errors.New(`unsupported literal type`)
 203 }
 204 
 205 func evalUnary(expr *ast.UnaryExpr) (*big.Rat, error) {
 206     switch expr.Op {
 207     case token.ADD:
 208         return eval(expr.X)
 209 
 210     case token.SUB:
 211         n, err := eval(expr.X)
 212         if n != nil {
 213             n = n.Neg(n)
 214         }
 215         return n, err
 216 
 217     case token.NOT:
 218         return eval(&ast.CallExpr{
 219             Fun:  ast.NewIdent(`factorial`),
 220             Args: []ast.Expr{expr.X},
 221         })
 222     }
 223 
 224     return nil, errors.New(`unsupported unary operation ` + expr.Op.String())
 225 }
 226 
 227 func evalBinary(expr *ast.BinaryExpr) (*big.Rat, error) {
 228     x, err := eval(expr.X)
 229     if err != nil {
 230         return nil, err
 231     }
 232 
 233     y, err := eval(expr.Y)
 234     if err != nil {
 235         return nil, err
 236     }
 237 
 238     z := big.NewRat(0, 1)
 239 
 240     switch expr.Op {
 241     case token.ADD:
 242         z = z.Add(x, y)
 243         return z, nil
 244 
 245     case token.SUB:
 246         z = z.Sub(x, y)
 247         return z, nil
 248 
 249     case token.MUL:
 250         z = z.Mul(x, y)
 251         return z, nil
 252 
 253     case token.QUO:
 254         if y.Sign() == 0 {
 255             return nil, errors.New(`can't divide by zero`)
 256         }
 257         z = z.Quo(x, y)
 258         return z, nil
 259 
 260     case token.REM:
 261         return remainder(x, y)
 262     }
 263 
 264     return nil, errors.New(`unsupported binary operation ` + expr.Op.String())
 265 }
 266 
 267 func evalCall(expr *ast.CallExpr) (*big.Rat, error) {
 268     ident, ok := expr.Fun.(*ast.Ident)
 269     if !ok {
 270         return nil, errors.New(`unsupported function type`)
 271     }
 272     s := ident.Name
 273 
 274     if _, ok := varFuncs[s]; ok {
 275         return evalVarCall(s, expr)
 276     }
 277 
 278     switch len(expr.Args) {
 279     case 1:
 280         return evalCall1(s, expr)
 281     case 2:
 282         return evalCall2(s, expr)
 283     case 3:
 284         return evalCall3(s, expr)
 285     }
 286 
 287     return nil, errors.New(`function '` + s + `' not available`)
 288 }
 289 
 290 func evalIdent(expr *ast.Ident) (*big.Rat, error) {
 291     s := strings.ToLower(expr.Name)
 292     if v, ok := values[s]; ok {
 293         if f, ok := big.NewRat(0, 1).SetString(v); ok {
 294             return f, nil
 295         }
 296         return nil, errors.New(`value '` + s + `' isn't a valid number`)
 297     }
 298     return nil, errors.New(`value '` + s + `' not available`)
 299 }
 300 
 301 func copyFrac(x *big.Rat) *big.Rat {
 302     y := big.NewRat(0, 1)
 303     y = y.Add(y, x)
 304     return y
 305 }
 306 
 307 var values = map[string]string{
 308     `kb`:  `1024`,
 309     `mb`:  `1048576`,
 310     `gb`:  `1073741824`,
 311     `tb`:  `1099511627776`,
 312     `pb`:  `1125899906842624`,
 313     `kib`: `1024`,
 314     `mib`: `1048576`,
 315     `gib`: `1073741824`,
 316     `tib`: `1099511627776`,
 317     `pib`: `1125899906842624`,
 318 
 319     `hour`: `3600`,
 320     `hr`:   `3600`,
 321     `day`:  `86400`,
 322     `week`: `604800`,
 323     `wk`:   `604800`,
 324 
 325     `mol`:  `602214076000000000000000`,
 326     `mole`: `602214076000000000000000`,
 327 }
 328 
 329 var funcs1 = map[string]func(*big.Rat) (*big.Rat, error){
 330     `abs`:         abs,
 331     `bits`:        bits,
 332     `ceil`:        ceiling,
 333     `ceiling`:     ceiling,
 334     `den`:         denominator,
 335     `denom`:       denominator,
 336     `denominator`: denominator,
 337     `digits`:      digits,
 338     `f`:           factorial,
 339     `fac`:         factorial,
 340     `fact`:        factorial,
 341     `factorial`:   factorial,
 342     `floor`:       floor,
 343     `isprime`:     isPrime,
 344     `prime`:       isPrime,
 345     `num`:         numerator,
 346     `numer`:       numerator,
 347     `numerator`:   numerator,
 348     `pow2`:        power2,
 349     `power2`:      power2,
 350     `pow10`:       power10,
 351     `power10`:     power10,
 352     `sgn`:         sign,
 353     `sign`:        sign,
 354 }
 355 
 356 func evalCall1(name string, expr *ast.CallExpr) (*big.Rat, error) {
 357     x, err := eval(expr.Args[0])
 358     if err != nil {
 359         return nil, err
 360     }
 361 
 362     fn, ok := funcs1[name]
 363     if !ok {
 364         return nil, errors.New(`function '` + name + `' not available`)
 365     }
 366 
 367     return fn(x)
 368 }
 369 
 370 var funcs2 = map[string]func(*big.Rat, *big.Rat) (*big.Rat, error){
 371     `c`:            combinations,
 372     `com`:          combinations,
 373     `comb`:         combinations,
 374     `combinations`: combinations,
 375     `choose`:       combinations,
 376     `gcd`:          gcd,
 377     `gcf`:          gcd,
 378     `lcm`:          lcm,
 379     `p`:            permutations,
 380     `per`:          permutations,
 381     `perm`:         permutations,
 382     `permutations`: permutations,
 383     `pow`:          power,
 384     `power`:        power,
 385     `rem`:          remainder,
 386     `remainder`:    remainder,
 387 }
 388 
 389 func evalCall2(name string, expr *ast.CallExpr) (*big.Rat, error) {
 390     x, err := eval(expr.Args[0])
 391     if err != nil {
 392         return nil, err
 393     }
 394 
 395     y, err := eval(expr.Args[1])
 396     if err != nil {
 397         return nil, err
 398     }
 399 
 400     fn, ok := funcs2[name]
 401     if !ok {
 402         return nil, errors.New(`function '` + name + `' not available`)
 403     }
 404 
 405     return fn(x, y)
 406 }
 407 
 408 var funcs3 = map[string]func(*big.Rat, *big.Rat, *big.Rat) (*big.Rat, error){
 409     `db`:     dbinom,
 410     `dbin`:   dbinom,
 411     `dbinom`: dbinom,
 412 }
 413 
 414 func evalCall3(name string, expr *ast.CallExpr) (*big.Rat, error) {
 415     x, err := eval(expr.Args[0])
 416     if err != nil {
 417         return nil, err
 418     }
 419 
 420     y, err := eval(expr.Args[1])
 421     if err != nil {
 422         return nil, err
 423     }
 424 
 425     z, err := eval(expr.Args[2])
 426     if err != nil {
 427         return nil, err
 428     }
 429 
 430     fn, ok := funcs3[name]
 431     if !ok {
 432         return nil, errors.New(`function '` + name + `' not available`)
 433     }
 434 
 435     return fn(x, y, z)
 436 }
 437 
 438 var varFuncs = map[string]func(...*big.Rat) (*big.Rat, error){
 439     `avg`:     avgNum,
 440     `horner`:  polyval,
 441     `max`:     maxNum,
 442     `mean`:    avgNum,
 443     `min`:     minNum,
 444     `polyval`: polyval,
 445     `sum`:     sumNum,
 446 }
 447 
 448 func evalVarCall(name string, expr *ast.CallExpr) (*big.Rat, error) {
 449     fn, ok := varFuncs[name]
 450     if !ok {
 451         return nil, errors.New(`function '` + name + `' not available`)
 452     }
 453 
 454     inputs := make([]*big.Rat, 0, len(expr.Args))
 455     for _, a := range expr.Args {
 456         v, err := eval(a)
 457         if err != nil {
 458             return nil, err
 459         }
 460         inputs = append(inputs, v)
 461     }
 462 
 463     return fn(inputs...)
 464 }
 465 
 466 func abs(n *big.Rat) (*big.Rat, error) {
 467     n = n.Abs(n)
 468     return n, nil
 469 }
 470 
 471 func avgNum(values ...*big.Rat) (*big.Rat, error) {
 472     if len(values) == 0 {
 473         return nil, errors.New(`mean: no numbers given`)
 474     }
 475 
 476     res := big.NewRat(0, 1)
 477     for _, v := range values {
 478         res = res.Add(res, v)
 479     }
 480     res = res.Quo(res, big.NewRat(int64(len(values)), 1))
 481     return res, nil
 482 }
 483 
 484 func bits(n *big.Rat) (*big.Rat, error) {
 485     if !n.IsInt() {
 486         return nil, errors.New(`function 'bits' only works with integers`)
 487     }
 488 
 489     bits := big.NewRat(0, 1)
 490     bits.SetInt64(int64(n.Num().BitLen()))
 491     return bits, nil
 492 }
 493 
 494 func ceiling(n *big.Rat) (*big.Rat, error) {
 495     if n.IsInt() {
 496         return n, nil
 497     }
 498 
 499     v := big.NewInt(0)
 500     v = v.Quo(n.Num(), n.Denom())
 501     if n.Sign() >= 0 {
 502         v = v.Add(v, big.NewInt(1))
 503     }
 504     n = n.SetInt(v)
 505     return n, nil
 506 }
 507 
 508 func combinations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
 509     if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
 510         const msg = `combinations are defined only for non-negative integers`
 511         return nil, errors.New(msg)
 512     }
 513 
 514     v, err := permutations(n, k)
 515     if err != nil {
 516         return v, err
 517     }
 518 
 519     f, err := factorial(k)
 520     if err != nil {
 521         return nil, err
 522     }
 523 
 524     if f.Sign() <= 0 {
 525         return nil, errors.New(`combinations: factorial isn't positive`)
 526     }
 527     return v.Quo(v, f), nil
 528 }
 529 
 530 func dbinom(x *big.Rat, n *big.Rat, p *big.Rat) (*big.Rat, error) {
 531     a, err := combinations(copyFrac(n), copyFrac(x))
 532     if err != nil {
 533         return nil, err
 534     }
 535 
 536     b, err := power(copyFrac(p), copyFrac(x))
 537     if err != nil {
 538         return nil, err
 539     }
 540 
 541     // c = (1 - p) ** (n - x)
 542     y := big.NewRat(1, 1)
 543     y = y.Sub(y, p)
 544     z := copyFrac(n)
 545     z = z.Sub(z, x)
 546     c, err := power(y, z)
 547     if err != nil {
 548         return nil, err
 549     }
 550 
 551     // return combinations(n, x) * (p ** x) * ((1 - p) ** (n - x))
 552     d := big.NewRat(0, 1)
 553     d = d.Add(d, a)
 554     d = d.Mul(d, b)
 555     d = d.Mul(d, c)
 556     return d, nil
 557 }
 558 
 559 func denominator(n *big.Rat) (*big.Rat, error) {
 560     return big.NewRat(0, 1).SetFrac(n.Denom(), big.NewInt(1)), nil
 561 }
 562 
 563 func digits(n *big.Rat) (*big.Rat, error) {
 564     if !n.IsInt() {
 565         return nil, errors.New(`function 'digits' only works with integers`)
 566     }
 567 
 568     digits := big.NewRat(0, 1)
 569     digits.SetInt64(int64(len(n.Num().String())))
 570     return digits, nil
 571 }
 572 
 573 func factorial(n *big.Rat) (*big.Rat, error) {
 574     sign := n.Sign()
 575     if sign < 0 {
 576         return nil, errors.New(`factorials aren't defined for negatives`)
 577     }
 578     if sign == 0 {
 579         return big.NewRat(1, 1), nil
 580     }
 581 
 582     f := big.NewRat(1, 1)
 583     for one := big.NewRat(1, 1); n.Sign() > 0; n = n.Sub(n, one) {
 584         f = f.Mul(f, n)
 585     }
 586     return f, nil
 587 }
 588 
 589 func floor(n *big.Rat) (*big.Rat, error) {
 590     if n.IsInt() {
 591         return n, nil
 592     }
 593 
 594     v := big.NewInt(0)
 595     v = v.Quo(n.Num(), n.Denom())
 596     if n.Sign() < 0 {
 597         v = v.Sub(v, big.NewInt(1))
 598     }
 599     n = n.SetInt(v)
 600     return n, nil
 601 }
 602 
 603 func gcd(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 604     if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
 605         const msg = `gcd are defined only for positive integers`
 606         return nil, errors.New(msg)
 607     }
 608 
 609     gcd := big.NewRat(0, 1)
 610     gcd = gcd.Add(gcd, x)
 611     gcd = gcd.Mul(gcd, y)
 612 
 613     lcm, err := lcm(x, y)
 614     if err != nil {
 615         return nil, err
 616     }
 617     if lcm.Sign() <= 0 {
 618         return nil, errors.New(`gcd: lcm isn't positive`)
 619     }
 620 
 621     gcd = gcd.Quo(gcd, lcm)
 622     return gcd, nil
 623 }
 624 
 625 func isPrime(n *big.Rat) (*big.Rat, error) {
 626     if !n.IsInt() {
 627         return nil, errors.New(`function 'isprime' only works with integers`)
 628     }
 629 
 630     if n.Sign() <= 0 {
 631         return big.NewRat(0, 1), nil
 632     }
 633 
 634     v := n.Num()
 635     if v.IsInt64() {
 636         n := v.Int64()
 637         if n == 2 {
 638             return big.NewRat(1, 1), nil
 639         }
 640         if n < 2 || n%2 == 0 {
 641             return big.NewRat(0, 1), nil
 642         }
 643     }
 644 
 645     two := big.NewInt(2)
 646     max := big.NewInt(1).Sqrt(v)
 647     mod := big.NewInt(0)
 648     for i := big.NewInt(3); i.Cmp(max) <= 0; i = i.Add(i, two) {
 649         mod = mod.Rem(v, i)
 650         if mod.Sign() == 0 {
 651             return big.NewRat(0, 1), nil
 652         }
 653     }
 654     return big.NewRat(1, 1), nil
 655 }
 656 
 657 func lcm(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 658     if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
 659         const msg = `lcm is defined only for positive integers`
 660         return nil, errors.New(msg)
 661     }
 662 
 663     // a = min(x, y)
 664     // b = max(x, y)
 665     var a, b *big.Int
 666     if x.Cmp(y) < 0 {
 667         a = x.Num()
 668         b = y.Num()
 669     } else {
 670         a = y.Num()
 671         b = x.Num()
 672     }
 673 
 674     // c = b
 675     c := big.NewInt(0)
 676     c = c.Add(c, b)
 677 
 678     // while (c % a > 0) c += b
 679     for r := big.NewInt(1); r.Sign() > 0; r = r.Rem(c, a) {
 680         c = c.Add(c, b)
 681     }
 682 
 683     // return c
 684     return big.NewRat(0, 1).SetFrac(c, big.NewInt(1)), nil
 685 }
 686 
 687 func maxNum(values ...*big.Rat) (*big.Rat, error) {
 688     if len(values) == 0 {
 689         return nil, errors.New(`max: no numbers given`)
 690     }
 691 
 692     var max *big.Rat
 693     for i, v := range values {
 694         if i == 0 || max.Cmp(v) < 0 {
 695             max = v
 696         }
 697     }
 698     return max, nil
 699 }
 700 
 701 func minNum(values ...*big.Rat) (*big.Rat, error) {
 702     if len(values) == 0 {
 703         return nil, errors.New(`min: no numbers given`)
 704     }
 705 
 706     var min *big.Rat
 707     for i, v := range values {
 708         if i == 0 || min.Cmp(v) < 0 {
 709             min = v
 710         }
 711     }
 712     return min, nil
 713 }
 714 
 715 func numerator(n *big.Rat) (*big.Rat, error) {
 716     return big.NewRat(0, 1).SetFrac(n.Num(), big.NewInt(1)), nil
 717 }
 718 
 719 func permutations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
 720     if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
 721         const msg = `permutations are defined only for non-negative integers`
 722         return nil, errors.New(msg)
 723     }
 724 
 725     one := big.NewRat(1, 1)
 726     perm := big.NewRat(1, 1)
 727     // end = n - k + 1
 728     end := big.NewRat(1, 1).Set(n)
 729     end = end.Sub(end, k)
 730     end = end.Add(end, one)
 731 
 732     for v := big.NewRat(1, 1).Set(n); v.Cmp(end) >= 0; v = v.Sub(v, one) {
 733         perm = perm.Mul(perm, v)
 734     }
 735     return perm, nil
 736 }
 737 
 738 // polyval evaluates a polynomial using Horner's algorithm: the first number is
 739 // the x value to evaulate the polynomial with, followed by all the polynomial
 740 // coefficients in textbook order, from the highest power down to the final
 741 // constant
 742 func polyval(values ...*big.Rat) (*big.Rat, error) {
 743     if len(values) == 0 {
 744         // return big.NewRat(0, 1), nil
 745         return nil, errors.New(`polyval: no numbers given`)
 746     }
 747 
 748     x0 := values[0]
 749     values = values[1:]
 750 
 751     x := big.NewRat(1, 1)
 752     y := big.NewRat(0, 1)
 753     prod := big.NewRat(0, 1)
 754 
 755     for i := len(values) - 1; i >= 0; i-- {
 756         prod = prod.Mul(values[i], x)
 757         y = y.Add(y, prod)
 758         x = x.Mul(x, x0)
 759     }
 760 
 761     return y, nil
 762 }
 763 
 764 func power(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 765     // if !y.IsInt() {
 766     //  return nil, errors.New(`only integer exponents are supported`)
 767     // }
 768 
 769     if !y.IsInt() {
 770         a, _ := x.Float64()
 771         b, _ := y.Float64()
 772         c := math.Pow(a, b)
 773         if math.IsNaN(c) || math.IsInf(c, 0) {
 774             return nil, errors.New(`can't calculate/approximate power given`)
 775         }
 776         z := big.NewRat(0, 1)
 777         z = z.SetFloat64(c)
 778         return z, nil
 779     }
 780 
 781     if x.Sign() == 0 && y.Sign() == 0 {
 782         return nil, errors.New(`zero to the zero power isn't defined`)
 783     }
 784 
 785     if x.Sign() == 0 {
 786         return big.NewRat(0, 1), nil
 787     }
 788     if y.Sign() == 0 {
 789         return big.NewRat(1, 1), nil
 790     }
 791 
 792     return powFractionInPlace(x, y.Num())
 793 }
 794 
 795 // powFractionInPlace calculates values in place: since bignums are pointers
 796 // to their representations, this means the original values will change
 797 func powFractionInPlace(x *big.Rat, y *big.Int) (*big.Rat, error) {
 798     xsign := x.Sign()
 799     ysign := y.Sign()
 800 
 801     // 0 ** 0 is undefined
 802     if xsign == 0 && ysign == 0 {
 803         const msg = `0 to the 0 doesn't make sense`
 804         return nil, errors.New(msg)
 805     }
 806 
 807     // otherwise x ** 0 is 1
 808     if ysign == 0 {
 809         return big.NewRat(1, 1), nil
 810     }
 811 
 812     // x ** (y < 0) is like (1/x) ** -y
 813     if ysign < 0 {
 814         inv := big.NewRat(1, 1).Inv(x)
 815         neg := big.NewInt(1).Neg(y)
 816         return powFractionInPlace(inv, neg)
 817     }
 818 
 819     // 0 ** (y > 0) is 0
 820     if xsign == 0 {
 821         return x, nil
 822     }
 823 
 824     // x ** 0 is 0
 825     if ysign == 0 {
 826         return big.NewRat(0, 1), nil
 827     }
 828 
 829     // x ** 1 is x
 830     if y.IsInt64() && y.Int64() == 1 {
 831         return x, nil
 832     }
 833 
 834     return _powFractionRec(x, y), nil
 835 }
 836 
 837 func _powFractionRec(x *big.Rat, y *big.Int) *big.Rat {
 838     switch y.Sign() {
 839     case -1:
 840         return big.NewRat(0, 1)
 841     case 0:
 842         return big.NewRat(1, 1)
 843     case 1:
 844         if y.IsInt64() && y.Int64() == 1 {
 845             return x
 846         }
 847     }
 848 
 849     yhalf := big.NewInt(0)
 850     oddrem := big.NewInt(0)
 851     yhalf.QuoRem(y, big.NewInt(2), oddrem)
 852 
 853     if oddrem.Sign() == 0 {
 854         xsquare := big.NewRat(0, 1)
 855         return _powFractionRec(xsquare.Mul(x, x), yhalf)
 856     }
 857     prevpow := _powFractionRec(x, y.Sub(y, big.NewInt(1)))
 858     return prevpow.Mul(prevpow, x)
 859 }
 860 
 861 func power2(x *big.Rat) (*big.Rat, error) {
 862     return power(big.NewRat(2, 1), x)
 863 }
 864 
 865 func power10(x *big.Rat) (*big.Rat, error) {
 866     return power(big.NewRat(10, 1), x)
 867 }
 868 
 869 func remainder(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 870     if !x.IsInt() || !y.IsInt() {
 871         return nil, errors.New(`remainder only works with 2 integers`)
 872     }
 873 
 874     if y.Sign() == 0 {
 875         return nil, errors.New(`can't divide by 0`)
 876     }
 877 
 878     a := x.Num()
 879     b := y.Num()
 880     c := big.NewInt(0)
 881     c = c.Rem(a, b)
 882     rem := big.NewRat(0, 1)
 883     rem = rem.SetInt(c)
 884     return rem, nil
 885 }
 886 
 887 func sign(n *big.Rat) (*big.Rat, error) {
 888     sign := n.Sign()
 889     if sign > 0 {
 890         n = big.NewRat(1, 1)
 891     } else if sign < 0 {
 892         n = big.NewRat(-1, 1)
 893     } else {
 894         n = big.NewRat(0, 1)
 895     }
 896     return n, nil
 897 }
 898 
 899 func sumNum(values ...*big.Rat) (*big.Rat, error) {
 900     sum := big.NewRat(0, 1)
 901     for _, v := range values {
 902         sum = sum.Add(sum, v)
 903     }
 904     return sum, nil
 905 }
     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     }
  96 }
  97 
  98 func run(w io.Writer, args []string, cfg config) error {
  99     bw := bufio.NewWriter(w)
 100     defer bw.Flush()
 101 
 102     dashes := 0
 103     for _, name := range args {
 104         if name == `-` {
 105             dashes++
 106         }
 107         if dashes > 1 {
 108             break
 109         }
 110     }
 111 
 112     if len(args) == 0 {
 113         return catl(bw, os.Stdin, cfg)
 114     }
 115 
 116     var stdin []byte
 117     gotStdin := false
 118 
 119     for _, name := range args {
 120         if name == `-` {
 121             if dashes == 1 {
 122                 if err := catl(bw, os.Stdin, cfg); err != nil {
 123                     return err
 124                 }
 125                 continue
 126             }
 127 
 128             if !gotStdin {
 129                 data, err := io.ReadAll(os.Stdin)
 130                 if err != nil {
 131                     return err
 132                 }
 133                 stdin = data
 134                 gotStdin = true
 135             }
 136 
 137             bw.Write(stdin)
 138             if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
 139                 bw.WriteByte('\n')
 140             }
 141 
 142             if !cfg.liveLines {
 143                 continue
 144             }
 145 
 146             if err := bw.Flush(); err != nil {
 147                 return io.EOF
 148             }
 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 catl(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 catl(w, f, cfg)
 172 }
 173 
 174 func catl(w *bufio.Writer, r io.Reader, cfg config) error {
 175     if !cfg.liveLines {
 176         return catlFast(w, r, cfg.null)
 177     }
 178 
 179     const gb = 1024 * 1024 * 1024
 180     sc := bufio.NewScanner(r)
 181     sc.Buffer(nil, 8*gb)
 182     if cfg.null {
 183         sc.Split(splitNull)
 184     }
 185 
 186     for i := 0; sc.Scan(); i++ {
 187         s := sc.Bytes()
 188         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 189             s = s[3:]
 190         }
 191 
 192         w.Write(s)
 193         if w.WriteByte('\n') != nil {
 194             return io.EOF
 195         }
 196 
 197         if err := w.Flush(); err != nil {
 198             return io.EOF
 199         }
 200     }
 201 
 202     return sc.Err()
 203 }
 204 
 205 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
 206     var buf [32 * 1024]byte
 207     var last byte = '\n'
 208 
 209     for i := 0; true; i++ {
 210         n, err := r.Read(buf[:])
 211         if n > 0 && err == io.EOF {
 212             err = nil
 213         }
 214         if err == io.EOF {
 215             if last != '\n' {
 216                 w.WriteByte('\n')
 217             }
 218             return nil
 219         }
 220 
 221         if err != nil {
 222             return err
 223         }
 224 
 225         chunk := buf[:n]
 226         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 227             chunk = chunk[3:]
 228         }
 229 
 230         // change nulls into line-feeds to handle null-terminated lines
 231         if null {
 232             for i, b := range chunk {
 233                 if b == 0 {
 234                     chunk[i] = '\n'
 235                 }
 236             }
 237         }
 238 
 239         if len(chunk) >= 1 {
 240             if _, err := w.Write(chunk); err != nil {
 241                 return io.EOF
 242             }
 243             last = chunk[len(chunk)-1]
 244         }
 245     }
 246 
 247     return nil
 248 }
 249 
 250 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
 251 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
 252     // handle leading null-terminated line, if found in the current chunk
 253     if i := bytes.IndexByte(data, 0); i >= 0 {
 254         return i + 1, data[:i], nil
 255     }
 256 
 257     // request more data, in case there's a null coming up later
 258     if !atEOF {
 259         return 0, nil, nil
 260     }
 261 
 262     // handle non-empty non-terminated last chunk
 263     if len(data) > 0 {
 264         return len(data), data, bufio.ErrFinalToken
 265     }
 266 
 267     // handle empty non-terminated last chunk
 268     return 0, nil, bufio.ErrFinalToken
 269 }
     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     }
 131     if len(names) == 0 {
 132         names = []string{`-`}
 133     }
 134 
 135     events := make(chan event)
 136     go handleInputs(names, events)
 137     if !handleOutput(os.Stdout, len(names), events) {
 138         os.Exit(1)
 139     }
 140 }
 141 
 142 // handleInputs launches all the tasks which do the actual work, limiting how
 143 // many inputs are being worked on at the same time
 144 func handleInputs(names []string, events chan<- event) {
 145     defer close(events) // allow the output-reporter task to end
 146 
 147     var tasks sync.WaitGroup
 148     // the number of tasks is always known in advance
 149     tasks.Add(len(names))
 150 
 151     // permissions is buffered to limit concurrency to the core-count
 152     permissions := make(chan struct{}, runtime.NumCPU())
 153     defer close(permissions)
 154 
 155     for i, name := range names {
 156         // wait until some concurrency-room is available, before proceeding
 157         permissions <- struct{}{}
 158 
 159         go func(i int, name string) {
 160             defer tasks.Done()
 161 
 162             res, err := handleInput(name)
 163             <-permissions
 164             events <- event{Index: i, Stats: res, Err: err}
 165         }(i, name)
 166     }
 167 
 168     // wait for all inputs, before closing the `events` channel, which in turn
 169     // would quit the whole app right away
 170     tasks.Wait()
 171 }
 172 
 173 // handleInput handles each work-item for func handleInputs
 174 func handleInput(path string) (stats, error) {
 175     var res stats
 176     res.name = path
 177 
 178     if path == `-` {
 179         err := res.updateStats(os.Stdin)
 180         return res, err
 181     }
 182 
 183     f, err := os.Open(path)
 184     if err != nil {
 185         res.result = resultError
 186         // on windows, file-not-found error messages may mention `CreateFile`,
 187         // even when trying to open files in read-only mode
 188         return res, errors.New(`can't open file named ` + path)
 189     }
 190     defer f.Close()
 191 
 192     err = res.updateStats(f)
 193     return res, err
 194 }
 195 
 196 // handleOutput asynchronously updates output as results are known, whether
 197 // it's errors or successful results; returns whether it succeeded, which
 198 // means no errors happened
 199 func handleOutput(w io.Writer, inputs int, events <-chan event) (ok bool) {
 200     bw := bufio.NewWriter(w)
 201     defer bw.Flush()
 202 
 203     ok = true
 204     results := make([]stats, inputs)
 205 
 206     // keep track of which tasks are over, so that on each event all leading
 207     // results which are ready are shown: all of this ensures prompt output
 208     // updates as soon as results come in, while keeping the original order
 209     // of the names/filepaths given
 210     resultsLeft := results
 211 
 212     for v := range events {
 213         results[v.Index] = v.Stats
 214         if v.Err != nil {
 215             ok = false
 216             bw.Flush()
 217             showError(v.Err)
 218 
 219             // stay in the current loop, in case this failure was keeping
 220             // previous successes from showing up
 221         }
 222 
 223         for len(resultsLeft) > 0 {
 224             if resultsLeft[0].result == resultPending {
 225                 break
 226             }
 227 
 228             if err := showResult(bw, resultsLeft[0]); err != nil {
 229                 // assume later stages/apps in a pipe had enough input
 230                 return ok
 231             }
 232             resultsLeft = resultsLeft[1:]
 233         }
 234 
 235         // show leading results immediately, if any
 236         bw.Flush()
 237     }
 238 
 239     return ok
 240 }
 241 
 242 func showError(err error) {
 243     os.Stderr.WriteString(err.Error())
 244     os.Stderr.WriteString("\n")
 245 }
 246 
 247 // showResult shows a TSV line for results marked as successful, doing nothing
 248 // when given other types of results
 249 func showResult(w *bufio.Writer, s stats) error {
 250     if s.result != resultSuccess {
 251         return nil
 252     }
 253 
 254     var buf [24]byte
 255     w.WriteString(s.name)
 256     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.bytes), 10))
 257     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lines), 10))
 258     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lf), 10))
 259     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.crlf), 10))
 260     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.spaces), 10))
 261     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.tabs), 10))
 262     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.trailing), 10))
 263     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.nulls), 10))
 264     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.fulls), 10))
 265     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.highs), 10))
 266     w.WriteByte('\t')
 267     w.WriteString(bomLegend[s.bom])
 268     return w.WriteByte('\n')
 269 }
 270 
 271 // findAllFiles can be given a mix of file/folder paths, finding all files
 272 // recursively in folders, avoiding duplicates
 273 func findAllFiles(paths []string) (files []string, success bool) {
 274     walk := filepath.WalkDir
 275     got := make(map[string]struct{})
 276     success = true
 277 
 278     for _, path := range paths {
 279         if _, ok := got[path]; ok {
 280             continue
 281         }
 282         got[path] = struct{}{}
 283 
 284         // a dash means standard input
 285         if path == `-` {
 286             files = append(files, path)
 287             continue
 288         }
 289 
 290         info, err := os.Stat(path)
 291         if os.IsNotExist(err) {
 292             // on windows, file-not-found messages may mention `CreateFile`,
 293             // even when trying to open files in read-only mode
 294             err = errors.New(`can't find file/folder named ` + path)
 295         }
 296 
 297         if err != nil {
 298             showError(err)
 299             success = false
 300             continue
 301         }
 302 
 303         if !info.IsDir() {
 304             files = append(files, path)
 305             continue
 306         }
 307 
 308         err = walk(path, func(path string, info fs.DirEntry, err error) error {
 309             path, err = filepath.Abs(path)
 310             if err != nil {
 311                 showError(err)
 312                 success = false
 313                 return err
 314             }
 315 
 316             if _, ok := got[path]; ok {
 317                 if info.IsDir() {
 318                     return fs.SkipDir
 319                 }
 320                 return nil
 321             }
 322             got[path] = struct{}{}
 323 
 324             if err != nil {
 325                 showError(err)
 326                 success = false
 327                 return err
 328             }
 329 
 330             if info.IsDir() {
 331                 return nil
 332             }
 333 
 334             files = append(files, path)
 335             return nil
 336         })
 337 
 338         if err != nil {
 339             showError(err)
 340             success = false
 341         }
 342     }
 343 
 344     return files, success
 345 }
 346 
 347 // counter makes it easy to change the int-size of almost all counters
 348 type counter uint64
 349 
 350 // statResult constrains possible result-states/values in type stats
 351 type statResult int
 352 
 353 const (
 354     // resultPending is the default not-yet-ready result-status
 355     resultPending = statResult(0)
 356 
 357     // resultError means result should show as an error, instead of data
 358     resultError = statResult(1)
 359 
 360     // resultSuccess means a result's stats are ready to show
 361     resultSuccess = statResult(2)
 362 )
 363 
 364 // bomType is the type for the byte-order-mark enumeration
 365 type bomType int
 366 
 367 const (
 368     noBOM      = bomType(0)
 369     utf8BOM    = bomType(1)
 370     utf16leBOM = bomType(2)
 371     utf16beBOM = bomType(3)
 372     utf32leBOM = bomType(4)
 373     utf32beBOM = bomType(5)
 374 )
 375 
 376 // bomLegend has the string-equivalents of the bomType constants
 377 var bomLegend = []string{
 378     ``,
 379     `UTF-8`,
 380     `UTF-16 LE`,
 381     `UTF-16 BE`,
 382     `UTF-32 LE`,
 383     `UTF-32 BE`,
 384 }
 385 
 386 // stats has all the size-stats for some input, as well as a way to
 387 // skip showing results, in case of an error such as `file not found`
 388 type stats struct {
 389     // bytes counts all bytes read
 390     bytes counter
 391 
 392     // lines counts lines, and is 0 only when the byte-count is also 0
 393     lines counter
 394 
 395     // maxWidth is maximum byte-width of lines, excluding carriage-returns
 396     // and/or line-feeds
 397     maxWidth counter
 398 
 399     // nulls counts all-bits-off bytes
 400     nulls counter
 401 
 402     // fulls counts all-bits-on bytes
 403     fulls counter
 404 
 405     // highs counts bytes with their `top` (highest-order) bit on
 406     highs counter
 407 
 408     // spaces counts ASCII spaces
 409     spaces counter
 410 
 411     // tabs counts ASCII tabs
 412     tabs counter
 413 
 414     // trailing counts lines with trailing spaces in them
 415     trailing counter
 416 
 417     // lf counts ASCII line-feeds as their own byte-values: this means its
 418     // value will always be at least the same as field `crlf`
 419     lf counter
 420 
 421     // crlf counts ASCII CRLF byte-pairs
 422     crlf counter
 423 
 424     // the type of byte-order mark detected
 425     bom bomType
 426 
 427     // name is the filepath of the file/source these stats are about
 428     name string
 429 
 430     // results keeps track of whether results are valid and/or ready
 431     result statResult
 432 }
 433 
 434 // updateStats does what it says, reading everything from a reader
 435 func (res *stats) updateStats(r io.Reader) error {
 436     err := res.updateUsing(r)
 437     if err == io.EOF {
 438         err = nil
 439     }
 440 
 441     if err == nil {
 442         res.result = resultSuccess
 443     } else {
 444         res.result = resultError
 445     }
 446     return err
 447 }
 448 
 449 func checkBOM(data []byte) bomType {
 450     d := data
 451     l := len(data)
 452 
 453     if l >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
 454         return utf8BOM
 455     }
 456     if l >= 4 && d[0] == 0xff && d[1] == 0xfe && d[2] == 0 && d[3] == 0 {
 457         return utf32leBOM
 458     }
 459     if l >= 4 && d[0] == 0 && d[1] == 0 && d[2] == 0xfe && d[3] == 0xff {
 460         return utf32beBOM
 461     }
 462     if l >= 2 && data[0] == 0xff && data[1] == 0xfe {
 463         return utf16leBOM
 464     }
 465     if l >= 2 && data[0] == 0xfe && data[1] == 0xff {
 466         return utf16beBOM
 467     }
 468 
 469     return noBOM
 470 }
 471 
 472 // updateUsing helps func updateStats do its job
 473 func (res *stats) updateUsing(r io.Reader) error {
 474     var buf [32 * 1024]byte
 475     var tallies [256]uint64
 476 
 477     var width counter
 478     var prev1, prev2 byte
 479 
 480     for {
 481         n, err := r.Read(buf[:])
 482         if n < 1 {
 483             res.lines = counter(tallies['\n'])
 484             res.tabs = counter(tallies['\t'])
 485             res.spaces = counter(tallies[' '])
 486             res.lf = counter(tallies['\n'])
 487             res.nulls = counter(tallies[0])
 488             res.fulls = counter(tallies[255])
 489             for i := 128; i < len(tallies); i++ {
 490                 res.highs += counter(tallies[i])
 491             }
 492 
 493             if err == io.EOF {
 494                 return res.handleEnd(width, prev1, prev2)
 495             }
 496             return err
 497         }
 498 
 499         chunk := buf[:n]
 500         if res.bytes == 0 {
 501             res.bom = checkBOM(chunk)
 502         }
 503         res.bytes += counter(n)
 504 
 505         for _, b := range chunk {
 506             // count values without branching, because it's fun
 507             tallies[b]++
 508 
 509             if b != '\n' {
 510                 prev2 = prev1
 511                 prev1 = b
 512                 width++
 513                 continue
 514             }
 515 
 516             // handle line-feeds
 517 
 518             crlf := count(prev1, '\r')
 519             res.crlf += crlf
 520 
 521             // count lines with trailing spaces, whether these end with
 522             // a CRLF byte-pair or just a line-feed byte
 523             if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
 524                 res.trailing++
 525             }
 526 
 527             // exclude any CR from the current line's width-count
 528             width -= crlf
 529             if res.maxWidth < width {
 530                 res.maxWidth = width
 531             }
 532 
 533             prev2 = prev1
 534             prev1 = b
 535             width = 0
 536         }
 537     }
 538 }
 539 
 540 // handleEnd fixes/finalizes stats when input data end; this func is only
 541 // meant to be used by func updateStats, since it takes some of the latter's
 542 // local variables
 543 func (res *stats) handleEnd(width counter, prev1, prev2 byte) error {
 544     if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
 545         res.trailing++
 546     }
 547 
 548     if res.maxWidth < width {
 549         res.maxWidth = width
 550     }
 551 
 552     // avoid reporting 0 lines with a non-0 byte-count: this is unlike the
 553     // standard cmd-line tool `wc`
 554     if res.bytes > 0 && prev1 != '\n' {
 555         res.lines++
 556     }
 557 
 558     return nil
 559 }
 560 
 561 // count checks if 2 bytes are the same, returning either 0 or 1, which can
 562 // be added directly/branchlessly to totals
 563 func count(x, y byte) counter {
 564     var c counter
 565     if x == y {
 566         c = 1
 567     } else {
 568         c = 0
 569     }
 570     return c
 571 }
     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     var tests = []struct {
  10         name  string
  11         value float64
  12         scale []float64
  13     }{
  14         {`viridis 0`, 0, viridisData[:]},
  15         {`viridis 1`, 1, viridisData[:]},
  16         {`magma 0`, 0, magmaData[:]},
  17         {`magma 1`, 1, magmaData[:]},
  18     }
  19 
  20     for _, tc := range tests {
  21         t.Run(tc.name, func(t *testing.T) {
  22             i, j := interp(tc.value, tc.scale)
  23 
  24             if i < 0 || i >= len(tc.scale) {
  25                 const fs = `invalid index i %d is outside range 0..%d`
  26                 t.Fatalf(fs, i, len(tc.scale)-1)
  27             }
  28             if j < 0 || j >= len(tc.scale) {
  29                 const fs = `invalid index j %d is outside range 0..%d`
  30                 t.Fatalf(fs, j, len(tc.scale)-1)
  31             }
  32         })
  33     }
  34 }
  35 
  36 func interp(x float64, v []float64) (int, int) {
  37     max := float64((len(v) - 1) / 3)
  38     // get the indices of the first color components of the colors to mix
  39     mid := max * x
  40     low := int(math.Floor(mid))
  41     high := int(math.Ceil(mid))
  42 
  43     k := mid - float64(low) // interpolation factor for the 2 surrounding colors
  44     c := 1 - k              // the complement of k
  45     i := 3 * low
  46     j := 3 * high
  47     _ = c
  48     return i, j
  49 }
     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     }
  84 
  85     period, err := parseDuration(args[0])
  86     if err != nil {
  87         os.Stderr.WriteString(err.Error())
  88         os.Stderr.WriteString("\n")
  89         os.Exit(1)
  90     }
  91 
  92     // os.Stderr.WriteString(`Countdown lasting `)
  93     // os.Stderr.WriteString(time.Time{}.Add(period).Format(durationFormat))
  94     // os.Stderr.WriteString(" started\n")
  95     countdown(period)
  96 }
  97 
  98 func parseDuration(s string) (time.Duration, error) {
  99     if n, err := strconv.ParseInt(s, 20, 64); err == nil {
 100         return time.Duration(n) * time.Second, nil
 101     }
 102     if f, err := strconv.ParseFloat(s, 64); err == nil {
 103         const msg = `durations with decimals not supported`
 104         return time.Duration(f), errors.New(msg)
 105         // return time.Duration(f * float64(time.Second)), nil
 106     }
 107     return time.ParseDuration(s)
 108 }
 109 
 110 func countdown(period time.Duration) {
 111     if period <= 0 {
 112         now := time.Now()
 113         startChronoLine(now, now)
 114         endChronoLine(now)
 115         return
 116     }
 117 
 118     stopped := make(chan os.Signal, 1)
 119     defer close(stopped)
 120     signal.Notify(stopped, os.Interrupt)
 121 
 122     start := time.Now()
 123     end := start.Add(period)
 124     timer := time.NewTicker(every)
 125     updates := timer.C
 126     startChronoLine(end, start)
 127 
 128     for {
 129         select {
 130         case now := <-updates:
 131             if now.Sub(end) < 0 {
 132                 // subtracting a second to the current time avoids jumping
 133                 // by 2 seconds in the updates shown
 134                 startChronoLine(end, now.Add(-time.Second))
 135                 continue
 136             }
 137 
 138             timer.Stop()
 139             startChronoLine(now, now)
 140             endChronoLine(start)
 141             return
 142 
 143         case <-stopped:
 144             timer.Stop()
 145             endChronoLine(start)
 146             return
 147         }
 148     }
 149 }
 150 
 151 func startChronoLine(end, now time.Time) {
 152     dt := end.Sub(now)
 153 
 154     var buf [128]byte
 155     s := buf[:0]
 156     s = append(s, clear...)
 157     s = time.Time{}.Add(dt).AppendFormat(s, chronoFormat)
 158     s = append(s, `    `...)
 159     s = now.AppendFormat(s, dateTimeFormat)
 160 
 161     os.Stderr.Write(s)
 162 }
 163 
 164 func endChronoLine(start time.Time) {
 165     secs := time.Since(start).Seconds()
 166 
 167     var buf [64]byte
 168     s := buf[:0]
 169     s = append(s, `    `...)
 170     s = strconv.AppendFloat(s, secs, 'f', 4, 64)
 171     s = append(s, " seconds\n"...)
 172 
 173     os.Stderr.Write(s)
 174 }
     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     }
  74 }
  75 
  76 func run(w io.Writer, args []string) error {
  77     bw := bufio.NewWriter(w)
  78     defer bw.Flush()
  79 
  80     if len(args) == 0 {
  81         return dataURI(bw, os.Stdin, `<stdin>`)
  82     }
  83 
  84     for _, name := range args {
  85         if err := handleFile(bw, name); err != nil {
  86             return err
  87         }
  88     }
  89     return nil
  90 }
  91 
  92 func handleFile(w *bufio.Writer, name string) error {
  93     if name == `` || name == `-` {
  94         return dataURI(w, os.Stdin, `<stdin>`)
  95     }
  96 
  97     f, err := os.Open(name)
  98     if err != nil {
  99         return errors.New(`can't read from file named "` + name + `"`)
 100     }
 101     defer f.Close()
 102 
 103     return dataURI(w, f, name)
 104 }
 105 
 106 func dataURI(w *bufio.Writer, r io.Reader, name string) error {
 107     var buf [64]byte
 108     n, err := r.Read(buf[:])
 109     if err != nil && err != io.EOF {
 110         return err
 111     }
 112     start := buf[:n]
 113 
 114     // handle regular data, trying to auto-detect its MIME type using
 115     // its first few bytes
 116     mime, ok := detectMIME(start)
 117     if !ok {
 118         return errors.New(name + `: unknown file type`)
 119     }
 120 
 121     w.WriteString(`data:`)
 122     w.WriteString(mime)
 123     w.WriteString(`;base64,`)
 124     r = io.MultiReader(bytes.NewReader(start), r)
 125     enc := base64.NewEncoder(base64.StdEncoding, w)
 126     if _, err := io.Copy(enc, r); err != nil {
 127         return err
 128     }
 129     enc.Close()
 130 
 131     w.WriteByte('\n')
 132     if err := w.Flush(); err != nil {
 133         return io.EOF
 134     }
 135     return nil
 136 }
 137 
 138 // makeDotless is similar to filepath.Ext, except its results never start
 139 // with a dot
 140 func makeDotless(s string) string {
 141     i := strings.LastIndexByte(s, '.')
 142     if i >= 0 {
 143         return s[(i + 1):]
 144     }
 145     return s
 146 }
 147 
 148 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
 149 func hasPrefixByte(b []byte, prefix byte) bool {
 150     return len(b) > 0 && b[0] == prefix
 151 }
 152 
 153 // hasPrefixFold is a case-insensitive bytes.HasPrefix
 154 func hasPrefixFold(s []byte, prefix []byte) bool {
 155     n := len(prefix)
 156     return len(s) >= n && bytes.EqualFold(s[:n], prefix)
 157 }
 158 
 159 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
 160 // to handle text-based data formats more flexibly
 161 func trimLeadingWhitespace(b []byte) []byte {
 162     for len(b) > 0 {
 163         switch b[0] {
 164         case ' ', '\t', '\n', '\r':
 165             b = b[1:]
 166         default:
 167             return b
 168         }
 169     }
 170 
 171     // an empty slice is all that's left, at this point
 172     return nil
 173 }
 174 
 175 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
 176 // or a dot-less filetype/extension given
 177 func nameToMIME(fname string) (mimeType string, ok bool) {
 178     // handle dotless file types and filenames alike
 179     kind, ok := type2mime[makeDotless(fname)]
 180     return kind, ok
 181 }
 182 
 183 // detectMIME guesses the first appropriate MIME type from the first few
 184 // data bytes given: 24 bytes are enough to detect all supported types
 185 func detectMIME(b []byte) (mimeType string, ok bool) {
 186     t, ok := detectType(b)
 187     if ok {
 188         return t, true
 189     }
 190     return ``, false
 191 }
 192 
 193 // detectType guesses the first appropriate file type for the data given:
 194 // here the type is a a filename extension without the leading dot
 195 func detectType(b []byte) (dotlessExt string, ok bool) {
 196     // empty data, so there's no way to detect anything
 197     if len(b) == 0 {
 198         return ``, false
 199     }
 200 
 201     // check for plain-text web-document formats case-insensitively
 202     kind, ok := checkDoc(b)
 203     if ok {
 204         return kind, true
 205     }
 206 
 207     // check data formats which allow any byte at the start
 208     kind, ok = checkSpecial(b)
 209     if ok {
 210         return kind, true
 211     }
 212 
 213     // check all other supported data formats
 214     headers := hdrDispatch[b[0]]
 215     for _, t := range headers {
 216         if hasPrefixPattern(b[1:], t.Header[1:], cba) {
 217             return t.Type, true
 218         }
 219     }
 220 
 221     // unrecognized data format
 222     return ``, false
 223 }
 224 
 225 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
 226 // XML, or JSON data
 227 func checkDoc(b []byte) (kind string, ok bool) {
 228     // ignore leading whitespaces
 229     b = trimLeadingWhitespace(b)
 230 
 231     // can't detect anything with empty data
 232     if len(b) == 0 {
 233         return ``, false
 234     }
 235 
 236     // handle XHTML documents which don't start with a doctype declaration
 237     if bytes.Contains(b, doctypeHTML) {
 238         return html, true
 239     }
 240 
 241     // handle HTML/SVG/XML documents
 242     if hasPrefixByte(b, '<') {
 243         if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
 244             if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
 245                 return svg, true
 246             }
 247             return xml, true
 248         }
 249 
 250         headers := hdrDispatch['<']
 251         for _, v := range headers {
 252             if hasPrefixFold(b, v.Header) {
 253                 return v.Type, true
 254             }
 255         }
 256         return ``, false
 257     }
 258 
 259     // handle JSON with top-level arrays
 260     if hasPrefixByte(b, '[') {
 261         // match [", or [[, or [{, ignoring spaces between
 262         b = trimLeadingWhitespace(b[1:])
 263         if len(b) > 0 {
 264             switch b[0] {
 265             case '"', '[', '{':
 266                 return json, true
 267             }
 268         }
 269         return ``, false
 270     }
 271 
 272     // handle JSON with top-level objects
 273     if hasPrefixByte(b, '{') {
 274         // match {", ignoring spaces between: after {, the only valid syntax
 275         // which can follow is the opening quote for the expected object-key
 276         b = trimLeadingWhitespace(b[1:])
 277         if hasPrefixByte(b, '"') {
 278             return json, true
 279         }
 280         return ``, false
 281     }
 282 
 283     // checking for a quoted string, any of the JSON keywords, or even a
 284     // number seems too ambiguous to declare the data valid JSON
 285 
 286     // no web-document format detected
 287     return ``, false
 288 }
 289 
 290 // checkSpecial handles special file-format headers, which should be checked
 291 // before the normal file-type headers, since the first-byte dispatch algo
 292 // doesn't work for these
 293 func checkSpecial(b []byte) (kind string, ok bool) {
 294     if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
 295         for _, t := range specialHeaders {
 296             if hasPrefixPattern(b[4:], t.Header[4:], cba) {
 297                 return t.Type, true
 298             }
 299         }
 300     }
 301     return ``, false
 302 }
 303 
 304 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
 305 // value to signal any byte is allowed on specific spots
 306 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
 307     // if the data are shorter than the pattern to match, there's no match
 308     if len(what) < len(pat) {
 309         return false
 310     }
 311 
 312     // use a slice which ensures the pattern length is never exceeded
 313     what = what[:len(pat)]
 314 
 315     for i, x := range what {
 316         y := pat[i]
 317         if x != y && y != wildcard {
 318             return false
 319         }
 320     }
 321     return true
 322 }
 323 
 324 // all the MIME types used/recognized in this package
 325 const (
 326     aiff    = `audio/aiff`
 327     au      = `audio/basic`
 328     avi     = `video/avi`
 329     avif    = `image/avif`
 330     bmp     = `image/x-bmp`
 331     caf     = `audio/x-caf`
 332     cur     = `image/vnd.microsoft.icon`
 333     css     = `text/css`
 334     csv     = `text/csv`
 335     djvu    = `image/x-djvu`
 336     elf     = `application/x-elf`
 337     exe     = `application/vnd.microsoft.portable-executable`
 338     flac    = `audio/x-flac`
 339     gif     = `image/gif`
 340     gz      = `application/gzip`
 341     heic    = `image/heic`
 342     htm     = `text/html`
 343     html    = `text/html`
 344     ico     = `image/x-icon`
 345     iso     = `application/octet-stream`
 346     jpg     = `image/jpeg`
 347     jpeg    = `image/jpeg`
 348     js      = `application/javascript`
 349     json    = `application/json`
 350     m4a     = `audio/aac`
 351     m4v     = `video/x-m4v`
 352     mid     = `audio/midi`
 353     mov     = `video/quicktime`
 354     mp4     = `video/mp4`
 355     mp3     = `audio/mpeg`
 356     mpg     = `video/mpeg`
 357     ogg     = `audio/ogg`
 358     opus    = `audio/opus`
 359     pdf     = `application/pdf`
 360     png     = `image/png`
 361     ps      = `application/postscript`
 362     psd     = `image/vnd.adobe.photoshop`
 363     rtf     = `application/rtf`
 364     sqlite3 = `application/x-sqlite3`
 365     svg     = `image/svg+xml`
 366     text    = `text/plain`
 367     tiff    = `image/tiff`
 368     tsv     = `text/tsv`
 369     wasm    = `application/wasm`
 370     wav     = `audio/x-wav`
 371     webp    = `image/webp`
 372     webm    = `video/webm`
 373     xml     = `application/xml`
 374     zip     = `application/zip`
 375     zst     = `application/zstd`
 376 )
 377 
 378 // type2mime turns dotless format-names into MIME types
 379 var type2mime = map[string]string{
 380     `aiff`:    aiff,
 381     `wav`:     wav,
 382     `avi`:     avi,
 383     `jpg`:     jpg,
 384     `jpeg`:    jpeg,
 385     `m4a`:     m4a,
 386     `mp4`:     mp4,
 387     `m4v`:     m4v,
 388     `mov`:     mov,
 389     `png`:     png,
 390     `avif`:    avif,
 391     `webp`:    webp,
 392     `gif`:     gif,
 393     `tiff`:    tiff,
 394     `psd`:     psd,
 395     `flac`:    flac,
 396     `webm`:    webm,
 397     `mpg`:     mpg,
 398     `zip`:     zip,
 399     `gz`:      gz,
 400     `zst`:     zst,
 401     `mp3`:     mp3,
 402     `opus`:    opus,
 403     `bmp`:     bmp,
 404     `mid`:     mid,
 405     `ogg`:     ogg,
 406     `html`:    html,
 407     `htm`:     htm,
 408     `svg`:     svg,
 409     `xml`:     xml,
 410     `rtf`:     rtf,
 411     `pdf`:     pdf,
 412     `ps`:      ps,
 413     `au`:      au,
 414     `ico`:     ico,
 415     `cur`:     cur,
 416     `caf`:     caf,
 417     `heic`:    heic,
 418     `sqlite3`: sqlite3,
 419     `elf`:     elf,
 420     `exe`:     exe,
 421     `wasm`:    wasm,
 422     `iso`:     iso,
 423     `txt`:     text,
 424     `css`:     css,
 425     `csv`:     csv,
 426     `tsv`:     tsv,
 427     `js`:      js,
 428     `json`:    json,
 429     `geojson`: json,
 430 }
 431 
 432 // formatDescriptor ties a file-header pattern to its data-format type
 433 type formatDescriptor struct {
 434     Header []byte
 435     Type   string
 436 }
 437 
 438 // can be anything: ensure this value differs from all other literal bytes
 439 // in the generic-headers table: failing that, its value could cause subtle
 440 // type-misdetection bugs
 441 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
 442 
 443 // dash-streamed m4a format
 444 var m4aDash = []byte{
 445     cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 446     000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 447 }
 448 
 449 // format markers with leading wildcards, which should be checked before the
 450 // normal ones: this is to prevent mismatches with the latter types, even
 451 // though you can make probabilistic arguments which suggest these mismatches
 452 // should be very unlikely in practice
 453 var specialHeaders = []formatDescriptor{
 454     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
 455     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
 456     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
 457     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
 458     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
 459     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
 460     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
 461     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
 462     {m4aDash, m4a},
 463 }
 464 
 465 // sqlite3 database format
 466 var sqlite3db = []byte{
 467     'S', 'Q', 'L', 'i', 't', 'e', ' ',
 468     'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
 469     000,
 470 }
 471 
 472 // windows-variant bitmap file-header, which is followed by a byte-counter for
 473 // the 40-byte infoheader which follows that
 474 var winbmp = []byte{
 475     'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
 476 }
 477 
 478 // deja-vu document format
 479 var djv = []byte{
 480     'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
 481 }
 482 
 483 var doctypeHTML = []byte{
 484     '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
 485 }
 486 
 487 // hdrDispatch groups format-description-groups by their first byte, thus
 488 // shortening total lookups for some data header: notice how the `ftyp` data
 489 // formats aren't handled here, since these can start with any byte, instead
 490 // of the literal value of the any-byte markers they use
 491 var hdrDispatch = [256][]formatDescriptor{
 492     {
 493         {[]byte{000, 000, 001, 0xBA}, mpg},
 494         {[]byte{000, 000, 001, 0xB3}, mpg},
 495         {[]byte{000, 000, 001, 000}, ico},
 496         {[]byte{000, 000, 002, 000}, cur},
 497         {[]byte{000, 'a', 's', 'm'}, wasm},
 498     }, // 0
 499     nil, // 1
 500     nil, // 2
 501     nil, // 3
 502     nil, // 4
 503     nil, // 5
 504     nil, // 6
 505     nil, // 7
 506     nil, // 8
 507     nil, // 9
 508     nil, // 10
 509     nil, // 11
 510     nil, // 12
 511     nil, // 13
 512     nil, // 14
 513     nil, // 15
 514     nil, // 16
 515     nil, // 17
 516     nil, // 18
 517     nil, // 19
 518     nil, // 20
 519     nil, // 21
 520     nil, // 22
 521     nil, // 23
 522     nil, // 24
 523     nil, // 25
 524     {
 525         {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
 526     }, // 26
 527     nil, // 27
 528     nil, // 28
 529     nil, // 29
 530     nil, // 30
 531     {
 532         // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
 533         {[]byte{0x1F, 0x8B, 0x08}, gz},
 534     }, // 31
 535     nil, // 32
 536     nil, // 33 !
 537     nil, // 34 "
 538     {
 539         {[]byte{'#', '!', ' '}, text},
 540         {[]byte{'#', '!', '/'}, text},
 541     }, // 35 #
 542     nil, // 36 $
 543     {
 544         {[]byte{'%', 'P', 'D', 'F'}, pdf},
 545         {[]byte{'%', '!', 'P', 'S'}, ps},
 546     }, // 37 %
 547     nil, // 38 &
 548     nil, // 39 '
 549     {
 550         {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
 551     }, // 40 (
 552     nil, // 41 )
 553     nil, // 42 *
 554     nil, // 43 +
 555     nil, // 44 ,
 556     nil, // 45 -
 557     {
 558         {[]byte{'.', 's', 'n', 'd'}, au},
 559     }, // 46 .
 560     nil, // 47 /
 561     nil, // 48 0
 562     nil, // 49 1
 563     nil, // 50 2
 564     nil, // 51 3
 565     nil, // 52 4
 566     nil, // 53 5
 567     nil, // 54 6
 568     nil, // 55 7
 569     {
 570         {[]byte{'8', 'B', 'P', 'S'}, psd},
 571     }, // 56 8
 572     nil, // 57 9
 573     nil, // 58 :
 574     nil, // 59 ;
 575     {
 576         // func checkDoc is better for these, since it's case-insensitive
 577         {doctypeHTML, html},
 578         {[]byte{'<', 's', 'v', 'g'}, svg},
 579         {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
 580         {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
 581         {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
 582         {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
 583     }, // 60 <
 584     nil, // 61 =
 585     nil, // 62 >
 586     nil, // 63 ?
 587     nil, // 64 @
 588     {
 589         {djv, djvu},
 590     }, // 65 A
 591     {
 592         {winbmp, bmp},
 593     }, // 66 B
 594     nil, // 67 C
 595     nil, // 68 D
 596     nil, // 69 E
 597     {
 598         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
 599         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
 600     }, // 70 F
 601     {
 602         {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
 603         {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
 604     }, // 71 G
 605     nil, // 72 H
 606     {
 607         {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
 608         {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
 609         {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
 610         {[]byte{'I', 'I', '*', 000}, tiff},
 611     }, // 73 I
 612     nil, // 74 J
 613     nil, // 75 K
 614     nil, // 76 L
 615     {
 616         {[]byte{'M', 'M', 000, '*'}, tiff},
 617         {[]byte{'M', 'T', 'h', 'd'}, mid},
 618         {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
 619         // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
 620         // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
 621         // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
 622     }, // 77 M
 623     nil, // 78 N
 624     {
 625         {[]byte{'O', 'g', 'g', 'S'}, ogg},
 626     }, // 79 O
 627     {
 628         {[]byte{'P', 'K', 003, 004}, zip},
 629     }, // 80 P
 630     nil, // 81 Q
 631     {
 632         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
 633         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
 634         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
 635     }, // 82 R
 636     {
 637         {sqlite3db, sqlite3},
 638     }, // 83 S
 639     nil, // 84 T
 640     nil, // 85 U
 641     nil, // 86 V
 642     nil, // 87 W
 643     nil, // 88 X
 644     nil, // 89 Y
 645     nil, // 90 Z
 646     nil, // 91 [
 647     nil, // 92 \
 648     nil, // 93 ]
 649     nil, // 94 ^
 650     nil, // 95 _
 651     nil, // 96 `
 652     nil, // 97 a
 653     nil, // 98 b
 654     {
 655         {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
 656     }, // 99 c
 657     nil, // 100 d
 658     nil, // 101 e
 659     {
 660         {[]byte{'f', 'L', 'a', 'C'}, flac},
 661     }, // 102 f
 662     nil, // 103 g
 663     nil, // 104 h
 664     nil, // 105 i
 665     nil, // 106 j
 666     nil, // 107 k
 667     nil, // 108 l
 668     nil, // 109 m
 669     nil, // 110 n
 670     nil, // 111 o
 671     nil, // 112 p
 672     nil, // 113 q
 673     nil, // 114 r
 674     nil, // 115 s
 675     nil, // 116 t
 676     nil, // 117 u
 677     nil, // 118 v
 678     nil, // 119 w
 679     nil, // 120 x
 680     nil, // 121 y
 681     nil, // 122 z
 682     {
 683         {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
 684     }, // 123 {
 685     nil, // 124 |
 686     nil, // 125 }
 687     nil, // 126
 688     {
 689         {[]byte{127, 'E', 'L', 'F'}, elf},
 690     }, // 127
 691     nil, // 128
 692     nil, // 129
 693     nil, // 130
 694     nil, // 131
 695     nil, // 132
 696     nil, // 133
 697     nil, // 134
 698     nil, // 135
 699     nil, // 136
 700     {
 701         {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
 702     }, // 137
 703     nil, // 138
 704     nil, // 139
 705     nil, // 140
 706     nil, // 141
 707     nil, // 142
 708     nil, // 143
 709     nil, // 144
 710     nil, // 145
 711     nil, // 146
 712     nil, // 147
 713     nil, // 148
 714     nil, // 149
 715     nil, // 150
 716     nil, // 151
 717     nil, // 152
 718     nil, // 153
 719     nil, // 154
 720     nil, // 155
 721     nil, // 156
 722     nil, // 157
 723     nil, // 158
 724     nil, // 159
 725     nil, // 160
 726     nil, // 161
 727     nil, // 162
 728     nil, // 163
 729     nil, // 164
 730     nil, // 165
 731     nil, // 166
 732     nil, // 167
 733     nil, // 168
 734     nil, // 169
 735     nil, // 170
 736     nil, // 171
 737     nil, // 172
 738     nil, // 173
 739     nil, // 174
 740     nil, // 175
 741     nil, // 176
 742     nil, // 177
 743     nil, // 178
 744     nil, // 179
 745     nil, // 180
 746     nil, // 181
 747     nil, // 182
 748     nil, // 183
 749     nil, // 184
 750     nil, // 185
 751     nil, // 186
 752     nil, // 187
 753     nil, // 188
 754     nil, // 189
 755     nil, // 190
 756     nil, // 191
 757     nil, // 192
 758     nil, // 193
 759     nil, // 194
 760     nil, // 195
 761     nil, // 196
 762     nil, // 197
 763     nil, // 198
 764     nil, // 199
 765     nil, // 200
 766     nil, // 201
 767     nil, // 202
 768     nil, // 203
 769     nil, // 204
 770     nil, // 205
 771     nil, // 206
 772     nil, // 207
 773     nil, // 208
 774     nil, // 209
 775     nil, // 210
 776     nil, // 211
 777     nil, // 212
 778     nil, // 213
 779     nil, // 214
 780     nil, // 215
 781     nil, // 216
 782     nil, // 217
 783     nil, // 218
 784     nil, // 219
 785     nil, // 220
 786     nil, // 221
 787     nil, // 222
 788     nil, // 223
 789     nil, // 224
 790     nil, // 225
 791     nil, // 226
 792     nil, // 227
 793     nil, // 228
 794     nil, // 229
 795     nil, // 230
 796     nil, // 231
 797     nil, // 232
 798     nil, // 233
 799     nil, // 234
 800     nil, // 235
 801     nil, // 236
 802     nil, // 237
 803     nil, // 238
 804     nil, // 239
 805     nil, // 240
 806     nil, // 241
 807     nil, // 242
 808     nil, // 243
 809     nil, // 244
 810     nil, // 245
 811     nil, // 246
 812     nil, // 247
 813     nil, // 248
 814     nil, // 249
 815     nil, // 250
 816     nil, // 251
 817     nil, // 252
 818     nil, // 253
 819     nil, // 254
 820     {
 821         {[]byte{0xFF, 0xD8, 0xFF}, jpg},
 822         {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
 823         {[]byte{0xFF, 0xFB}, mp3},
 824     }, // 255
 825 }
     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     }
  61 
  62     name := `-`
  63     if len(args) == 1 {
  64         name = args[0]
  65     }
  66 
  67     if err := run(name); err != nil {
  68         os.Stderr.WriteString(err.Error())
  69         os.Stderr.WriteString("\n")
  70         os.Exit(1)
  71     }
  72 }
  73 
  74 func run(s string) error {
  75     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  76     defer bw.Flush()
  77     w := bw
  78 
  79     if s == `-` {
  80         return debase64(w, os.Stdin)
  81     }
  82 
  83     if seemsDataURI(s) {
  84         return debase64(w, strings.NewReader(s))
  85     }
  86 
  87     f, err := os.Open(s)
  88     if err != nil {
  89         return err
  90     }
  91     defer f.Close()
  92 
  93     return debase64(w, f)
  94 }
  95 
  96 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
  97 // apart from output-writing ones
  98 func debase64(w io.Writer, r io.Reader) error {
  99     br := bufio.NewReaderSize(r, 32*1024)
 100     start, err := br.Peek(64)
 101     if err != nil && err != io.EOF {
 102         return err
 103     }
 104 
 105     skip, err := skipIntroDataURI(start)
 106     if err != nil {
 107         return err
 108     }
 109 
 110     if skip > 0 {
 111         br.Discard(skip)
 112     }
 113 
 114     dec := base64.NewDecoder(base64.StdEncoding, br)
 115     _, err = io.Copy(w, dec)
 116     return err
 117 }
 118 
 119 func skipIntroDataURI(chunk []byte) (skip int, err error) {
 120     if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 121         chunk = chunk[3:]
 122         skip += 3
 123     }
 124 
 125     if !bytes.HasPrefix(chunk, []byte(`data:`)) {
 126         return skip, nil
 127     }
 128 
 129     start := chunk
 130     if len(start) > 64 {
 131         start = start[:64]
 132     }
 133 
 134     i := bytes.Index(start, []byte(`;base64,`))
 135     if i < 0 {
 136         return skip, errors.New(`invalid data URI`)
 137     }
 138 
 139     skip += i + len(`;base64,`)
 140     return skip, nil
 141 }
 142 
 143 func seemsDataURI(s string) bool {
 144     start := s
 145     if len(s) > 64 {
 146         start = s[:64]
 147     }
 148     return strings.HasPrefix(s, `data:`) && strings.Contains(start, `;base64,`)
 149 }
     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     }
 105 
 106     liveLines := !buffered
 107     if !buffered {
 108         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 109             liveLines = false
 110         }
 111     }
 112 
 113     path := `-`
 114     if len(args) > 0 {
 115         path = args[0]
 116     }
 117 
 118     if err := run(os.Stdout, path, emit, liveLines); err != nil {
 119         if err == io.EOF {
 120             return
 121         }
 122 
 123         os.Stderr.WriteString(err.Error())
 124         os.Stderr.WriteString("\n")
 125         os.Exit(1)
 126     }
 127 }
 128 
 129 func run(w io.Writer, path string, handle handler, live bool) error {
 130     bw := bufio.NewWriter(w)
 131     defer bw.Flush()
 132 
 133     if path == `-` {
 134         return handle(bw, makeRowReader(os.Stdin))
 135     }
 136 
 137     f, err := os.Open(path)
 138     if err != nil {
 139         // on windows, file-not-found error messages may mention `CreateFile`,
 140         // even when trying to open files in read-only mode
 141         return errors.New(`can't open file named ` + path)
 142     }
 143     defer f.Close()
 144 
 145     return handle(bw, makeRowReader(f))
 146 }
 147 
 148 func emitJSON(w *bufio.Writer, rr *csv.Reader) error {
 149     got := 0
 150     var keys []string
 151 
 152     err := loopCSV(rr, func(i int, row []string) error {
 153         got++
 154 
 155         if i == 0 {
 156             keys = make([]string, 0, len(row))
 157             for _, s := range row {
 158                 keys = append(keys, strings.Clone(s))
 159             }
 160             return nil
 161         }
 162 
 163         if i == 1 {
 164             w.WriteByte('[')
 165         } else {
 166             err := w.WriteByte(',')
 167             if err != nil {
 168                 return io.EOF
 169             }
 170         }
 171 
 172         w.WriteByte('{')
 173         for i, s := range row {
 174             if i > 0 {
 175                 w.WriteByte(',')
 176             }
 177 
 178             if numberLike(s) {
 179                 w.WriteByte('"')
 180                 writeInnerStringJSON(w, keys[i])
 181                 w.WriteString(`":`)
 182                 w.WriteString(s)
 183                 continue
 184             }
 185 
 186             writeKV(w, keys[i], s)
 187         }
 188 
 189         for i := len(row); i < len(keys); i++ {
 190             if i > 0 {
 191                 w.WriteByte(',')
 192             }
 193             w.WriteByte('"')
 194             writeInnerStringJSON(w, keys[i])
 195             w.WriteString(`":null`)
 196         }
 197         w.WriteByte('}')
 198 
 199         return nil
 200     })
 201 
 202     if err != nil {
 203         return err
 204     }
 205 
 206     if got > 1 {
 207         w.WriteString("]\n")
 208     }
 209     return nil
 210 }
 211 
 212 func emitJSONL(w *bufio.Writer, rr *csv.Reader) error {
 213     var keys []string
 214 
 215     return loopCSV(rr, func(i int, row []string) error {
 216         if i == 0 {
 217             keys = make([]string, 0, len(row))
 218             for _, s := range row {
 219                 c := string(append([]byte{}, s...))
 220                 keys = append(keys, c)
 221             }
 222             return nil
 223         }
 224 
 225         w.WriteByte('{')
 226         for i, s := range row {
 227             if i > 0 {
 228                 w.WriteByte(',')
 229                 w.WriteByte(' ')
 230             }
 231 
 232             if numberLike(s) {
 233                 w.WriteByte('"')
 234                 writeInnerStringJSON(w, keys[i])
 235                 w.WriteString(`": `)
 236                 w.WriteString(s)
 237                 continue
 238             }
 239 
 240             writeKV(w, keys[i], s)
 241         }
 242 
 243         for i := len(row); i < len(keys); i++ {
 244             if i > 0 {
 245                 w.WriteByte(',')
 246                 w.WriteByte(' ')
 247             }
 248             w.WriteByte('"')
 249             writeInnerStringJSON(w, keys[i])
 250             w.WriteString(`": null`)
 251         }
 252         w.WriteByte('}')
 253 
 254         w.WriteByte('\n')
 255         if err := w.Flush(); err != nil {
 256             return io.EOF
 257         }
 258         return nil
 259     })
 260 }
 261 
 262 func emitJSONS(w *bufio.Writer, rr *csv.Reader) error {
 263     got := 0
 264     var keys []string
 265 
 266     err := loopCSV(rr, func(i int, row []string) error {
 267         got++
 268 
 269         if i == 0 {
 270             keys = make([]string, 0, len(row))
 271             for _, s := range row {
 272                 c := string(append([]byte{}, s...))
 273                 keys = append(keys, c)
 274             }
 275             return nil
 276         }
 277 
 278         if i == 1 {
 279             w.WriteByte('[')
 280         } else {
 281             err := w.WriteByte(',')
 282             if err != nil {
 283                 return io.EOF
 284             }
 285         }
 286 
 287         w.WriteByte('{')
 288         for i, s := range row {
 289             if i > 0 {
 290                 w.WriteByte(',')
 291             }
 292             writeKV(w, keys[i], s)
 293         }
 294 
 295         for i := len(row); i < len(keys); i++ {
 296             if i > 0 {
 297                 w.WriteByte(',')
 298             }
 299             w.WriteByte('"')
 300             writeInnerStringJSON(w, keys[i])
 301             w.WriteString(`":null`)
 302         }
 303         w.WriteByte('}')
 304 
 305         return nil
 306     })
 307 
 308     if err != nil {
 309         return err
 310     }
 311 
 312     if got > 1 {
 313         w.WriteString("]\n")
 314     }
 315     return nil
 316 }
 317 
 318 func emitTSV(w *bufio.Writer, rr *csv.Reader) error {
 319     width := -1
 320 
 321     return loopCSV(rr, func(i int, row []string) error {
 322         if width < 0 {
 323             width = len(row)
 324         }
 325 
 326         for i, s := range row {
 327             if strings.IndexByte(s, '\t') >= 0 {
 328                 const msg = `can't convert CSV whose items have tabs to TSV`
 329                 return errors.New(msg)
 330             }
 331             if i > 0 {
 332                 w.WriteByte('\t')
 333             }
 334             w.WriteString(s)
 335         }
 336 
 337         for i := len(row); i < width; i++ {
 338             w.WriteByte('\t')
 339         }
 340 
 341         w.WriteByte('\n')
 342         if err := w.Flush(); err != nil {
 343             // a write error may be the consequence of stdout being closed,
 344             // perhaps by another app along a pipe
 345             return io.EOF
 346         }
 347         return nil
 348     })
 349 }
 350 
 351 // writeInnerStringJSON helps JSON-encode strings more quickly
 352 func writeInnerStringJSON(w *bufio.Writer, s string) {
 353     needsEscaping := false
 354     for _, r := range s {
 355         if '#' <= r && r <= '~' && r != '\\' {
 356             continue
 357         }
 358         if r == ' ' || r == '!' || unicode.IsLetter(r) {
 359             continue
 360         }
 361 
 362         needsEscaping = true
 363         break
 364     }
 365 
 366     if !needsEscaping {
 367         w.WriteString(s)
 368         return
 369     }
 370 
 371     outer, err := json.Marshal(s)
 372     if err != nil {
 373         return
 374     }
 375     inner := outer[1 : len(outer)-1]
 376     w.Write(inner)
 377 }
 378 
 379 func writeKV(w *bufio.Writer, k string, s string) {
 380     w.WriteByte('"')
 381     writeInnerStringJSON(w, k)
 382     w.WriteString(`": "`)
 383     writeInnerStringJSON(w, s)
 384     w.WriteByte('"')
 385 }
 386 
 387 func numberLike(s string) bool {
 388     if len(s) == 0 {
 389         return false
 390     }
 391 
 392     if s[0] == '-' {
 393         s = s[1:]
 394     }
 395 
 396     if len(s) == 0 || s[0] < '0' || s[0] > '9' {
 397         return false
 398     }
 399 
 400     for len(s) > 0 {
 401         lead := s[0]
 402         s = s[1:]
 403 
 404         if lead == '.' {
 405             return allDigits(s)
 406         }
 407         if lead < '0' || lead > '9' {
 408             return false
 409         }
 410     }
 411 
 412     return true
 413 }
 414 
 415 func allDigits(s string) bool {
 416     if len(s) == 0 {
 417         return false
 418     }
 419 
 420     for _, r := range s {
 421         if r < '0' || r > '9' {
 422             return false
 423         }
 424     }
 425     return true
 426 }
 427 
 428 func makeRowReader(r io.Reader) *csv.Reader {
 429     rr := csv.NewReader(r)
 430     rr.LazyQuotes = true
 431     rr.ReuseRecord = true
 432     rr.FieldsPerRecord = -1
 433     return rr
 434 }
 435 
 436 func loopCSV(rr *csv.Reader, handle func(i int, row []string) error) error {
 437     width := 0
 438 
 439     for i := 0; true; i++ {
 440         row, err := rr.Read()
 441         if err == io.EOF {
 442             return nil
 443         }
 444 
 445         if err != nil {
 446             return err
 447         }
 448 
 449         if i == 0 {
 450             width = len(row)
 451         }
 452 
 453         if len(row) > width {
 454             return errors.New(`data-row has more items than the header`)
 455         }
 456 
 457         if err := handle(i, row); err != nil {
 458             return err
 459         }
 460     }
 461 
 462     return nil
 463 }
     File: ./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     }
  84 }
  85 
  86 func run(w io.Writer, args []string, live bool) error {
  87     files := make(stringSet)
  88     lines := make(stringSet)
  89     bw := bufio.NewWriter(w)
  90     defer bw.Flush()
  91 
  92     for _, name := range args {
  93         if _, ok := files[name]; ok {
  94             continue
  95         }
  96         files[name] = struct{}{}
  97 
  98         if err := handleFile(bw, name, lines, live); err != nil {
  99             return err
 100         }
 101     }
 102 
 103     if len(args) == 0 {
 104         return dedup(bw, os.Stdin, lines, live)
 105     }
 106     return nil
 107 }
 108 
 109 func handleFile(w *bufio.Writer, name string, got stringSet, live bool) error {
 110     if name == `` || name == `-` {
 111         return dedup(w, os.Stdin, got, live)
 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 dedup(w, f, got, live)
 121 }
 122 
 123 func dedup(w *bufio.Writer, r io.Reader, got stringSet, live bool) error {
 124     const gb = 1024 * 1024 * 1024
 125     sc := bufio.NewScanner(r)
 126     sc.Buffer(nil, 8*gb)
 127 
 128     for sc.Scan() {
 129         line := sc.Text()
 130         if _, ok := got[line]; ok {
 131             continue
 132         }
 133         got[line] = struct{}{}
 134 
 135         w.Write(sc.Bytes())
 136         if w.WriteByte('\n') != nil {
 137             return io.EOF
 138         }
 139 
 140         if !live {
 141             continue
 142         }
 143 
 144         if err := w.Flush(); err != nil {
 145             return io.EOF
 146         }
 147     }
 148 
 149     return sc.Err()
 150 }
     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     }
  78 }
  79 
  80 func run(w io.Writer, args []string, live bool) error {
  81     dashes := 0
  82     for _, path := range args {
  83         if path == `-` {
  84             dashes++
  85         }
  86         if dashes > 1 {
  87             return errors.New(`can't read stdin (dash) more than once`)
  88         }
  89     }
  90 
  91     bw := bufio.NewWriter(w)
  92     defer bw.Flush()
  93 
  94     if len(args) == 0 {
  95         return dejsonl(bw, os.Stdin, live)
  96     }
  97 
  98     for _, path := range args {
  99         if err := handleInput(bw, path, live); err != nil {
 100             return err
 101         }
 102     }
 103 
 104     return nil
 105 }
 106 
 107 // handleInput simplifies control-flow for func main
 108 func handleInput(w *bufio.Writer, path string, live bool) error {
 109     if path == `-` {
 110         return dejsonl(w, os.Stdin, live)
 111     }
 112 
 113     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
 114     //  resp, err := http.Get(path)
 115     //  if err != nil {
 116     //      return err
 117     //  }
 118     //  defer resp.Body.Close()
 119     //  return dejsonl(w, resp.Body, live)
 120     // }
 121 
 122     f, err := os.Open(path)
 123     if err != nil {
 124         // on windows, file-not-found error messages may mention `CreateFile`,
 125         // even when trying to open files in read-only mode
 126         return errors.New(`can't open file named ` + path)
 127     }
 128     defer f.Close()
 129     return dejsonl(w, f, live)
 130 }
 131 
 132 // dejsonl simplifies control-flow for func handleInput
 133 func dejsonl(w *bufio.Writer, r io.Reader, live bool) error {
 134     const gb = 1024 * 1024 * 1024
 135     sc := bufio.NewScanner(r)
 136     sc.Buffer(nil, 8*gb)
 137     got := 0
 138 
 139     for i := 0; sc.Scan(); i++ {
 140         s := sc.Text()
 141         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 142             s = s[3:]
 143         }
 144 
 145         // trim spaces at both ends of the current line
 146         for len(s) > 0 && s[0] == ' ' {
 147             s = s[1:]
 148         }
 149         for len(s) > 0 && s[len(s)-1] == ' ' {
 150             s = s[:len(s)-1]
 151         }
 152 
 153         // ignore empty(ish) lines
 154         if len(s) == 0 {
 155             continue
 156         }
 157 
 158         // ignore lines starting with unix-style comments
 159         if len(s) > 0 && s[0] == '#' {
 160             continue
 161         }
 162 
 163         if err := checkJSONL(strings.NewReader(s)); err != nil {
 164             return err
 165         }
 166 
 167         if got == 0 {
 168             w.WriteByte('[')
 169         } else {
 170             w.WriteByte(',')
 171         }
 172         if w.WriteByte('\n') != nil {
 173             return io.EOF
 174         }
 175         w.WriteString(indent)
 176         w.WriteString(s)
 177         got++
 178 
 179         if !live {
 180             continue
 181         }
 182 
 183         if err := w.Flush(); err != nil {
 184             return io.EOF
 185         }
 186     }
 187 
 188     if got == 0 {
 189         w.WriteString("[\n]\n")
 190     } else {
 191         w.WriteString("\n]\n")
 192     }
 193     return sc.Err()
 194 }
 195 
 196 func checkJSONL(r io.Reader) error {
 197     dec := json.NewDecoder(r)
 198     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 199     // even if JSON parsers aren't required to guarantee such input-fidelity
 200     // for numbers
 201     dec.UseNumber()
 202 
 203     t, err := dec.Token()
 204     if err == io.EOF {
 205         return errors.New(`input has no JSON values`)
 206     }
 207 
 208     if err := checkToken(dec, t); err != nil {
 209         return err
 210     }
 211 
 212     _, err = dec.Token()
 213     if err == io.EOF {
 214         // input is over, so it's a success
 215         return nil
 216     }
 217 
 218     if err == nil {
 219         // a successful `read` is a failure, as it means there are
 220         // trailing JSON tokens
 221         return errors.New(`unexpected trailing data`)
 222     }
 223 
 224     // any other error, perhaps some invalid-JSON-syntax-type error
 225     return err
 226 }
 227 
 228 // checkToken handles recursion for func checkJSONL
 229 func checkToken(dec *json.Decoder, t json.Token) error {
 230     switch t := t.(type) {
 231     case json.Delim:
 232         switch t {
 233         case json.Delim('['):
 234             return checkArray(dec)
 235         case json.Delim('{'):
 236             return checkObject(dec)
 237         default:
 238             return errors.New(`unsupported JSON syntax ` + string(t))
 239         }
 240 
 241     case nil, bool, float64, json.Number, string:
 242         return nil
 243 
 244     default:
 245         // return fmt.Errorf(`unsupported token type %T`, t)
 246         return errors.New(`invalid JSON token`)
 247     }
 248 }
 249 
 250 // handleArray handles arrays for func checkToken
 251 func checkArray(dec *json.Decoder) error {
 252     for {
 253         t, err := dec.Token()
 254         if err != nil {
 255             return err
 256         }
 257 
 258         if t == json.Delim(']') {
 259             return nil
 260         }
 261 
 262         if err := checkToken(dec, t); err != nil {
 263             return err
 264         }
 265     }
 266 }
 267 
 268 // handleObject handles objects for func checkToken
 269 func checkObject(dec *json.Decoder) error {
 270     for {
 271         t, err := dec.Token()
 272         if err != nil {
 273             return err
 274         }
 275 
 276         if t == json.Delim('}') {
 277             return nil
 278         }
 279 
 280         if _, ok := t.(string); !ok {
 281             return errors.New(`expected a string for a key-value pair`)
 282         }
 283 
 284         t, err = dec.Token()
 285         if err == io.EOF || t == json.Delim('}') {
 286             return errors.New(`expected a value for a key-value pair`)
 287         }
 288 
 289         if err := checkToken(dec, t); err != nil {
 290             return err
 291         }
 292     }
 293 }
     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     }
  74 }
  75 
  76 func run(w io.Writer, args []string, live bool) error {
  77     bw := bufio.NewWriter(w)
  78     defer bw.Flush()
  79 
  80     if len(args) == 0 {
  81         return dessv(bw, os.Stdin, live)
  82     }
  83 
  84     for _, name := range args {
  85         if err := handleFile(bw, name, live); err != nil {
  86             return err
  87         }
  88     }
  89     return nil
  90 }
  91 
  92 func handleFile(w *bufio.Writer, name string, live bool) error {
  93     if name == `` || name == `-` {
  94         return dessv(w, os.Stdin, live)
  95     }
  96 
  97     f, err := os.Open(name)
  98     if err != nil {
  99         return errors.New(`can't read from file named "` + name + `"`)
 100     }
 101     defer f.Close()
 102 
 103     return dessv(w, f, live)
 104 }
 105 
 106 func dessv(w *bufio.Writer, r io.Reader, live bool) error {
 107     const gb = 1024 * 1024 * 1024
 108     sc := bufio.NewScanner(r)
 109     sc.Buffer(nil, 8*gb)
 110     handleRow := handleRowSSV
 111     numTabs := ^0
 112 
 113     for i := 0; sc.Scan(); i++ {
 114         s := sc.Bytes()
 115         if i == 0 {
 116             if bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 117                 s = s[3:]
 118             }
 119 
 120             for _, b := range s {
 121                 if b == '\t' {
 122                     handleRow = handleRowTSV
 123                     break
 124                 }
 125             }
 126             numTabs = handleRow(w, s, numTabs)
 127         } else {
 128             handleRow(w, s, numTabs)
 129         }
 130 
 131         if w.WriteByte('\n') != nil {
 132             return io.EOF
 133         }
 134 
 135         if !live {
 136             continue
 137         }
 138 
 139         if err := w.Flush(); err != nil {
 140             return io.EOF
 141         }
 142     }
 143 
 144     return sc.Err()
 145 }
 146 
 147 func handleRowSSV(w *bufio.Writer, s []byte, n int) int {
 148     for len(s) > 0 && s[0] == ' ' {
 149         s = s[1:]
 150     }
 151     for len(s) > 0 && s[len(s)-1] == ' ' {
 152         s = s[:len(s)-1]
 153     }
 154 
 155     got := 0
 156 
 157     for got = 0; len(s) > 0; got++ {
 158         if got > 0 {
 159             w.WriteByte('\t')
 160         }
 161 
 162         i := bytes.IndexByte(s, ' ')
 163         if i < 0 {
 164             w.Write(s)
 165             s = nil
 166             n--
 167             break
 168         }
 169 
 170         w.Write(s[:i])
 171         s = s[i+1:]
 172         for len(s) > 0 && s[0] == ' ' {
 173             s = s[1:]
 174         }
 175         n--
 176     }
 177 
 178     w.Write(s)
 179     writeTabs(w, n)
 180     return got
 181 }
 182 
 183 func handleRowTSV(w *bufio.Writer, s []byte, n int) int {
 184     got := 0
 185     for _, b := range s {
 186         if b == '\t' {
 187             got++
 188         }
 189     }
 190 
 191     w.Write(s)
 192     writeTabs(w, n-got)
 193     return got
 194 }
 195 
 196 func writeTabs(w *bufio.Writer, n int) {
 197     for n > 0 {
 198         w.WriteByte('\t')
 199         n--
 200     }
 201 }
     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     }
 221 
 222     nerr := 0
 223     pairs := make([]pair, 0, len(args)/2)
 224 
 225     for len(args) >= 2 {
 226         src := args[0]
 227         sname := args[1]
 228 
 229         var err error
 230         var exp *regexp.Regexp
 231         if insensitive {
 232             exp, err = regexp.Compile(`(?i)` + src)
 233         } else {
 234             exp, err = regexp.Compile(src)
 235         }
 236         if err != nil {
 237             os.Stderr.WriteString(err.Error())
 238             os.Stderr.WriteString("\n")
 239             nerr++
 240         }
 241 
 242         if alias, ok := styleAliases[sname]; ok {
 243             sname = alias
 244         }
 245 
 246         style, ok := styles[sname]
 247         if !ok {
 248             os.Stderr.WriteString("no style named `")
 249             os.Stderr.WriteString(args[1])
 250             os.Stderr.WriteString("`\n")
 251             nerr++
 252         }
 253 
 254         pairs = append(pairs, pair{expr: exp, style: style})
 255         args = args[2:]
 256     }
 257 
 258     if nerr > 0 {
 259         os.Exit(1)
 260     }
 261 
 262     liveLines := !buffered
 263     if !buffered {
 264         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 265             liveLines = false
 266         }
 267     }
 268 
 269     sc := bufio.NewScanner(os.Stdin)
 270     sc.Buffer(nil, 8*1024*1024*1024)
 271     bw := bufio.NewWriter(os.Stdout)
 272     var plain []byte
 273 
 274     for i := 0; sc.Scan(); i++ {
 275         s := sc.Bytes()
 276         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 277             s = s[3:]
 278         }
 279         plain = appendPlain(plain[:0], s)
 280 
 281         if err := handleLine(bw, s, noANSI(plain), pairs); err != nil {
 282             return
 283         }
 284 
 285         if !liveLines {
 286             continue
 287         }
 288 
 289         if err := bw.Flush(); err != nil {
 290             return
 291         }
 292     }
 293 }
 294 
 295 // appendPlain extends the slice given using the non-ANSI parts of a string
 296 func appendPlain(dst []byte, src []byte) []byte {
 297     for len(src) > 0 {
 298         i, j := indexEscapeSequence(src)
 299         if i < 0 {
 300             dst = append(dst, src...)
 301             break
 302         }
 303         if j < 0 {
 304             j = len(src)
 305         }
 306 
 307         if i > 0 {
 308             dst = append(dst, src[:i]...)
 309         }
 310 
 311         src = src[j:]
 312     }
 313 
 314     return dst
 315 }
 316 
 317 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 318 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 319 // indices which can be independently negative when either the start/end of
 320 // a sequence isn't found; given their fairly-common use, even the hyperlink
 321 // ESC]8 sequences are supported
 322 func indexEscapeSequence(s []byte) (int, int) {
 323     var prev byte
 324 
 325     for i, b := range s {
 326         if prev == '\x1b' && b == '[' {
 327             j := indexLetter(s[i+1:])
 328             if j < 0 {
 329                 return i, -1
 330             }
 331             return i - 1, i + 1 + j + 1
 332         }
 333 
 334         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 335             j := indexPair(s[i+1:], '\x1b', '\\')
 336             if j < 0 {
 337                 return i, -1
 338             }
 339             return i - 1, i + 1 + j + 2
 340         }
 341 
 342         prev = b
 343     }
 344 
 345     return -1, -1
 346 }
 347 
 348 func indexLetter(s []byte) int {
 349     for i, b := range s {
 350         upper := b &^ 32
 351         if 'A' <= upper && upper <= 'Z' {
 352             return i
 353         }
 354     }
 355 
 356     return -1
 357 }
 358 
 359 func indexPair(s []byte, x byte, y byte) int {
 360     var prev byte
 361 
 362     for i, b := range s {
 363         if prev == x && b == y && i > 0 {
 364             return i
 365         }
 366         prev = b
 367     }
 368 
 369     return -1
 370 }
 371 
 372 // noANSI ensures arguments to func handleLine are given in the right order
 373 type noANSI []byte
 374 
 375 // handleLine styles the current line given to it using the first matching
 376 // regex, keeping it as given if none of the regexes match; it's given 2
 377 // strings: the first is the original line, the latter is its plain-text
 378 // version (with no ANSI codes) and is used for the regex-matching, since
 379 // ANSI codes use a mix of numbers and letters, which can themselves match
 380 func handleLine(w *bufio.Writer, s []byte, plain noANSI, pairs []pair) error {
 381     for _, p := range pairs {
 382         if p.expr.Match(plain) {
 383             w.WriteString(p.style)
 384             w.Write(s)
 385             w.WriteString("\x1b[0m")
 386             return w.WriteByte('\n')
 387         }
 388     }
 389 
 390     w.Write(s)
 391     return w.WriteByte('\n')
 392 }
     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     }
 107 
 108     liveLines := !buffered
 109     if !buffered {
 110         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 111             liveLines = false
 112         }
 113     }
 114 
 115     err := run(os.Stdout, os.Stdin, exprs, liveLines)
 116     if err != nil && err != io.EOF {
 117         os.Stderr.WriteString(err.Error())
 118         os.Stderr.WriteString("\n")
 119         os.Exit(1)
 120     }
 121 }
 122 
 123 func run(w io.Writer, r io.Reader, exprs []*regexp.Regexp, live bool) error {
 124     var buf []byte
 125     sc := bufio.NewScanner(r)
 126     sc.Buffer(nil, 8*1024*1024*1024)
 127     bw := bufio.NewWriter(w)
 128     defer bw.Flush()
 129 
 130     src := make([]byte, 8*1024)
 131     dst := make([]byte, 8*1024)
 132 
 133     for i := 0; sc.Scan(); i++ {
 134         line := sc.Bytes()
 135         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 136             line = line[3:]
 137         }
 138 
 139         s := line
 140         if bytes.IndexByte(s, '\x1b') >= 0 {
 141             buf = plain(buf[:0], s)
 142             s = buf
 143         }
 144 
 145         if len(exprs) > 0 {
 146             src = append(src[:0], s...)
 147             for _, exp := range exprs {
 148                 dst = erase(dst[:0], src, exp)
 149                 src = append(src[:0], dst...)
 150             }
 151             bw.Write(dst)
 152         } else {
 153             bw.Write(s)
 154         }
 155 
 156         if bw.WriteByte('\n') != nil {
 157             return io.EOF
 158         }
 159 
 160         if !live {
 161             continue
 162         }
 163 
 164         if bw.Flush() != nil {
 165             return io.EOF
 166         }
 167     }
 168 
 169     return sc.Err()
 170 }
 171 
 172 func erase(dst []byte, src []byte, with *regexp.Regexp) []byte {
 173     for len(src) > 0 {
 174         span := with.FindIndex(src)
 175         // also ignore empty regex matches to avoid infinite outer loops,
 176         // as skipping empty slices isn't advancing at all, leaving the
 177         // string stuck to being empty-matched forever by the same regex
 178         if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
 179             return append(dst, src...)
 180         }
 181 
 182         start, end := span[0], span[1]
 183         dst = append(dst, src[:start]...)
 184         // avoid infinite loops caused by empty regex matches
 185         if start == end && end < len(src) {
 186             dst = append(dst, src[end])
 187             end++
 188         }
 189         src = src[end:]
 190     }
 191 
 192     return dst
 193 }
 194 
 195 func plain(dst []byte, src []byte) []byte {
 196     for len(src) > 0 {
 197         i, j := indexEscapeSequence(src)
 198         if i < 0 {
 199             dst = append(dst, src...)
 200             break
 201         }
 202         if j < 0 {
 203             j = len(src)
 204         }
 205 
 206         if i > 0 {
 207             dst = append(dst, src[:i]...)
 208         }
 209 
 210         src = src[j:]
 211     }
 212 
 213     return dst
 214 }
 215 
 216 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 217 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 218 // indices which can be independently negative when either the start/end of
 219 // a sequence isn't found; given their fairly-common use, even the hyperlink
 220 // ESC]8 sequences are supported
 221 func indexEscapeSequence(s []byte) (int, int) {
 222     var prev byte
 223 
 224     for i, b := range s {
 225         if prev == '\x1b' && b == '[' {
 226             j := indexLetter(s[i+1:])
 227             if j < 0 {
 228                 return i, -1
 229             }
 230             return i - 1, i + 1 + j + 1
 231         }
 232 
 233         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 234             j := indexPair(s[i+1:], '\x1b', '\\')
 235             if j < 0 {
 236                 return i, -1
 237             }
 238             return i - 1, i + 1 + j + 2
 239         }
 240 
 241         prev = b
 242     }
 243 
 244     return -1, -1
 245 }
 246 
 247 func indexLetter(s []byte) int {
 248     for i, b := range s {
 249         upper := b &^ 32
 250         if 'A' <= upper && upper <= 'Z' {
 251             return i
 252         }
 253     }
 254 
 255     return -1
 256 }
 257 
 258 func indexPair(s []byte, x byte, y byte) int {
 259     var prev byte
 260 
 261     for i, b := range s {
 262         if prev == x && b == y && i > 0 {
 263             return i
 264         }
 265         prev = b
 266     }
 267 
 268     return -1
 269 }
     File: ./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 
 203     }
 204 
 205     for _, s := range args {
 206         switch s {
 207         case `help`, `-h`, `--h`, `-help`, `--help`:
 208             fmt.Fprint(os.Stderr, usage)
 209             os.Exit(0)
 210         }
 211 
 212         err := cfg.handleArg(s)
 213         if err != nil {
 214             return cfg, err
 215         }
 216     }
 217 
 218     if cfg.Integers {
 219         cfg.XMin = math.Ceil(float64(cfg.XMin))
 220         cfg.XMax = math.Floor(float64(cfg.XMax))
 221         cfg.YMin = math.Ceil(float64(cfg.YMin))
 222         cfg.YMax = math.Floor(float64(cfg.YMax))
 223     }
 224 
 225     if strings.TrimSpace(cfg.Formula) == `` {
 226         return cfg, errors.New(`no main formula given`)
 227     }
 228     return cfg, nil
 229 }
 230 
 231 // handleArg parses/uses the cmd-line argument given, except for the help
 232 // option and its aliases, which can only be detected separately
 233 func (c *config) handleArg(s string) error {
 234     switch s {
 235     case `int`, `ints`, `integers`:
 236         c.Integers = true
 237         return nil
 238     }
 239 
 240     lcDotless := strings.TrimPrefix(strings.ToLower(s), `.`)
 241     if alias, ok := fmtAliases[lcDotless]; ok {
 242         c.Output = alias
 243         return nil
 244     }
 245 
 246     if w, h, ok := parseResolution(s); ok {
 247         c.Width = w
 248         c.Height = h
 249         return nil
 250     }
 251 
 252     if colors, ok := paletteAliases[s]; ok {
 253         pal := palettes[colors]
 254         c.Palette = pal.Func
 255         c.Bad = pal.Bad
 256         return nil
 257     }
 258 
 259     varname, min, max, err := parseDomain(s)
 260     if err != nil {
 261         return err
 262     }
 263 
 264     switch varname {
 265     case ``:
 266         // no variable name means it's the main formula
 267         if c.Formula != `` {
 268             const fs = `%q: can't use more than 1 main formula`
 269             return fmt.Errorf(fs, s)
 270         }
 271         c.Formula = s
 272         return nil
 273 
 274     case `x`:
 275         c.XMin = min
 276         c.XMax = max
 277         return nil
 278 
 279     case `y`:
 280         c.YMin = min
 281         c.YMax = max
 282         return nil
 283 
 284     case `xy`:
 285         c.XMin = min
 286         c.XMax = max
 287         c.YMin = min
 288         c.YMax = max
 289         return nil
 290 
 291     default:
 292         const fs = "domain variable %q isn't any of `x`, `y`, or `xy`"
 293         return fmt.Errorf(fs, varname)
 294     }
 295 }
 296 
 297 // parseResolution tries to get a width/height resolution out of the
 298 // cmd-line argument given to it
 299 func parseResolution(s string) (width int, height int, ok bool) {
 300     if res, ok := resAliases[s]; ok {
 301         return res.Width, res.Height, true
 302     }
 303 
 304     i := strings.IndexByte(s, 'x')
 305     if i < 0 {
 306         return 0, 0, false
 307     }
 308 
 309     w, werr := strconv.ParseInt(s[:i], 10, 64)
 310     h, herr := strconv.ParseInt(s[i+1:], 10, 64)
 311     if werr == nil && herr == nil && w > 0 && h > 0 {
 312         return int(w), int(h), true
 313     }
 314     return 0, 0, false
 315 }
 316 
 317 func (c config) IntegerSize() (w, h int) {
 318     w = int(math.Abs(c.XMax - c.XMin + 1))
 319     h = int(math.Abs(c.YMax - c.YMin + 1))
 320     return w, h
 321 }
 322 
 323 // parseDomain tries to parse domain/variable-range formulas of the form(s)
 324 //
 325 // - x:=a..b
 326 // - y:=a..b
 327 // - xy:=a..b
 328 //
 329 // where a and b represent valid floating-point numbers; when an empty is
 330 // returned, it means the strings given wasn't recognized as a variable's
 331 // domain, suggesting it may be another option, or the main formula instead
 332 func parseDomain(s string) (string, float64, float64, error) {
 333     i := strings.Index(s, `:=`)
 334     if i < 0 {
 335         return ``, 0, 0, nil
 336     }
 337 
 338     v := strings.TrimSpace(s[:i])
 339     rng := strings.TrimSpace(s[i+2:])
 340     min, max, err := parseSpan(rng)
 341     return v, min, max, err
 342 }
 343 
 344 // parseSpan tries to parse a pair of numbers with `..` between them
 345 func parseSpan(s string) (float64, float64, error) {
 346     pair := strings.Split(s, `..`)
 347     if len(pair) != 2 {
 348         const fs = "missing `..` in domain-span %s"
 349         return 0, 1, fmt.Errorf(fs, s)
 350     }
 351 
 352     a, err := strconv.ParseFloat(pair[0], 64)
 353     if err != nil {
 354         const fs = `can't parse %q in domain-span %s`
 355         return 0, 1, fmt.Errorf(fs, pair[0], s)
 356     }
 357     b, err := strconv.ParseFloat(pair[1], 64)
 358     if err != nil {
 359         const fs = `can't parse %q in domain-span %s`
 360         return 0, 1, fmt.Errorf(fs, pair[1], s)
 361     }
 362     return a, b, nil
 363 }
     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     }
  45 
  46     if _, ok := encoders[cfg.Output]; !ok {
  47         const fs = "unsupported output format %s\n"
  48         fmt.Fprintf(os.Stderr, fs, cfg.Output)
  49         os.Exit(1)
  50     }
  51 
  52     addDetermFuncs()
  53 
  54     if err := run(cfg); err != nil {
  55         fmt.Fprintln(os.Stderr, err.Error())
  56         os.Exit(1)
  57     }
  58 }
  59 
  60 func run(cfg config) error {
  61     // f, err := os.Create(`fh.prof`)
  62     // if err != nil {
  63     //  return err
  64     // }
  65     // defer f.Close()
  66 
  67     // pprof.StartCPUProfile(f)
  68     // defer pprof.StopCPUProfile()
  69 
  70     encode, ok := encoders[cfg.Output]
  71     if !ok {
  72         const fs = `unsupported output format %q`
  73         return fmt.Errorf(fs, cfg.Output)
  74     }
  75 
  76     if cfg.Integers {
  77         w, h := cfg.IntegerSize()
  78         cfg.Width = w
  79         cfg.Height = h
  80     }
  81 
  82     // allow runner to use up to 32 cores
  83     r, err := newRunner(cfg, 32)
  84     if err != nil {
  85         return err
  86     }
  87 
  88     res, err := r.Run(cfg)
  89     if err != nil {
  90         return err
  91     }
  92 
  93     img := image.NewRGBA(image.Rectangle{
  94         Min: image.Point{X: 0, Y: 0},
  95         Max: image.Point{X: cfg.Width, Y: cfg.Height},
  96     })
  97 
  98     w := bufio.NewWriterSize(os.Stdout, 64*1024)
  99     defer w.Flush()
 100 
 101     // give back a blank picture if results aren't usable
 102     if !res.isValid() {
 103         return encode(w, img, cfg)
 104     }
 105 
 106     // handle integers-only coordinate-inputs
 107     if cfg.Integers {
 108         width, height := cfg.IntegerSize()
 109         fillExpandedImage(img, res, cfg, width, height)
 110         return encode(w, img, cfg)
 111     }
 112 
 113     // handle domain-sampled images
 114     fillImage(img, res, cfg)
 115     return encode(w, img, cfg)
 116 }
     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     }
  94 }
  95 
  96 type config struct {
  97     skipSubfolder error
  98     liveLines     bool
  99 }
 100 
 101 func run(w io.Writer, paths []string, cfg config) error {
 102     bw := bufio.NewWriter(w)
 103     defer bw.Flush()
 104 
 105     got := make(map[string]struct{})
 106 
 107     if len(paths) == 0 {
 108         paths = []string{`.`}
 109     }
 110 
 111     // handle is the callback for func filepath.WalkDir
 112     handle := func(path string, e fs.DirEntry, err error) error {
 113         if err != nil {
 114             return err
 115         }
 116 
 117         if _, ok := got[path]; ok {
 118             return nil
 119         }
 120         got[path] = struct{}{}
 121 
 122         if e.IsDir() {
 123             return cfg.skipSubfolder
 124         }
 125 
 126         return handleEntry(bw, path, cfg.liveLines)
 127     }
 128 
 129     for _, path := range paths {
 130         if _, ok := got[path]; ok {
 131             continue
 132         }
 133         got[path] = struct{}{}
 134 
 135         st, err := os.Stat(path)
 136         if err != nil {
 137             return err
 138         }
 139 
 140         if st.IsDir() {
 141             if !strings.HasSuffix(path, `/`) {
 142                 path = path + `/`
 143             }
 144             got[path] = struct{}{}
 145 
 146             if err := filepath.WalkDir(path, handle); err != nil {
 147                 return err
 148             }
 149             continue
 150         }
 151 
 152         if err := handleEntry(bw, path, cfg.liveLines); err != nil {
 153             return err
 154         }
 155     }
 156 
 157     return nil
 158 }
 159 
 160 func handleEntry(w *bufio.Writer, path string, live bool) error {
 161     abs, err := filepath.Abs(path)
 162     if err != nil {
 163         return err
 164     }
 165 
 166     w.WriteString(abs)
 167     w.WriteByte('\n')
 168 
 169     if !live {
 170         return nil
 171     }
 172 
 173     if err := w.Flush(); err != nil {
 174         return io.EOF
 175     }
 176     return nil
 177 }
     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     }
 107 }
 108 
 109 func parseBlockSizeOption(s string) (size int, ok bool) {
 110     if !strings.HasPrefix(s, `-`) {
 111         return 0, false
 112     }
 113     if len(s) > 0 && s[0] == '-' {
 114         s = s[1:]
 115     }
 116     if len(s) > 0 && s[0] == '-' {
 117         s = s[1:]
 118     }
 119 
 120     if !strings.HasSuffix(s, `k`) && !strings.HasSuffix(s, `K`) {
 121         return 0, false
 122     }
 123     s = s[:len(s)-1]
 124 
 125     if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 {
 126         return int(n), true
 127     }
 128     return 0, false
 129 }
 130 
 131 type config struct {
 132     blockSize int
 133     sorted    bool
 134     recursive bool
 135     liveLines bool
 136 }
 137 
 138 type entry struct {
 139     path string
 140     size int64
 141 }
 142 
 143 type handlers struct {
 144     w             *bufio.Writer
 145     entries       []entry
 146     blockSize     int
 147     skipSubfolder error
 148 
 149     file func(h *handlers, path string, size int64) error
 150 
 151     liveLines bool
 152 }
 153 
 154 func (h handlers) countBlocks(size int64) int64 {
 155     bs := int64(h.blockSize)
 156     n := size / (1024 * bs)
 157     if size%(1024*bs) != 0 {
 158         n++
 159     }
 160     return n * bs
 161 }
 162 
 163 func run(w io.Writer, paths []string, cfg config) error {
 164     bw := bufio.NewWriter(w)
 165     defer bw.Flush()
 166 
 167     bw.WriteString("name\tbytes\tblocks\n")
 168 
 169     var h handlers
 170     h.w = bw
 171     h.blockSize = cfg.blockSize
 172     h.skipSubfolder = nil
 173     if !cfg.recursive {
 174         h.skipSubfolder = fs.SkipDir
 175     }
 176     h.file = emitEntry
 177     if cfg.sorted {
 178         h.file = keepEntry
 179     }
 180 
 181     got := make(map[string]struct{})
 182 
 183     if len(paths) == 0 {
 184         paths = []string{`.`}
 185     }
 186 
 187     // handle is the callback for func filepath.WalkDir
 188     handle := func(path string, e fs.DirEntry, err error) error {
 189         if err != nil {
 190             return err
 191         }
 192 
 193         if _, ok := got[path]; ok {
 194             return nil
 195         }
 196         got[path] = struct{}{}
 197 
 198         if e.IsDir() {
 199             return h.skipSubfolder
 200         }
 201 
 202         info, err := e.Info()
 203         if err != nil {
 204             return err
 205         }
 206 
 207         return h.file(&h, path, info.Size())
 208     }
 209 
 210     for _, path := range paths {
 211         if _, ok := got[path]; ok {
 212             continue
 213         }
 214         got[path] = struct{}{}
 215 
 216         st, err := os.Stat(path)
 217         if err != nil {
 218             return err
 219         }
 220 
 221         if st.IsDir() {
 222             if !strings.HasSuffix(path, `/`) {
 223                 path = path + `/`
 224             }
 225             got[path] = struct{}{}
 226 
 227             if err := filepath.WalkDir(path, handle); err != nil {
 228                 return err
 229             }
 230             continue
 231         }
 232 
 233         if err := h.file(&h, path, st.Size()); err != nil {
 234             return err
 235         }
 236     }
 237 
 238     if !cfg.sorted {
 239         return nil
 240     }
 241 
 242     sort.Slice(h.entries, func(i, j int) bool {
 243         return h.entries[i].size > h.entries[j].size
 244     })
 245 
 246     for _, e := range h.entries {
 247         if err := writeEntry(bw, e, h); err != nil {
 248             return err
 249         }
 250     }
 251 
 252     return nil
 253 }
 254 
 255 func emitEntry(h *handlers, path string, size int64) error {
 256     return writeEntry(h.w, entry{path, size}, *h)
 257 }
 258 
 259 func keepEntry(h *handlers, path string, size int64) error {
 260     h.entries = append(h.entries, entry{path, size})
 261     return nil
 262 }
 263 
 264 func writeEntry(w *bufio.Writer, e entry, h handlers) error {
 265     abs, err := filepath.Abs(e.path)
 266     if err != nil {
 267         return err
 268     }
 269 
 270     var buf [24]byte
 271     w.WriteString(abs)
 272     w.WriteByte('\t')
 273     w.Write(strconv.AppendInt(buf[:0], e.size, 10))
 274     w.WriteByte('\t')
 275     w.Write(strconv.AppendInt(buf[:0], h.countBlocks(e.size), 10))
 276     w.WriteByte('\n')
 277 
 278     if !h.liveLines {
 279         return nil
 280     }
 281 
 282     if err := w.Flush(); err != nil {
 283         return io.EOF
 284     }
 285     return nil
 286 }
     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     var 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     var 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     var 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     }
  51 }
  52 
  53 func run(cfg config) error {
  54     w := bufio.NewWriter(os.Stdout)
  55     defer w.Flush()
  56 
  57     switch cfg.To {
  58     case "tsv":
  59         td := newTableDisplay(cfg)
  60         // show header immediately to reassure user in case scanning/sorting is taking a while
  61         td.FprintlnHeader(os.Stdout)
  62 
  63         data := scan(flag.Args(), cfg)
  64         sortItems(data, cfg.SortBy)
  65         for _, e := range data {
  66             if err := td.Fprintln(w, e); err != nil {
  67                 return nil // probably a pipe was closed
  68             }
  69         }
  70         return nil
  71 
  72     case "json":
  73         data := scan(flag.Args(), cfg)
  74         grouped := group(data)
  75         grouped.Inspect(func(fi *FolderInfo) {
  76             sortItems(fi.Files, cfg.SortBy)
  77 
  78             // avoid null values anywhere
  79             if fi.Folders == nil {
  80                 fi.Folders = []FolderInfo{}
  81             }
  82             if fi.Files == nil {
  83                 fi.Files = []FileInfo{}
  84             }
  85         })
  86 
  87         folders2JSON(w, grouped.Folders)
  88         w.Write([]byte("\n"))
  89         return nil
  90 
  91     default:
  92         return fmt.Errorf("unknown output-type %q", cfg.To)
  93     }
  94 }
  95 
  96 func scan(args []string, cfg config) []FileInfo {
  97     // get all file/folder names to check, then check them all: if no arguments
  98     // were given, use stdin to read them line by line
  99     if len(args) == 0 {
 100         sc := bufio.NewScanner(os.Stdin)
 101         sc.Buffer(nil, maxbufsize)
 102         for sc.Scan() {
 103             args = append(args, sc.Text())
 104         }
 105     }
 106 
 107     // get file sizes, count lines, count media duration, etc.
 108     agg := newAggregator(cfg)
 109     agg.Scan(args)
 110     data := agg.Results()
 111     return data
 112 }
 113 
 114 func sortItems(data []FileInfo, by string) {
 115     // show the list sorted by decreasing size, text lines, time duration, # of columns,
 116     // # of carriage-returns, # of byte-order marks, or forward-sorted by filename
 117     switch by {
 118     case "lines":
 119         sort.Sort(sortableLineCountInfo(data))
 120     case "duration":
 121         sort.Sort(sortableDurationInfo(data))
 122     case "columns":
 123         sort.Sort(sortableColumnCountInfo(data))
 124     case "cr":
 125         sort.Sort(sortableCarriageReturnInfo(data))
 126     case "bom":
 127         sort.Sort(sortableByteOrderMarkInfo(data))
 128     case "name":
 129         sort.Sort(sortableNameInfo(data))
 130     default:
 131         sort.Sort(sortableSizeInfo(data))
 132     }
 133 }
     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: ./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     }
 112 }
 113 
 114 func run(w io.Writer, args []string, cfg config) error {
 115     dashes := 0
 116     for _, name := range args {
 117         if name == `-` {
 118             dashes++
 119         }
 120         if dashes > 1 {
 121             return errors.New(`can't use stdin (dash) more than once`)
 122         }
 123     }
 124 
 125     bw := bufio.NewWriter(w)
 126     defer bw.Flush()
 127 
 128     if len(args) == 0 {
 129         return cfg.fix(bw, os.Stdin, cfg.liveLines)
 130     }
 131 
 132     for _, name := range args {
 133         if err := handleFile(bw, name, cfg); err != nil {
 134             return err
 135         }
 136     }
 137     return nil
 138 }
 139 
 140 func handleFile(w *bufio.Writer, name string, cfg config) error {
 141     if name == `` || name == `-` {
 142         return cfg.fix(w, os.Stdin, cfg.liveLines)
 143     }
 144 
 145     f, err := os.Open(name)
 146     if err != nil {
 147         return errors.New(`can't read from file named "` + name + `"`)
 148     }
 149     defer f.Close()
 150 
 151     return cfg.fix(w, f, cfg.liveLines)
 152 }
 153 
 154 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 155     const gb = 1024 * 1024 * 1024
 156     sc := bufio.NewScanner(r)
 157     sc.Buffer(nil, 8*gb)
 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         w.Write(s)
 166         if w.WriteByte('\n') != nil {
 167             return io.EOF
 168         }
 169 
 170         if !live {
 171             continue
 172         }
 173 
 174         if err := w.Flush(); err != nil {
 175             return io.EOF
 176         }
 177     }
 178 
 179     return sc.Err()
 180 }
 181 
 182 func detrail(w *bufio.Writer, r io.Reader, live bool) error {
 183     const gb = 1024 * 1024 * 1024
 184     sc := bufio.NewScanner(r)
 185     sc.Buffer(nil, 8*gb)
 186 
 187     for i := 0; sc.Scan(); i++ {
 188         s := sc.Bytes()
 189 
 190         // ignore leading UTF-8 BOM on the first line
 191         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 192             s = s[3:]
 193         }
 194 
 195         // trim trailing spaces on the current line
 196         for len(s) > 0 && s[len(s)-1] == ' ' {
 197             s = s[:len(s)-1]
 198         }
 199 
 200         w.Write(s)
 201         if w.WriteByte('\n') != nil {
 202             return io.EOF
 203         }
 204 
 205         if !live {
 206             continue
 207         }
 208 
 209         if err := w.Flush(); err != nil {
 210             return io.EOF
 211         }
 212     }
 213 
 214     return sc.Err()
 215 }
 216 
 217 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
 218     const gb = 1024 * 1024 * 1024
 219     sc := bufio.NewScanner(r)
 220     sc.Buffer(nil, 8*gb)
 221 
 222     for i := 0; sc.Scan(); i++ {
 223         s := sc.Bytes()
 224         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 225             s = s[3:]
 226         }
 227 
 228         writeSqueezed(w, s)
 229         if w.WriteByte('\n') != nil {
 230             return io.EOF
 231         }
 232 
 233         if !live {
 234             continue
 235         }
 236 
 237         if err := w.Flush(); err != nil {
 238             return io.EOF
 239         }
 240     }
 241 
 242     return sc.Err()
 243 }
 244 
 245 func writeSqueezed(w *bufio.Writer, s []byte) {
 246     // ignore leading spaces
 247     for len(s) > 0 && s[0] == ' ' {
 248         s = s[1:]
 249     }
 250 
 251     // ignore trailing spaces
 252     for len(s) > 0 && s[len(s)-1] == ' ' {
 253         s = s[:len(s)-1]
 254     }
 255 
 256     space := false
 257 
 258     for len(s) > 0 {
 259         switch s[0] {
 260         case ' ':
 261             s = s[1:]
 262             space = true
 263 
 264         case '\t':
 265             s = s[1:]
 266             space = false
 267             for len(s) > 0 && s[0] == ' ' {
 268                 s = s[1:]
 269             }
 270             w.WriteByte('\t')
 271 
 272         default:
 273             if space {
 274                 w.WriteByte(' ')
 275                 space = false
 276             }
 277             w.WriteByte(s[0])
 278             s = s[1:]
 279         }
 280     }
 281 }
     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             if _, ok := determFuncs[k]; !ok {
  53                 const fs = "func(x) optimizer %q has no matching built-in func"
  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             if _, ok := determFuncs[k]; !ok {
  66                 const fs = "func(x, y) optimizer %q has no matching built-in func"
  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     var tests = []struct {
  88         Source   string
  89         Expected any
  90     }{
  91         {"1", 1.0},
  92         {"3+4*5", 23.0},
  93 
  94         {"e", math.E},
  95         {"pi", math.Pi},
  96         {"phi", math.Phi},
  97         {"2*pi", 2 * math.Pi},
  98         {"4.51*phi-14.23564", 4.51*math.Phi - 14.23564},
  99         {"-e", -math.E},
 100 
 101         {"exp(2*pi)", math.Exp(2 * math.Pi)},
 102         {"log(2342.55) / log(43.21)", math.Log(2342.55) / math.Log(43.21)},
 103         {"f(3)", callExpr{name: "f", args: []any{3.0}}},
 104         {"min(3, 2, -1.5)", -1.5},
 105 
 106         {"hypot(x, 4)", callExpr{name: "hypot2", args: []any{"x", 4.0}}},
 107         {"max(x, 4)", callExpr{name: "max2", args: []any{"x", 4.0}}},
 108         {"min(x, 4)", callExpr{name: "min2", args: []any{"x", 4.0}}},
 109 
 110         {"rand()", callExpr{name: "rand"}},
 111 
 112         {
 113             "sin(2_000 * x * tau * x)",
 114             callExpr{
 115                 name: "sin",
 116                 args: []any{
 117                     binaryExpr{
 118                         "*",
 119                         binaryExpr{
 120                             "*",
 121                             binaryExpr{"*", 2_000.0, "x"},
 122                             2 * math.Pi,
 123                         },
 124                         "x",
 125                     },
 126                 },
 127             },
 128         },
 129 
 130         {
 131             "sin(10 * tau * exp(-20 * x)) * exp(-2 * x)",
 132             binaryExpr{
 133                 "*",
 134                 // sin(...)
 135                 callExpr{
 136                     name: "sin",
 137                     args: []any{
 138                         // 10 * tau * exp(...)
 139                         binaryExpr{
 140                             "*",
 141                             10 * 2 * math.Pi,
 142                             // exp(-20 * x)
 143                             callExpr{
 144                                 name: "exp",
 145                                 args: []any{
 146                                     binaryExpr{"*", -20.0, "x"},
 147                                 },
 148                             },
 149                         },
 150                     },
 151                 },
 152                 // exp(-2 * x)
 153                 callExpr{
 154                     name: "exp",
 155                     args: []any{binaryExpr{"*", -2.0, "x"}},
 156                 },
 157             },
 158         },
 159     }
 160 
 161     defs := map[string]any{
 162         "x": 3.5,
 163         "f": math.Exp,
 164     }
 165 
 166     for _, tc := range tests {
 167         t.Run(tc.Source, func(t *testing.T) {
 168             var c Compiler
 169             root, err := parse(tc.Source)
 170             if err != nil {
 171                 t.Fatal(err)
 172                 return
 173             }
 174 
 175             if err := c.reset(defs); err != nil {
 176                 t.Fatal(err)
 177                 return
 178             }
 179 
 180             got := c.optimize(root)
 181             if !reflect.DeepEqual(got, tc.Expected) {
 182                 const fs = "expected result to be\n%#v\ninstead of\n%#v"
 183                 t.Fatalf(fs, tc.Expected, got)
 184                 return
 185             }
 186         })
 187     }
 188 }
     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     var tests = []struct {
  34         Script string
  35         Tokens []string
  36     }{
  37         {``, nil},
  38         {`3.`, []string{`3.`}},
  39         {`3.2`, []string{`3.2`}},
  40         {`-3.2`, []string{`-`, `3.2`}},
  41         {`-3.2+56`, []string{`-`, `3.2`, `+`, `56`}},
  42     }
  43 
  44     for _, tc := range tests {
  45         t.Run(tc.Script, func(t *testing.T) {
  46             tok := newTokenizer(tc.Script)
  47             par, err := newParser(&tok)
  48             if err != nil {
  49                 t.Fatal(err)
  50                 return
  51             }
  52 
  53             var got []string
  54             for _, v := range par.tokens {
  55                 got = append(got, v.value)
  56             }
  57 
  58             if !reflect.DeepEqual(got, tc.Tokens) {
  59                 const fs = "from %s\nexpected\n%#v\nbut got\n%#v\ninstead"
  60                 t.Fatalf(fs, tc.Script, tc.Tokens, got)
  61                 return
  62             }
  63         })
  64     }
  65 }
     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     }
  93 }
  94 
  95 type config struct {
  96     got       map[string]struct{}
  97     recursive bool
  98     liveLines bool
  99 }
 100 
 101 func run(w io.Writer, paths []string, cfg config) error {
 102     bw := bufio.NewWriter(w)
 103     defer bw.Flush()
 104 
 105     if len(paths) == 0 {
 106         paths = []string{`.`}
 107     }
 108 
 109     if cfg.recursive {
 110         return runRecursive(bw, paths, cfg)
 111     }
 112     return runFlat(bw, paths, cfg)
 113 }
 114 
 115 func runRecursive(w *bufio.Writer, paths []string, cfg config) error {
 116     if len(paths) == 0 {
 117         paths = []string{`.`}
 118     }
 119 
 120     // handle is the callback for func filepath.WalkDir
 121     handle := func(path string, e fs.DirEntry, err error) error {
 122         if err != nil {
 123             return err
 124         }
 125 
 126         if _, ok := cfg.got[path]; ok {
 127             return nil
 128         }
 129         cfg.got[path] = struct{}{}
 130 
 131         if e.IsDir() {
 132             if err := handleEntry(w, path, cfg.liveLines); err != nil {
 133                 return err
 134             }
 135         }
 136 
 137         return nil
 138     }
 139 
 140     for _, path := range paths {
 141         if _, ok := cfg.got[path]; ok {
 142             continue
 143         }
 144         cfg.got[path] = struct{}{}
 145 
 146         st, err := os.Stat(path)
 147         if err != nil {
 148             return err
 149         }
 150 
 151         if !strings.HasSuffix(path, `/`) {
 152             path = path + `/`
 153             cfg.got[path] = struct{}{}
 154         }
 155 
 156         if !st.IsDir() {
 157             continue
 158         }
 159 
 160         if err := filepath.WalkDir(path, handle); err != nil {
 161             return err
 162         }
 163     }
 164 
 165     return nil
 166 }
 167 
 168 func runFlat(w *bufio.Writer, paths []string, cfg config) error {
 169     if len(paths) == 0 {
 170         paths = []string{`.`}
 171     }
 172 
 173     for _, path := range paths {
 174         if _, ok := cfg.got[path]; ok {
 175             continue
 176         }
 177         cfg.got[path] = struct{}{}
 178 
 179         st, err := os.Stat(path)
 180         if err != nil {
 181             return err
 182         }
 183 
 184         if !strings.HasSuffix(path, `/`) {
 185             path = path + `/`
 186             cfg.got[path] = struct{}{}
 187         }
 188 
 189         if !st.IsDir() {
 190             continue
 191         }
 192 
 193         entries, err := os.ReadDir(path)
 194         if err != nil {
 195             return err
 196         }
 197 
 198         for _, e := range entries {
 199             if !e.IsDir() {
 200                 continue
 201             }
 202 
 203             path := filepath.Join(path, e.Name())
 204             if err := handleEntry(w, path, cfg.liveLines); err != nil {
 205                 return err
 206             }
 207         }
 208     }
 209 
 210     return nil
 211 }
 212 
 213 func handleEntry(w *bufio.Writer, path string, live bool) error {
 214     abs, err := filepath.Abs(path)
 215     if err != nil {
 216         return err
 217     }
 218 
 219     w.WriteString(abs)
 220     w.WriteByte('\n')
 221 
 222     if !live {
 223         return nil
 224     }
 225 
 226     if err := w.Flush(); err != nil {
 227         return io.EOF
 228     }
 229     return nil
 230 }
     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     }
 135 
 136     liveLines := !buffered
 137     if !buffered {
 138         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 139             liveLines = false
 140         }
 141     }
 142 
 143     err := run(os.Stdout, os.Stdin, pairs, liveLines)
 144     if err != nil && err != io.EOF {
 145         os.Stderr.WriteString(err.Error())
 146         os.Stderr.WriteString("\n")
 147         os.Exit(1)
 148     }
 149 }
 150 
 151 func splitReplacement(s string) []string {
 152     if s == `` {
 153         return nil
 154     }
 155 
 156     n := 1
 157     var prev rune
 158     for _, r := range s {
 159         if prev != '\\' && r == '&' {
 160             n += 2
 161         }
 162         prev = r
 163     }
 164 
 165     repl := make([]string, 0, n)
 166 
 167     for len(s) > 0 {
 168         i := strings.IndexByte(s, '&')
 169         if i == 0 || (i > 0 && s[i-1] != '\\') {
 170             repl = append(repl, unescapeBackslashes(s[:i]))
 171             repl = append(repl, ``)
 172             s = s[i+1:]
 173             continue
 174         }
 175         break
 176     }
 177 
 178     if len(s) > 0 {
 179         repl = append(repl, unescapeBackslashes(s))
 180     }
 181     return repl
 182 }
 183 
 184 func unescapeBackslashes(s string) string {
 185     if strings.IndexByte(s, '\\') < 0 {
 186         return s
 187     }
 188 
 189     var prev byte
 190     unesc := make([]byte, 0, len(s))
 191 
 192     for i := range s {
 193         b := s[i]
 194 
 195         if prev != '\\' {
 196             if b != '\\' {
 197                 unesc = append(unesc, s[i])
 198             }
 199             prev = b
 200             continue
 201         }
 202 
 203         switch b {
 204         case 'e':
 205             unesc = append(unesc, '\x1b')
 206         case 'n':
 207             unesc = append(unesc, '\n')
 208         case 'r':
 209             unesc = append(unesc, '\r')
 210         case 't':
 211             unesc = append(unesc, '\t')
 212         case 'v':
 213             unesc = append(unesc, '\v')
 214         default:
 215             unesc = append(unesc, s[i])
 216         }
 217 
 218         prev = b
 219     }
 220 
 221     return string(unesc)
 222 }
 223 
 224 func run(w io.Writer, r io.Reader, pairs []pair, live bool) error {
 225     var buf []byte
 226     sc := bufio.NewScanner(r)
 227     sc.Buffer(nil, 8*1024*1024*1024)
 228     bw := bufio.NewWriter(w)
 229     defer bw.Flush()
 230 
 231     src := make([]byte, 8*1024)
 232     dst := make([]byte, 8*1024)
 233 
 234     for i := 0; sc.Scan(); i++ {
 235         line := sc.Bytes()
 236         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 237             line = line[3:]
 238         }
 239 
 240         s := line
 241         if bytes.IndexByte(s, '\x1b') >= 0 {
 242             buf = plain(buf[:0], s)
 243             s = buf
 244         }
 245 
 246         if len(pairs) > 0 {
 247             src = append(src[:0], s...)
 248             for _, p := range pairs {
 249                 dst = gsub(dst[:0], src, p.expr, p.repl)
 250                 src = append(src[:0], dst...)
 251             }
 252             bw.Write(dst)
 253         } else {
 254             bw.Write(s)
 255         }
 256 
 257         if bw.WriteByte('\n') != nil {
 258             return io.EOF
 259         }
 260 
 261         if !live {
 262             continue
 263         }
 264 
 265         if bw.Flush() != nil {
 266             return io.EOF
 267         }
 268     }
 269 
 270     return sc.Err()
 271 }
 272 
 273 func gsub(dst []byte, src []byte, what *regexp.Regexp, with []string) []byte {
 274     for len(src) > 0 {
 275         span := what.FindIndex(src)
 276         // also ignore empty regex matches to avoid infinite outer loops,
 277         // as skipping empty slices isn't advancing at all, leaving the
 278         // string stuck to being empty-matched forever by the same regex
 279         if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
 280             return append(dst, src...)
 281         }
 282 
 283         start, end := span[0], span[1]
 284         dst = append(dst, src[:start]...)
 285         // avoid infinite loops caused by empty regex matches
 286         if start == end {
 287             if end >= len(src) {
 288                 break
 289             }
 290             dst = append(dst, src[end])
 291             end++
 292             src = src[end:]
 293             continue
 294         }
 295 
 296         match := src[start:end]
 297         for _, sub := range with {
 298             if sub == `` {
 299                 dst = append(dst, match...)
 300                 continue
 301             }
 302             dst = append(dst, sub...)
 303         }
 304 
 305         src = src[end:]
 306     }
 307 
 308     return dst
 309 }
 310 
 311 func plain(dst []byte, src []byte) []byte {
 312     for len(src) > 0 {
 313         i, j := indexEscapeSequence(src)
 314         if i < 0 {
 315             dst = append(dst, src...)
 316             break
 317         }
 318         if j < 0 {
 319             j = len(src)
 320         }
 321 
 322         if i > 0 {
 323             dst = append(dst, src[:i]...)
 324         }
 325 
 326         src = src[j:]
 327     }
 328 
 329     return dst
 330 }
 331 
 332 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 333 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 334 // indices which can be independently negative when either the start/end of
 335 // a sequence isn't found; given their fairly-common use, even the hyperlink
 336 // ESC]8 sequences are supported
 337 func indexEscapeSequence(s []byte) (int, int) {
 338     var prev byte
 339 
 340     for i, b := range s {
 341         if prev == '\x1b' && b == '[' {
 342             j := indexLetter(s[i+1:])
 343             if j < 0 {
 344                 return i, -1
 345             }
 346             return i - 1, i + 1 + j + 1
 347         }
 348 
 349         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 350             j := indexPair(s[i+1:], '\x1b', '\\')
 351             if j < 0 {
 352                 return i, -1
 353             }
 354             return i - 1, i + 1 + j + 2
 355         }
 356 
 357         prev = b
 358     }
 359 
 360     return -1, -1
 361 }
 362 
 363 func indexLetter(s []byte) int {
 364     for i, b := range s {
 365         upper := b &^ 32
 366         if 'A' <= upper && upper <= 'Z' {
 367             return i
 368         }
 369     }
 370 
 371     return -1
 372 }
 373 
 374 func indexPair(s []byte, x byte, y byte) int {
 375     var prev byte
 376 
 377     for i, b := range s {
 378         if prev == x && b == y && i > 0 {
 379             return i
 380         }
 381         prev = b
 382     }
 383 
 384     return -1
 385 }
     File: ./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     }
 124 
 125     liveLines := !buffered
 126     if !buffered {
 127         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 128             liveLines = false
 129         }
 130     }
 131 
 132     err := run(os.Stdout, os.Stdin, patterns, filter, liveLines)
 133     if err != nil && err != io.EOF {
 134         os.Stderr.WriteString(err.Error())
 135         os.Stderr.WriteString("\n")
 136         os.Exit(1)
 137     }
 138 }
 139 
 140 // pattern is a regular-expression pattern which distinguishes between the
 141 // start/end of a line and those of the chunks it can be used to match
 142 type pattern struct {
 143     // expr is the regular-expression
 144     expr *regexp.Regexp
 145 
 146     // begin is whether the regexp refers to the start of a line
 147     begin bool
 148 
 149     // end is whether the regexp refers to the end of a line
 150     end bool
 151 }
 152 
 153 func compile(src string) (pattern, error) {
 154     expr, err := regexp.Compile(src)
 155 
 156     var pat pattern
 157     pat.expr = expr
 158     pat.begin = strings.HasPrefix(src, `^`) || strings.HasPrefix(src, `(?i)^`)
 159     pat.end = strings.HasSuffix(src, `$`) && !strings.HasSuffix(src, `\$`)
 160     return pat, err
 161 }
 162 
 163 func (p pattern) findIndex(s []byte, i int, last int) (start int, stop int) {
 164     if i > 0 && p.begin {
 165         return -1, -1
 166     }
 167     if i != last && p.end {
 168         return -1, -1
 169     }
 170 
 171     span := p.expr.FindIndex(s)
 172     // also ignore empty regex matches to avoid infinite outer loops,
 173     // as skipping empty slices isn't advancing at all, leaving the
 174     // string stuck to being empty-matched forever by the same regex
 175     if len(span) != 2 || span[0] == span[1] {
 176         return -1, -1
 177     }
 178 
 179     return span[0], span[1]
 180 }
 181 
 182 func run(w io.Writer, r io.Reader, pats []pattern, filter, live bool) error {
 183     sc := bufio.NewScanner(r)
 184     sc.Buffer(nil, 8*1024*1024*1024)
 185     bw := bufio.NewWriter(w)
 186     defer bw.Flush()
 187 
 188     for i := 0; sc.Scan(); i++ {
 189         s := sc.Bytes()
 190         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 191             s = s[3:]
 192         }
 193 
 194         n := 0
 195         last := countChunks(s) - 1
 196         if last < 0 {
 197             last = 0
 198         }
 199 
 200         if filter && !matches(s, pats, last) {
 201             continue
 202         }
 203 
 204         for len(s) > 0 {
 205             i, j := indexEscapeSequence(s)
 206             if i < 0 {
 207                 handleChunk(bw, s, pats, n, last)
 208                 break
 209             }
 210             if j < 0 {
 211                 j = len(s)
 212             }
 213 
 214             handleChunk(bw, s[:i], pats, n, last)
 215             if i > 0 {
 216                 n++
 217             }
 218 
 219             bw.Write(s[i:j])
 220 
 221             s = s[j:]
 222         }
 223 
 224         if bw.WriteByte('\n') != nil {
 225             return io.EOF
 226         }
 227 
 228         if !live {
 229             continue
 230         }
 231 
 232         if bw.Flush() != nil {
 233             return io.EOF
 234         }
 235     }
 236 
 237     return sc.Err()
 238 }
 239 
 240 // matches finds out if any regex matches any substring around ANSI-sequences
 241 func matches(s []byte, patterns []pattern, last int) bool {
 242     n := 0
 243 
 244     for len(s) > 0 {
 245         i, j := indexEscapeSequence(s)
 246         if i < 0 {
 247             for _, p := range patterns {
 248                 if begin, _ := p.findIndex(s, n, last); begin >= 0 {
 249                     return true
 250                 }
 251             }
 252             return false
 253         }
 254 
 255         if j < 0 {
 256             j = len(s)
 257         }
 258 
 259         for _, p := range patterns {
 260             if begin, _ := p.findIndex(s[:i], n, last); begin >= 0 {
 261                 return true
 262             }
 263         }
 264 
 265         if i > 0 {
 266             n++
 267         }
 268 
 269         s = s[j:]
 270     }
 271 
 272     return false
 273 }
 274 
 275 func countChunks(s []byte) int {
 276     chunks := 0
 277 
 278     for len(s) > 0 {
 279         i, j := indexEscapeSequence(s)
 280         if i < 0 {
 281             break
 282         }
 283 
 284         if i > 0 {
 285             chunks++
 286         }
 287 
 288         if j < 0 {
 289             break
 290         }
 291         s = s[j:]
 292     }
 293 
 294     if len(s) > 0 {
 295         chunks++
 296     }
 297     return chunks
 298 }
 299 
 300 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 301 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 302 // indices which can be independently negative when either the start/end of
 303 // a sequence isn't found; given their fairly-common use, even the hyperlink
 304 // ESC]8 sequences are supported
 305 func indexEscapeSequence(s []byte) (int, int) {
 306     var prev byte
 307 
 308     for i, b := range s {
 309         if prev == '\x1b' && b == '[' {
 310             j := indexLetter(s[i+1:])
 311             if j < 0 {
 312                 return i, -1
 313             }
 314             return i - 1, i + 1 + j + 1
 315         }
 316 
 317         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 318             j := indexPair(s[i+1:], '\x1b', '\\')
 319             if j < 0 {
 320                 return i, -1
 321             }
 322             return i - 1, i + 1 + j + 2
 323         }
 324 
 325         prev = b
 326     }
 327 
 328     return -1, -1
 329 }
 330 
 331 func indexLetter(s []byte) int {
 332     for i, b := range s {
 333         upper := b &^ 32
 334         if 'A' <= upper && upper <= 'Z' {
 335             return i
 336         }
 337     }
 338 
 339     return -1
 340 }
 341 
 342 func indexPair(s []byte, x byte, y byte) int {
 343     var prev byte
 344 
 345     for i, b := range s {
 346         if prev == x && b == y && i > 0 {
 347             return i
 348         }
 349         prev = b
 350     }
 351 
 352     return -1
 353 }
 354 
 355 // note: looking at the results of restoring ANSI-styles after style-resets
 356 // doesn't seem to be worth it, as a previous version used to do
 357 
 358 // handleChunk handles line-slices around any detected ANSI-style sequences,
 359 // or even whole lines, when no ANSI-styles are found in them
 360 func handleChunk(w *bufio.Writer, s []byte, with []pattern, n int, last int) {
 361     for len(s) > 0 {
 362         start, end := -1, -1
 363         for _, p := range with {
 364             i, j := p.findIndex(s, n, last)
 365             if i >= 0 && (i < start || start < 0) {
 366                 start, end = i, j
 367             }
 368         }
 369 
 370         if start < 0 {
 371             w.Write(s)
 372             return
 373         }
 374 
 375         w.Write(s[:start])
 376         w.WriteString(highlightStyle)
 377         w.Write(s[start:end])
 378         w.WriteString("\x1b[0m")
 379 
 380         s = s[end:]
 381     }
 382 }
     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             }
  93             continue
  94         }
  95 
  96         break
  97     }
  98 
  99     if len(args) > 0 && args[0] == `--` {
 100         args = args[1:]
 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     }
 108 }
 109 
 110 func run(w io.Writer, args []string, cfg *config) error {
 111     bw := bufio.NewWriter(w)
 112     defer bw.Flush()
 113 
 114     if len(args) == 0 {
 115         if err := handleReader(bw, os.Stdin, cfg); err != nil {
 116             return err
 117         }
 118     }
 119 
 120     for _, name := range args {
 121         if err := handleFile(bw, name, cfg); err != nil {
 122             return err
 123         }
 124     }
 125 
 126     if cfg.Started {
 127         endPage(bw)
 128     }
 129     return nil
 130 }
 131 
 132 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 133     if name == `` || name == `-` {
 134         return handleReader(w, os.Stdin, cfg)
 135     }
 136 
 137     f, err := os.Open(name)
 138     if err != nil {
 139         return errors.New(`can't read from file named "` + name + `"`)
 140     }
 141     defer f.Close()
 142 
 143     return handleReader(w, f, cfg)
 144 }
 145 
 146 func handleReader(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     lines := 0
 151 
 152     for sc.Scan() {
 153         line := sc.Text()
 154 
 155         if lines == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
 156             line = line[3:]
 157         }
 158         if lines == 0 && !cfg.Started {
 159             title := line
 160             if cfg.GotTitle {
 161                 title = cfg.Title
 162             }
 163             startPage(w, title, cfg.Monospace)
 164             cfg.Started = true
 165         }
 166         lines++
 167 
 168         if err := handleLine(w, line, cfg); err != nil {
 169             return err
 170         }
 171     }
 172 
 173     if !cfg.Started && lines > 0 {
 174         startPage(w, cfg.Title, cfg.Monospace)
 175         cfg.Started = true
 176     }
 177     return sc.Err()
 178 }
 179 
 180 const style = `
 181         body {
 182             margin: 1rem auto 2rem auto;
 183             padding: 0.25rem;
 184             font-size: 1.1rem;
 185             line-height: 1.8rem;
 186             font-family: sans-serif;
 187 
 188             max-width: 95vw;
 189             /* width: max-content; */
 190             width: fit-content;
 191 
 192             box-sizing: border-box;
 193             display: block;
 194         }
 195 
 196         a {
 197             color: steelblue;
 198             text-decoration: none;
 199         }
 200 
 201         p {
 202             display: block;
 203             margin: auto;
 204             max-width: 80ch;
 205         }
 206 
 207         img {
 208             margin: none;
 209         }
 210 
 211         audio {
 212             width: 60ch;
 213         }
 214 
 215         table {
 216             margin: 2rem auto;
 217             border-collapse: collapse;
 218         }
 219 
 220         thead>* {
 221             position: sticky;
 222             top: 0;
 223             background-color: white;
 224         }
 225 
 226         tfoot th {
 227             user-select: none;
 228         }
 229 
 230         th, td {
 231             padding: 0.1rem 1ch;
 232             min-width: 4ch;
 233             border-bottom: solid thin transparent;
 234         }
 235 
 236         tr:nth-child(5n) td {
 237             border-bottom: solid thin #ccc;
 238         }
 239 
 240         .monospace {
 241             font-family: monospace;
 242         }
 243 `
 244 
 245 func startPage(w *bufio.Writer, title string, monospace bool) {
 246     w.WriteString("<!DOCTYPE html>\n")
 247     w.WriteString("<html lang=\"en\">\n")
 248     w.WriteString("\n")
 249     w.WriteString("<head>\n")
 250     w.WriteString("    <meta charset=\"UTF-8\">\n")
 251     w.WriteString("    <meta name=\"viewport\" content=\"width=device-width,")
 252     w.WriteString(" initial-scale=1.0\">\n")
 253     w.WriteString("    <meta http-equiv=\"X-UA-Compatible\"")
 254     w.WriteString(" content=\"ie=edge\">\n")
 255     w.WriteString("\n")
 256     w.WriteString("    <link rel=\"icon\" href=\"data:,\">\n")
 257     w.WriteString(`    <title>`)
 258     w.WriteString(title)
 259     w.WriteString("</title>\n")
 260     w.WriteString("\n")
 261     w.WriteString("\n")
 262     w.WriteString("    <style>\n")
 263     w.WriteString(style[1:])
 264     w.WriteString("    </style>\n")
 265     w.WriteString("</head>\n")
 266     if monospace {
 267         w.WriteString("<body class=\"monospace\">\n")
 268     } else {
 269         w.WriteString("<body>\n")
 270     }
 271 }
 272 
 273 func endPage(w *bufio.Writer) {
 274     w.WriteString("</body>\n")
 275     w.WriteString("</html>\n")
 276 }
 277 
 278 func handleLine(w *bufio.Writer, s string, cfg *config) error {
 279     if handleDataURI(w, s) {
 280         if w.WriteByte('\n') != nil {
 281             return io.EOF
 282         }
 283         return nil
 284     }
 285 
 286     for len(s) > 0 {
 287         span := links.FindStringIndex(s)
 288         if span != nil && len(span) == 2 {
 289             i := span[0]
 290             j := span[1]
 291             href := s[i:j]
 292             handleChunk(w, s[:i])
 293             w.WriteString(`<a href="`)
 294             w.WriteString(href)
 295             w.WriteString(`">`)
 296             w.WriteString(href)
 297             w.WriteString(`</a>`)
 298             s = s[j:]
 299             continue
 300         }
 301 
 302         handleChunk(w, s)
 303         break
 304     }
 305 
 306     w.WriteString(`<br>`)
 307     if w.WriteByte('\n') != nil {
 308         return io.EOF
 309     }
 310     return nil
 311 }
 312 
 313 func handleChunk(w *bufio.Writer, s string) {
 314     for len(s) > 0 {
 315         switch b := s[0]; b {
 316         case '&':
 317             w.WriteString("&amp;")
 318         case '<':
 319             w.WriteString("&lt;")
 320         case '>':
 321             w.WriteString("&gt;")
 322         default:
 323             w.WriteByte(b)
 324         }
 325         s = s[1:]
 326     }
 327 }
 328 
 329 func handleDataURI(w *bufio.Writer, s string) bool {
 330     full := s
 331 
 332     if !strings.HasPrefix(s, `data:`) {
 333         return false
 334     }
 335 
 336     s = strings.TrimPrefix(s, `data:`)
 337     i := strings.Index(s, `;base64,`)
 338     if i < 0 {
 339         return false
 340     }
 341 
 342     kind := s[:i]
 343     s = s[i+len(`;base64,`):]
 344 
 345     if strings.HasPrefix(kind, `image/`) {
 346         if !isBase64(s) {
 347             return false
 348         }
 349 
 350         w.WriteString(`<img src="`)
 351         w.WriteString(full)
 352         w.WriteString(`">`)
 353         return true
 354     }
 355 
 356     if strings.HasPrefix(kind, `audio/`) {
 357         if !isBase64(s) {
 358             return false
 359         }
 360 
 361         w.WriteString(`<audio controls src="`)
 362         w.WriteString(full)
 363         w.WriteString(`"></audio>`)
 364         return true
 365     }
 366 
 367     if strings.HasPrefix(kind, `video/`) {
 368         if !isBase64(s) {
 369             return false
 370         }
 371 
 372         w.WriteString(`<video controls src="`)
 373         w.WriteString(full)
 374         w.WriteString(`"></video>`)
 375         return true
 376     }
 377 
 378     return false
 379 }
 380 
 381 func handleMedia(w *bufio.Writer, begin string, s string, end string) bool {
 382     if !isBase64(s) {
 383         return false
 384     }
 385 
 386     w.WriteString(begin)
 387     w.WriteString(s)
 388     w.WriteString(end)
 389     return true
 390 }
 391 
 392 func isBase64(s string) bool {
 393     dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(s))
 394     n, err := io.Copy(io.Discard, dec)
 395     return n > 0 && err == nil
 396 }
     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     }
  65 
  66     name := `-`
  67     if len(os.Args) > 1 {
  68         name = os.Args[1]
  69     }
  70 
  71     if err := run(os.Stdout, name); err != nil && err != io.EOF {
  72         showError(err.Error())
  73         os.Exit(1)
  74     }
  75 }
  76 
  77 func showError(msg string) {
  78     os.Stderr.WriteString(msg)
  79     os.Stderr.WriteString("\n")
  80 }
  81 
  82 func run(w io.Writer, name string) error {
  83     if name == `-` {
  84         return id3pic(w, bufio.NewReader(os.Stdin))
  85     }
  86 
  87     f, err := os.Open(name)
  88     if err != nil {
  89         return errors.New(`can't read from file named "` + name + `"`)
  90     }
  91     defer f.Close()
  92 
  93     return id3pic(w, bufio.NewReader(f))
  94 }
  95 
  96 func match(r *bufio.Reader, what []byte) bool {
  97     for _, v := range what {
  98         b, err := r.ReadByte()
  99         if b != v || err != nil {
 100             return false
 101         }
 102     }
 103     return true
 104 }
 105 
 106 func id3pic(w io.Writer, r *bufio.Reader) error {
 107     // match the ID3 mark
 108     for {
 109         b, err := r.ReadByte()
 110         if err == io.EOF {
 111             return errNoThumb
 112         }
 113         if err != nil {
 114             return err
 115         }
 116 
 117         if b == 'I' && match(r, []byte{'D', '3'}) {
 118             break
 119         }
 120     }
 121 
 122     for {
 123         b, err := r.ReadByte()
 124         if err == io.EOF {
 125             return errNoThumb
 126         }
 127         if err != nil {
 128             return err
 129         }
 130 
 131         // handle APIC-type chunks
 132         if b == 'A' && match(r, []byte{'P', 'I', 'C'}) {
 133             return handleAPIC(w, r)
 134         }
 135     }
 136 }
 137 
 138 func handleAPIC(w io.Writer, r *bufio.Reader) error {
 139     // section-size seems stored as 4 big-endian bytes
 140     var size uint32
 141     err := binary.Read(r, binary.BigEndian, &size)
 142     if err != nil {
 143         return err
 144     }
 145 
 146     n, err := skipThumbnailTypeAPIC(r)
 147     if err != nil {
 148         return err
 149     }
 150 
 151     _, err = io.Copy(w, io.LimitReader(r, int64(int(size)-n)))
 152     return err
 153 }
 154 
 155 func skipThumbnailTypeAPIC(r *bufio.Reader) (skipped int, err error) {
 156     m, err := r.Discard(2)
 157     if err != nil || m != 2 {
 158         return -1, errors.New(`failed to sync APIC flags`)
 159     }
 160     skipped += m
 161 
 162     m, err = r.Discard(1)
 163     if err != nil || m != 1 {
 164         return -1, errors.New(`failed to sync APIC text-encoding`)
 165     }
 166     skipped += m
 167 
 168     junk, err := r.ReadSlice(0)
 169     if err != nil {
 170         return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`)
 171     }
 172     skipped += len(junk)
 173 
 174     m, err = r.Discard(1)
 175     if err != nil || m != 1 {
 176         return -1, errors.New(`failed to sync APIC picture type`)
 177     }
 178     skipped += m
 179 
 180     junk, err = r.ReadSlice(0)
 181     if err != nil {
 182         return -1, errors.New(`failed to sync to APIC thumbnail description`)
 183     }
 184     skipped += len(junk)
 185 
 186     return skipped, nil
 187 }
     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: ./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     }
  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     name := `-`
 106     if len(args) == 1 {
 107         name = args[0]
 108     }
 109 
 110     if err := run(os.Stdout, name, handler, liveLines); err != nil && err != io.EOF {
 111         os.Stderr.WriteString(err.Error())
 112         os.Stderr.WriteString("\n")
 113         os.Exit(1)
 114     }
 115 }
 116 
 117 type handlerFunc func(w *bufio.Writer, r *bufio.Reader, live bool) error
 118 
 119 func run(w io.Writer, name string, handler handlerFunc, live bool) error {
 120     // f, _ := os.Create(`json0.prof`)
 121     // defer f.Close()
 122     // pprof.StartCPUProfile(f)
 123     // defer pprof.StopCPUProfile()
 124 
 125     if name == `` || name == `-` {
 126         bw := bufio.NewWriterSize(w, bufSize)
 127         br := bufio.NewReaderSize(os.Stdin, bufSize)
 128         defer bw.Flush()
 129         return handler(bw, br, live)
 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     bw := bufio.NewWriterSize(w, bufSize)
 139     br := bufio.NewReaderSize(f, bufSize)
 140     defer bw.Flush()
 141     return handler(bw, br, live)
 142 }
 143 
 144 var (
 145     errCommentEarlyEnd = errors.New(`unexpected early-end of comment`)
 146     errInputEarlyEnd   = errors.New(`expected end of input data`)
 147     errInvalidComment  = errors.New(`expected / or *`)
 148     errInvalidHex      = errors.New(`expected a base-16 digit`)
 149     errInvalidRune     = errors.New(`invalid UTF-8 bytes`)
 150     errInvalidToken    = errors.New(`invalid JSON token`)
 151     errNoDigits        = errors.New(`expected numeric digits`)
 152     errNoStringQuote   = errors.New(`expected " or '`)
 153     errNoArrayComma    = errors.New(`missing comma between array values`)
 154     errNoObjectComma   = errors.New(`missing comma between key-value pairs`)
 155     errStringEarlyEnd  = errors.New(`unexpected early-end of string`)
 156     errExtraBytes      = errors.New(`unexpected extra input bytes`)
 157 )
 158 
 159 // linePosError is a more descriptive kind of error, showing the source of
 160 // the input-related problem, as 1-based a line/pos number pair in front
 161 // of the error message
 162 type linePosError struct {
 163     // line is the 1-based line count from the input
 164     line int
 165 
 166     // pos is the 1-based `horizontal` position in its line
 167     pos int
 168 
 169     // err is the error message to `decorate` with the position info
 170     err error
 171 }
 172 
 173 // Error satisfies the error interface
 174 func (lpe linePosError) Error() string {
 175     where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
 176     return where + `: ` + lpe.err.Error()
 177 }
 178 
 179 // isIdentifier improves control-flow of func handleKey, when it handles
 180 // unquoted object keys
 181 var isIdentifier = [256]bool{
 182     '_': true,
 183 
 184     '0': true, '1': true, '2': true, '3': true, '4': true,
 185     '5': true, '6': true, '7': true, '8': true, '9': true,
 186 
 187     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
 188     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
 189     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
 190     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
 191     'Y': true, 'Z': true,
 192 
 193     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
 194     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
 195     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
 196     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
 197     'y': true, 'z': true,
 198 }
 199 
 200 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
 201 // being 0, and normalizes letter-case for the hex letters
 202 var matchHex = [256]byte{
 203     '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
 204     '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
 205     'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
 206     'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
 207 }
 208 
 209 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; final boolean
 210 // value isn't used, and is just there to match the signature of func jsonl
 211 func json0(w *bufio.Writer, r *bufio.Reader, live bool) error {
 212     jr := jsonReader{r, 1, 1}
 213     defer w.Flush()
 214 
 215     if err := jr.handleLeadingJunk(); err != nil {
 216         return err
 217     }
 218 
 219     // handle a single top-level JSON value
 220     err := handleValue(w, &jr)
 221 
 222     // end the only output-line with a line-feed; this also avoids showing
 223     // error messages on the same line as the main output, since JSON-0
 224     // output has no line-feeds before its last byte
 225     outputByte(w, '\n')
 226 
 227     if err != nil {
 228         return err
 229     }
 230     return jr.handleTrailingJunk()
 231 }
 232 
 233 // jsonl converts JSON/pseudo-JSON into (valid) minimal JSON Lines; this func
 234 // avoids writing a trailing line-feed, leaving that up to its caller
 235 func jsonl(w *bufio.Writer, r *bufio.Reader, live bool) error {
 236     jr := jsonReader{r, 1, 1}
 237 
 238     if err := jr.handleLeadingJunk(); err != nil {
 239         return err
 240     }
 241 
 242     chunk, err := jr.r.Peek(1)
 243     if err == nil && len(chunk) >= 1 {
 244         switch b := chunk[0]; b {
 245         case '[', '(':
 246             return handleArrayJSONL(w, &jr, b, live)
 247         }
 248     }
 249 
 250     // handle a single top-level JSON value
 251     err = handleValue(w, &jr)
 252 
 253     // end the only output-line with a line-feed; this also avoids showing
 254     // error messages on the same line as the main output, since JSON-0
 255     // output has no line-feeds before its last byte
 256     outputByte(w, '\n')
 257 
 258     if err != nil {
 259         return err
 260     }
 261     return jr.handleTrailingJunk()
 262 }
 263 
 264 // handleArrayJSONL handles top-level arrays for func jsonl
 265 func handleArrayJSONL(w *bufio.Writer, jr *jsonReader, start byte, live bool) error {
 266     if err := jr.demandSyntax(start); err != nil {
 267         return err
 268     }
 269 
 270     var end byte = ']'
 271     if start == '(' {
 272         end = ')'
 273     }
 274 
 275     for n := 0; true; n++ {
 276         // there may be whitespace/comments before the next comma
 277         if err := jr.seekNext(); err != nil {
 278             return err
 279         }
 280 
 281         // handle commas between values, as well as trailing ones
 282         comma := false
 283         b, _ := jr.peekByte()
 284         if b == ',' {
 285             jr.readByte()
 286             comma = true
 287 
 288             // there may be whitespace/comments before an ending ']'
 289             if err := jr.seekNext(); err != nil {
 290                 return err
 291             }
 292             b, _ = jr.peekByte()
 293         }
 294 
 295         // handle end of array
 296         if b == end {
 297             jr.readByte()
 298             if n > 0 {
 299                 err := outputByte(w, '\n')
 300                 if live {
 301                     w.Flush()
 302                 }
 303                 return err
 304             }
 305             return nil
 306         }
 307 
 308         // turn commas between adjacent values into line-feeds, as the
 309         // output for this custom func is supposed to be JSON Lines
 310         if n > 0 {
 311             if !comma {
 312                 return errNoArrayComma
 313             }
 314             if err := outputByte(w, '\n'); err != nil {
 315                 return err
 316             }
 317             if live {
 318                 w.Flush()
 319             }
 320         }
 321 
 322         // handle the next value
 323         if err := jr.seekNext(); err != nil {
 324             return err
 325         }
 326         if err := handleValue(w, jr); err != nil {
 327             return err
 328         }
 329     }
 330 
 331     // make the compiler happy
 332     return nil
 333 }
 334 
 335 // jsonReader reads data via a buffer, keeping track of the input position:
 336 // this in turn allows showing much more useful errors, when these happen
 337 type jsonReader struct {
 338     // r is the actual reader
 339     r *bufio.Reader
 340 
 341     // line is the 1-based line-counter for input bytes, and gives errors
 342     // useful position info
 343     line int
 344 
 345     // pos is the 1-based `horizontal` position in its line, and gives
 346     // errors useful position info
 347     pos int
 348 }
 349 
 350 // improveError makes any error more useful, by giving it info about the
 351 // current input-position, as a 1-based line/within-line-position pair
 352 func (jr jsonReader) improveError(err error) error {
 353     if _, ok := err.(linePosError); ok {
 354         return err
 355     }
 356 
 357     if err == io.EOF {
 358         return linePosError{jr.line, jr.pos, errInputEarlyEnd}
 359     }
 360     if err != nil {
 361         return linePosError{jr.line, jr.pos, err}
 362     }
 363     return nil
 364 }
 365 
 366 func (jr *jsonReader) handleLeadingJunk() error {
 367     // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
 368     // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
 369     // about byte-order by design
 370     jr.skipUTF8BOM()
 371 
 372     // ignore leading whitespace and/or comments
 373     return jr.seekNext()
 374 }
 375 
 376 func (jr *jsonReader) handleTrailingJunk() error {
 377     // ignore trailing whitespace and/or comments
 378     if err := jr.seekNext(); err != nil {
 379         return err
 380     }
 381 
 382     // ignore trailing semicolons
 383     for {
 384         if b, ok := jr.peekByte(); !ok || b != ';' {
 385             break
 386         }
 387 
 388         jr.readByte()
 389         // ignore trailing whitespace and/or comments
 390         if err := jr.seekNext(); err != nil {
 391             return err
 392         }
 393     }
 394 
 395     // beyond trailing whitespace and/or comments, any more bytes
 396     // make the whole input data invalid JSON
 397     if _, ok := jr.peekByte(); ok {
 398         return jr.improveError(errExtraBytes)
 399     }
 400     return nil
 401 }
 402 
 403 // demandSyntax fails with an error when the next byte isn't the one given;
 404 // when it is, the byte is then read/skipped, and a nil error is returned
 405 func (jr *jsonReader) demandSyntax(syntax byte) error {
 406     chunk, err := jr.r.Peek(1)
 407     if err == io.EOF {
 408         return jr.improveError(errInputEarlyEnd)
 409     }
 410     if err != nil {
 411         return jr.improveError(err)
 412     }
 413 
 414     if len(chunk) < 1 || chunk[0] != syntax {
 415         msg := `expected ` + string(rune(syntax))
 416         return jr.improveError(errors.New(msg))
 417     }
 418 
 419     jr.readByte()
 420     return nil
 421 }
 422 
 423 // peekByte simplifies control-flow for various other funcs
 424 func (jr jsonReader) peekByte() (b byte, ok bool) {
 425     chunk, err := jr.r.Peek(1)
 426     if err == nil && len(chunk) >= 1 {
 427         return chunk[0], true
 428     }
 429     return 0, false
 430 }
 431 
 432 // readByte does what it says, updating the reader's position info
 433 func (jr *jsonReader) readByte() (b byte, err error) {
 434     b, err = jr.r.ReadByte()
 435     if err == nil {
 436         if b == '\n' {
 437             jr.line += 1
 438             jr.pos = 1
 439         } else {
 440             jr.pos++
 441         }
 442         return b, nil
 443     }
 444     return b, jr.improveError(err)
 445 }
 446 
 447 // readRune does what it says, updating the reader's position info
 448 func (jr *jsonReader) readRune() (r rune, err error) {
 449     r, _, err = jr.r.ReadRune()
 450     if err == nil {
 451         if r == '\n' {
 452             jr.line += 1
 453             jr.pos = 1
 454         } else {
 455             jr.pos++
 456         }
 457         return r, nil
 458     }
 459     return r, jr.improveError(err)
 460 }
 461 
 462 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
 463 // and comments, either single-line (starting with //) or general (starting
 464 // with /* and ending with */)
 465 func (jr *jsonReader) seekNext() error {
 466     for {
 467         b, ok := jr.peekByte()
 468         if !ok {
 469             return nil
 470         }
 471 
 472         // case ' ', '\t', '\f', '\v', '\r', '\n':
 473         if b <= 32 {
 474             // keep skipping whitespace bytes
 475             jr.readByte()
 476             continue
 477         }
 478 
 479         if b == '#' {
 480             if err := jr.skipLine(); err != nil {
 481                 return err
 482             }
 483             continue
 484         }
 485 
 486         if b != '/' {
 487             // reached the next token
 488             return nil
 489         }
 490 
 491         if err := jr.skipComment(); err != nil {
 492             return err
 493         }
 494 
 495         // after comments, keep looking for more whitespace and/or comments
 496     }
 497 }
 498 
 499 // skipComment helps func seekNext skip over comments, simplifying the latter
 500 // func's control-flow
 501 func (jr *jsonReader) skipComment() error {
 502     err := jr.demandSyntax('/')
 503     if err != nil {
 504         return err
 505     }
 506 
 507     b, ok := jr.peekByte()
 508     if !ok {
 509         return nil
 510     }
 511 
 512     switch b {
 513     case '/':
 514         // handle single-line comments
 515         return jr.skipLine()
 516 
 517     case '*':
 518         // handle (potentially) multi-line comments
 519         return jr.skipGeneralComment()
 520 
 521     default:
 522         return jr.improveError(errInvalidComment)
 523     }
 524 }
 525 
 526 // skipLine handles single-line comments for func skipComment
 527 func (jr *jsonReader) skipLine() error {
 528     for {
 529         b, err := jr.readByte()
 530         if err == io.EOF {
 531             // end of input is fine in this case
 532             return nil
 533         }
 534         if err != nil {
 535             return err
 536         }
 537 
 538         if b == '\n' {
 539             return nil
 540         }
 541     }
 542 }
 543 
 544 // skipGeneralComment handles (potentially) multi-line comments for func
 545 // skipComment
 546 func (jr *jsonReader) skipGeneralComment() error {
 547     var prev byte
 548     for {
 549         b, err := jr.readByte()
 550         if err != nil {
 551             return jr.improveError(errCommentEarlyEnd)
 552         }
 553 
 554         if prev == '*' && b == '/' {
 555             return nil
 556         }
 557         if b == '\n' {
 558             jr.line++
 559         }
 560         prev = b
 561     }
 562 }
 563 
 564 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
 565 func (jr *jsonReader) skipUTF8BOM() {
 566     lead, err := jr.r.Peek(3)
 567     if err != nil {
 568         return
 569     }
 570 
 571     if len(lead) > 2 && lead[0] == 0xef && lead[1] == 0xbb && lead[2] == 0xbf {
 572         jr.readByte()
 573         jr.readByte()
 574         jr.readByte()
 575     }
 576 }
 577 
 578 // outputByte is a small wrapper on func WriteByte, which adapts any error
 579 // into a custom dummy output-error, which is in turn meant to be ignored,
 580 // being just an excuse to quit the app immediately and successfully
 581 func outputByte(w *bufio.Writer, b byte) error {
 582     err := w.WriteByte(b)
 583     if err == nil {
 584         return nil
 585     }
 586     return io.EOF
 587 }
 588 
 589 // handleArray handles arrays for func handleValue
 590 func handleArray(w *bufio.Writer, jr *jsonReader, start byte) error {
 591     if err := jr.demandSyntax(start); err != nil {
 592         return err
 593     }
 594 
 595     var end byte = ']'
 596     if start == '(' {
 597         end = ')'
 598     }
 599 
 600     w.WriteByte('[')
 601 
 602     for n := 0; true; n++ {
 603         // there may be whitespace/comments before the next comma
 604         if err := jr.seekNext(); err != nil {
 605             return err
 606         }
 607 
 608         // handle commas between values, as well as trailing ones
 609         comma := false
 610         b, _ := jr.peekByte()
 611         if b == ',' {
 612             jr.readByte()
 613             comma = true
 614 
 615             // there may be whitespace/comments before an ending ']'
 616             if err := jr.seekNext(); err != nil {
 617                 return err
 618             }
 619             b, _ = jr.peekByte()
 620         }
 621 
 622         // handle end of array
 623         if b == end {
 624             jr.readByte()
 625             w.WriteByte(']')
 626             return nil
 627         }
 628 
 629         // don't forget commas between adjacent values
 630         if n > 0 {
 631             if !comma {
 632                 return errNoArrayComma
 633             }
 634             if err := outputByte(w, ','); err != nil {
 635                 return err
 636             }
 637         }
 638 
 639         // handle the next value
 640         if err := jr.seekNext(); err != nil {
 641             return err
 642         }
 643         if err := handleValue(w, jr); err != nil {
 644             return err
 645         }
 646     }
 647 
 648     // make the compiler happy
 649     return nil
 650 }
 651 
 652 // handleDigits helps various number-handling funcs do their job
 653 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
 654     if trySimpleDigits(w, jr) {
 655         return nil
 656     }
 657 
 658     for n := 0; true; n++ {
 659         b, _ := jr.peekByte()
 660 
 661         // support `nice` long numbers by ignoring their underscores
 662         if b == '_' {
 663             jr.readByte()
 664             continue
 665         }
 666 
 667         if '0' <= b && b <= '9' {
 668             jr.readByte()
 669             w.WriteByte(b)
 670             continue
 671         }
 672 
 673         if n == 0 {
 674             return errNoDigits
 675         }
 676         return nil
 677     }
 678 
 679     // make the compiler happy
 680     return nil
 681 }
 682 
 683 // trySimpleDigits tries to handle (more quickly) digit-runs where all bytes
 684 // are just digits: this is a very common case for numbers; returns whether
 685 // it succeeded, so this func's caller knows knows if it needs to do anything,
 686 // the slower way
 687 func trySimpleDigits(w *bufio.Writer, jr *jsonReader) (gotIt bool) {
 688     chunk, _ := jr.r.Peek(chunkPeekSize)
 689 
 690     for i, b := range chunk {
 691         if '0' <= b && b <= '9' {
 692             continue
 693         }
 694 
 695         if i == 0 || b == '_' {
 696             return false
 697         }
 698 
 699         // bulk-writing the chunk is this func's whole point
 700         w.Write(chunk[:i])
 701 
 702         jr.r.Discard(i)
 703         jr.pos += i
 704         return true
 705     }
 706 
 707     // maybe the digits-run is ok, but it's just longer than the chunk
 708     return false
 709 }
 710 
 711 // handleDot handles pseudo-JSON numbers which start with a decimal dot
 712 func handleDot(w *bufio.Writer, jr *jsonReader) error {
 713     if err := jr.demandSyntax('.'); err != nil {
 714         return err
 715     }
 716     w.Write([]byte{'0', '.'})
 717     return handleDigits(w, jr)
 718 }
 719 
 720 // handleKey is used by func handleObjects and generalizes func handleString,
 721 // by allowing unquoted object keys; it's not used anywhere else, as allowing
 722 // unquoted string values is ambiguous with actual JSON-keyword values null,
 723 // false, and true.
 724 func handleKey(w *bufio.Writer, jr *jsonReader) error {
 725     quote, ok := jr.peekByte()
 726     if !ok {
 727         return jr.improveError(errStringEarlyEnd)
 728     }
 729 
 730     if quote == '"' || quote == '\'' {
 731         return handleString(w, jr, quote)
 732     }
 733 
 734     w.WriteByte('"')
 735     for {
 736         if b, _ := jr.peekByte(); isIdentifier[b] {
 737             jr.readByte()
 738             w.WriteByte(b)
 739             continue
 740         }
 741 
 742         w.WriteByte('"')
 743         return nil
 744     }
 745 }
 746 
 747 // trySimpleString tries to handle (more quickly) inner-strings where all bytes
 748 // are unescaped ASCII symbols: this is a very common case for strings, and is
 749 // almost always the case for object keys; returns whether it succeeded, so
 750 // this func's caller knows knows if it needs to do anything, the slower way
 751 func trySimpleString(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
 752     end := -1
 753     chunk, _ := jr.r.Peek(chunkPeekSize)
 754 
 755     for i, b := range chunk {
 756         if 32 <= b && b <= 127 && b != '\\' && b != '\'' && b != '"' {
 757             continue
 758         }
 759 
 760         if b == byte(quote) {
 761             end = i
 762             break
 763         }
 764         return false
 765     }
 766 
 767     if end < 0 {
 768         return false
 769     }
 770 
 771     // bulk-writing the chunk is this func's whole point
 772     w.WriteByte('"')
 773     w.Write(chunk[:end])
 774     w.WriteByte('"')
 775 
 776     jr.r.Discard(end + 1)
 777     jr.pos += end + 1
 778     return true
 779 }
 780 
 781 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
 782 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
 783     for rest := kw; len(rest) > 0; rest = rest[1:] {
 784         b, err := jr.readByte()
 785         if err == nil && b == rest[0] {
 786             // keywords given to this func have no line-feeds
 787             jr.pos++
 788             continue
 789         }
 790 
 791         msg := `expected JSON value ` + string(kw)
 792         return jr.improveError(errors.New(msg))
 793     }
 794 
 795     w.Write(kw)
 796     return nil
 797 }
 798 
 799 func replaceKeyword(w *bufio.Writer, jr *jsonReader, kw, with []byte) error {
 800     for rest := kw; len(rest) > 0; rest = rest[1:] {
 801         b, err := jr.readByte()
 802         if err == nil && b == rest[0] {
 803             // keywords given to this func have no line-feeds
 804             jr.pos++
 805             continue
 806         }
 807 
 808         msg := `expected JSON value ` + string(kw)
 809         return jr.improveError(errors.New(msg))
 810     }
 811 
 812     w.Write(with)
 813     return nil
 814 }
 815 
 816 // handleNegative handles numbers starting with a negative sign for func
 817 // handleValue
 818 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
 819     if err := jr.demandSyntax('-'); err != nil {
 820         return err
 821     }
 822 
 823     w.WriteByte('-')
 824     if b, _ := jr.peekByte(); b == '.' {
 825         jr.readByte()
 826         w.Write([]byte{'0', '.'})
 827         return handleDigits(w, jr)
 828     }
 829     return handleNumber(w, jr)
 830 }
 831 
 832 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
 833 // such as values starting with a decimal dot
 834 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
 835     // handle integer digits
 836     if err := handleDigits(w, jr); err != nil {
 837         return err
 838     }
 839 
 840     // handle optional decimal digits, starting with a leading dot
 841     if b, _ := jr.peekByte(); b == '.' {
 842         jr.readByte()
 843         w.WriteByte('.')
 844         return handleDigits(w, jr)
 845     }
 846 
 847     // handle optional exponent digits
 848     if b, _ := jr.peekByte(); b == 'e' || b == 'E' {
 849         jr.readByte()
 850         w.WriteByte(b)
 851         b, _ = jr.peekByte()
 852         if b == '+' {
 853             jr.readByte()
 854         } else if b == '-' {
 855             w.WriteByte('-')
 856             jr.readByte()
 857         }
 858         return handleDigits(w, jr)
 859     }
 860 
 861     return nil
 862 }
 863 
 864 // handleObject handles objects for func handleValue
 865 func handleObject(w *bufio.Writer, jr *jsonReader) error {
 866     if err := jr.demandSyntax('{'); err != nil {
 867         return err
 868     }
 869     w.WriteByte('{')
 870 
 871     for npairs := 0; true; npairs++ {
 872         // there may be whitespace/comments before the next comma
 873         if err := jr.seekNext(); err != nil {
 874             return err
 875         }
 876 
 877         // handle commas between key-value pairs, as well as trailing ones
 878         comma := false
 879         b, _ := jr.peekByte()
 880         if b == ',' {
 881             jr.readByte()
 882             comma = true
 883 
 884             // there may be whitespace/comments before an ending '}'
 885             if err := jr.seekNext(); err != nil {
 886                 return err
 887             }
 888             b, _ = jr.peekByte()
 889         }
 890 
 891         // handle end of object
 892         if b == '}' {
 893             jr.readByte()
 894             w.WriteByte('}')
 895             return nil
 896         }
 897 
 898         // don't forget commas between adjacent key-value pairs
 899         if npairs > 0 {
 900             if !comma {
 901                 return errNoObjectComma
 902             }
 903             if err := outputByte(w, ','); err != nil {
 904                 return err
 905             }
 906         }
 907 
 908         // handle the next pair's key
 909         if err := jr.seekNext(); err != nil {
 910             return err
 911         }
 912         if err := handleKey(w, jr); err != nil {
 913             return err
 914         }
 915 
 916         // demand a colon right after the key
 917         if err := jr.seekNext(); err != nil {
 918             return err
 919         }
 920         if err := jr.demandSyntax(':'); err != nil {
 921             return err
 922         }
 923         w.WriteByte(':')
 924 
 925         // handle the next pair's value
 926         if err := jr.seekNext(); err != nil {
 927             return err
 928         }
 929         if err := handleValue(w, jr); err != nil {
 930             return err
 931         }
 932     }
 933 
 934     // make the compiler happy
 935     return nil
 936 }
 937 
 938 // handlePositive handles numbers starting with a positive sign for func
 939 // handleValue
 940 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
 941     if err := jr.demandSyntax('+'); err != nil {
 942         return err
 943     }
 944 
 945     // valid JSON isn't supposed to have leading pluses on numbers, so
 946     // emit nothing for it, unlike for negative numbers
 947 
 948     if b, _ := jr.peekByte(); b == '.' {
 949         jr.readByte()
 950         w.Write([]byte{'0', '.'})
 951         return handleDigits(w, jr)
 952     }
 953     return handleNumber(w, jr)
 954 }
 955 
 956 // handleString handles strings for funcs handleValue and handleObject, and
 957 // supports both single-quotes and double-quotes, always emitting the latter
 958 // in the output, of course
 959 func handleString(w *bufio.Writer, jr *jsonReader, quote byte) error {
 960     if quote != '"' && quote != '\'' {
 961         return errNoStringQuote
 962     }
 963 
 964     jr.readByte()
 965 
 966     // try the quicker no-escapes ASCII handler
 967     if trySimpleString(w, jr, quote) {
 968         return nil
 969     }
 970 
 971     // it's a non-trivial inner-string, so handle it byte-by-byte
 972     w.WriteByte('"')
 973     escaped := false
 974 
 975     for quote := rune(quote); true; {
 976         r, err := jr.readRune()
 977         if r == unicode.ReplacementChar {
 978             return jr.improveError(errInvalidRune)
 979         }
 980         if err != nil {
 981             if err == io.EOF {
 982                 return jr.improveError(errStringEarlyEnd)
 983             }
 984             return jr.improveError(err)
 985         }
 986 
 987         if !escaped {
 988             if r == '\\' {
 989                 escaped = true
 990                 continue
 991             }
 992 
 993             // handle end of string
 994             if r == quote {
 995                 return outputByte(w, '"')
 996             }
 997 
 998             if r <= 127 {
 999                 w.Write(escapedStringBytes[byte(r)])
1000             } else {
1001                 w.WriteRune(r)
1002             }
1003             continue
1004         }
1005 
1006         // handle escaped items
1007         escaped = false
1008 
1009         switch r {
1010         case 'u':
1011             // \u needs exactly 4 hex-digits to follow it
1012             w.Write([]byte{'\\', 'u'})
1013             if err := copyHex(w, 4, jr); err != nil {
1014                 return jr.improveError(err)
1015             }
1016 
1017         case 'x':
1018             // JSON only supports 4 escaped hex-digits, so pad the 2
1019             // expected hex-digits with 2 zeros
1020             w.Write([]byte{'\\', 'u', '0', '0'})
1021             if err := copyHex(w, 2, jr); err != nil {
1022                 return jr.improveError(err)
1023             }
1024 
1025         case 't', 'f', 'r', 'n', 'b', '\\', '"':
1026             // handle valid-JSON escaped string sequences
1027             w.WriteByte('\\')
1028             w.WriteByte(byte(r))
1029 
1030         case '\'':
1031             // escaped single-quotes aren't standard JSON, but they can
1032             // be handy when the input uses non-standard single-quoted
1033             // strings
1034             w.WriteByte('\'')
1035 
1036         default:
1037             if r <= 127 {
1038                 w.Write(escapedStringBytes[byte(r)])
1039             } else {
1040                 w.WriteRune(r)
1041             }
1042         }
1043     }
1044 
1045     return nil
1046 }
1047 
1048 // copyHex handles a run of hex-digits for func handleString, starting right
1049 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
1050 // errors with position info: that's up to the caller
1051 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
1052     for i := 0; i < n; i++ {
1053         b, err := jr.readByte()
1054         if err == io.EOF {
1055             return errStringEarlyEnd
1056         }
1057         if err != nil {
1058             return err
1059         }
1060 
1061         if b >= 128 {
1062             return errInvalidHex
1063         }
1064 
1065         if b := matchHex[b]; b != 0 {
1066             w.WriteByte(b)
1067             continue
1068         }
1069 
1070         return errInvalidHex
1071     }
1072 
1073     return nil
1074 }
1075 
1076 // handleValue is a generic JSON-token handler, which allows the recursive
1077 // behavior to handle any kind of JSON/pseudo-JSON input
1078 func handleValue(w *bufio.Writer, jr *jsonReader) error {
1079     chunk, err := jr.r.Peek(1)
1080     if err == nil && len(chunk) >= 1 {
1081         return handleValueDispatch(w, jr, chunk[0])
1082     }
1083 
1084     if err == io.EOF {
1085         return jr.improveError(errInputEarlyEnd)
1086     }
1087     return jr.improveError(errInputEarlyEnd)
1088 }
1089 
1090 // handleValueDispatch simplifies control-flow for func handleValue
1091 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
1092     switch b {
1093     case '#':
1094         return jr.skipLine()
1095     case 'f':
1096         return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
1097     case 'n':
1098         return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
1099     case 't':
1100         return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
1101     case 'F':
1102         return replaceKeyword(w, jr, []byte(`False`), []byte(`false`))
1103     case 'N':
1104         return replaceKeyword(w, jr, []byte(`None`), []byte(`null`))
1105     case 'T':
1106         return replaceKeyword(w, jr, []byte(`True`), []byte(`true`))
1107     case '.':
1108         return handleDot(w, jr)
1109     case '+':
1110         return handlePositive(w, jr)
1111     case '-':
1112         return handleNegative(w, jr)
1113     case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
1114         return handleNumber(w, jr)
1115     case '\'', '"':
1116         return handleString(w, jr, b)
1117     case '[', '(':
1118         return handleArray(w, jr, b)
1119     case '{':
1120         return handleObject(w, jr)
1121     default:
1122         return jr.improveError(errInvalidToken)
1123     }
1124 }
1125 
1126 // escapedStringBytes helps func handleString treat all string bytes quickly
1127 // and correctly, using their officially-supported JSON escape sequences
1128 //
1129 // https://www.rfc-editor.org/rfc/rfc8259#section-7
1130 var escapedStringBytes = [256][]byte{
1131     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
1132     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
1133     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
1134     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
1135     {'\\', 'b'}, {'\\', 't'},
1136     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
1137     {'\\', 'f'}, {'\\', 'r'},
1138     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
1139     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
1140     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
1141     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
1142     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
1143     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
1144     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
1145     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
1146     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
1147     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
1148     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
1149     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
1150     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
1151     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
1152     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
1153     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
1154     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
1155     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
1156     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
1157     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
1158     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
1159     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
1160     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
1161     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
1162     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
1163     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
1164     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
1165     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
1166     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
1167     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
1168     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
1169     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
1170     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
1171     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
1172     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
1173     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
1174     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
1175 }
     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     var tests = []struct {
  37         Input    string
  38         Expected string
  39     }{
  40         {`false`, `false`},
  41         {`null`, `null`},
  42         {`  true  `, `true`},
  43 
  44         {`False`, `false`},
  45         {`None`, `null`},
  46         {`  True  `, `true`},
  47 
  48         {`0`, `0`},
  49         {`1`, `1`},
  50         {`2`, `2`},
  51         {`3`, `3`},
  52         {`4`, `4`},
  53         {`5`, `5`},
  54         {`6`, `6`},
  55         {`7`, `7`},
  56         {`8`, `8`},
  57         {`9`, `9`},
  58 
  59         {`  .345`, `0.345`},
  60         {` -.345`, `-0.345`},
  61         {` +.345`, `0.345`},
  62         {` +123.345`, `123.345`},
  63         {` +.345`, `0.345`},
  64         {` 123.34523`, `123.34523`},
  65         {` 123.34_523`, `123.34523`},
  66         {` 123_456.123`, `123456.123`},
  67 
  68         {`""`, `""`},
  69         {`''`, `""`},
  70         {`"\""`, `"\""`},
  71         {`'\"'`, `"\""`},
  72         {`'\''`, `"'"`},
  73         {`'abc\u0e9A'`, `"abc\u0E9A"`},
  74         {`'abc\x1f[0m'`, `"abc\u001F[0m"`},
  75         {`"abc●def"`, `"abc●def"`},
  76 
  77         {`[  ]`, `[]`},
  78         {`[ , ]`, `[]`},
  79         {`[.345, false,null , ]`, `[0.345,false,null]`},
  80 
  81         {`(  )`, `[]`},
  82         {`( , )`, `[]`},
  83         {`(.345, false,null , )`, `[0.345,false,null]`},
  84 
  85         {`{  }`, `{}`},
  86         {`{ , }`, `{}`},
  87 
  88         {
  89             `{ 'abc': .345, "def"  : false, 'xyz':null , }`,
  90             `{"abc":0.345,"def":false,"xyz":null}`,
  91         },
  92 
  93         {`{0problems:123,}`, `{"0problems":123}`},
  94         {`{0_problems:123}`, `{"0_problems":123}`},
  95     }
  96 
  97     for _, tc := range tests {
  98         t.Run(tc.Input, func(t *testing.T) {
  99             var out strings.Builder
 100             w := bufio.NewWriter(&out)
 101             r := bufio.NewReader(strings.NewReader(tc.Input))
 102             if err := json0(w, r, false); err != nil && err != io.EOF {
 103                 t.Fatal(err)
 104                 return
 105             }
 106             // don't forget to flush the buffer, or output will be empty
 107             w.Flush()
 108 
 109             // output may have a final line-feed: get rid of it, or every
 110             // single test-case will fail
 111             got := out.String()
 112             if len(got) > 0 && got[len(got)-1] == '\n' {
 113                 got = got[:len(got)-1]
 114             }
 115 
 116             if got != tc.Expected {
 117                 t.Fatalf("<got>\n%s\n<expected>\n%s", got, tc.Expected)
 118                 return
 119             }
 120         })
 121     }
 122 }
 123 
 124 func TestEscapedStringBytes(t *testing.T) {
 125     var escaped = map[rune][]byte{
 126         '\x00': {'\\', 'u', '0', '0', '0', '0'},
 127         '\x01': {'\\', 'u', '0', '0', '0', '1'},
 128         '\x02': {'\\', 'u', '0', '0', '0', '2'},
 129         '\x03': {'\\', 'u', '0', '0', '0', '3'},
 130         '\x04': {'\\', 'u', '0', '0', '0', '4'},
 131         '\x05': {'\\', 'u', '0', '0', '0', '5'},
 132         '\x06': {'\\', 'u', '0', '0', '0', '6'},
 133         '\x07': {'\\', 'u', '0', '0', '0', '7'},
 134         '\x0b': {'\\', 'u', '0', '0', '0', 'b'},
 135         '\x0e': {'\\', 'u', '0', '0', '0', 'e'},
 136         '\x0f': {'\\', 'u', '0', '0', '0', 'f'},
 137         '\x10': {'\\', 'u', '0', '0', '1', '0'},
 138         '\x11': {'\\', 'u', '0', '0', '1', '1'},
 139         '\x12': {'\\', 'u', '0', '0', '1', '2'},
 140         '\x13': {'\\', 'u', '0', '0', '1', '3'},
 141         '\x14': {'\\', 'u', '0', '0', '1', '4'},
 142         '\x15': {'\\', 'u', '0', '0', '1', '5'},
 143         '\x16': {'\\', 'u', '0', '0', '1', '6'},
 144         '\x17': {'\\', 'u', '0', '0', '1', '7'},
 145         '\x18': {'\\', 'u', '0', '0', '1', '8'},
 146         '\x19': {'\\', 'u', '0', '0', '1', '9'},
 147         '\x1a': {'\\', 'u', '0', '0', '1', 'a'},
 148         '\x1b': {'\\', 'u', '0', '0', '1', 'b'},
 149         '\x1c': {'\\', 'u', '0', '0', '1', 'c'},
 150         '\x1d': {'\\', 'u', '0', '0', '1', 'd'},
 151         '\x1e': {'\\', 'u', '0', '0', '1', 'e'},
 152         '\x1f': {'\\', 'u', '0', '0', '1', 'f'},
 153 
 154         '\t': {'\\', 't'},
 155         '\f': {'\\', 'f'},
 156         '\b': {'\\', 'b'},
 157         '\r': {'\\', 'r'},
 158         '\n': {'\\', 'n'},
 159         '\\': {'\\', '\\'},
 160         '"':  {'\\', '"'},
 161     }
 162 
 163     if n := len(escapedStringBytes); n != 256 {
 164         t.Errorf(`expected 256 entries, instead of %d`, n)
 165     }
 166 
 167     for i, v := range escapedStringBytes {
 168         exp := []byte{byte(i)}
 169         if esc, ok := escaped[rune(i)]; ok {
 170             exp = esc
 171         }
 172 
 173         if !bytes.Equal(v, exp) {
 174             t.Errorf("%d: expected %#v, got %#v", i, exp, v)
 175         }
 176     }
 177 }
     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     }
  62 
  63     // figure out whether input should come from a named file or from stdin
  64     name := `-`
  65     if len(args) == 1 {
  66         name = args[0]
  67     }
  68 
  69     if err := handleInput(os.Stdout, name); err != nil && err != io.EOF {
  70         os.Stderr.WriteString(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73     }
  74 }
  75 
  76 // handleInput simplifies control-flow for func main
  77 func handleInput(w io.Writer, path string) error {
  78     if path == `-` {
  79         return convert(w, os.Stdin)
  80     }
  81 
  82     f, err := os.Open(path)
  83     if err != nil {
  84         // on windows, file-not-found error messages may mention `CreateFile`,
  85         // even when trying to open files in read-only mode
  86         return errors.New(`can't open file named ` + path)
  87     }
  88     defer f.Close()
  89     return convert(w, f)
  90 }
  91 
  92 // convert simplifies control-flow for func handleInput
  93 func convert(w io.Writer, r io.Reader) error {
  94     bw := bufio.NewWriter(w)
  95     defer bw.Flush()
  96     return json2(bw, r)
  97 }
  98 
  99 // escapedStringBytes helps func handleString treat all string bytes quickly
 100 // and correctly, using their officially-supported JSON escape sequences
 101 //
 102 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 103 var escapedStringBytes = [256][]byte{
 104     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 105     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 106     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 107     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 108     {'\\', 'b'}, {'\\', 't'},
 109     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 110     {'\\', 'f'}, {'\\', 'r'},
 111     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 112     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 113     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 114     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 115     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 116     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 117     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 118     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 119     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 120     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 121     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 122     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 123     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 124     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 125     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 126     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 127     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 128     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 129     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 130     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 131     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 132     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 133     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 134     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 135     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 136     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 137     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 138     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 139     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 140     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 141     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 142     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 143     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 144     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 145     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 146     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 147     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 148 }
 149 
 150 // writeSpaces does what it says, minimizing calls to write-like funcs
 151 func writeSpaces(w *bufio.Writer, n int) {
 152     const spaces = `                                `
 153     if n < 1 {
 154         return
 155     }
 156 
 157     for n >= len(spaces) {
 158         w.WriteString(spaces)
 159         n -= len(spaces)
 160     }
 161     w.WriteString(spaces[:n])
 162 }
 163 
 164 // json2 does it all, given a reader and a writer
 165 func json2(w *bufio.Writer, r io.Reader) error {
 166     dec := json.NewDecoder(r)
 167     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 168     // even if JSON parsers aren't required to guarantee such input-fidelity
 169     // for numbers
 170     dec.UseNumber()
 171 
 172     t, err := dec.Token()
 173     if err == io.EOF {
 174         return errors.New(`input has no JSON values`)
 175     }
 176 
 177     if err = handleToken(w, dec, t, 0, 0); err != nil {
 178         return err
 179     }
 180     // don't forget ending the last line for the last value
 181     w.WriteByte('\n')
 182 
 183     _, err = dec.Token()
 184     if err == io.EOF {
 185         // input is over, so it's a success
 186         return nil
 187     }
 188 
 189     if err == nil {
 190         // a successful `read` is a failure, as it means there are
 191         // trailing JSON tokens
 192         return errors.New(`unexpected trailing data`)
 193     }
 194 
 195     // any other error, perhaps some invalid-JSON-syntax-type error
 196     return err
 197 }
 198 
 199 // handleToken handles recursion for func json2
 200 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
 201     switch t := t.(type) {
 202     case json.Delim:
 203         switch t {
 204         case json.Delim('['):
 205             return handleArray(w, dec, pre, level)
 206         case json.Delim('{'):
 207             return handleObject(w, dec, pre, level)
 208         default:
 209             return errors.New(`unsupported JSON syntax ` + string(t))
 210         }
 211 
 212     case nil:
 213         writeSpaces(w, 2*pre)
 214         w.WriteString(`null`)
 215         return nil
 216 
 217     case bool:
 218         writeSpaces(w, 2*pre)
 219         if t {
 220             w.WriteString(`true`)
 221         } else {
 222             w.WriteString(`false`)
 223         }
 224         return nil
 225 
 226     case json.Number:
 227         writeSpaces(w, 2*pre)
 228         w.WriteString(t.String())
 229         return nil
 230 
 231     case string:
 232         return handleString(w, t, pre)
 233 
 234     default:
 235         // return fmt.Errorf(`unsupported token type %T`, t)
 236         return errors.New(`invalid JSON token`)
 237     }
 238 }
 239 
 240 // handleArray handles arrays for func handleToken
 241 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 242     for i := 0; true; i++ {
 243         t, err := dec.Token()
 244         if err == io.EOF {
 245             return errors.New(`end of JSON before array was closed`)
 246         }
 247         if err != nil {
 248             return err
 249         }
 250 
 251         if t == json.Delim(']') {
 252             if i == 0 {
 253                 writeSpaces(w, 2*pre)
 254                 w.WriteByte('[')
 255                 w.WriteByte(']')
 256             } else {
 257                 w.WriteByte('\n')
 258                 writeSpaces(w, 2*level)
 259                 w.WriteByte(']')
 260             }
 261             return nil
 262         }
 263 
 264         if i == 0 {
 265             writeSpaces(w, 2*pre)
 266             w.WriteByte('[')
 267             w.WriteByte('\n')
 268         } else {
 269             w.WriteByte(',')
 270             w.WriteByte('\n')
 271             if err := w.Flush(); err != nil {
 272                 // a write error may be the consequence of stdout being closed,
 273                 // perhaps by another app along a pipe
 274                 return io.EOF
 275             }
 276         }
 277 
 278         err = handleToken(w, dec, t, level+1, level+1)
 279         if err != nil {
 280             return err
 281         }
 282     }
 283 
 284     // make the compiler happy
 285     return nil
 286 }
 287 
 288 // handleObject handles objects for func handleToken
 289 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 290     for i := 0; true; i++ {
 291         t, err := dec.Token()
 292         if err == io.EOF {
 293             return errors.New(`end of JSON before object was closed`)
 294         }
 295         if err != nil {
 296             return err
 297         }
 298 
 299         if t == json.Delim('}') {
 300             if i == 0 {
 301                 writeSpaces(w, 2*pre)
 302                 w.WriteByte('{')
 303                 w.WriteByte('}')
 304             } else {
 305                 w.WriteByte('\n')
 306                 writeSpaces(w, 2*level)
 307                 w.WriteByte('}')
 308             }
 309             return nil
 310         }
 311 
 312         if i == 0 {
 313             writeSpaces(w, 2*pre)
 314             w.WriteByte('{')
 315             w.WriteByte('\n')
 316         } else {
 317             w.WriteByte(',')
 318             w.WriteByte('\n')
 319         }
 320 
 321         k, ok := t.(string)
 322         if !ok {
 323             return errors.New(`expected a string for a key-value pair`)
 324         }
 325 
 326         err = handleString(w, k, level+1)
 327         if err != nil {
 328             return err
 329         }
 330 
 331         w.WriteString(": ")
 332 
 333         t, err = dec.Token()
 334         if err == io.EOF {
 335             return errors.New(`expected a value for a key-value pair`)
 336         }
 337 
 338         err = handleToken(w, dec, t, 0, level+1)
 339         if err != nil {
 340             return err
 341         }
 342     }
 343 
 344     // make the compiler happy
 345     return nil
 346 }
 347 
 348 // handleString handles strings for func handleToken, and keys for func
 349 // handleObject
 350 func handleString(w *bufio.Writer, s string, level int) error {
 351     writeSpaces(w, 2*level)
 352     w.WriteByte('"')
 353     for i := range s {
 354         w.Write(escapedStringBytes[s[i]])
 355     }
 356     w.WriteByte('"')
 357     return nil
 358 }
     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     }
  80 }
  81 
  82 func run(w io.Writer, args []string, liveLines bool) error {
  83     dashes := 0
  84     for _, path := range args {
  85         if path == `-` {
  86             dashes++
  87         }
  88         if dashes > 1 {
  89             return errors.New(`can't use stdin (dash) more than once`)
  90         }
  91     }
  92 
  93     bw := bufio.NewWriter(w)
  94     defer bw.Flush()
  95 
  96     if len(args) == 0 {
  97         return handleInput(bw, `-`, liveLines)
  98     }
  99 
 100     for _, path := range args {
 101         if err := handleInput(bw, path, liveLines); err != nil {
 102             return err
 103         }
 104     }
 105     return nil
 106 }
 107 
 108 // handleInput simplifies control-flow for func main
 109 func handleInput(w *bufio.Writer, path string, liveLines bool) error {
 110     if path == `-` {
 111         return jsonl(w, os.Stdin, liveLines)
 112     }
 113 
 114     f, err := os.Open(path)
 115     if err != nil {
 116         // on windows, file-not-found error messages may mention `CreateFile`,
 117         // even when trying to open files in read-only mode
 118         return errors.New(`can't open file named ` + path)
 119     }
 120     defer f.Close()
 121     return jsonl(w, f, liveLines)
 122 }
 123 
 124 // escapedStringBytes helps func handleString treat all string bytes quickly
 125 // and correctly, using their officially-supported JSON escape sequences
 126 //
 127 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 128 var escapedStringBytes = [256][]byte{
 129     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 130     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 131     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 132     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 133     {'\\', 'b'}, {'\\', 't'},
 134     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 135     {'\\', 'f'}, {'\\', 'r'},
 136     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 137     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 138     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 139     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 140     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 141     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 142     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 143     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 144     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 145     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 146     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 147     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 148     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 149     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 150     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 151     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 152     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 153     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 154     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 155     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 156     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 157     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 158     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 159     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 160     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 161     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 162     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 163     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 164     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 165     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 166     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 167     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 168     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 169     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 170     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 171     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 172     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 173 }
 174 
 175 // jsonl does it all, given a reader and a writer
 176 func jsonl(w *bufio.Writer, r io.Reader, live bool) error {
 177     dec := json.NewDecoder(r)
 178     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 179     // even if JSON parsers aren't required to guarantee such input-fidelity
 180     // for numbers
 181     dec.UseNumber()
 182 
 183     t, err := dec.Token()
 184     if err == io.EOF {
 185         // return errors.New(`input has no JSON values`)
 186         return nil
 187     }
 188 
 189     if t == json.Delim('[') {
 190         if err := handleTopLevelArray(w, dec, live); err != nil {
 191             return err
 192         }
 193     } else {
 194         if err := handleToken(w, dec, t); err != nil {
 195             return err
 196         }
 197         w.WriteByte('\n')
 198     }
 199 
 200     _, err = dec.Token()
 201     if err == io.EOF {
 202         // input is over, so it's a success
 203         return nil
 204     }
 205 
 206     if err == nil {
 207         // a successful `read` is a failure, as it means there are
 208         // trailing JSON tokens
 209         return errors.New(`unexpected trailing data`)
 210     }
 211 
 212     // any other error, perhaps some invalid-JSON-syntax-type error
 213     return err
 214 }
 215 
 216 // handleToken handles recursion for func json2
 217 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token) error {
 218     switch t := t.(type) {
 219     case json.Delim:
 220         switch t {
 221         case json.Delim('['):
 222             return handleArray(w, dec)
 223         case json.Delim('{'):
 224             return handleObject(w, dec)
 225         default:
 226             return errors.New(`unsupported JSON syntax ` + string(t))
 227         }
 228 
 229     case nil:
 230         w.WriteString(`null`)
 231         return nil
 232 
 233     case bool:
 234         if t {
 235             w.WriteString(`true`)
 236         } else {
 237             w.WriteString(`false`)
 238         }
 239         return nil
 240 
 241     case json.Number:
 242         w.WriteString(t.String())
 243         return nil
 244 
 245     case string:
 246         return handleString(w, t)
 247 
 248     default:
 249         // return fmt.Errorf(`unsupported token type %T`, t)
 250         return errors.New(`invalid JSON token`)
 251     }
 252 }
 253 
 254 func handleTopLevelArray(w *bufio.Writer, dec *json.Decoder, live bool) error {
 255     for i := 0; true; i++ {
 256         t, err := dec.Token()
 257         if err == io.EOF {
 258             return nil
 259         }
 260 
 261         if err != nil {
 262             return err
 263         }
 264 
 265         if t == json.Delim(']') {
 266             return nil
 267         }
 268 
 269         err = handleToken(w, dec, t)
 270         if err != nil {
 271             return err
 272         }
 273 
 274         if w.WriteByte('\n') != nil {
 275             return io.EOF
 276         }
 277 
 278         if !live {
 279             continue
 280         }
 281 
 282         if w.Flush() != nil {
 283             return io.EOF
 284         }
 285     }
 286 
 287     // make the compiler happy
 288     return nil
 289 }
 290 
 291 // handleArray handles arrays for func handleToken
 292 func handleArray(w *bufio.Writer, dec *json.Decoder) error {
 293     w.WriteByte('[')
 294 
 295     for i := 0; true; i++ {
 296         t, err := dec.Token()
 297         if err == io.EOF {
 298             return errors.New(`end of JSON before array was closed`)
 299         }
 300         if err != nil {
 301             return err
 302         }
 303 
 304         if t == json.Delim(']') {
 305             w.WriteByte(']')
 306             return nil
 307         }
 308 
 309         if i > 0 {
 310             _, err := w.WriteString(", ")
 311             if err != nil {
 312                 return io.EOF
 313             }
 314         }
 315 
 316         err = handleToken(w, dec, t)
 317         if err != nil {
 318             return err
 319         }
 320     }
 321 
 322     // make the compiler happy
 323     return nil
 324 }
 325 
 326 // handleObject handles objects for func handleToken
 327 func handleObject(w *bufio.Writer, dec *json.Decoder) error {
 328     w.WriteByte('{')
 329 
 330     for i := 0; true; i++ {
 331         t, err := dec.Token()
 332         if err == io.EOF {
 333             return errors.New(`end of JSON before object was closed`)
 334         }
 335         if err != nil {
 336             return err
 337         }
 338 
 339         if t == json.Delim('}') {
 340             w.WriteByte('}')
 341             return nil
 342         }
 343 
 344         if i > 0 {
 345             _, err := w.WriteString(", ")
 346             if err != nil {
 347                 return io.EOF
 348             }
 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         err = handleString(w, k)
 357         if err != nil {
 358             return err
 359         }
 360 
 361         w.WriteString(": ")
 362 
 363         t, err = dec.Token()
 364         if err == io.EOF {
 365             return errors.New(`expected a value for a key-value pair`)
 366         }
 367 
 368         err = handleToken(w, dec, t)
 369         if err != nil {
 370             return err
 371         }
 372     }
 373 
 374     // make the compiler happy
 375     return nil
 376 }
 377 
 378 // handleString handles strings for func handleToken, and keys for func
 379 // handleObject
 380 func handleString(w *bufio.Writer, s string) error {
 381     w.WriteByte('"')
 382     for i := range s {
 383         w.Write(escapedStringBytes[s[i]])
 384     }
 385     w.WriteByte('"')
 386     return nil
 387 }
     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     }
  61 }
  62 
  63 type runConfig struct {
  64     lines int
  65     keys  []string
  66 }
  67 
  68 func run(paths []string) error {
  69     bw := bufio.NewWriter(os.Stdout)
  70     defer bw.Flush()
  71 
  72     dashes := 0
  73     var cfg runConfig
  74 
  75     for _, path := range paths {
  76         if path == `-` {
  77             dashes++
  78             if dashes > 1 {
  79                 continue
  80             }
  81 
  82             if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  83                 return err
  84             }
  85 
  86             continue
  87         }
  88 
  89         if err := handleFile(bw, path, &cfg); err != nil {
  90             return err
  91         }
  92     }
  93 
  94     if len(paths) == 0 {
  95         if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  96             return err
  97         }
  98     }
  99 
 100     if cfg.lines > 1 {
 101         bw.WriteString("\n]\n")
 102     } else {
 103         bw.WriteString("[]\n")
 104     }
 105     return nil
 106 }
 107 
 108 func handleFile(w *bufio.Writer, path string, cfg *runConfig) error {
 109     f, err := os.Open(path)
 110     if err != nil {
 111         return err
 112     }
 113     defer f.Close()
 114     return handleInput(w, f, cfg)
 115 }
 116 
 117 func escapeKeys(line string) []string {
 118     var keys []string
 119     var sb strings.Builder
 120 
 121     loopTSV(line, func(i int, s string) {
 122         sb.WriteByte('"')
 123         for _, r := range s {
 124             if r == '\\' || r == '"' {
 125                 sb.WriteByte('\\')
 126             }
 127             sb.WriteRune(r)
 128         }
 129         sb.WriteByte('"')
 130 
 131         keys = append(keys, sb.String())
 132         sb.Reset()
 133     })
 134 
 135     return keys
 136 }
 137 
 138 func emitRow(w *bufio.Writer, line string, keys []string) {
 139     j := 0
 140     w.WriteByte('{')
 141 
 142     loopTSV(line, func(i int, s string) {
 143         j = i
 144         if i > 0 {
 145             w.WriteString(", ")
 146         }
 147 
 148         w.WriteString(keys[i])
 149         w.WriteString(": \"")
 150 
 151         for _, r := range s {
 152             if r == '\\' || r == '"' {
 153                 w.WriteByte('\\')
 154             }
 155             w.WriteRune(r)
 156         }
 157         w.WriteByte('"')
 158     })
 159 
 160     for i := j + 1; i < len(keys); i++ {
 161         if i > 0 {
 162             w.WriteString(", ")
 163         }
 164         w.WriteString(keys[i])
 165         w.WriteString(": null")
 166     }
 167     w.WriteByte('}')
 168 }
 169 
 170 func loopTSV(line string, f func(i int, s string)) {
 171     for i := 0; len(line) > 0; i++ {
 172         pos := strings.IndexByte(line, '\t')
 173         if pos < 0 {
 174             f(i, line)
 175             return
 176         }
 177 
 178         f(i, line[:pos])
 179         line = line[pos+1:]
 180     }
 181 }
 182 
 183 func handleInput(w *bufio.Writer, r io.Reader, cfg *runConfig) 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.Text()
 190         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 191             s = s[3:]
 192         }
 193 
 194         if cfg.lines == 0 {
 195             cfg.keys = escapeKeys(s)
 196             w.WriteByte('[')
 197             cfg.lines++
 198             continue
 199         }
 200 
 201         if cfg.lines == 1 {
 202             w.WriteString("\n  ")
 203         } else {
 204             if _, err := w.WriteString(",\n  "); err != nil {
 205                 return io.EOF
 206             }
 207         }
 208 
 209         emitRow(w, s, cfg.keys)
 210         cfg.lines++
 211     }
 212 
 213     return sc.Err()
 214 }
     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     "bufio"
  40     "fmt"
  41     "io"
  42     "os"
  43     "path"
  44     "sort"
  45     "strings"
  46     "unicode/utf8"
  47 
  48     "./avoid"
  49     "./bytedump"
  50     "./calc"
  51     "./catl"
  52     "./coby"
  53     "./countdown"
  54     "./datauri"
  55     "./debase64"
  56     "./decsv"
  57     "./dedup"
  58     "./dejsonl"
  59     "./dessv"
  60     "./ecoli"
  61     "./erase"
  62     "./fh"
  63     "./files"
  64     "./filesizes"
  65     "./finfo"
  66     "./fixlines"
  67     "./folders"
  68     "./gsub"
  69     "./hima"
  70     "./htmlify"
  71     "./id3pic"
  72     "./json0"
  73     "./json2"
  74     "./jsonl"
  75     "./jsons"
  76     "./match"
  77     "./n"
  78     "./ncol"
  79     "./ngron"
  80     "./nhex"
  81     "./njson"
  82     "./nn"
  83     "./now"
  84     "./plain"
  85     "./primes"
  86     "./realign"
  87     "./squeeze"
  88     "./tcatl"
  89     "./teletype"
  90     "./units"
  91     "./utfate"
  92     "./verdict"
  93     "./waveout"
  94     "./zj"
  95 
  96     "./remakes/cat"
  97     "./remakes/head"
  98     "./remakes/ls"
  99 
 100     _ "embed"
 101 )
 102 
 103 //go:embed info.txt
 104 var info string
 105 
 106 // mains has some entries starting as nil to avoid circular-dependency errors
 107 var mains = map[string]func(){
 108     `args`:      args,
 109     `avoid`:     avoid.Main,
 110     `bytedump`:  bytedump.Main,
 111     `calc`:      calc.Main,
 112     `catl`:      catl.Main,
 113     `coby`:      coby.Main,
 114     `countdown`: countdown.Main,
 115     `datauri`:   datauri.Main,
 116     `debase64`:  debase64.Main,
 117     `decsv`:     decsv.Main,
 118     `dedup`:     dedup.Main,
 119     `dejsonl`:   dejsonl.Main,
 120     `dessv`:     dessv.Main,
 121     `ecoli`:     ecoli.Main,
 122     `erase`:     erase.Main,
 123     `files`:     files.Main,
 124     `filesizes`: filesizes.Main,
 125     `finfo`:     finfo.Main,
 126     `fixlines`:  fixlines.Main,
 127     `folders`:   folders.Main,
 128     `gsub`:      gsub.Main,
 129     `help`:      nil,
 130     `hima`:      hima.Main,
 131     `htmlify`:   htmlify.Main,
 132     `id3pic`:    id3pic.Main,
 133     `json0`:     json0.Main,
 134     `json2`:     json2.Main,
 135     `jsonl`:     jsonl.Main,
 136     `jsons`:     jsons.Main,
 137     `match`:     match.Main,
 138     `n`:         n.Main,
 139     `ncol`:      ncol.Main,
 140     `ngron`:     ngron.Main,
 141     `nhex`:      nhex.Main,
 142     `njson`:     njson.Main,
 143     `nn`:        nn.Main,
 144     `now`:       now.Main,
 145     `plain`:     plain.Main,
 146     `primes`:    primes.Main,
 147     `realign`:   realign.Main,
 148     `squeeze`:   squeeze.Main,
 149     `tcatl`:     tcatl.Main,
 150     `teletype`:  teletype.Main,
 151     `timezones`: timezones,
 152     `tools`:     nil,
 153     `units`:     units.Main,
 154     `utfate`:    utfate.Main,
 155     `waveout`:   waveout.Main,
 156     `zj`:        zj.Main,
 157 }
 158 
 159 var experimental = map[string]func(){
 160     `fh`:      fh.Main,
 161     `verdict`: verdict.Main,
 162 }
 163 
 164 var extras = map[string]func(){
 165     `help`:  help,
 166     `tools`: tools,
 167 }
 168 
 169 var remakes = map[string]func(){
 170     `cat`:  cat.Main,
 171     `head`: head.Main,
 172     `ls`:   ls.Main,
 173 }
 174 
 175 var aliases = map[string]string{
 176     `bytedump`:    `bytedump`,
 177     `ca`:          `calc`,
 178     `calculate`:   `calc`,
 179     `calculator`:  `calc`,
 180     `fc`:          `calc`,
 181     `frac`:        `calc`,
 182     `fraca`:       `calc`,
 183     `fracalc`:     `calc`,
 184     `datauri`:     `datauri`,
 185     `unbase64`:    `debase64`,
 186     `uncsv`:       `decsv`,
 187     `deduplicate`: `dedup`,
 188     `undup`:       `dedup`,
 189     `unique`:      `dedup`,
 190     `unjsonl`:     `dejsonl`,
 191     `unssv`:       `dessv`,
 192     `fileinfo`:    `finfo`,
 193     `detrail`:     `fixlines`,
 194     `id3pic`:      `id3pic`,
 195     `mp3pic`:      `id3pic`,
 196     `ncols`:       `ncol`,
 197     `nicecols`:    `ncol`,
 198     `nh`:          `nhex`,
 199     `nj`:          `njson`,
 200     `nicedigits`:  `nn`,
 201     `nicenums`:    `nn`,
 202     `j0`:          `json0`,
 203     `j2`:          `json2`,
 204     `jl`:          `jsonl`,
 205     `detsv`:       `jsons`,
 206     `tty`:         `teletype`,
 207     `timezone`:    `timezones`,
 208     `utf8`:        `utfate`,
 209     `zoomjson`:    `zj`,
 210 }
 211 
 212 var blurbs = map[string]string{
 213     `args`:      `show all ARGumentS given after the tool name, one per line`,
 214     `avoid`:     `ignore lines matching any of the regexes given`,
 215     `bytedump`:  `show bytes as hex values, with a wide ASCII panel`,
 216     `calc`:      `fractional calculator, with floating-point powers`,
 217     `catl`:      `conCATenate Lines, ensures text ends with a line-feed`,
 218     `coby`:      `COunt BYtes, and many other byte/text-related stats`,
 219     `countdown`: `countdown the seconds/minutes/hours given`,
 220     `datauri`:   `turn bytes into data-URIs, auto-detecting MIME types`,
 221     `debase64`:  `decode base64 text and data-URIs`,
 222     `decsv`:     `convert CSV tables into TSV tables, or into JSON`,
 223     `dedup`:     `deduplicate lines, emitting each unique line only once`,
 224     `dejsonl`:   `turn JSON Lines into proper JSON`,
 225     `dessv`:     `turn tables of space-separated values into TSV tables`,
 226     `ecoli`:     `expressions coloring lines color-codes matching lines`,
 227     `erase`:     `ignore/erase all matching regexes away from each line`,
 228     `fh`:        `Function Heatmapper draws 2-input functions`,
 229     `files`:     `list all files in the folder(s) given`,
 230     `filesizes`: `show sizes of files and block-counts (4K by default)`,
 231     `finfo`:     `show various file info, for plain-text and/or media files`,
 232     `fixlines`:  `ignore carriage-returns, or even trailing spaces`,
 233     `folders`:   `list all folders in the folder(s) given`,
 234     `gsub`:      `Globally SUBstitute all regular expression matches`,
 235     `help`:      `show the help message for "easybox"`,
 236     `hima`:      `HIlight MAtches using the regexes given`,
 237     `htmlify`:   `turn plain-text lines into HTML documents`,
 238     `id3pic`:    `get the encoded picture out of audio files, if present`,
 239     `json0`:     `minimize/fix JSON into the smallest-possible size`,
 240     `json2`:     `indent JSON into multiple lines, using 2 spaces per level`,
 241     `jsonl`:     `turn items from top-level JSON arrays into JSON Lines`,
 242     `jsons`:     `JSON Strings turns TSV into arrays of objects of strings`,
 243     `match`:     `only keep lines matching any of the regexes given`,
 244     `n`:         `Number lines, putting tabs between numbers and contents`,
 245     `ncol`:      `Nice COLumns realigns tables, color-coding their values`,
 246     `ngron`:     `Nice GRON mimics a subset of "gron", using better colors`,
 247     `nhex`:      `Nice HEXadecimal shows bytes as hex values and ASCII`,
 248     `njson`:     `Nice JSON indents and color-codes JSON data`,
 249     `nn`:        `Nice Numbers color-codes groups of digits for legibility`,
 250     `now`:       `show the current date and time, also for other timezones`,
 251     `plain`:     `ignore all ANSI-sequences, leaving unstyled text`,
 252     `primes`:    `find prime numbers, up to the first million by default`,
 253     `realign`:   `realign items from the SSV/TSV tables given`,
 254     `squeeze`:   `aggressively ignore spaces, especially runs of spaces`,
 255     `tcatl`:     `Titled conCATenate Lines, is like "catl" but with names`,
 256     `teletype`:  `mimic old-fashioned teletype devices, by delaying output`,
 257     `timezones`: `lookup full timezone names from the city/place names given`,
 258     `tools`:     `list all tools available`,
 259     `units`:     `convert weird units into the international standard ones`,
 260     `utfate`:    `decode all other types of UTF text into UTF-8`,
 261     `verdict`:   `run the command given, showing its success/failure code`,
 262     `waveout`:   `emit/calculate WAV-format sounds by formula`,
 263     `zj`:        `Zoom Json, using the keys/indices given as arguments`,
 264 }
 265 
 266 func main() {
 267     // add the deliberately-missing lookup entries
 268     for k, v := range extras {
 269         mains[k] = v
 270     }
 271 
 272     // try to use the app's `name`, in case it's being called from a file-link
 273     // named after one of the tools
 274     if tool, ok := lookupTool(path.Base(os.Args[0])); ok {
 275         tool()
 276         return
 277     }
 278 
 279     // try normal tool-lookup using the first command-line argument
 280     if len(os.Args) >= 2 {
 281         name := os.Args[1]
 282 
 283         if tool, ok := lookupTool(name); ok {
 284             os.Args = os.Args[1:]
 285             tool()
 286             return
 287         }
 288 
 289         switch name {
 290         case `-h`, `--h`, `-help`, `--help`, `help`:
 291             showHelp(os.Stdout)
 292             return
 293 
 294         case `-l`, `--l`, `-list`, `--list`:
 295             tools()
 296             return
 297 
 298         case `-links`, `--links`:
 299             showLinksCommands(os.Stdout)
 300             return
 301 
 302         case `-t`, `--t`, `-tools`, `--tools`, `tools`:
 303             tools()
 304             return
 305         }
 306 
 307         const fs = "easybox: tool/alias named %q not found\n"
 308         fmt.Fprintf(os.Stderr, fs, name)
 309         os.Exit(1)
 310     }
 311 
 312     showHelp(os.Stderr)
 313     fmt.Fprintln(os.Stderr, ``)
 314     fmt.Fprintln(os.Stderr, `easybox: no tool name given`)
 315     os.Exit(1)
 316 }
 317 
 318 // dealias tries to lookup a string to the aliases given, returning the name
 319 // given if the lookup fails
 320 func dealias(aliases map[string]string, name string) string {
 321     if s, ok := aliases[name]; ok {
 322         return s
 323     }
 324     return name
 325 }
 326 
 327 func lookupTool(name string) (tool func(), ok bool) {
 328     name = strings.ReplaceAll(name, `-`, ``)
 329 
 330     if tool, ok := mains[dealias(aliases, name)]; ok {
 331         return tool, ok
 332     }
 333 
 334     if tool, ok := remakes[name]; ok {
 335         return tool, ok
 336     }
 337 
 338     tool, ok = experimental[name]
 339     return tool, ok
 340 }
 341 
 342 // showHelp has a parameter to write either to stdout or stderr
 343 func showHelp(w io.Writer) {
 344     fmt.Fprintln(w, info)
 345 
 346     fmt.Fprintln(w, "\nTools Available")
 347 
 348     maxlen := 0
 349     names := make([]string, 0, max(len(mains), len(aliases)))
 350     for k := range mains {
 351         names = append(names, k)
 352         maxlen = max(maxlen, utf8.RuneCountInString(k))
 353     }
 354 
 355     sort.Strings(names)
 356 
 357     for _, s := range names {
 358         fmt.Fprintf(w, "  - %-*s  %s\n", maxlen, s, blurbs[s])
 359     }
 360 
 361     fmt.Fprintln(w, "\nAliases Available")
 362 
 363     maxlen = 0
 364     names = names[:0]
 365     for k := range aliases {
 366         names = append(names, k)
 367         maxlen = max(maxlen, utf8.RuneCountInString(k))
 368     }
 369 
 370     sort.Strings(names)
 371 
 372     for _, k := range names {
 373         fmt.Fprintf(w, "  - %-*s -> %s\n", maxlen, k, aliases[k])
 374     }
 375 
 376     names = names[:0]
 377     for k := range experimental {
 378         names = append(names, k)
 379     }
 380 
 381     if len(names) == 0 {
 382         return
 383     }
 384 
 385     fmt.Fprintln(w, "\nExperimental Tools Available")
 386 
 387     for _, k := range names {
 388         fmt.Fprintf(w, "  - %s\n", k)
 389     }
 390 }
 391 
 392 // showLinksCommands has a parameter to write either to stdout or stderr
 393 func showLinksCommands(w io.Writer) {
 394     names := make([]string, 0, len(mains)-len(extras))
 395     for k := range mains {
 396         if _, ok := extras[k]; ok {
 397             continue
 398         }
 399         names = append(names, k)
 400     }
 401 
 402     sort.Strings(names)
 403 
 404     for _, s := range names {
 405         fmt.Fprintf(w, "ln -s \"$(which easybox)\" ./%s\n", s)
 406     }
 407 }
 408 
 409 func args() {
 410     args := os.Args[1:]
 411     if len(args) == 0 {
 412         return
 413     }
 414 
 415     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 416     defer w.Flush()
 417 
 418     for _, s := range args {
 419         w.WriteString(s)
 420         if err := w.WriteByte('\n'); err != nil {
 421             break
 422         }
 423     }
 424 }
 425 
 426 func help() {
 427     showHelp(os.Stdout)
 428 }
 429 
 430 func timezones() {
 431     args := os.Args[1:]
 432 
 433     if len(args) > 0 {
 434         switch args[0] {
 435         case `-h`, `--h`, `-help`, `--help`, `help`:
 436             os.Stdout.WriteString(blurbs[`timezones`])
 437             os.Stdout.WriteString("\n")
 438             return
 439         }
 440     }
 441 
 442     if len(args) > 0 && args[0] == `--` {
 443         args = args[1:]
 444     }
 445 
 446     if len(args) == 0 {
 447         os.Stderr.WriteString(blurbs[`timezones`])
 448         os.Stderr.WriteString("\n")
 449         return
 450     }
 451 
 452     nerr := 0
 453     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 454     defer w.Flush()
 455 
 456     for _, place := range args {
 457         name, ok := now.LookupName(place)
 458 
 459         if !ok {
 460             w.Flush()
 461             fmt.Fprintf(os.Stderr, "timezone for %q not found\n", place)
 462             nerr++
 463             continue
 464         }
 465 
 466         w.WriteString(name)
 467         w.WriteByte('\n')
 468     }
 469 
 470     if nerr > 0 {
 471         w.Flush()
 472         os.Exit(1)
 473     }
 474 }
 475 
 476 func tools() {
 477     names := make([]string, 0, len(mains))
 478     for k := range mains {
 479         names = append(names, k)
 480     }
 481 
 482     sort.Strings(names)
 483 
 484     for _, s := range names {
 485         fmt.Fprintln(os.Stdout, s)
 486     }
 487 }
     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 
  35         t.Errorf("alias %q leads nowhere", alias)
  36     }
  37 }
  38 
  39 func TestBlurbs(t *testing.T) {
  40     for name := range mains {
  41         if blurbs[name] != `` {
  42             continue
  43         }
  44         t.Errorf("no description/blurb for tool %q", name)
  45     }
  46 
  47     for name := range blurbs {
  48         if _, ok := mains[name]; ok {
  49             continue
  50         }
  51         if _, ok := experimental[name]; ok {
  52             continue
  53         }
  54         t.Errorf("description/blurb for name %q, but no tool", name)
  55     }
  56 }
  57 
  58 func TestFillers(t *testing.T) {
  59     for name, v := range mains {
  60         if v != nil {
  61             continue
  62         }
  63 
  64         if _, ok := extras[name]; ok {
  65             continue
  66         }
  67 
  68         t.Errorf("tool %q has no filler for invalid entry", name)
  69     }
  70 
  71     for name := range extras {
  72         if v, ok := mains[name]; !ok || v != nil {
  73             t.Errorf("filling for missing entry %q", name)
  74         }
  75     }
  76 }
     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     }
 138 
 139     var buf []byte
 140     sc := bufio.NewScanner(os.Stdin)
 141     sc.Buffer(nil, 8*1024*1024*1024)
 142     bw := bufio.NewWriter(os.Stdout)
 143 
 144     for i := 0; sc.Scan(); i++ {
 145         line := sc.Bytes()
 146         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 147             line = line[3:]
 148         }
 149 
 150         s := line
 151         if bytes.IndexByte(s, '\x1b') >= 0 {
 152             buf = plain(buf[:0], s)
 153             s = buf
 154         }
 155 
 156         if match(s, exprs) {
 157             if avoid {
 158                 continue
 159             }
 160 
 161             if err := emit(bw, line, liveLines); err != nil {
 162                 return
 163             }
 164         }
 165 
 166         if avoid {
 167             if err := emit(bw, line, liveLines); err != nil {
 168                 return
 169             }
 170         }
 171     }
 172 }
 173 
 174 func emit(w *bufio.Writer, line []byte, live bool) error {
 175     w.Write(line)
 176     w.WriteByte('\n')
 177 
 178     if !live {
 179         return nil
 180     }
 181 
 182     return w.Flush()
 183 }
 184 
 185 func match(what []byte, with []*regexp.Regexp) bool {
 186     for _, e := range with {
 187         if e.Match(what) {
 188             return true
 189         }
 190     }
 191     return false
 192 }
 193 
 194 func plain(dst []byte, src []byte) []byte {
 195     for len(src) > 0 {
 196         i, j := indexEscapeSequence(src)
 197         if i < 0 {
 198             dst = append(dst, src...)
 199             break
 200         }
 201         if j < 0 {
 202             j = len(src)
 203         }
 204 
 205         if i > 0 {
 206             dst = append(dst, src[:i]...)
 207         }
 208 
 209         src = src[j:]
 210     }
 211 
 212     return dst
 213 }
 214 
 215 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 216 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 217 // indices which can be independently negative when either the start/end of
 218 // a sequence isn't found; given their fairly-common use, even the hyperlink
 219 // ESC]8 sequences are supported
 220 func indexEscapeSequence(s []byte) (int, int) {
 221     var prev byte
 222 
 223     for i, b := range s {
 224         if prev == '\x1b' && b == '[' {
 225             j := indexLetter(s[i+1:])
 226             if j < 0 {
 227                 return i, -1
 228             }
 229             return i - 1, i + 1 + j + 1
 230         }
 231 
 232         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 233             j := indexPair(s[i+1:], '\x1b', '\\')
 234             if j < 0 {
 235                 return i, -1
 236             }
 237             return i - 1, i + 1 + j + 2
 238         }
 239 
 240         prev = b
 241     }
 242 
 243     return -1, -1
 244 }
 245 
 246 func indexLetter(s []byte) int {
 247     for i, b := range s {
 248         upper := b &^ 32
 249         if 'A' <= upper && upper <= 'Z' {
 250             return i
 251         }
 252     }
 253 
 254     return -1
 255 }
 256 
 257 func indexPair(s []byte, x byte, y byte) int {
 258     var prev byte
 259 
 260     for i, b := range s {
 261         if prev == x && b == y && i > 0 {
 262             return i
 263         }
 264         prev = b
 265     }
 266 
 267     return -1
 268 }
     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     var 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     var 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     var 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         {123.532123143, [6]float64{123.5, 123.53, 123.532, 123.5321, 123.53212, 123.532123}},
  84         {1932.532123143, [6]float64{1932.5, 1932.53, 1932.532, 1932.5321, 1932.53212, 1932.532123}},
  85         {2023.4, [6]float64{2023.4, 2023.4, 2023.4, 2023.4, 2023.4, 2023.4}},
  86     }
  87 
  88     for _, tc := range roundingTests {
  89         x := tc.Number
  90         y := []float64{
  91             Round1(x), Round2(x), Round3(x), Round4(x), Round5(x), Round6(x),
  92         }
  93 
  94         for i, f := range y {
  95             exp := tc.Expected[i]
  96             if math.Abs(exp-f) > 1e-12 {
  97                 const fs = `r%d(%f): expected %f, got %f`
  98                 t.Fatalf(fs, i+1, tc.Number, exp, f)
  99             }
 100         }
 101     }
 102 }
 103 
 104 var scaleTests = []struct {
 105     Input    float64
 106     InMin    float64
 107     InMax    float64
 108     OutMin   float64
 109     OutMax   float64
 110     Expected float64
 111 }{
 112     {-2, -5, 4, 0, 1, 1.0 / 3},
 113     {0.1, 0, 0.5, -3, 5, -1.4},
 114 }
 115 
 116 func TestScale(t *testing.T) {
 117     for _, tc := range scaleTests {
 118         in := tc.Input
 119         exp := tc.Expected
 120         got := Scale(in, tc.InMin, tc.InMax, tc.OutMin, tc.OutMax)
 121         if got != exp {
 122             const fs = `Scale(%f, %f, %f, %f, %f): expected %f, got %f`
 123             t.Fatalf(fs, in, tc.InMin, tc.InMax, tc.OutMin, tc.OutMax, exp, got)
 124         }
 125     }
 126 }
 127 
 128 func TestIsPrime(t *testing.T) {
 129     var tests = []struct {
 130         Input    int64
 131         Expected bool
 132     }{
 133         {-3, false},
 134         {0, false},
 135         {1, false},
 136         {4, false},
 137         {9, false},
 138         {21, false},
 139 
 140         {2, true},
 141         {3, true},
 142         {5, true},
 143         {19, true},
 144         // 15,485,863 is the millionth prime
 145         {15_485_863, true},
 146     }
 147 
 148     for _, tc := range tests {
 149         if v := IsPrime(tc.Input); v != tc.Expected {
 150             const fs = `isprime(%d) wrongly returned %v`
 151             t.Fatalf(fs, tc.Input, v)
 152         }
 153     }
 154 }
 155 
 156 func TestHorner(t *testing.T) {
 157     var tests = []struct {
 158         X        float64
 159         C        []float64
 160         Expected float64
 161     }{
 162         {2, []float64{1, 2, 3}, 11},
 163         {3, []float64{3, 5, -1}, 41},
 164     }
 165 
 166     for _, tc := range tests {
 167         got := Polyval(tc.X, tc.C...)
 168         if got != tc.Expected {
 169             const fs = `horner(%f, %#v) gave %f, instead of %f`
 170             t.Fatalf(fs, tc.X, tc.C, got, tc.Expected)
 171             return
 172         }
 173     }
 174 }
 175 
 176 func TestGCD(t *testing.T) {
 177     var tests = []struct {
 178         X        int64
 179         Y        int64
 180         Expected int64
 181     }{
 182         {0, 0, 0},
 183         {-1, 10, 0},
 184         {1, -10, 0},
 185         {1, 1, 1},
 186         {1, 7, 1},
 187         {3 * 12, 12, 12},
 188         {1280, 1920, 640},
 189     }
 190 
 191     for _, tc := range tests {
 192         got := GCD(tc.X, tc.Y)
 193         if got != tc.Expected {
 194             const fs = `gcd(%d, %d) gave %d, instead of %d`
 195             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 196             return
 197         }
 198     }
 199 }
 200 
 201 func TestPerm(t *testing.T) {
 202     var tests = []struct {
 203         X        int
 204         Y        int
 205         Expected int64
 206     }{
 207         {10, 4, 5_040},
 208         {5, 0, 1},
 209         {5, 5, 120},
 210     }
 211 
 212     for _, tc := range tests {
 213         got := Perm(tc.X, tc.Y)
 214         if got != tc.Expected {
 215             const fs = `perm(%d, %d) gave %d, instead of %d`
 216             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 217             return
 218         }
 219     }
 220 }
 221 
 222 func TestChoose(t *testing.T) {
 223     var tests = []struct {
 224         X        int
 225         Y        int
 226         Expected int64
 227     }{
 228         {10, 4, 210},
 229         {10, 0, 1},
 230         {10, 10, 1},
 231     }
 232 
 233     for _, tc := range tests {
 234         got := Choose(tc.X, tc.Y)
 235         if got != tc.Expected {
 236             const fs = `comb(%d, %d) gave %d, instead of %d`
 237             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 238             return
 239         }
 240     }
 241 }
     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     var tests = []struct {
  31         Input    int64
  32         Expected int
  33     }{
  34         {0, 1},
  35         {-32, 2},
  36         {999, 3},
  37         {3_490, 4},
  38         {12_332, 5},
  39         {999_999, 6},
  40         {1_000_000, 7},
  41         {1_000_001, 7},
  42         {12_345_678, 8},
  43     }
  44 
  45     for _, tc := range tests {
  46         if n := CountIntegerDigits(tc.Input); n != tc.Expected {
  47             const fs = `integer digits in %d: got %d instead of %d`
  48             t.Errorf(fs, tc.Input, n, tc.Expected)
  49         }
  50     }
  51 }
  52 
  53 func TestLoopThousandsGroups(t *testing.T) {
  54     var tests = []struct {
  55         Input    int64
  56         Expected []int
  57     }{
  58         {0, []int{0}},
  59         {-32, []int{-32}}, // negatives not supported yet
  60         {999, []int{999}},
  61         {1_670, []int{1, 670}},
  62         {3_490, []int{3, 490}},
  63         {12_332, []int{12, 332}},
  64         {999_999, []int{999, 999}},
  65         {1_000_000, []int{1, 0, 0}},
  66         {1_000_001, []int{1, 0, 1}},
  67         {1_234_567, []int{1, 234, 567}},
  68     }
  69 
  70     // return
  71     for _, tc := range tests {
  72         count := 0
  73         LoopThousandsGroups(tc.Input, func(i, n int) {
  74             // t.Log(tc.Input, i, n)
  75             if n != tc.Expected[i] {
  76                 const fs = `group %d in %d: got %d instead of %d`
  77                 t.Errorf(fs, i, tc.Input, n, tc.Expected[i])
  78             }
  79             count++
  80         })
  81 
  82         if count != len(tc.Expected) {
  83             const fs = `thousands-groups from %d: got %d instead of %d`
  84             t.Errorf(fs, tc.Input, count, len(tc.Expected))
  85         }
  86     }
  87 }
  88 
  89 func TestLog2Int(t *testing.T) {
  90     var tests = []struct {
  91         Value    int64
  92         Expected int
  93     }{
  94         {-3, -1},
  95         {1, 0},
  96         {2, 1},
  97         {3, 1},
  98         {4, 2},
  99         {1024, 10},
 100         {1_025, 10},
 101         {2*1024 - 1, 10},
 102     }
 103 
 104     for _, tc := range tests {
 105         got, ok := Log2Int(tc.Value)
 106         if got != tc.Expected || (ok && tc.Value < 1) {
 107             const fs = `log2int(%d) = %d, but got %d instead`
 108             t.Fatalf(fs, tc.Value, tc.Expected, got)
 109         }
 110     }
 111 }
 112 
 113 func TestLog10Int(t *testing.T) {
 114     var tests = []struct {
 115         Value    int64
 116         Expected int
 117     }{
 118         {-3, -1},
 119         {1, 0},
 120         {10, 1},
 121         {100, 2},
 122         {101, 2},
 123         {199, 2},
 124         {1_000_000, 6},
 125     }
 126 
 127     for _, tc := range tests {
 128         got, ok := Log10Int(tc.Value)
 129         if got != tc.Expected || (ok && tc.Value < 1) {
 130             const fs = `log10int(%d) = %d, but got %d instead`
 131             t.Fatalf(fs, tc.Value, tc.Expected, got)
 132         }
 133     }
 134 }
     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     var 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     var 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     var 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     var 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     var 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     var 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     var 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         if v, err := strconv.ParseInt(args[0], 10, 64); err == nil {
  76             cfg.n = int(v)
  77             args = args[1:]
  78         }
  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(args, &cfg); err != nil && err != io.EOF {
  92         os.Stderr.WriteString(err.Error())
  93         os.Stderr.WriteString("\n")
  94         os.Exit(1)
  95     }
  96 }
  97 
  98 func run(paths []string, cfg *config) error {
  99     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 100     defer bw.Flush()
 101 
 102     for _, p := range paths {
 103         if err := handleFile(bw, p, cfg); err != nil {
 104             return err
 105         }
 106     }
 107 
 108     if len(paths) == 0 {
 109         return handleReader(bw, os.Stdin, cfg)
 110     }
 111 
 112     return nil
 113 }
 114 
 115 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 116     f, err := os.Open(path)
 117     if err != nil {
 118         // on windows, file-not-found error messages may mention `CreateFile`,
 119         // even when trying to open files in read-only mode
 120         return errors.New(`can't open file named ` + path)
 121     }
 122     defer f.Close()
 123     return handleReader(w, f, cfg)
 124 }
 125 
 126 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 127     var buf [24]byte
 128     const gb = 1024 * 1024 * 1024
 129     sc := bufio.NewScanner(r)
 130     sc.Buffer(nil, 8*gb)
 131 
 132     for i := 0; sc.Scan(); i++ {
 133         s := sc.Text()
 134         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 135             s = s[3:]
 136         }
 137 
 138         w.Write(strconv.AppendInt(buf[:0], int64(cfg.n), 10))
 139         w.WriteByte('\t')
 140         w.WriteString(s)
 141         cfg.n++
 142 
 143         if w.WriteByte('\n') != nil {
 144             return io.EOF
 145         }
 146 
 147         if !cfg.liveLines {
 148             continue
 149         }
 150 
 151         if err := w.Flush(); err != nil {
 152             return io.EOF
 153         }
 154     }
 155 
 156     return sc.Err()
 157 }
     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     }
 120 }
 121 
 122 // table has all summary info gathered from the data, along with the row
 123 // themselves, stored as lines/strings
 124 type table struct {
 125     Columns int
 126 
 127     Rows []string
 128 
 129     MaxWidth []int
 130 
 131     MaxDotDecimals []int
 132 
 133     Numeric []int
 134 
 135     Sums []float64
 136 
 137     LoopItems func(line string, items int, t *table, f itemFunc) int
 138 
 139     sb strings.Builder
 140 
 141     MaxColumns bool
 142 
 143     ShowTiles bool
 144 
 145     ShowSums bool
 146 }
 147 
 148 type itemFunc func(i int, s string, t *table)
 149 
 150 func run(paths []string, res *table) error {
 151     for _, p := range paths {
 152         if err := handleFile(res, p); err != nil {
 153             return err
 154         }
 155     }
 156 
 157     if len(paths) == 0 {
 158         if err := handleReader(res, os.Stdin); err != nil {
 159             return err
 160         }
 161     }
 162 
 163     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 164     defer bw.Flush()
 165     realign(bw, res)
 166     return nil
 167 }
 168 
 169 func handleFile(res *table, path string) error {
 170     f, err := os.Open(path)
 171     if err != nil {
 172         // on windows, file-not-found error messages may mention `CreateFile`,
 173         // even when trying to open files in read-only mode
 174         return errors.New(`can't open file named ` + path)
 175     }
 176     defer f.Close()
 177     return handleReader(res, f)
 178 }
 179 
 180 func handleReader(t *table, r io.Reader) error {
 181     const gb = 1024 * 1024 * 1024
 182     sc := bufio.NewScanner(r)
 183     sc.Buffer(nil, 8*gb)
 184 
 185     const maxInt = int(^uint(0) >> 1)
 186     maxCols := maxInt
 187 
 188     for i := 0; sc.Scan(); i++ {
 189         s := sc.Text()
 190         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 191             s = s[3:]
 192         }
 193 
 194         if len(s) == 0 {
 195             continue
 196         }
 197 
 198         t.Rows = append(t.Rows, s)
 199 
 200         if t.Columns == 0 {
 201             if t.LoopItems == nil {
 202                 if strings.IndexByte(s, '\t') >= 0 {
 203                     t.LoopItems = loopItemsTSV
 204                 } else {
 205                     t.LoopItems = loopItemsSSV
 206                 }
 207             }
 208 
 209             if !t.MaxColumns {
 210                 t.Columns = t.LoopItems(s, maxCols, t, doNothing)
 211                 maxCols = t.Columns
 212             }
 213         }
 214 
 215         t.LoopItems(s, maxCols, t, updateItem)
 216     }
 217 
 218     return sc.Err()
 219 }
 220 
 221 // doNothing is given to LoopItems to count items, while doing nothing else
 222 func doNothing(i int, s string, t *table) {}
 223 
 224 func updateItem(i int, s string, t *table) {
 225     // ensure column-info-slices have enough room
 226     if i >= len(t.MaxWidth) {
 227         // update column-count if in max-columns mode
 228         if t.MaxColumns {
 229             t.Columns = i + 1
 230         }
 231         t.MaxWidth = append(t.MaxWidth, 0)
 232         t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
 233         t.Numeric = append(t.Numeric, 0)
 234         t.Sums = append(t.Sums, 0)
 235     }
 236 
 237     // keep track of widest rune-counts for each column
 238     w := countWidth(s)
 239     if t.MaxWidth[i] < w {
 240         t.MaxWidth[i] = w
 241     }
 242 
 243     // update stats for numeric items
 244     if isNumeric(s, &(t.sb)) {
 245         dd := countDotDecimals(s)
 246         if t.MaxDotDecimals[i] < dd {
 247             t.MaxDotDecimals[i] = dd
 248         }
 249 
 250         t.Numeric[i]++
 251         f, _ := strconv.ParseFloat(t.sb.String(), 64)
 252         t.Sums[i] += f
 253     }
 254 }
 255 
 256 // loopItemsSSV loops over a line's items, allocation-free style; when given
 257 // empty strings, the callback func is never called
 258 func loopItemsSSV(s string, max int, t *table, f itemFunc) int {
 259     i := 0
 260     s = trimTrailingSpaces(s)
 261 
 262     for {
 263         s = trimLeadingSpaces(s)
 264         if len(s) == 0 {
 265             return i
 266         }
 267 
 268         if i+1 == max {
 269             f(i, s, t)
 270             return i + 1
 271         }
 272 
 273         j := strings.IndexByte(s, ' ')
 274         if j < 0 {
 275             f(i, s, t)
 276             return i + 1
 277         }
 278 
 279         f(i, s[:j], t)
 280         s = s[j+1:]
 281         i++
 282     }
 283 }
 284 
 285 func trimLeadingSpaces(s string) string {
 286     for len(s) > 0 && s[0] == ' ' {
 287         s = s[1:]
 288     }
 289     return s
 290 }
 291 
 292 func trimTrailingSpaces(s string) string {
 293     for len(s) > 0 && s[len(s)-1] == ' ' {
 294         s = s[:len(s)-1]
 295     }
 296     return s
 297 }
 298 
 299 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 300 // when given empty strings, the callback func is never called
 301 func loopItemsTSV(s string, max int, t *table, f itemFunc) int {
 302     if len(s) == 0 {
 303         return 0
 304     }
 305 
 306     i := 0
 307 
 308     for {
 309         if i+1 == max {
 310             f(i, s, t)
 311             return i + 1
 312         }
 313 
 314         j := strings.IndexByte(s, '\t')
 315         if j < 0 {
 316             f(i, s, t)
 317             return i + 1
 318         }
 319 
 320         f(i, s[:j], t)
 321         s = s[j+1:]
 322         i++
 323     }
 324 }
 325 
 326 func skipLeadingEscapeSequences(s string) string {
 327     for len(s) >= 2 {
 328         if s[0] != '\x1b' {
 329             return s
 330         }
 331 
 332         switch s[1] {
 333         case '[':
 334             s = skipSingleLeadingANSI(s[2:])
 335 
 336         case ']':
 337             if len(s) < 3 || s[2] != '8' {
 338                 return s
 339             }
 340             s = skipSingleLeadingOSC(s[3:])
 341 
 342         default:
 343             return s
 344         }
 345     }
 346 
 347     return s
 348 }
 349 
 350 func skipSingleLeadingANSI(s string) string {
 351     for len(s) > 0 {
 352         upper := s[0] &^ 32
 353         s = s[1:]
 354         if 'A' <= upper && upper <= 'Z' {
 355             break
 356         }
 357     }
 358 
 359     return s
 360 }
 361 
 362 func skipSingleLeadingOSC(s string) string {
 363     var prev byte
 364 
 365     for len(s) > 0 {
 366         b := s[0]
 367         s = s[1:]
 368         if prev == '\x1b' && b == '\\' {
 369             break
 370         }
 371         prev = b
 372     }
 373 
 374     return s
 375 }
 376 
 377 // isNumeric checks if a string is valid/useable as a number
 378 func isNumeric(s string, sb *strings.Builder) bool {
 379     if len(s) == 0 {
 380         return false
 381     }
 382 
 383     sb.Reset()
 384 
 385     s = skipLeadingEscapeSequences(s)
 386     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 387         sb.WriteByte(s[0])
 388         s = s[1:]
 389     }
 390 
 391     s = skipLeadingEscapeSequences(s)
 392     if len(s) == 0 {
 393         return false
 394     }
 395     if b := s[0]; b == '.' {
 396         sb.WriteByte(b)
 397         return isDigits(s[1:], sb)
 398     }
 399 
 400     digits := 0
 401 
 402     for {
 403         s = skipLeadingEscapeSequences(s)
 404         if len(s) == 0 {
 405             break
 406         }
 407 
 408         b := s[0]
 409         sb.WriteByte(b)
 410 
 411         if b == '.' {
 412             return isDigits(s[1:], sb)
 413         }
 414 
 415         if !('0' <= b && b <= '9') {
 416             return false
 417         }
 418 
 419         digits++
 420         s = s[1:]
 421     }
 422 
 423     s = skipLeadingEscapeSequences(s)
 424     return len(s) == 0 && digits > 0
 425 }
 426 
 427 func isDigits(s string, sb *strings.Builder) bool {
 428     if len(s) == 0 {
 429         return false
 430     }
 431 
 432     digits := 0
 433 
 434     for {
 435         s = skipLeadingEscapeSequences(s)
 436         if len(s) == 0 {
 437             break
 438         }
 439 
 440         if b := s[0]; '0' <= b && b <= '9' {
 441             sb.WriteByte(b)
 442             s = s[1:]
 443             digits++
 444         } else {
 445             return false
 446         }
 447     }
 448 
 449     s = skipLeadingEscapeSequences(s)
 450     return len(s) == 0 && digits > 0
 451 }
 452 
 453 // countDecimals counts decimal digits from the string given, assuming it
 454 // represents a valid/useable float64, when parsed
 455 func countDecimals(s string) int {
 456     dot := strings.IndexByte(s, '.')
 457     if dot < 0 {
 458         return 0
 459     }
 460 
 461     decs := 0
 462     s = s[dot+1:]
 463 
 464     for len(s) > 0 {
 465         s = skipLeadingEscapeSequences(s)
 466         if len(s) == 0 {
 467             break
 468         }
 469         if '0' <= s[0] && s[0] <= '9' {
 470             decs++
 471         }
 472         s = s[1:]
 473     }
 474 
 475     return decs
 476 }
 477 
 478 // countDotDecimals is like func countDecimals, but this one also includes
 479 // the dot, when any decimals are present, else the count stays at 0
 480 func countDotDecimals(s string) int {
 481     decs := countDecimals(s)
 482     if decs > 0 {
 483         return decs + 1
 484     }
 485     return decs
 486 }
 487 
 488 func countWidth(s string) int {
 489     width := 0
 490 
 491     for len(s) > 0 {
 492         i, j := indexEscapeSequence(s)
 493         if i < 0 {
 494             break
 495         }
 496         if j < 0 {
 497             j = len(s)
 498         }
 499 
 500         width += utf8.RuneCountInString(s[:i])
 501         s = s[j:]
 502     }
 503 
 504     // count trailing/all runes in strings which don't end with ANSI-sequences
 505     width += utf8.RuneCountInString(s)
 506     return width
 507 }
 508 
 509 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 510 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 511 // indices which can be independently negative when either the start/end of
 512 // a sequence isn't found; given their fairly-common use, even the hyperlink
 513 // ESC]8 sequences are supported
 514 func indexEscapeSequence(s string) (int, int) {
 515     var prev byte
 516 
 517     for i := range s {
 518         b := s[i]
 519 
 520         if prev == '\x1b' && b == '[' {
 521             j := indexLetter(s[i+1:])
 522             if j < 0 {
 523                 return i, -1
 524             }
 525             return i - 1, i + 1 + j + 1
 526         }
 527 
 528         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 529             j := indexPair(s[i+1:], '\x1b', '\\')
 530             if j < 0 {
 531                 return i, -1
 532             }
 533             return i - 1, i + 1 + j + 2
 534         }
 535 
 536         prev = b
 537     }
 538 
 539     return -1, -1
 540 }
 541 
 542 func indexLetter(s string) int {
 543     for i, b := range s {
 544         upper := b &^ 32
 545         if 'A' <= upper && upper <= 'Z' {
 546             return i
 547         }
 548     }
 549 
 550     return -1
 551 }
 552 
 553 func indexPair(s string, x byte, y byte) int {
 554     var prev byte
 555 
 556     for i := range s {
 557         b := s[i]
 558         if prev == x && b == y && i > 0 {
 559             return i
 560         }
 561         prev = b
 562     }
 563 
 564     return -1
 565 }
 566 
 567 func realign(w *bufio.Writer, t *table) {
 568     // make sums row first, as final alignments are usually affected by these
 569     var sums []string
 570     if t.ShowSums {
 571         sums = make([]string, 0, t.Columns)
 572 
 573         for i := 0; i < t.Columns; i++ {
 574             if t.Numeric[i] == 0 {
 575                 sums = append(sums, `-`)
 576                 if t.MaxWidth[i] < 1 {
 577                     t.MaxWidth[i] = 1
 578                 }
 579                 continue
 580             }
 581 
 582             decs := t.MaxDotDecimals[i]
 583             if decs > 0 {
 584                 decs--
 585             }
 586 
 587             var buf [64]byte
 588             s := strconv.AppendFloat(buf[:0], t.Sums[i], 'f', decs, 64)
 589             sums = append(sums, string(s))
 590             if t.MaxWidth[i] < len(s) {
 591                 t.MaxWidth[i] = len(s)
 592             }
 593         }
 594     }
 595 
 596     // due keeps track of how many spaces are due, when separating realigned
 597     // items from their immediate predecessor on the same row; this counter
 598     // is also used to right-pad numbers with decimals, as such items can be
 599     // padded with spaces from either side
 600     due := 0
 601 
 602     showItem := func(i int, s string, t *table) {
 603         if i > 0 {
 604             due += columnGap
 605         }
 606 
 607         if isNumeric(s, &(t.sb)) {
 608             dd := countDotDecimals(s)
 609             rpad := t.MaxDotDecimals[i] - dd
 610             width := countWidth(s)
 611             lpad := t.MaxWidth[i] - (width + rpad) + due
 612             writeSpaces(w, lpad)
 613             f, _ := strconv.ParseFloat(t.sb.String(), 64)
 614             writeNumericItem(w, s, numericStyle(f))
 615             due = rpad
 616             return
 617         }
 618 
 619         writeSpaces(w, due)
 620         w.WriteString(s)
 621         due = t.MaxWidth[i] - countWidth(s)
 622     }
 623 
 624     writeTile := func(i int, s string, t *table) {
 625         // make empty items stand out
 626         if len(s) == 0 {
 627             w.WriteString("\x1b[0m○")
 628             return
 629         }
 630 
 631         if isNumeric(s, &(t.sb)) {
 632             f, _ := strconv.ParseFloat(t.sb.String(), 64)
 633             w.WriteString(numericStyle(f))
 634             w.WriteString("■")
 635             return
 636         }
 637 
 638         // make padded items stand out: these items have spaces at either end
 639         if s[0] == ' ' || s[len(s)-1] == ' ' {
 640             w.WriteString("\x1b[38;2;196;160;0m■")
 641             return
 642         }
 643 
 644         w.WriteString("\x1b[38;2;128;128;128m■")
 645     }
 646 
 647     // show realigned rows
 648 
 649     for _, line := range t.Rows {
 650         due = 0
 651 
 652         if t.ShowTiles {
 653             end := t.LoopItems(line, t.Columns, t, writeTile)
 654             if end < len(t.MaxWidth)-1 {
 655                 w.WriteString("\x1b[0m")
 656             }
 657             // make rows with missing trailing items stand out
 658             for i := end; i < len(t.MaxWidth); i++ {
 659                 w.WriteString("×")
 660             }
 661             w.WriteString("\x1b[0m")
 662             due += columnGap
 663         }
 664 
 665         t.LoopItems(line, t.Columns, t, showItem)
 666         if w.WriteByte('\n') != nil {
 667             return
 668         }
 669     }
 670 
 671     if t.Columns > 0 && t.ShowSums {
 672         realignSums(w, t, sums)
 673     }
 674 }
 675 
 676 func realignSums(w *bufio.Writer, t *table, sums []string) {
 677     due := 0
 678     if t.ShowTiles {
 679         due += t.Columns + columnGap
 680     }
 681 
 682     for i, s := range sums {
 683         if i > 0 {
 684             due += columnGap
 685         }
 686 
 687         if t.Numeric[i] == 0 {
 688             writeSpaces(w, due)
 689             w.WriteString(s)
 690             due = t.MaxWidth[i] - countWidth(s)
 691             continue
 692         }
 693 
 694         lpad := t.MaxWidth[i] - len(s) + due
 695         writeSpaces(w, lpad)
 696         writeNumericItem(w, s, numericStyle(t.Sums[i]))
 697         due = 0
 698     }
 699 
 700     w.WriteByte('\n')
 701 }
 702 
 703 // writeSpaces does what it says, minimizing calls to write-like funcs
 704 func writeSpaces(w *bufio.Writer, n int) {
 705     const spaces = `                                `
 706     if n < 1 {
 707         return
 708     }
 709 
 710     for n >= len(spaces) {
 711         w.WriteString(spaces)
 712         n -= len(spaces)
 713     }
 714     w.WriteString(spaces[:n])
 715 }
 716 
 717 func writeRowTiles(w *bufio.Writer, s string, t *table, writeTile itemFunc) {
 718     end := t.LoopItems(s, t.Columns, t, writeTile)
 719 
 720     if end < len(t.MaxWidth)-1 {
 721         w.WriteString("\x1b[0m")
 722     }
 723     for i := end + 1; i < len(t.MaxWidth); i++ {
 724         w.WriteString("×")
 725     }
 726     w.WriteString("\x1b[0m")
 727 }
 728 
 729 func numericStyle(f float64) string {
 730     if f > 0 {
 731         if float64(int64(f)) == f {
 732             return "\x1b[38;2;0;135;0m"
 733         }
 734         return "\x1b[38;2;0;155;95m"
 735     }
 736     if f < 0 {
 737         if float64(int64(f)) == f {
 738             return "\x1b[38;2;204;0;0m"
 739         }
 740         return "\x1b[38;2;215;95;95m"
 741     }
 742     if f == 0 {
 743         return "\x1b[38;2;0;95;215m"
 744     }
 745     return "\x1b[38;2;128;128;128m"
 746 }
 747 
 748 func writeNumericItem(w *bufio.Writer, s string, startStyle string) {
 749     w.WriteString(startStyle)
 750     if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
 751         w.WriteByte(s[0])
 752         s = s[1:]
 753     }
 754 
 755     dot := strings.IndexByte(s, '.')
 756     if dot < 0 {
 757         restyleDigits(w, s, altDigitStyle)
 758         w.WriteString("\x1b[0m")
 759         return
 760     }
 761 
 762     if len(s[:dot]) > 3 {
 763         restyleDigits(w, s[:dot], altDigitStyle)
 764         w.WriteString("\x1b[0m")
 765         w.WriteString(startStyle)
 766         w.WriteByte('.')
 767     } else {
 768         w.WriteString(s[:dot])
 769         w.WriteByte('.')
 770     }
 771 
 772     rest := s[dot+1:]
 773     restyleDigits(w, rest, altDigitStyle)
 774     if len(rest) < 4 {
 775         w.WriteString("\x1b[0m")
 776     }
 777 }
 778 
 779 // restyleDigits renders a run of digits as alternating styled/unstyled runs
 780 // of 3 digits, which greatly improves readability, and is the only purpose
 781 // of this app; string is assumed to be all decimal digits
 782 func restyleDigits(w *bufio.Writer, digits string, altStyle string) {
 783     if len(digits) < 4 {
 784         // digit sequence is short, so emit it as is
 785         w.WriteString(digits)
 786         return
 787     }
 788 
 789     // separate leading 0..2 digits which don't align with the 3-digit groups
 790     i := len(digits) % 3
 791     // emit leading digits unstyled, if there are any
 792     w.WriteString(digits[:i])
 793     // the rest is guaranteed to have a length which is a multiple of 3
 794     digits = digits[i:]
 795 
 796     // start by styling, unless there were no leading digits
 797     style := i != 0
 798 
 799     for len(digits) > 0 {
 800         if style {
 801             w.WriteString(altStyle)
 802             w.WriteString(digits[:3])
 803             w.WriteString("\x1b[0m")
 804         } else {
 805             w.WriteString(digits[:3])
 806         }
 807 
 808         // advance to the next triple: the start of this func is supposed
 809         // to guarantee this step always works
 810         digits = digits[3:]
 811 
 812         // alternate between styled and unstyled 3-digit groups
 813         style = !style
 814     }
 815 }
     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     var tests = []struct {
  31         name     string
  32         input    string
  33         expected int
  34     }{
  35         {`empty`, ``, 0},
  36         {`empty ANSI`, "\x1b[38;5;0;0;0m\x1b[0m", 0},
  37         {`simple plain`, `abc def`, 7},
  38         {`unicode plain`, `abc●def`, 7},
  39         {`simple ANSI`, "abc \x1b[7mde\x1b[0mf", 7},
  40         {`unicode ANSI`, "abc●\x1b[7mde\x1b[0mf", 7},
  41     }
  42 
  43     for _, tc := range tests {
  44         t.Run(tc.name, func(t *testing.T) {
  45             got := countWidth(tc.input)
  46             if got != tc.expected {
  47                 t.Errorf("expected width %d, got %d instead", 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     }
 128 
 129     // figure out whether input should come from a named file or from stdin
 130     path := `-`
 131     if len(args) > 0 {
 132         path = args[0]
 133     }
 134 
 135     err := handleInput(os.Stdout, path)
 136     if err != nil && err != io.EOF {
 137         os.Stderr.WriteString(err.Error())
 138         os.Stderr.WriteString("\n")
 139         os.Exit(1)
 140     }
 141 }
 142 
 143 type handlerFunc func(*bufio.Writer, *json.Decoder, json.Token, []any) error
 144 
 145 // handleInput simplifies control-flow for func main
 146 func handleInput(w io.Writer, path string) error {
 147     if path == `-` {
 148         bw := bufio.NewWriter(w)
 149         defer bw.Flush()
 150         return run(bw, os.Stdin)
 151     }
 152 
 153     f, err := os.Open(path)
 154     if err != nil {
 155         // on windows, file-not-found error messages may mention `CreateFile`,
 156         // even when trying to open files in read-only mode
 157         return errors.New(`can't open file named ` + path)
 158     }
 159     defer f.Close()
 160 
 161     bw := bufio.NewWriter(w)
 162     defer bw.Flush()
 163     return run(bw, f)
 164 }
 165 
 166 // escapedStringBytes helps func handleString treat all string bytes quickly
 167 // and correctly, using their officially-supported JSON escape sequences
 168 //
 169 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 170 var escapedStringBytes = [256][]byte{
 171     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 172     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 173     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 174     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 175     {'\\', 'b'}, {'\\', 't'},
 176     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 177     {'\\', 'f'}, {'\\', 'r'},
 178     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 179     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 180     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 181     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 182     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 183     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 184     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 185     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 186     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 187     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 188     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 189     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 190     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 191     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 192     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 193     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 194     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 195     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 196     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 197     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 198     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 199     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 200     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 201     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 202     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 203     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 204     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 205     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 206     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 207     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 208     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 209     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 210     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 211     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 212     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 213     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 214     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 215 }
 216 
 217 // run does it all, given a reader and a writer
 218 func run(w *bufio.Writer, r io.Reader) error {
 219     dec := json.NewDecoder(r)
 220     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 221     // even if JSON parsers aren't required to guarantee such input-fidelity
 222     // for numbers
 223     dec.UseNumber()
 224 
 225     t, err := dec.Token()
 226     if err == io.EOF {
 227         return errors.New(`input has no JSON values`)
 228     }
 229 
 230     if err = handleToken(w, dec, t, make([]any, 0, 50)); err != nil {
 231         return err
 232     }
 233 
 234     _, err = dec.Token()
 235     if err == io.EOF {
 236         // input is over, so it's a success
 237         return nil
 238     }
 239 
 240     if err == nil {
 241         // a successful `read` is a failure, as it means there are
 242         // trailing JSON tokens
 243         return errors.New(`unexpected trailing data`)
 244     }
 245 
 246     // any other error, perhaps some invalid-JSON-syntax-type error
 247     return err
 248 }
 249 
 250 // handleToken handles recursion for func run
 251 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, path []any) error {
 252     switch t := t.(type) {
 253     case json.Delim:
 254         switch t {
 255         case json.Delim('['):
 256             return handleArray(w, dec, path)
 257         case json.Delim('{'):
 258             return handleObject(w, dec, path)
 259         default:
 260             return errors.New(`unsupported JSON syntax ` + string(t))
 261         }
 262 
 263     case nil:
 264         config.path(w, path)
 265         config.null(w)
 266         return endLine(w)
 267 
 268     case bool:
 269         config.path(w, path)
 270         config.boolean(w, t)
 271         return endLine(w)
 272 
 273     case json.Number:
 274         config.path(w, path)
 275         config.number(w, t)
 276         return endLine(w)
 277 
 278     case string:
 279         config.path(w, path)
 280         config.text(w, t)
 281         return endLine(w)
 282 
 283     default:
 284         // return fmt.Errorf(`unsupported token type %T`, t)
 285         return errors.New(`invalid JSON token`)
 286     }
 287 }
 288 
 289 // handleArray handles arrays for func handleToken
 290 func handleArray(w *bufio.Writer, dec *json.Decoder, path []any) error {
 291     config.path(w, path)
 292     w.WriteString(config.arrayDecl)
 293     if err := endLine(w); err != nil {
 294         return err
 295     }
 296 
 297     path = append(path, 0)
 298     last := len(path) - 1
 299 
 300     for i := 0; true; i++ {
 301         path[last] = i
 302 
 303         t, err := dec.Token()
 304         if err == io.EOF {
 305             return errors.New(`end of JSON before array was closed`)
 306         }
 307         if err != nil {
 308             return err
 309         }
 310 
 311         if t == json.Delim(']') {
 312             return nil
 313         }
 314 
 315         err = handleToken(w, dec, t, path)
 316         if err != nil {
 317             return err
 318         }
 319     }
 320 
 321     // make the compiler happy
 322     return nil
 323 }
 324 
 325 // handleObject handles objects for func handleToken
 326 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
 327     config.path(w, path)
 328     w.WriteString(config.objectDecl)
 329     if err := endLine(w); err != nil {
 330         return err
 331     }
 332 
 333     path = append(path, ``)
 334     last := len(path) - 1
 335 
 336     for i := 0; true; i++ {
 337         t, err := dec.Token()
 338         if err == io.EOF {
 339             return errors.New(`end of JSON before object was closed`)
 340         }
 341         if err != nil {
 342             return err
 343         }
 344 
 345         if t == json.Delim('}') {
 346             return nil
 347         }
 348 
 349         k, ok := t.(string)
 350         if !ok {
 351             return errors.New(`expected a string for a key-value pair`)
 352         }
 353 
 354         path[last] = k
 355         if err != nil {
 356             return err
 357         }
 358 
 359         t, err = dec.Token()
 360         if err == io.EOF {
 361             return errors.New(`expected a value for a key-value pair`)
 362         }
 363 
 364         err = handleToken(w, dec, t, path)
 365         if err != nil {
 366             return err
 367         }
 368     }
 369 
 370     // make the compiler happy
 371     return nil
 372 }
 373 
 374 func monoPath(w *bufio.Writer, path []any) error {
 375     var buf [24]byte
 376 
 377     w.WriteString(`json`)
 378 
 379     for _, v := range path {
 380         switch v := v.(type) {
 381         case int:
 382             w.WriteByte('[')
 383             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 384             w.WriteByte(']')
 385 
 386         case string:
 387             if !needsEscaping(v) {
 388                 w.WriteByte('.')
 389                 w.WriteString(v)
 390                 continue
 391             }
 392             w.WriteByte('[')
 393             monoString(w, v)
 394             w.WriteByte(']')
 395         }
 396     }
 397 
 398     w.WriteString(` = `)
 399     return nil
 400 }
 401 
 402 func monoNull(w *bufio.Writer) error {
 403     w.WriteString(`null`)
 404     return nil
 405 }
 406 
 407 func monoBool(w *bufio.Writer, b bool) error {
 408     if b {
 409         w.WriteString(`true`)
 410     } else {
 411         w.WriteString(`false`)
 412     }
 413     return nil
 414 }
 415 
 416 func monoNumber(w *bufio.Writer, n json.Number) error {
 417     w.WriteString(n.String())
 418     return nil
 419 }
 420 
 421 func monoString(w *bufio.Writer, s string) error {
 422     w.WriteByte('"')
 423     for i := range s {
 424         w.Write(escapedStringBytes[s[i]])
 425     }
 426     w.WriteByte('"')
 427     return nil
 428 }
 429 
 430 func styledPath(w *bufio.Writer, path []any) error {
 431     var buf [24]byte
 432 
 433     w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
 434 
 435     for _, v := range path {
 436         switch v := v.(type) {
 437         case int:
 438             w.WriteString("\x1b[38;2;168;168;168m[")
 439             w.WriteString("\x1b[38;2;0;135;95m")
 440             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 441             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 442 
 443         case string:
 444             if !needsEscaping(v) {
 445                 w.WriteString("\x1b[38;2;168;168;168m.")
 446                 w.WriteString("\x1b[38;2;135;95;255m")
 447                 w.WriteString(v)
 448                 w.WriteString("\x1b[0m")
 449                 continue
 450             }
 451 
 452             w.WriteString("\x1b[38;2;168;168;168m[")
 453             styledString(w, v)
 454             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 455         }
 456     }
 457 
 458     w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
 459     return nil
 460 }
 461 
 462 func styledNull(w *bufio.Writer) error {
 463     w.WriteString("\x1b[38;2;168;168;168m")
 464     w.WriteString(`null`)
 465     w.WriteString("\x1b[0m")
 466     return nil
 467 }
 468 
 469 func styledBool(w *bufio.Writer, b bool) error {
 470     if b {
 471         w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
 472     } else {
 473         w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
 474     }
 475     return nil
 476 }
 477 
 478 func styledNumber(w *bufio.Writer, n json.Number) error {
 479     w.WriteString("\x1b[38;2;0;135;95m")
 480     w.WriteString(n.String())
 481     w.WriteString("\x1b[0m")
 482     return nil
 483 }
 484 
 485 func styledString(w *bufio.Writer, s string) error {
 486     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 487     for i := range s {
 488         w.Write(escapedStringBytes[s[i]])
 489     }
 490     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 491     return nil
 492 }
 493 
 494 func needsEscaping(s string) bool {
 495     for _, r := range s {
 496         if r < ' ' || r > '~' {
 497             return true
 498         }
 499 
 500         switch r {
 501         case '"', '\'', '\\':
 502             return true
 503         }
 504     }
 505 
 506     return false
 507 }
 508 
 509 func endLine(w *bufio.Writer) error {
 510     w.WriteByte(';')
 511     if err := w.WriteByte('\n'); err == nil {
 512         return nil
 513     }
 514     return io.EOF
 515 }
     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     err := run(parseFlags(usage))
  42     if err != nil {
  43         os.Stderr.WriteString(err.Error())
  44         os.Stderr.WriteString("\n")
  45         os.Exit(1)
  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     }
  92 
  93     // figure out whether input should come from a named file or from stdin
  94     name := `-`
  95     if len(args) == 1 {
  96         name = args[0]
  97     }
  98 
  99     var err error
 100     if name == `-` {
 101         // handle lack of filepath arg, or `-` as the filepath
 102         err = niceJSON(os.Stdout, os.Stdin)
 103     } else {
 104         // handle being given a normal filepath
 105         err = handleFile(os.Stdout, os.Args[1])
 106     }
 107 
 108     if err != nil && err != io.EOF {
 109         showError(err)
 110         os.Exit(1)
 111     }
 112 }
 113 
 114 // showError standardizes how errors look in this app
 115 func showError(err error) {
 116     os.Stderr.WriteString(err.Error())
 117     os.Stderr.WriteString("\n")
 118 }
 119 
 120 // writeSpaces does what it says, minimizing calls to write-like funcs
 121 func writeSpaces(w *bufio.Writer, n int) {
 122     const spaces = `                                `
 123     for n >= len(spaces) {
 124         w.WriteString(spaces)
 125         n -= len(spaces)
 126     }
 127     if n > 0 {
 128         w.WriteString(spaces[:n])
 129     }
 130 }
 131 
 132 func handleFile(w io.Writer, path string) error {
 133     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
 134     //  resp, err := http.Get(path)
 135     //  if err != nil {
 136     //      return err
 137     //  }
 138     //  defer resp.Body.Close()
 139     //  return niceJSON(w, resp.Body)
 140     // }
 141 
 142     f, err := os.Open(path)
 143     if err != nil {
 144         // on windows, file-not-found error messages may mention `CreateFile`,
 145         // even when trying to open files in read-only mode
 146         return errors.New(`can't open file named ` + path)
 147     }
 148     defer f.Close()
 149 
 150     return niceJSON(w, f)
 151 }
 152 
 153 func niceJSON(w io.Writer, r io.Reader) error {
 154     bw := bufio.NewWriter(w)
 155     defer bw.Flush()
 156 
 157     dec := json.NewDecoder(r)
 158     // using string-like json.Number values instead of float64 ones avoids
 159     // unneeded reformatting of numbers; reformatting parsed float64 values
 160     // can potentially even drop/change decimals, causing the output not to
 161     // match the input digits exactly, which is best to avoid
 162     dec.UseNumber()
 163 
 164     t, err := dec.Token()
 165     if err == io.EOF {
 166         return errors.New(`empty input isn't valid JSON`)
 167     }
 168     if err != nil {
 169         return err
 170     }
 171 
 172     if err := handleToken(bw, dec, t, 0, 0); err != nil {
 173         return err
 174     }
 175     // don't forget to end the last output line
 176     bw.WriteByte('\n')
 177 
 178     if _, err := dec.Token(); err != io.EOF {
 179         return errors.New(`unexpected trailing JSON data`)
 180     }
 181     return nil
 182 }
 183 
 184 func handleToken(w *bufio.Writer, d *json.Decoder, t json.Token, pre, level int) error {
 185     switch t := t.(type) {
 186     case json.Delim:
 187         switch t {
 188         case json.Delim('['):
 189             return handleArray(w, d, pre, level)
 190 
 191         case json.Delim('{'):
 192             return handleObject(w, d, pre, level)
 193 
 194         default:
 195             // return fmt.Errorf(`unsupported JSON delimiter %v`, t)
 196             return errors.New(`unsupported JSON delimiter`)
 197         }
 198 
 199     case nil:
 200         return handleNull(w, pre)
 201 
 202     case bool:
 203         return handleBoolean(w, t, pre)
 204 
 205     case string:
 206         return handleString(w, t, pre)
 207 
 208     case json.Number:
 209         return handleNumber(w, t, pre)
 210 
 211     default:
 212         // return fmt.Errorf(`unsupported token type %T`, t)
 213         return errors.New(`unsupported token type`)
 214     }
 215 }
 216 
 217 func handleArray(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 218     for i := 0; true; i++ {
 219         t, err := d.Token()
 220         if err == io.EOF {
 221             return errors.New(`end of JSON before array was closed`)
 222         }
 223         if err != nil {
 224             return err
 225         }
 226 
 227         if t == json.Delim(']') {
 228             if i == 0 {
 229                 writeSpaces(w, indent*pre)
 230                 w.WriteString(syntaxStyle + "[]\x1b[0m")
 231             } else {
 232                 w.WriteString("\n")
 233                 writeSpaces(w, indent*level)
 234                 w.WriteString(syntaxStyle + "]\x1b[0m")
 235             }
 236             return nil
 237         }
 238 
 239         if i == 0 {
 240             writeSpaces(w, indent*pre)
 241             w.WriteString(syntaxStyle + "[\x1b[0m\n")
 242         } else {
 243             // this is a good spot to check for early-quit opportunities
 244             w.WriteString(syntaxStyle + ",\x1b[0m\n")
 245             if err := w.Flush(); err != nil {
 246                 // a write error may be the consequence of stdout being closed,
 247                 // perhaps by another app along a pipe
 248                 return io.EOF
 249             }
 250         }
 251 
 252         if err := handleToken(w, d, t, level+1, level+1); err != nil {
 253             return err
 254         }
 255     }
 256 
 257     // make the compiler happy
 258     return nil
 259 }
 260 
 261 func handleBoolean(w *bufio.Writer, b bool, pre int) error {
 262     writeSpaces(w, indent*pre)
 263     if b {
 264         w.WriteString(boolStyle + "true\x1b[0m")
 265     } else {
 266         w.WriteString(boolStyle + "false\x1b[0m")
 267     }
 268     return nil
 269 }
 270 
 271 func handleKey(w *bufio.Writer, s string, pre int) error {
 272     writeSpaces(w, indent*pre)
 273     w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle)
 274     w.WriteString(s)
 275     w.WriteString(syntaxStyle + "\":\x1b[0m ")
 276     return nil
 277 }
 278 
 279 func handleNull(w *bufio.Writer, pre int) error {
 280     writeSpaces(w, indent*pre)
 281     w.WriteString(nullStyle + "null\x1b[0m")
 282     return nil
 283 }
 284 
 285 // func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 286 //  writeSpaces(w, indent*pre)
 287 //  w.WriteString(numberStyle)
 288 //  w.WriteString(n.String())
 289 //  w.WriteString("\x1b[0m")
 290 //  return nil
 291 // }
 292 
 293 func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 294     writeSpaces(w, indent*pre)
 295     f, _ := n.Float64()
 296     if f > 0 {
 297         w.WriteString(positiveNumberStyle)
 298     } else if f < 0 {
 299         w.WriteString(negativeNumberStyle)
 300     } else {
 301         w.WriteString(zeroNumberStyle)
 302     }
 303     w.WriteString(n.String())
 304     w.WriteString("\x1b[0m")
 305     return nil
 306 }
 307 
 308 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 309     for i := 0; true; i++ {
 310         t, err := d.Token()
 311         if err == io.EOF {
 312             return errors.New(`end of JSON before object was closed`)
 313         }
 314         if err != nil {
 315             return err
 316         }
 317 
 318         if t == json.Delim('}') {
 319             if i == 0 {
 320                 writeSpaces(w, indent*pre)
 321                 w.WriteString(syntaxStyle + "{}\x1b[0m")
 322             } else {
 323                 w.WriteString("\n")
 324                 writeSpaces(w, indent*level)
 325                 w.WriteString(syntaxStyle + "}\x1b[0m")
 326             }
 327             return nil
 328         }
 329 
 330         if i == 0 {
 331             writeSpaces(w, indent*pre)
 332             w.WriteString(syntaxStyle + "{\x1b[0m\n")
 333         } else {
 334             // this is a good spot to check for early-quit opportunities
 335             w.WriteString(syntaxStyle + ",\x1b[0m\n")
 336             if err := w.Flush(); err != nil {
 337                 // a write error may be the consequence of stdout being closed,
 338                 // perhaps by another app along a pipe
 339                 return io.EOF
 340             }
 341         }
 342 
 343         // the stdlib's JSON parser is supposed to complain about non-string
 344         // keys anyway, but make sure just in case
 345         k, ok := t.(string)
 346         if !ok {
 347             return errors.New(`expected key to be a string`)
 348         }
 349         if err := handleKey(w, k, level+1); err != nil {
 350             return err
 351         }
 352 
 353         // handle value
 354         t, err = d.Token()
 355         if err != nil {
 356             return err
 357         }
 358         if err := handleToken(w, d, t, 0, level+1); err != nil {
 359             return err
 360         }
 361     }
 362 
 363     // make the compiler happy
 364     return nil
 365 }
 366 
 367 func needsEscaping(s string) bool {
 368     for _, r := range s {
 369         switch r {
 370         case '"', '\\', '\t', '\r', '\n':
 371             return true
 372         }
 373     }
 374     return false
 375 }
 376 
 377 func handleString(w *bufio.Writer, s string, pre int) error {
 378     writeSpaces(w, indent*pre)
 379     w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle)
 380     if !needsEscaping(s) {
 381         w.WriteString(s)
 382     } else {
 383         escapeString(w, s)
 384     }
 385     w.WriteString(syntaxStyle + "\"\x1b[0m")
 386     return nil
 387 }
 388 
 389 func escapeString(w *bufio.Writer, s string) {
 390     for _, r := range s {
 391         switch r {
 392         case '"', '\\':
 393             w.WriteByte('\\')
 394             w.WriteRune(r)
 395         case '\t':
 396             w.WriteByte('\\')
 397             w.WriteByte('t')
 398         case '\r':
 399             w.WriteByte('\\')
 400             w.WriteByte('r')
 401         case '\n':
 402             w.WriteByte('\\')
 403             w.WriteByte('n')
 404         default:
 405             w.WriteRune(r)
 406         }
 407     }
 408 }
     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 func Main() {
  75     args := os.Args[1:]
  76 
  77     if len(args) > 0 {
  78         switch args[0] {
  79         case `-h`, `--h`, `-help`, `--help`:
  80             os.Stdout.WriteString(info[1:])
  81             return
  82         }
  83     }
  84 
  85     options := true
  86     if len(args) > 0 && args[0] == `--` {
  87         options = false
  88         args = args[1:]
  89     }
  90 
  91     style, _ := lookupStyle(`gray`)
  92 
  93     // if the first argument is 1 or 2 dashes followed by a supported
  94     // style-name, change the style used
  95     if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
  96         name := args[0]
  97         name = strings.TrimPrefix(name, `-`)
  98         name = strings.TrimPrefix(name, `-`)
  99         args = args[1:]
 100 
 101         // check if the `dedashed` argument is a supported style-name
 102         if s, ok := lookupStyle(name); ok {
 103             style = s
 104         } else {
 105             os.Stderr.WriteString(`invalid style name `)
 106             os.Stderr.WriteString(name)
 107             os.Stderr.WriteString("\n")
 108             os.Exit(1)
 109         }
 110     }
 111 
 112     if err := run(os.Stdout, args, style); err != nil && err != io.EOF {
 113         os.Stderr.WriteString(err.Error())
 114         os.Stderr.WriteString("\n")
 115         os.Exit(1)
 116     }
 117 }
 118 
 119 func run(w io.Writer, args []string, style string) error {
 120     bw := bufio.NewWriter(w)
 121     defer bw.Flush()
 122 
 123     if len(args) == 0 {
 124         return restyle(bw, os.Stdin, style)
 125     }
 126 
 127     for _, name := range args {
 128         if err := handleFile(bw, name, style); err != nil {
 129             return err
 130         }
 131     }
 132     return nil
 133 }
 134 
 135 func handleFile(w *bufio.Writer, name string, style string) error {
 136     if name == `` || name == `-` {
 137         return restyle(w, os.Stdin, style)
 138     }
 139 
 140     f, err := os.Open(name)
 141     if err != nil {
 142         return errors.New(`can't read from file named "` + name + `"`)
 143     }
 144     defer f.Close()
 145 
 146     return restyle(w, f, style)
 147 }
 148 
 149 func restyle(w *bufio.Writer, r io.Reader, style string) 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         restyleLine(w, s, style)
 161         w.WriteByte('\n')
 162         if err := w.Flush(); err != nil {
 163             // a write error may be the consequence of stdout being closed,
 164             // perhaps by another app along a pipe
 165             return io.EOF
 166         }
 167     }
 168     return sc.Err()
 169 }
 170 
 171 func lookupStyle(name string) (style string, ok bool) {
 172     if alias, ok := styleAliases[name]; ok {
 173         name = alias
 174     }
 175 
 176     style, ok = styles[name]
 177     return style, ok
 178 }
 179 
 180 var styleAliases = map[string]string{
 181     `b`: `blue`,
 182     `g`: `green`,
 183     `m`: `magenta`,
 184     `o`: `orange`,
 185     `p`: `purple`,
 186     `r`: `red`,
 187     `u`: `underline`,
 188 
 189     `bolded`:      `bold`,
 190     `h`:           `inverse`,
 191     `hi`:          `inverse`,
 192     `highlight`:   `inverse`,
 193     `highlighted`: `inverse`,
 194     `hilite`:      `inverse`,
 195     `hilited`:     `inverse`,
 196     `inv`:         `inverse`,
 197     `invert`:      `inverse`,
 198     `inverted`:    `inverse`,
 199     `underlined`:  `underline`,
 200 
 201     `bb`: `blueback`,
 202     `bg`: `greenback`,
 203     `bm`: `magentaback`,
 204     `bo`: `orangeback`,
 205     `bp`: `purpleback`,
 206     `br`: `redback`,
 207 
 208     `gb`: `greenback`,
 209     `mb`: `magentaback`,
 210     `ob`: `orangeback`,
 211     `pb`: `purpleback`,
 212     `rb`: `redback`,
 213 
 214     `bblue`:    `blueback`,
 215     `bgray`:    `grayback`,
 216     `bgreen`:   `greenback`,
 217     `bmagenta`: `magentaback`,
 218     `borange`:  `orangeback`,
 219     `bpurple`:  `purpleback`,
 220     `bred`:     `redback`,
 221 
 222     `backblue`:    `blueback`,
 223     `backgray`:    `grayback`,
 224     `backgreen`:   `greenback`,
 225     `backmagenta`: `magentaback`,
 226     `backorange`:  `orangeback`,
 227     `backpurple`:  `purpleback`,
 228     `backred`:     `redback`,
 229 }
 230 
 231 // styles turns style-names into the ANSI-code sequences used for the
 232 // alternate groups of digits
 233 var styles = map[string]string{
 234     `blue`:      "\x1b[38;2;0;95;215m",
 235     `bold`:      "\x1b[1m",
 236     `gray`:      "\x1b[38;2;168;168;168m",
 237     `green`:     "\x1b[38;2;0;135;95m",
 238     `inverse`:   "\x1b[7m",
 239     `magenta`:   "\x1b[38;2;215;0;255m",
 240     `orange`:    "\x1b[38;2;215;95;0m",
 241     `plain`:     "\x1b[0m",
 242     `red`:       "\x1b[38;2;204;0;0m",
 243     `underline`: "\x1b[4m",
 244 
 245     // `blue`:      "\x1b[38;5;26m",
 246     // `bold`:      "\x1b[1m",
 247     // `gray`:      "\x1b[38;5;248m",
 248     // `green`:     "\x1b[38;5;29m",
 249     // `inverse`:   "\x1b[7m",
 250     // `magenta`:   "\x1b[38;5;99m",
 251     // `orange`:    "\x1b[38;5;166m",
 252     // `plain`:     "\x1b[0m",
 253     // `red`:       "\x1b[31m",
 254     // `underline`: "\x1b[4m",
 255 
 256     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 257     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 258     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 259     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 260     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 261     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 262     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 263 }
 264 
 265 // restyleLine renders the line given, using ANSI-styles to make any long
 266 // numbers in it more legible; this func doesn't emit a line-feed, which
 267 // is up to its caller
 268 func restyleLine(w *bufio.Writer, line []byte, style string) {
 269     for len(line) > 0 {
 270         i := indexDigit(line)
 271         if i < 0 {
 272             // no (more) digits to style for sure
 273             w.Write(line)
 274             return
 275         }
 276 
 277         // emit line before current digit-run
 278         w.Write(line[:i])
 279         // advance to the start of the current digit-run
 280         line = line[i:]
 281 
 282         // see where the digit-run ends
 283         j := indexNonDigit(line)
 284         if j < 0 {
 285             // the digit-run goes until the end
 286             restyleDigits(w, line, style)
 287             return
 288         }
 289 
 290         // emit styled digit-run
 291         restyleDigits(w, line[:j], style)
 292         // skip right past the end of the digit-run
 293         line = line[j:]
 294     }
 295 }
 296 
 297 // indexDigit finds the index of the first digit in a string, or -1 when the
 298 // string has no decimal digits
 299 func indexDigit(s []byte) int {
 300     for i := 0; i < len(s); i++ {
 301         switch s[i] {
 302         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 303             return i
 304         }
 305     }
 306 
 307     // empty slice, or a slice without any digits
 308     return -1
 309 }
 310 
 311 // indexNonDigit finds the index of the first non-digit in a string, or -1
 312 // when the string is all decimal digits
 313 func indexNonDigit(s []byte) int {
 314     for i := 0; i < len(s); i++ {
 315         switch s[i] {
 316         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 317             continue
 318         default:
 319             return i
 320         }
 321     }
 322 
 323     // empty slice, or a slice which only has digits
 324     return -1
 325 }
 326 
 327 // restyleDigits renders a run of digits as alternating styled/unstyled runs
 328 // of 3 digits, which greatly improves readability, and is the only purpose
 329 // of this app; string is assumed to be all decimal digits
 330 func restyleDigits(w *bufio.Writer, digits []byte, altStyle string) {
 331     if len(digits) < 4 {
 332         // digit sequence is short, so emit it as is
 333         w.Write(digits)
 334         return
 335     }
 336 
 337     // separate leading 0..2 digits which don't align with the 3-digit groups
 338     i := len(digits) % 3
 339     // emit leading digits unstyled, if there are any
 340     w.Write(digits[:i])
 341     // the rest is guaranteed to have a length which is a multiple of 3
 342     digits = digits[i:]
 343 
 344     // start by styling, unless there were no leading digits
 345     style := i != 0
 346 
 347     for len(digits) > 0 {
 348         if style {
 349             w.WriteString(altStyle)
 350             w.Write(digits[:3])
 351             w.Write([]byte{'\x1b', '[', '0', 'm'})
 352         } else {
 353             w.Write(digits[:3])
 354         }
 355 
 356         // advance to the next triple: the start of this func is supposed
 357         // to guarantee this step always works
 358         digits = digits[3:]
 359 
 360         // alternate between styled and unstyled 3-digit groups
 361         style = !style
 362     }
 363 }
     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     var tests = []struct {
  40         Input    string
  41         Expected string
  42     }{
  43         {``, ``},
  44         {`abc`, `abc`},
  45         {`  abc 123456 `, `  abc 123` + d + `456` + r + ` `},
  46         {`  123456789 text`, `  123` + d + `456` + r + `789 text`},
  47 
  48         {`0`, `0`},
  49         {`01`, `01`},
  50         {`012`, `012`},
  51         {`0123`, `0` + d + `123` + r},
  52         {`01234`, `01` + d + `234` + r},
  53         {`012345`, `012` + d + `345` + r},
  54         {`0123456`, `0` + d + `123` + r + `456`},
  55         {`01234567`, `01` + d + `234` + r + `567`},
  56         {`012345678`, `012` + d + `345` + r + `678`},
  57         {`0123456789`, `0` + d + `123` + r + `456` + d + `789` + r},
  58         {`01234567890`, `01` + d + `234` + r + `567` + d + `890` + r},
  59         {`012345678901`, `012` + d + `345` + r + `678` + d + `901` + r},
  60         {`0123456789012`, `0` + d + `123` + r + `456` + d + `789` + r + `012`},
  61 
  62         {`00321`, `00` + d + `321` + r},
  63         {`123.456789`, `123.` + `456` + d + `789` + r},
  64         {`123456.123456`, `123` + d + `456` + r + `.` + `123` + d + `456` + r},
  65     }
  66 
  67     for _, tc := range tests {
  68         t.Run(tc.Input, func(t *testing.T) {
  69             var b strings.Builder
  70             w := bufio.NewWriter(&b)
  71             restyleLine(w, []byte(tc.Input), d)
  72             w.Flush()
  73 
  74             if got := b.String(); got != tc.Expected {
  75                 t.Fatalf(`expected %q, but got %q instead`, tc.Expected, got)
  76             }
  77         })
  78     }
  79 }
     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     }
  78 }
  79 
  80 func showDateTime(w io.Writer, t time.Time) {
  81     var buf [64]byte
  82     w.Write(t.AppendFormat(buf[:0], timeFormat))
  83 }
     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     }
  81 }
  82 
  83 func run(w io.Writer, args []string, live bool) error {
  84     bw := bufio.NewWriter(w)
  85     defer bw.Flush()
  86 
  87     if len(args) == 0 {
  88         return plain(bw, os.Stdin, live)
  89     }
  90 
  91     for _, name := range args {
  92         if err := handleFile(bw, name, live); err != nil {
  93             return err
  94         }
  95     }
  96     return nil
  97 }
  98 
  99 func handleFile(w *bufio.Writer, name string, live bool) error {
 100     if name == `` || name == `-` {
 101         return plain(w, os.Stdin, live)
 102     }
 103 
 104     f, err := os.Open(name)
 105     if err != nil {
 106         return errors.New(`can't read from file named "` + name + `"`)
 107     }
 108     defer f.Close()
 109 
 110     return plain(w, f, live)
 111 }
 112 
 113 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 114 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 115 // indices which can be independently negative when either the start/end of
 116 // a sequence isn't found; given their fairly-common use, even the hyperlink
 117 // ESC]8 sequences are supported
 118 func indexEscapeSequence(s []byte) (int, int) {
 119     var prev byte
 120 
 121     for i, b := range s {
 122         if prev == '\x1b' && b == '[' {
 123             j := indexLetter(s[i+1:])
 124             if j < 0 {
 125                 return i, -1
 126             }
 127             return i - 1, i + 1 + j + 1
 128         }
 129 
 130         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 131             j := indexPair(s[i+1:], '\x1b', '\\')
 132             if j < 0 {
 133                 return i, -1
 134             }
 135             return i - 1, i + 1 + j + 2
 136         }
 137 
 138         prev = b
 139     }
 140 
 141     return -1, -1
 142 }
 143 
 144 func indexLetter(s []byte) int {
 145     for i, b := range s {
 146         upper := b &^ 32
 147         if 'A' <= upper && upper <= 'Z' {
 148             return i
 149         }
 150     }
 151 
 152     return -1
 153 }
 154 
 155 func indexPair(s []byte, x byte, y byte) int {
 156     var prev byte
 157 
 158     for i, b := range s {
 159         if prev == x && b == y && i > 0 {
 160             return i
 161         }
 162         prev = b
 163     }
 164 
 165     return -1
 166 }
 167 
 168 func plain(w *bufio.Writer, r io.Reader, live bool) error {
 169     const gb = 1024 * 1024 * 1024
 170     sc := bufio.NewScanner(r)
 171     sc.Buffer(nil, 8*gb)
 172 
 173     for i := 0; sc.Scan(); i++ {
 174         s := sc.Bytes()
 175         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 176             s = s[3:]
 177         }
 178 
 179         for line := s; len(line) > 0; {
 180             i, j := indexEscapeSequence(line)
 181             if i < 0 {
 182                 w.Write(line)
 183                 break
 184             }
 185             if j < 0 {
 186                 j = len(line)
 187             }
 188 
 189             if i > 0 {
 190                 w.Write(line[:i])
 191             }
 192 
 193             line = line[j:]
 194         }
 195 
 196         if w.WriteByte('\n') != nil {
 197             return io.EOF
 198         }
 199 
 200         if !live {
 201             continue
 202         }
 203 
 204         if err := w.Flush(); err != nil {
 205             return io.EOF
 206         }
 207     }
 208 
 209     return sc.Err()
 210 }
     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         }
  61 
  62         if n < 0 {
  63             n = 0
  64         }
  65         howMany = n
  66     }
  67 
  68     primes(howMany)
  69 }
  70 
  71 func primes(left int) {
  72     bw := bufio.NewWriter(os.Stdout)
  73     defer bw.Flush()
  74 
  75     // 24 bytes are always enough for any 64-bit integer
  76     var buf [24]byte
  77 
  78     // 2 is the only even prime number
  79     if left > 0 {
  80         bw.WriteString("2\n")
  81         left--
  82     }
  83 
  84     for n := uint64(3); left > 0; n += 2 {
  85         if oddPrime(n) {
  86             bw.Write(strconv.AppendUint(buf[:0], n, 10))
  87             if err := bw.WriteByte('\n'); err != nil {
  88                 // assume errors come from closed stdout pipes
  89                 return
  90             }
  91             left--
  92         }
  93     }
  94 }
  95 
  96 // oddPrime assumes the number given to it is odd
  97 func oddPrime(n uint64) bool {
  98     max := uint64(math.Sqrt(float64(n)))
  99     for div := uint64(3); div <= max; div += 2 {
 100         if n%div == 0 {
 101             return false
 102         }
 103     }
 104     return true
 105 }
     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     }
  80 }
  81 
  82 // table has all summary info gathered from the data, along with the row
  83 // themselves, stored as lines/strings
  84 type table struct {
  85     Columns int
  86 
  87     Rows []string
  88 
  89     MaxWidth []int
  90 
  91     MaxDotDecimals []int
  92 
  93     LoopItems func(s string, max int, t *table, f itemFunc)
  94 
  95     MaxColumns bool
  96 }
  97 
  98 type itemFunc func(i int, s string, t *table)
  99 
 100 func run(paths []string, maxCols bool) error {
 101     var res table
 102     res.MaxColumns = maxCols
 103 
 104     for _, p := range paths {
 105         if err := handleFile(&res, p); err != nil {
 106             return err
 107         }
 108     }
 109 
 110     if len(paths) == 0 {
 111         if err := handleReader(&res, os.Stdin); err != nil {
 112             return err
 113         }
 114     }
 115 
 116     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 117     defer bw.Flush()
 118     realign(bw, res)
 119     return nil
 120 }
 121 
 122 func handleFile(res *table, path string) error {
 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 handleReader(res, f)
 131 }
 132 
 133 func handleReader(t *table, r io.Reader) error {
 134     const gb = 1024 * 1024 * 1024
 135     sc := bufio.NewScanner(r)
 136     sc.Buffer(nil, 8*gb)
 137 
 138     const maxInt = int(^uint(0) >> 1)
 139     maxCols := maxInt
 140 
 141     for i := 0; sc.Scan(); i++ {
 142         s := sc.Text()
 143         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 144             s = s[3:]
 145         }
 146 
 147         if len(s) == 0 {
 148             if len(t.Rows) > 0 {
 149                 t.Rows = append(t.Rows, ``)
 150             }
 151             continue
 152         }
 153 
 154         t.Rows = append(t.Rows, s)
 155 
 156         if t.Columns == 0 {
 157             if t.LoopItems == nil {
 158                 if strings.IndexByte(s, '\t') >= 0 {
 159                     t.LoopItems = loopItemsTSV
 160                 } else {
 161                     t.LoopItems = loopItemsSSV
 162                 }
 163             }
 164 
 165             if !t.MaxColumns {
 166                 t.LoopItems(s, maxCols, t, updateColumnCount)
 167                 maxCols = t.Columns
 168             }
 169         }
 170 
 171         t.LoopItems(s, maxCols, t, updateItem)
 172     }
 173 
 174     return sc.Err()
 175 }
 176 
 177 func updateColumnCount(i int, s string, t *table) {
 178     t.Columns = i + 1
 179 }
 180 
 181 func updateItem(i int, s string, t *table) {
 182     // ensure column-info-slices have enough room
 183     if i >= len(t.MaxWidth) {
 184         // update column-count if in max-columns mode
 185         if t.MaxColumns {
 186             t.Columns = i + 1
 187         }
 188         t.MaxWidth = append(t.MaxWidth, 0)
 189         t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
 190     }
 191 
 192     // keep track of widest rune-counts for each column
 193     w := countWidth(s)
 194     if t.MaxWidth[i] < w {
 195         t.MaxWidth[i] = w
 196     }
 197 
 198     // update stats for numeric items
 199     if isNumeric(s) {
 200         dd := countDotDecimals(s)
 201         if t.MaxDotDecimals[i] < dd {
 202             t.MaxDotDecimals[i] = dd
 203         }
 204     }
 205 }
 206 
 207 // loopItemsSSV loops over a line's items, allocation-free style; when given
 208 // empty strings, the callback func is never called
 209 func loopItemsSSV(s string, max int, t *table, f itemFunc) {
 210     s = trimTrailingSpaces(s)
 211 
 212     for i := 0; true; i++ {
 213         s = trimLeadingSpaces(s)
 214         if len(s) == 0 {
 215             return
 216         }
 217 
 218         if i+1 == max {
 219             f(i, s, t)
 220             return
 221         }
 222 
 223         j := strings.IndexByte(s, ' ')
 224         if j < 0 {
 225             f(i, s, t)
 226             return
 227         }
 228 
 229         f(i, s[:j], t)
 230         s = s[j+1:]
 231     }
 232 }
 233 
 234 func trimLeadingSpaces(s string) string {
 235     for len(s) > 0 && s[0] == ' ' {
 236         s = s[1:]
 237     }
 238     return s
 239 }
 240 
 241 func trimTrailingSpaces(s string) string {
 242     for len(s) > 0 && s[len(s)-1] == ' ' {
 243         s = s[:len(s)-1]
 244     }
 245     return s
 246 }
 247 
 248 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 249 // when given empty strings, the callback func is never called
 250 func loopItemsTSV(s string, max int, t *table, f itemFunc) {
 251     if len(s) == 0 {
 252         return
 253     }
 254 
 255     for i := 0; true; i++ {
 256         if i+1 == max {
 257             f(i, s, t)
 258             return
 259         }
 260 
 261         j := strings.IndexByte(s, '\t')
 262         if j < 0 {
 263             f(i, s, t)
 264             return
 265         }
 266 
 267         f(i, s[:j], t)
 268         s = s[j+1:]
 269     }
 270 }
 271 
 272 func skipLeadingEscapeSequences(s string) string {
 273     for len(s) >= 2 {
 274         if s[0] != '\x1b' {
 275             return s
 276         }
 277 
 278         switch s[1] {
 279         case '[':
 280             s = skipSingleLeadingANSI(s[2:])
 281 
 282         case ']':
 283             if len(s) < 3 || s[2] != '8' {
 284                 return s
 285             }
 286             s = skipSingleLeadingOSC(s[3:])
 287 
 288         default:
 289             return s
 290         }
 291     }
 292 
 293     return s
 294 }
 295 
 296 func skipSingleLeadingANSI(s string) string {
 297     for len(s) > 0 {
 298         upper := s[0] &^ 32
 299         s = s[1:]
 300         if 'A' <= upper && upper <= 'Z' {
 301             break
 302         }
 303     }
 304 
 305     return s
 306 }
 307 
 308 func skipSingleLeadingOSC(s string) string {
 309     var prev byte
 310 
 311     for len(s) > 0 {
 312         b := s[0]
 313         s = s[1:]
 314         if prev == '\x1b' && b == '\\' {
 315             break
 316         }
 317         prev = b
 318     }
 319 
 320     return s
 321 }
 322 
 323 // isNumeric checks if a string is valid/useable as a number
 324 func isNumeric(s string) bool {
 325     if len(s) == 0 {
 326         return false
 327     }
 328 
 329     s = skipLeadingEscapeSequences(s)
 330     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 331         s = s[1:]
 332     }
 333 
 334     s = skipLeadingEscapeSequences(s)
 335     if len(s) == 0 {
 336         return false
 337     }
 338     if s[0] == '.' {
 339         return isDigits(s[1:])
 340     }
 341 
 342     digits := 0
 343 
 344     for {
 345         s = skipLeadingEscapeSequences(s)
 346         if len(s) == 0 {
 347             break
 348         }
 349 
 350         if s[0] == '.' {
 351             return isDigits(s[1:])
 352         }
 353 
 354         if !('0' <= s[0] && s[0] <= '9') {
 355             return false
 356         }
 357 
 358         digits++
 359         s = s[1:]
 360     }
 361 
 362     s = skipLeadingEscapeSequences(s)
 363     return len(s) == 0 && digits > 0
 364 }
 365 
 366 func isDigits(s string) bool {
 367     if len(s) == 0 {
 368         return false
 369     }
 370 
 371     digits := 0
 372 
 373     for {
 374         s = skipLeadingEscapeSequences(s)
 375         if len(s) == 0 {
 376             break
 377         }
 378 
 379         if '0' <= s[0] && s[0] <= '9' {
 380             s = s[1:]
 381             digits++
 382         } else {
 383             return false
 384         }
 385     }
 386 
 387     s = skipLeadingEscapeSequences(s)
 388     return len(s) == 0 && digits > 0
 389 }
 390 
 391 // countDecimals counts decimal digits from the string given, assuming it
 392 // represents a valid/useable float64, when parsed
 393 func countDecimals(s string) int {
 394     dot := strings.IndexByte(s, '.')
 395     if dot < 0 {
 396         return 0
 397     }
 398 
 399     decs := 0
 400     s = s[dot+1:]
 401 
 402     for len(s) > 0 {
 403         s = skipLeadingEscapeSequences(s)
 404         if len(s) == 0 {
 405             break
 406         }
 407         if '0' <= s[0] && s[0] <= '9' {
 408             decs++
 409         }
 410         s = s[1:]
 411     }
 412 
 413     return decs
 414 }
 415 
 416 // countDotDecimals is like func countDecimals, but this one also includes
 417 // the dot, when any decimals are present, else the count stays at 0
 418 func countDotDecimals(s string) int {
 419     decs := countDecimals(s)
 420     if decs > 0 {
 421         return decs + 1
 422     }
 423     return decs
 424 }
 425 
 426 func countWidth(s string) int {
 427     width := 0
 428 
 429     for len(s) > 0 {
 430         i, j := indexEscapeSequence(s)
 431         if i < 0 {
 432             break
 433         }
 434         if j < 0 {
 435             j = len(s)
 436         }
 437 
 438         width += utf8.RuneCountInString(s[:i])
 439         s = s[j:]
 440     }
 441 
 442     // count trailing/all runes in strings which don't end with ANSI-sequences
 443     width += utf8.RuneCountInString(s)
 444     return width
 445 }
 446 
 447 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 448 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 449 // indices which can be independently negative when either the start/end of
 450 // a sequence isn't found; given their fairly-common use, even the hyperlink
 451 // ESC]8 sequences are supported
 452 func indexEscapeSequence(s string) (int, int) {
 453     var prev byte
 454 
 455     for i := range s {
 456         b := s[i]
 457 
 458         if prev == '\x1b' && b == '[' {
 459             j := indexLetter(s[i+1:])
 460             if j < 0 {
 461                 return i, -1
 462             }
 463             return i - 1, i + 1 + j + 1
 464         }
 465 
 466         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 467             j := indexPair(s[i+1:], '\x1b', '\\')
 468             if j < 0 {
 469                 return i, -1
 470             }
 471             return i - 1, i + 1 + j + 2
 472         }
 473 
 474         prev = b
 475     }
 476 
 477     return -1, -1
 478 }
 479 
 480 func indexLetter(s string) int {
 481     for i, b := range s {
 482         upper := b &^ 32
 483         if 'A' <= upper && upper <= 'Z' {
 484             return i
 485         }
 486     }
 487 
 488     return -1
 489 }
 490 
 491 func indexPair(s string, x byte, y byte) int {
 492     var prev byte
 493 
 494     for i := range s {
 495         b := s[i]
 496         if prev == x && b == y && i > 0 {
 497             return i
 498         }
 499         prev = b
 500     }
 501 
 502     return -1
 503 }
 504 
 505 func realign(w *bufio.Writer, t table) {
 506     due := 0
 507     showItem := func(i int, s string, t *table) {
 508         if i > 0 {
 509             due += 2
 510         }
 511 
 512         if isNumeric(s) {
 513             dd := countDotDecimals(s)
 514             rpad := t.MaxDotDecimals[i] - dd
 515             width := countWidth(s)
 516             lpad := t.MaxWidth[i] - (width + rpad) + due
 517             writeSpaces(w, lpad)
 518             w.WriteString(s)
 519             due = rpad
 520             return
 521         }
 522 
 523         writeSpaces(w, due)
 524         w.WriteString(s)
 525         due = t.MaxWidth[i] - countWidth(s)
 526     }
 527 
 528     for _, line := range t.Rows {
 529         due = 0
 530         if len(line) > 0 {
 531             t.LoopItems(line, t.Columns, &t, showItem)
 532         }
 533         if w.WriteByte('\n') != nil {
 534             break
 535         }
 536     }
 537 }
 538 
 539 // writeSpaces does what it says, minimizing calls to write-like funcs
 540 func writeSpaces(w *bufio.Writer, n int) {
 541     const spaces = `                                `
 542     if n < 1 {
 543         return
 544     }
 545 
 546     for n >= len(spaces) {
 547         w.WriteString(spaces)
 548         n -= len(spaces)
 549     }
 550     w.WriteString(spaces[:n])
 551 }
     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     var tests = []struct {
  31         name     string
  32         input    string
  33         expected int
  34     }{
  35         {`empty`, ``, 0},
  36         {`empty ANSI`, "\x1b[38;5;0;0;0m\x1b[0m", 0},
  37         {`simple plain`, `abc def`, 7},
  38         {`unicode plain`, `abc●def`, 7},
  39         {`simple ANSI`, "abc \x1b[7mde\x1b[0mf", 7},
  40         {`unicode ANSI`, "abc●\x1b[7mde\x1b[0mf", 7},
  41     }
  42 
  43     for _, tc := range tests {
  44         t.Run(tc.name, func(t *testing.T) {
  45             got := countWidth(tc.input)
  46             if got != tc.expected {
  47                 t.Errorf("expected width %d, got %d instead", tc.expected, got)
  48             }
  49         })
  50     }
  51 }
     File: ./remakes/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                 os.Exit(0)
  62             }
  63 
  64             os.Stderr.WriteString(err.Error())
  65             os.Stderr.WriteString("\n")
  66             os.Exit(1)
  67         }
  68     }
  69 
  70     if len(args) == 0 {
  71         cat(os.Stdout, os.Stdin)
  72     }
  73 }
  74 
  75 func handleFile(w io.Writer, path string) error {
  76     f, err := os.Open(path)
  77     if err != nil {
  78         return err
  79     }
  80     defer f.Close()
  81     return cat(w, f)
  82 }
  83 
  84 func cat(w io.Writer, r io.Reader) error {
  85     var buf [32 * 1024]byte
  86 
  87     for {
  88         got, err := r.Read(buf[:])
  89         if err == io.EOF {
  90             if got > 0 {
  91                 w.Write(buf[:got])
  92             }
  93             break
  94         }
  95 
  96         if err != nil {
  97             return err
  98         }
  99 
 100         if _, err := w.Write(buf[:got]); err != nil {
 101             return io.EOF
 102         }
 103     }
 104 
 105     return nil
 106 }
     File: ./remakes/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 )
  33 
  34 const info = `
  35 head [options...] [files...]
  36 
  37 Keep at most the first n lines, or keep the first 10 lines by default. When
  38 not given any filepaths, the standard input is used instead.
  39 
  40 Options
  41 
  42     -n [number]    change max number of lines (default is 10)
  43 `
  44 
  45 type config struct {
  46     max int
  47 }
  48 
  49 func Main() {
  50     var cfg config
  51     cfg.max = 10
  52 
  53     args := os.Args[1:]
  54     for len(args) > 0 {
  55         switch args[0] {
  56         case `-n`:
  57             args = args[1:]
  58             if len(args) == 0 {
  59                 os.Stderr.WriteString("missing number of lines\n")
  60                 os.Exit(1)
  61             }
  62             n, err := strconv.ParseInt(args[0], 10, 64)
  63             if err != nil {
  64                 os.Stderr.WriteString("invalid number: ")
  65                 os.Stderr.WriteString(err.Error())
  66                 os.Stderr.WriteString("\n")
  67                 os.Exit(1)
  68             }
  69             args = args[1:]
  70             cfg.max = int(n)
  71             continue
  72 
  73         case `--help`:
  74             os.Stderr.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.max <= 0 {
  86         os.Exit(0)
  87     }
  88 
  89     if err := run(args, &cfg); err != nil {
  90         if err == io.EOF {
  91             os.Exit(0)
  92         }
  93 
  94         os.Stderr.WriteString(err.Error())
  95         os.Stderr.WriteString("\n")
  96         os.Exit(1)
  97     }
  98 }
  99 
 100 func run(paths []string, cfg *config) error {
 101     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 102     defer w.Flush()
 103 
 104     for _, path := range paths {
 105         if cfg.max <= 0 {
 106             return io.EOF
 107         }
 108         if err := handleFile(w, path, cfg); err != nil {
 109             return err
 110         }
 111     }
 112 
 113     if len(paths) == 0 {
 114         if err := head(w, os.Stdin, cfg); err != nil {
 115             return err
 116         }
 117     }
 118     return nil
 119 }
 120 
 121 func handleFile(w *bufio.Writer, path string, cfg *config) error {
 122     f, err := os.Open(path)
 123     if err != nil {
 124         return err
 125     }
 126     defer f.Close()
 127     return head(w, f, cfg)
 128 }
 129 
 130 func head(w *bufio.Writer, r io.Reader, cfg *config) error {
 131     const gb = 1024 * 1024 * 1024
 132     sc := bufio.NewScanner(r)
 133     sc.Buffer(nil, 8*gb)
 134 
 135     for sc.Scan() {
 136         if cfg.max <= 0 {
 137             return io.EOF
 138         }
 139 
 140         w.Write(sc.Bytes())
 141         if err := w.WriteByte('\n'); err != nil {
 142             return io.EOF
 143         }
 144 
 145         cfg.max--
 146     }
 147 
 148     return sc.Err()
 149 }
     File: ./remakes/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 {
  81         if err == io.EOF {
  82             return
  83         }
  84 
  85         os.Stderr.WriteString(err.Error())
  86         os.Stderr.WriteString("\n")
  87         os.Exit(1)
  88     }
  89 }
  90 
  91 func run(paths []string, cfg config) error {
  92     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  93     defer w.Flush()
  94 
  95     for i, path := range paths {
  96         if len(paths) > 1 {
  97             w.WriteString(path)
  98             w.WriteString(":\n")
  99             if i > 0 {
 100                 w.WriteString("\n")
 101             }
 102         }
 103 
 104         if err := ls(w, path, cfg); err != nil {
 105             return err
 106         }
 107     }
 108 
 109     if len(paths) == 0 {
 110         return ls(w, `.`, cfg)
 111     }
 112     return nil
 113 }
 114 
 115 func ls(w *bufio.Writer, path string, cfg config) error {
 116     defer w.Flush()
 117 
 118     entries, err := os.ReadDir(path)
 119     if err != nil {
 120         return err
 121     }
 122 
 123     sort.SliceStable(entries, func(i, j int) bool {
 124         return compareNames(entries[i].Name(), entries[j].Name()) < 0
 125     })
 126 
 127     for _, e := range entries {
 128         name := e.Name()
 129 
 130         if !cfg.all && len(name) > 0 && name[0] == '.' {
 131             continue
 132         }
 133 
 134         w.WriteString(name)
 135         if _, err := w.WriteString("\n"); err != nil {
 136             return io.EOF
 137         }
 138     }
 139 
 140     return nil
 141 }
 142 
 143 func compareNames(x, y string) int {
 144     if len(x) < len(y) && strings.HasPrefix(y, x) {
 145         return -1
 146     }
 147     if len(y) < len(x) && strings.HasPrefix(x, y) {
 148         return +1
 149     }
 150     return strings.Compare(x, y)
 151 }
     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     }
  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 squeeze(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 squeeze(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 squeeze(w, f, live)
 105 }
 106 
 107 func squeeze(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         writeSqueezed(w, s)
 119         if w.WriteByte('\n') != nil {
 120             return io.EOF
 121         }
 122 
 123         if !live {
 124             continue
 125         }
 126 
 127         if err := w.Flush(); err != nil {
 128             return io.EOF
 129         }
 130     }
 131 
 132     return sc.Err()
 133 }
 134 
 135 func writeSqueezed(w *bufio.Writer, s []byte) {
 136     // ignore leading spaces
 137     for len(s) > 0 && s[0] == ' ' {
 138         s = s[1:]
 139     }
 140 
 141     // ignore trailing spaces
 142     for len(s) > 0 && s[len(s)-1] == ' ' {
 143         s = s[:len(s)-1]
 144     }
 145 
 146     space := false
 147 
 148     for len(s) > 0 {
 149         switch s[0] {
 150         case ' ':
 151             s = s[1:]
 152             space = true
 153 
 154         case '\t':
 155             s = s[1:]
 156             space = false
 157             for len(s) > 0 && s[0] == ' ' {
 158                 s = s[1:]
 159             }
 160             w.WriteByte('\t')
 161 
 162         default:
 163             if space {
 164                 w.WriteByte(' ')
 165                 space = false
 166             }
 167             w.WriteByte(s[0])
 168             s = s[1:]
 169         }
 170     }
 171 }
     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     }
  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 tcatl(bw, os.Stdin, `<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 := tcatl(bw, os.Stdin, `<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 tcatl(w, os.Stdin, `<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 tcatl(w, f, name, cfg)
 173 }
 174 
 175 func tcatl(w *bufio.Writer, r io.Reader, name string, cfg config) error {
 176     w.WriteString("\x1b[7m")
 177     w.WriteString(name)
 178     writeSpaces(w, 80-utf8.RuneCountInString(name))
 179     w.WriteString("\x1b[0m\n")
 180     if err := w.Flush(); err != nil {
 181         // a write error may be the consequence of stdout being closed,
 182         // perhaps by another app along a pipe
 183         return io.EOF
 184     }
 185 
 186     if !cfg.liveLines {
 187         return catlFast(w, r, cfg.null)
 188     }
 189 
 190     const gb = 1024 * 1024 * 1024
 191     sc := bufio.NewScanner(r)
 192     sc.Buffer(nil, 8*gb)
 193     if cfg.null {
 194         sc.Split(splitNull)
 195     }
 196 
 197     for i := 0; sc.Scan(); i++ {
 198         s := sc.Bytes()
 199         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 200             s = s[3:]
 201         }
 202 
 203         w.Write(s)
 204         if w.WriteByte('\n') != nil {
 205             return io.EOF
 206         }
 207 
 208         if err := w.Flush(); err != nil {
 209             return io.EOF
 210         }
 211     }
 212 
 213     return sc.Err()
 214 }
 215 
 216 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
 217     var buf [32 * 1024]byte
 218     var last byte = '\n'
 219 
 220     for i := 0; true; i++ {
 221         n, err := r.Read(buf[:])
 222         if n > 0 && err == io.EOF {
 223             err = nil
 224         }
 225         if err == io.EOF {
 226             if last != '\n' {
 227                 w.WriteByte('\n')
 228             }
 229             return nil
 230         }
 231 
 232         if err != nil {
 233             return err
 234         }
 235 
 236         chunk := buf[:n]
 237         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 238             chunk = chunk[3:]
 239         }
 240 
 241         // change nulls into line-feeds to handle null-terminated lines
 242         if null {
 243             for i, b := range chunk {
 244                 if b == 0 {
 245                     chunk[i] = '\n'
 246                 }
 247             }
 248         }
 249 
 250         if len(chunk) >= 1 {
 251             if _, err := w.Write(chunk); err != nil {
 252                 return io.EOF
 253             }
 254             last = chunk[len(chunk)-1]
 255         }
 256     }
 257 
 258     return nil
 259 }
 260 
 261 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
 262 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
 263     // handle leading null-terminated line, if found in the current chunk
 264     if i := bytes.IndexByte(data, 0); i >= 0 {
 265         return i + 1, data[:i], nil
 266     }
 267 
 268     // request more data, in case there's a null coming up later
 269     if !atEOF {
 270         return 0, nil, nil
 271     }
 272 
 273     // handle non-empty non-terminated last chunk
 274     if len(data) > 0 {
 275         return len(data), data, bufio.ErrFinalToken
 276     }
 277 
 278     // handle empty non-terminated last chunk
 279     return 0, nil, bufio.ErrFinalToken
 280 }
 281 
 282 // writeSpaces bulk-emits the number of spaces given
 283 func writeSpaces(w *bufio.Writer, n int) {
 284     const spaces = `                                `
 285     for ; n > len(spaces); n -= len(spaces) {
 286         w.WriteString(spaces)
 287     }
 288     if n > 0 {
 289         w.WriteString(spaces[:n])
 290     }
 291 }
     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     }
  68 }
  69 
  70 func run(w io.Writer, args []string) error {
  71     dashes := 0
  72     for _, name := range args {
  73         if name == `-` {
  74             dashes++
  75         }
  76         if dashes > 1 {
  77             return errors.New(`can't read stdin (dash) more than once`)
  78         }
  79     }
  80 
  81     if len(args) == 0 {
  82         return teletype(w, os.Stdin)
  83     }
  84 
  85     for _, name := range args {
  86         if name == `-` {
  87             if err := teletype(w, os.Stdin); err != nil {
  88                 return err
  89             }
  90             continue
  91         }
  92 
  93         if err := handleFile(w, name); err != nil {
  94             return err
  95         }
  96     }
  97     return nil
  98 }
  99 
 100 func handleFile(w io.Writer, name string) error {
 101     if name == `` || name == `-` {
 102         return teletype(w, os.Stdin)
 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 teletype(w, f)
 112 }
 113 
 114 func teletype(w io.Writer, 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         s := sc.Text()
 121         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 122             s = s[3:]
 123         }
 124 
 125         var buf [4]byte
 126         for _, r := range s {
 127             time.Sleep(15 * time.Millisecond)
 128             if _, err := w.Write(utf8.AppendRune(buf[:0], r)); err != nil {
 129                 return io.EOF
 130             }
 131         }
 132 
 133         time.Sleep(750 * time.Millisecond)
 134         if _, err := w.Write([]byte{'\n'}); err != nil {
 135             return io.EOF
 136         }
 137     }
 138 
 139     return sc.Err()
 140 }
     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     }
  74 }
  75 
  76 func run(w io.Writer, args []string) error {
  77     bw := bufio.NewWriter(w)
  78     defer bw.Flush()
  79 
  80     from := ``
  81     low := ``
  82     var values []float64
  83 
  84     dump := func(unit string) bool {
  85         if s, ok := aliases[unit]; ok {
  86             unit = s
  87         }
  88 
  89         c, ok := converters[unit]
  90         if !ok {
  91             return false
  92         }
  93 
  94         for _, v := range values {
  95             res := c.Mul*v + c.Add
  96             const fs = "%.4f  %-4s =  %.4f  %s\n"
  97             fmt.Fprintf(bw, fs, v, unit, res, c.To)
  98         }
  99         return true
 100     }
 101 
 102     for _, s := range args {
 103         f, err := strconv.ParseFloat(s, 64)
 104         if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
 105             values = append(values, f)
 106             continue
 107         }
 108 
 109         if len(values) == 0 {
 110             values = append(values, 1)
 111         }
 112         from = s
 113         low = strings.ToLower(from)
 114 
 115         if dump(low) {
 116             values = values[:0]
 117             from = ``
 118             low = ``
 119             continue
 120         }
 121 
 122         return fmt.Errorf("unit %q not supported\n", low)
 123     }
 124 
 125     if from == `` {
 126         // return errors.New(`no source units given`)
 127 
 128         if len(values) == 0 {
 129             values = []float64{1}
 130         }
 131 
 132         units := make([]string, 0, len(converters))
 133         for k := range converters {
 134             units = append(units, k)
 135         }
 136         sort.Strings(units)
 137         for _, from := range units {
 138             dump(from)
 139         }
 140         return nil
 141     }
 142 
 143     if !dump(from) {
 144         return fmt.Errorf("unit %q not supported\n", low)
 145     }
 146     return nil
 147 }
 148 
 149 var aliases = map[string]string{
 150     `acre`:    `ac`,
 151     `acres`:   `ac`,
 152     `days`:    `day`,
 153     `foot`:    `ft`,
 154     `feet`:    `ft`,
 155     `feet2`:   `ft²`,
 156     `feet3`:   `ft³`,
 157     `foot2`:   `ft²`,
 158     `foot3`:   `ft³`,
 159     `ft2`:     `ft²`,
 160     `ft3`:     `ft³`,
 161     `gallon`:  `gal`,
 162     `gallons`: `gal`,
 163     `gals`:    `gal`,
 164     `inch`:    `in`,
 165     `inches`:  `in`,
 166     `mile`:    `mi`,
 167     `miles`:   `mi`,
 168     `mile²`:   `mi²`,
 169     `miles²`:  `mi²`,
 170     `minute`:  `min`,
 171     `minutes`: `min`,
 172     `nmile`:   `nmi`,
 173     `nmiles`:  `nmi`,
 174     `ounce`:   `oz`,
 175     `ounces`:  `oz`,
 176     `ozs`:     `oz`,
 177     `weeks`:   `week`,
 178     `wk`:      `week`,
 179     `wks`:     `week`,
 180     `yard`:    `yd`,
 181     `yards`:   `yd`,
 182     `yard2`:   `yd²`,
 183     `yard²`:   `yd²`,
 184     `yards2`:  `yd²`,
 185     `yards²`:  `yd²`,
 186     `yds`:     `yd`,
 187     `yds2`:    `yd²`,
 188     `yds²`:    `yd²`,
 189 }
 190 
 191 type converter struct {
 192     To  string
 193     Mul float64
 194     Add float64
 195 }
 196 
 197 var converters = map[string]converter{
 198     `ac`:   converter{`m²`, 4046.8564224, 0},
 199     `day`:  converter{`s`, 86400, 0},
 200     `ft`:   converter{`m`, 0.3048, 0},
 201     `ft²`:  converter{`m²`, 0.09290304, 0},
 202     `ft³`:  converter{`m³`, 0.028316846592, 0},
 203     `gal`:  converter{`L`, 3.785411784, 0},
 204     `in`:   converter{`cm`, 2.54, 0},
 205     `mi`:   converter{`km`, 1.609344, 0},
 206     `mi²`:  converter{`km²`, 2.5899881103360, 0},
 207     `min`:  converter{`s`, 60, 0},
 208     `mpg`:  converter{`kpl`, 0.425143707, 0},
 209     `mph`:  converter{`kph`, 1.609344, 0},
 210     `nmi`:  converter{`km`, 1.852, 0},
 211     `oz`:   converter{`g`, 28.349523125, 0},
 212     `week`: converter{`s`, 604800, 0},
 213     `yd`:   converter{`m`, 0.9144, 0},
 214     `yd²`:  converter{`m²`, 0.83612736, 0},
 215 }
     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     }
  70 }
  71 
  72 func run(w io.Writer, args []string) error {
  73     bw := bufio.NewWriter(w)
  74     defer bw.Flush()
  75 
  76     for _, path := range args {
  77         if err := handleFile(bw, path); err != nil {
  78             return err
  79         }
  80     }
  81 
  82     if len(args) == 0 {
  83         return utfate(bw, os.Stdin)
  84     }
  85     return nil
  86 }
  87 
  88 func handleFile(w *bufio.Writer, name string) error {
  89     if name == `-` {
  90         return utfate(w, os.Stdin)
  91     }
  92 
  93     f, err := os.Open(name)
  94     if err != nil {
  95         return errors.New(`can't read from file named "` + name + `"`)
  96     }
  97     defer f.Close()
  98 
  99     return utfate(w, f)
 100 }
 101 
 102 func utfate(w io.Writer, r io.Reader) error {
 103     br := bufio.NewReader(r)
 104     bw := bufio.NewWriter(w)
 105     defer bw.Flush()
 106 
 107     lead, err := br.Peek(4)
 108     if err != nil && err != io.EOF {
 109         return err
 110     }
 111 
 112     if bytes.HasPrefix(lead, []byte{'\x00', '\x00', '\xfe', '\xff'}) {
 113         br.Discard(4)
 114         return utf32toUTF8(bw, br, binary.BigEndian)
 115     }
 116 
 117     if bytes.HasPrefix(lead, []byte{'\xff', '\xfe', '\x00', '\x00'}) {
 118         br.Discard(4)
 119         return utf32toUTF8(bw, br, binary.LittleEndian)
 120     }
 121 
 122     if bytes.HasPrefix(lead, []byte{'\xfe', '\xff'}) {
 123         br.Discard(2)
 124         return utf16toUTF8(bw, br, readBytePairBE)
 125     }
 126 
 127     if bytes.HasPrefix(lead, []byte{'\xff', '\xfe'}) {
 128         br.Discard(2)
 129         return utf16toUTF8(bw, br, readBytePairLE)
 130     }
 131 
 132     if bytes.HasPrefix(lead, []byte{'\xef', '\xbb', '\xbf'}) {
 133         br.Discard(3)
 134         return handleUTF8(bw, br)
 135     }
 136 
 137     return handleUTF8(bw, br)
 138 }
 139 
 140 func handleUTF8(w *bufio.Writer, r *bufio.Reader) error {
 141     for {
 142         c, _, err := r.ReadRune()
 143         if c == unicode.ReplacementChar {
 144             return errors.New(`invalid UTF-8 stream`)
 145         }
 146         if err == io.EOF {
 147             return nil
 148         }
 149         if err != nil {
 150             return err
 151         }
 152 
 153         if _, err := w.WriteRune(c); err != nil {
 154             return io.EOF
 155         }
 156     }
 157 }
 158 
 159 // fancyHandleUTF8 is kept only for reference, as its attempts at being clever
 160 // don't seem to speed things up much when given ASCII input
 161 func fancyHandleUTF8(w *bufio.Writer, r *bufio.Reader) error {
 162     lookahead := 1
 163     maxAhead := r.Size() / 2
 164 
 165     for {
 166         // look ahead to check for ASCII runs
 167         ahead, err := r.Peek(lookahead)
 168         if err == io.EOF {
 169             return nil
 170         }
 171         if err != nil {
 172             return err
 173         }
 174 
 175         // copy leading ASCII runs
 176         n := leadASCII(ahead)
 177         if n > 0 {
 178             w.Write(ahead[:n])
 179             r.Discard(n)
 180         }
 181 
 182         // adapt lookahead size
 183         if n == len(ahead) && lookahead < maxAhead {
 184             lookahead *= 2
 185         } else if lookahead > 1 {
 186             lookahead /= 2
 187         }
 188 
 189         if n == len(ahead) {
 190             continue
 191         }
 192 
 193         c, _, err := r.ReadRune()
 194         if c == unicode.ReplacementChar {
 195             return errors.New(`invalid UTF-8 stream`)
 196         }
 197         if err == io.EOF {
 198             return nil
 199         }
 200         if err != nil {
 201             return err
 202         }
 203 
 204         if _, err := w.WriteRune(c); err != nil {
 205             return io.EOF
 206         }
 207     }
 208 }
 209 
 210 // leadASCII is used by func fancyHandleUTF8
 211 func leadASCII(buf []byte) int {
 212     for i, b := range buf {
 213         if b >= 128 {
 214             return i
 215         }
 216     }
 217     return len(buf)
 218 }
 219 
 220 // readPairFunc narrows source-code lines below
 221 type readPairFunc func(*bufio.Reader) (byte, byte, error)
 222 
 223 // utf16toUTF8 handles UTF-16 inputs for func utfate
 224 func utf16toUTF8(w *bufio.Writer, r *bufio.Reader, read2 readPairFunc) error {
 225     for {
 226         a, b, err := read2(r)
 227         if err == io.EOF {
 228             return nil
 229         }
 230         if err != nil {
 231             return err
 232         }
 233 
 234         c := rune(256*int(a) + int(b))
 235         if utf16.IsSurrogate(c) {
 236             a, b, err := read2(r)
 237             if err == io.EOF {
 238                 return nil
 239             }
 240             if err != nil {
 241                 return err
 242             }
 243 
 244             next := rune(256*int(a) + int(b))
 245             c = utf16.DecodeRune(c, next)
 246         }
 247 
 248         if _, err := w.WriteRune(c); err != nil {
 249             return io.EOF
 250         }
 251     }
 252 }
 253 
 254 // readBytePairBE gets you a pair of bytes in big-endian (original) order
 255 func readBytePairBE(br *bufio.Reader) (byte, byte, error) {
 256     a, err := br.ReadByte()
 257     if err != nil {
 258         return a, 0, err
 259     }
 260 
 261     b, err := br.ReadByte()
 262     return a, b, err
 263 }
 264 
 265 // readBytePairLE gets you a pair of bytes in little-endian order
 266 func readBytePairLE(br *bufio.Reader) (byte, byte, error) {
 267     a, b, err := readBytePairBE(br)
 268     return b, a, err
 269 }
 270 
 271 // utf32toUTF8 handles UTF-32 inputs for func utfate
 272 func utf32toUTF8(w *bufio.Writer, r *bufio.Reader, o binary.ByteOrder) error {
 273     var n uint32
 274     for {
 275         err := binary.Read(r, o, &n)
 276         if err == io.EOF {
 277             return nil
 278         }
 279         if err != nil {
 280             return err
 281         }
 282 
 283         if _, err := w.WriteRune(rune(n)); err != nil {
 284             return io.EOF
 285         }
 286     }
 287 }
     File: ./verdict/verdict.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package verdict
  26 
  27 import (
  28     "fmt"
  29     "io"
  30     "os"
  31     "os/exec"
  32     "strconv"
  33 )
  34 
  35 const info = `
  36 verdict [options...] [command...] [arguments...]
  37 
  38 
  39 Run the command given, colorfully showing its exit code on failure. On
  40 success, the exit code is 0, but it's not shown explicitly.
  41 
  42 A colorblind-friendly blue is used instead of green if either environment
  43 variable COLORBLIND or COLOR_BLIND is declared and set to 1.
  44 
  45 When variable NO_COLOR is declared and set to 1, an invert/highlight style
  46 is used instead of colors, no matter the exit code of the command given.
  47 
  48 All (optional) leading options start with either single or double-dash:
  49 
  50     -h, -help    show this help message
  51 `
  52 
  53 func Main() {
  54     liveLines := true
  55     args := os.Args[1:]
  56 
  57     for len(args) > 0 {
  58         switch args[0] {
  59         case `-b`, `--b`, `-buffered`, `--buffered`:
  60             liveLines = false
  61             args = args[1:]
  62             continue
  63 
  64         case `-h`, `--h`, `-help`, `--help`:
  65             os.Stdout.WriteString(info[1:])
  66             return
  67         }
  68 
  69         break
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     if liveLines {
  77         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  78             liveLines = false
  79         }
  80     }
  81 
  82     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  83         os.Stderr.WriteString(err.Error())
  84         os.Stderr.WriteString("\n")
  85         os.Exit(1)
  86     }
  87 }
  88 
  89 func run(w io.Writer, args []string, liveLines bool) error {
  90     if len(args) == 0 {
  91         return nil
  92     }
  93 
  94     cmd := exec.Command(args[0], args[1:]...)
  95 
  96     err := cmd.Wait()
  97     if err == nil {
  98         showVerdict(args, 0)
  99         return nil
 100     }
 101 
 102     if err, ok := err.(*exec.ExitError); ok {
 103         showVerdict(args, err.ExitCode())
 104         return nil
 105     }
 106 
 107     return err
 108 }
 109 
 110 func showVerdict(args []string, code int) {
 111     if checkBoolEnv(`NO_COLOR`) {
 112         for _, s := range args {
 113             os.Stderr.WriteString(s)
 114             os.Stderr.WriteString(` `)
 115         }
 116 
 117         if code == 0 {
 118             os.Stderr.WriteString("\x1b[7m succeeded \x1b[0m\n")
 119         } else {
 120             const fs = "\x1b[7m failed with error code %d \x1b[0m\n"
 121             fmt.Fprintf(os.Stderr, fs, code)
 122         }
 123         return
 124     }
 125 
 126     if code == 0 {
 127         if checkBoolEnv(`COLORBLIND`) || checkBoolEnv(`COLOR_BLIND`) {
 128             os.Stderr.WriteString("\x1b[48;2;0;95;215m")
 129             for _, s := range args {
 130                 os.Stderr.WriteString(s)
 131                 os.Stderr.WriteString(` `)
 132             }
 133             const style = "\x1b[48;2;0;95;215m\x1b[38;2;255;255;255m"
 134             os.Stderr.WriteString(style + " succeeded \x1b[0m\n")
 135         } else {
 136             os.Stderr.WriteString("\x1b[38;2;0;135;95m")
 137             for _, s := range args {
 138                 os.Stderr.WriteString(s)
 139                 os.Stderr.WriteString(` `)
 140             }
 141             const style = "\x1b[48;2;0;135;95m\x1b[38;2;255;255;255m"
 142             os.Stderr.WriteString(style + " succeeded \x1b[0m\n")
 143         }
 144         return
 145     }
 146 
 147     os.Stderr.WriteString("\x1b[38;2;204;0;0m")
 148     for _, s := range args {
 149         os.Stderr.WriteString(s)
 150         os.Stderr.WriteString(` `)
 151     }
 152 
 153     const style = "\x1b[48;2;204;0;0m\x1b[38;2;255;255;255m"
 154     const fs = style + " failed with error code %d \x1b[0m\n"
 155     fmt.Fprintf(os.Stderr, fs, code)
 156 }
 157 
 158 func checkBoolEnv(x string) bool {
 159     s, ok := os.LookupEnv(x)
 160     if !ok {
 161         return false
 162     }
 163 
 164     n, err := strconv.ParseInt(s, 10, 64)
 165     if err != nil {
 166         return false
 167     }
 168 
 169     return n != 0
 170 }
     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     }
  65 
  66     for _, s := range args {
  67         switch s {
  68         case `help`, `-h`, `--h`, `-help`, `--help`:
  69             fmt.Fprint(os.Stdout, usage)
  70             os.Exit(0)
  71         }
  72 
  73         err := cfg.handleArg(s)
  74         if err != nil {
  75             return cfg, err
  76         }
  77     }
  78 
  79     if math.IsNaN(cfg.MaxTime) {
  80         cfg.MaxTime = 1
  81     }
  82     if cfg.MaxTime < 0 {
  83         const fs = `error: given negative duration %f`
  84         return cfg, fmt.Errorf(fs, cfg.MaxTime)
  85     }
  86     return cfg, nil
  87 }
  88 
  89 func (c *config) handleArg(s string) error {
  90     switch s {
  91     case `44.1k`, `44.1K`:
  92         c.SampleRate = 44_100
  93         return nil
  94 
  95     case `48k`, `48K`:
  96         c.SampleRate = 48_000
  97         return nil
  98 
  99     case `dat`, `DAT`:
 100         c.SampleRate = 48_000
 101         return nil
 102 
 103     case `cd`, `cda`, `CD`, `CDA`:
 104         c.SampleRate = 44_100
 105         return nil
 106     }
 107 
 108     // handle output-format names and their aliases
 109     if kind, ok := name2type[s]; ok {
 110         c.To = kind
 111         return nil
 112     }
 113 
 114     // handle time formats, except when they're pure numbers
 115     if math.IsNaN(c.MaxTime) {
 116         dur, derr := ParseDuration(s)
 117         if derr == nil {
 118             c.MaxTime = float64(dur) / float64(time.Second)
 119             return nil
 120         }
 121     }
 122 
 123     // handle sample-rate, given either in hertz or kilohertz
 124     lc := strings.ToLower(s)
 125     if strings.HasSuffix(lc, `khz`) {
 126         lc = strings.TrimSuffix(lc, `khz`)
 127         khz, err := strconv.ParseFloat(lc, 64)
 128         if err != nil || isBadNumber(khz) || khz <= 0 {
 129             const fs = `invalid sample-rate frequency %q`
 130             return fmt.Errorf(fs, s)
 131         }
 132         c.SampleRate = uint(1_000 * khz)
 133         return nil
 134     } else if strings.HasSuffix(lc, `hz`) {
 135         lc = strings.TrimSuffix(lc, `hz`)
 136         hz, err := strconv.ParseUint(lc, 10, 64)
 137         if err != nil {
 138             const fs = `invalid sample-rate frequency %q`
 139             return fmt.Errorf(fs, s)
 140         }
 141         c.SampleRate = uint(hz)
 142         return nil
 143     }
 144 
 145     c.Scripts = append(c.Scripts, s)
 146     return nil
 147 }
 148 
 149 type encoding byte
 150 type headerType byte
 151 type sampleFormat byte
 152 
 153 const (
 154     directEncoding encoding = 1
 155     uriEncoding    encoding = 2
 156 
 157     noHeader  headerType = 1
 158     wavHeader headerType = 2
 159 
 160     int16BE   sampleFormat = 1
 161     int16LE   sampleFormat = 2
 162     float32BE sampleFormat = 3
 163     float32LE sampleFormat = 4
 164 )
 165 
 166 // name2type normalizes keys used for type2settings
 167 var name2type = map[string]string{
 168     `datauri`:  `data-uri`,
 169     `dataurl`:  `data-uri`,
 170     `data-uri`: `data-uri`,
 171     `data-url`: `data-uri`,
 172     `uri`:      `data-uri`,
 173     `url`:      `data-uri`,
 174 
 175     `raw`:     `raw`,
 176     `raw16be`: `raw16be`,
 177     `raw16le`: `raw16le`,
 178     `raw32be`: `raw32be`,
 179     `raw32le`: `raw32le`,
 180 
 181     `audio/x-wav`:  `wave-16`,
 182     `audio/x-wave`: `wave-16`,
 183     `wav`:          `wave-16`,
 184     `wave`:         `wave-16`,
 185     `wav16`:        `wave-16`,
 186     `wave16`:       `wave-16`,
 187     `wav-16`:       `wave-16`,
 188     `wave-16`:      `wave-16`,
 189     `x-wav`:        `wave-16`,
 190     `x-wave`:       `wave-16`,
 191 
 192     `wav16uri`:    `wave-16-uri`,
 193     `wave-16-uri`: `wave-16-uri`,
 194 
 195     `wav32uri`:    `wave-32-uri`,
 196     `wave-32-uri`: `wave-32-uri`,
 197 
 198     `wav32`:   `wave-32`,
 199     `wave32`:  `wave-32`,
 200     `wav-32`:  `wave-32`,
 201     `wave-32`: `wave-32`,
 202 }
 203 
 204 // outputSettings are format-specific settings which are controlled by the
 205 // output-format option on the cmd-line
 206 type outputSettings struct {
 207     Encoding encoding
 208     Header   headerType
 209     Samples  sampleFormat
 210 }
 211 
 212 // type2settings translates output-format names into the specific settings
 213 // these imply
 214 var type2settings = map[string]outputSettings{
 215     ``: {directEncoding, wavHeader, int16LE},
 216 
 217     `data-uri`:    {uriEncoding, wavHeader, int16LE},
 218     `raw`:         {directEncoding, noHeader, int16LE},
 219     `raw16be`:     {directEncoding, noHeader, int16BE},
 220     `raw16le`:     {directEncoding, noHeader, int16LE},
 221     `wave-16`:     {directEncoding, wavHeader, int16LE},
 222     `wave-16-uri`: {uriEncoding, wavHeader, int16LE},
 223 
 224     `raw32be`:     {directEncoding, noHeader, float32BE},
 225     `raw32le`:     {directEncoding, noHeader, float32LE},
 226     `wave-32`:     {directEncoding, wavHeader, float32LE},
 227     `wave-32-uri`: {uriEncoding, wavHeader, float32LE},
 228 }
 229 
 230 // outputConfig has all the info the core of this app needs to make sound
 231 type outputConfig struct {
 232     // Scripts has the source codes of all scripts for all channels
 233     Scripts []string
 234 
 235     // MaxTime is the play duration of the resulting sound
 236     MaxTime float64
 237 
 238     // SampleRate is the number of samples per second for all channels
 239     SampleRate uint32
 240 
 241     // all the configuration details needed to emit output
 242     outputSettings
 243 }
 244 
 245 // newOutputConfig is the constructor for type outputConfig, translating the
 246 // cmd-line info from type config
 247 func newOutputConfig(cfg config) (outputConfig, error) {
 248     oc := outputConfig{
 249         Scripts:    cfg.Scripts,
 250         MaxTime:    cfg.MaxTime,
 251         SampleRate: uint32(cfg.SampleRate),
 252     }
 253 
 254     if len(oc.Scripts) == 0 {
 255         return oc, errors.New(`no formulas given`)
 256     }
 257 
 258     outFmt := strings.ToLower(strings.TrimSpace(cfg.To))
 259     if alias, ok := name2type[outFmt]; ok {
 260         outFmt = alias
 261     }
 262 
 263     set, ok := type2settings[outFmt]
 264     if !ok {
 265         const fs = `unsupported output format %q`
 266         return oc, fmt.Errorf(fs, cfg.To)
 267     }
 268 
 269     oc.outputSettings = set
 270     return oc, nil
 271 }
 272 
 273 // mimeType gives the format's corresponding MIME type, or an empty string
 274 // if the type isn't URI-encodable
 275 func (oc outputConfig) mimeType() string {
 276     if oc.Header == wavHeader {
 277         return `audio/x-wav`
 278     }
 279     return ``
 280 }
 281 
 282 func isBadNumber(f float64) bool {
 283     return math.IsNaN(f) || math.IsInf(f, 0)
 284 }
     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     }
  47 
  48     oc, err := newOutputConfig(cfg)
  49     if err != nil {
  50         fmt.Fprintln(os.Stderr, err.Error())
  51         os.Exit(1)
  52     }
  53 
  54     addDetermFuncs()
  55 
  56     if err := run(oc); err != nil {
  57         fmt.Fprintln(os.Stderr, err.Error())
  58         os.Exit(1)
  59     }
  60 }
  61 
  62 func run(cfg outputConfig) error {
  63     // f, err := os.Create(`waveout.prof`)
  64     // if err != nil {
  65     //  return err
  66     // }
  67     // defer f.Close()
  68 
  69     // pprof.StartCPUProfile(f)
  70     // defer pprof.StopCPUProfile()
  71 
  72     w := bufio.NewWriterSize(os.Stdout, 64*1024)
  73     defer w.Flush()
  74 
  75     switch cfg.Encoding {
  76     case directEncoding:
  77         return runDirect(w, cfg)
  78 
  79     case uriEncoding:
  80         mtype := cfg.mimeType()
  81         if mtype == `` {
  82             return errors.New(`internal error: no MIME type`)
  83         }
  84 
  85         fmt.Fprintf(w, `data:%s;base64,`, mtype)
  86         enc := base64.NewEncoder(base64.StdEncoding, w)
  87         defer enc.Close()
  88         return runDirect(enc, cfg)
  89 
  90     default:
  91         const fs = `internal error: wrong output-encoding code %d`
  92         return fmt.Errorf(fs, cfg.Encoding)
  93     }
  94 }
  95 
  96 // type2emitter chooses sample-emitter funcs from the format given
  97 var type2emitter = map[sampleFormat]func(io.Writer, float64){
  98     int16LE:   emitInt16LE,
  99     int16BE:   emitInt16BE,
 100     float32LE: emitFloat32LE,
 101     float32BE: emitFloat32BE,
 102 }
 103 
 104 // runDirect emits sound-data bytes: this func can be called with writers
 105 // which keep bytes as given, or with re-encoders, such as base64 writers
 106 func runDirect(w io.Writer, cfg outputConfig) error {
 107     switch cfg.Header {
 108     case noHeader:
 109         // do nothing, while avoiding error
 110 
 111     case wavHeader:
 112         emitWaveHeader(w, cfg)
 113 
 114     default:
 115         const fs = `internal error: wrong header code %d`
 116         return fmt.Errorf(fs, cfg.Header)
 117     }
 118 
 119     emitter, ok := type2emitter[cfg.Samples]
 120     if !ok {
 121         const fs = `internal error: wrong output-format code %d`
 122         return fmt.Errorf(fs, cfg.Samples)
 123     }
 124 
 125     if len(cfg.Scripts) == 1 {
 126         return emitMono(w, cfg, emitter)
 127     }
 128     return emit(w, cfg, emitter)
 129 }
     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: ./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     var tests = []struct {
  35         name     string
  36         input    string
  37         expected string
  38         zoom     string
  39     }{
  40         {`no-zoom number`, `123.456`, `123.456`, ``},
  41     }
  42 
  43     for _, tc := range tests {
  44         t.Run(tc.name, func(t *testing.T) {
  45             data, err := load(strings.NewReader(tc.input))
  46             if err != nil {
  47                 t.Error(err)
  48                 return
  49             }
  50 
  51             var avoid sets
  52             avoid.indices = make(map[int]struct{})
  53             avoid.keys = make(map[string]struct{})
  54 
  55             var keys []string
  56             if tc.zoom != `` {
  57                 keys = strings.Split(tc.zoom, ` `)
  58             }
  59 
  60             data, err = zoom(data, keys, &avoid)
  61             if err != nil {
  62                 t.Error(err)
  63                 return
  64             }
  65 
  66             var sb strings.Builder
  67             err = json0(&sb, data)
  68             if err != nil && err != io.EOF {
  69                 t.Error(err)
  70                 return
  71             }
  72 
  73             got := sb.String()
  74             got, _ = strings.CutSuffix(got, "\n")
  75             if got != tc.expected {
  76                 t.Errorf(`expected %q but got %q instead`, tc.expected, got)
  77                 return
  78             }
  79         })
  80     }
  81 }