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

     File: nj/info.txt
   1 nj [filepath...]
   2 
   3 Nice Json reads JSON, and emits it back as ANSI-styled indented lines, using
   4 2 spaces for each indentation level.

     File: nj/main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "encoding/json"
   6     "errors"
   7     "io"
   8     "os"
   9 
  10     _ "embed"
  11 )
  12 
  13 //go:embed info.txt
  14 var info string
  15 
  16 // indent is how many spaces each indentation level uses
  17 const indent = 2
  18 
  19 const (
  20     // boolStyle is bluish, and very distinct from all other colors used
  21     boolStyle = "\x1b[38;5;74m"
  22 
  23     // keyStyle is magenta, and very distinct from normal strings
  24     keyStyle = "\x1b[38;5;99m"
  25 
  26     // nullStyle is a light-gray, just like syntax elements, but the word
  27     // `null` is wide enough to stand out from syntax items at a glance
  28     nullStyle = syntaxStyle
  29 
  30     // numberStyle is a nice green
  31     numberStyle = "\x1b[38;5;29m"
  32 
  33     // stringStyle used to be bluish, but it's better to keep it plain,
  34     // which also minimizes how many different colors the output can show
  35     stringStyle = ""
  36 
  37     // stringStyle is bluish, but clearly darker than boolStyle
  38     // stringStyle = "\x1b[38;5;24m"
  39 
  40     // syntaxStyle is a light-gray, not too light, not too dark
  41     syntaxStyle = "\x1b[38;5;249m"
  42 )
  43 
  44 // errNoMoreOutput is a way to successfully quit the app right away, its
  45 // message never shown
  46 var errNoMoreOutput = errors.New(`no more output`)
  47 
  48 func main() {
  49     if len(os.Args) > 1 {
  50         switch os.Args[1] {
  51         case `-h`, `--h`, `-help`, `--help`:
  52             os.Stderr.WriteString(info)
  53             return
  54         }
  55     }
  56 
  57     if len(os.Args) > 2 {
  58         showError(errors.New(`multiple inputs not allowed`))
  59         os.Exit(1)
  60     }
  61 
  62     var err error
  63     if len(os.Args) == 1 || (len(os.Args) == 2 && os.Args[1] == `-`) {
  64         // handle lack of filepath arg, or `-` as the filepath
  65         err = niceJSON(os.Stdout, os.Stdin)
  66     } else {
  67         // handle being given a normal filepath
  68         err = handleFile(os.Stdout, os.Args[1])
  69     }
  70 
  71     if err != nil && err != errNoMoreOutput {
  72         showError(err)
  73         os.Exit(1)
  74     }
  75 }
  76 
  77 // showError standardizes how errors look in this app
  78 func showError(err error) {
  79     os.Stderr.WriteString("\x1b[31m")
  80     os.Stderr.WriteString(err.Error())
  81     os.Stderr.WriteString("\x1b[0m\n")
  82 }
  83 
  84 // writeSpaces does what it says, minimizing calls to write-like funcs
  85 func writeSpaces(w *bufio.Writer, n int) {
  86     const spaces = `                                `
  87     for n >= len(spaces) {
  88         w.WriteString(spaces)
  89         n -= len(spaces)
  90     }
  91     if n > 0 {
  92         w.WriteString(spaces[:n])
  93     }
  94 }
  95 
  96 func handleFile(w io.Writer, path string) error {
  97     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
  98     //  resp, err := http.Get(path)
  99     //  if err != nil {
 100     //      return err
 101     //  }
 102     //  defer resp.Body.Close()
 103     //  return niceJSON(w, resp.Body)
 104     // }
 105 
 106     f, err := os.Open(path)
 107     if err != nil {
 108         // on windows, file-not-found error messages may mention `CreateFile`,
 109         // even when trying to open files in read-only mode
 110         return errors.New(`can't open file named ` + path)
 111     }
 112     defer f.Close()
 113 
 114     return niceJSON(w, f)
 115 }
 116 
 117 func niceJSON(w io.Writer, r io.Reader) error {
 118     bw := bufio.NewWriter(w)
 119     defer bw.Flush()
 120 
 121     dec := json.NewDecoder(r)
 122     // using string-like json.Number values instead of float64 ones avoids
 123     // unneeded reformatting of numbers; reformatting parsed float64 values
 124     // can potentially even drop/change decimals, causing the output not to
 125     // match the input digits exactly, which is best to avoid
 126     dec.UseNumber()
 127 
 128     t, err := dec.Token()
 129     if err == io.EOF {
 130         return errors.New(`empty input isn't valid JSON`)
 131     }
 132     if err != nil {
 133         return err
 134     }
 135 
 136     if err := handleToken(bw, dec, t, 0, 0); err != nil {
 137         return err
 138     }
 139     // don't forget to end the last output line
 140     bw.WriteByte('\n')
 141 
 142     if _, err := dec.Token(); err != io.EOF {
 143         return errors.New(`unexpected trailing JSON data`)
 144     }
 145     return nil
 146 }
 147 
 148 func handleToken(w *bufio.Writer, d *json.Decoder, t json.Token, pre, level int) error {
 149     switch t := t.(type) {
 150     case json.Delim:
 151         switch t {
 152         case json.Delim('['):
 153             return handleArray(w, d, pre, level)
 154 
 155         case json.Delim('{'):
 156             return handleObject(w, d, pre, level)
 157 
 158         default:
 159             // return fmt.Errorf(`unsupported JSON delimiter %v`, t)
 160             return errors.New(`unsupported JSON delimiter`)
 161         }
 162 
 163     case nil:
 164         return handleNull(w, pre)
 165 
 166     case bool:
 167         return handleBoolean(w, t, pre)
 168 
 169     case string:
 170         return handleString(w, t, pre)
 171 
 172     case json.Number:
 173         return handleNumber(w, t, pre)
 174 
 175     default:
 176         // return fmt.Errorf(`unsupported token type %T`, t)
 177         return errors.New(`unsupported token type`)
 178     }
 179 }
 180 
 181 func handleArray(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 182     for i := 0; true; i++ {
 183         t, err := d.Token()
 184         if err != nil {
 185             return err
 186         }
 187 
 188         if t == json.Delim(']') {
 189             if i == 0 {
 190                 writeSpaces(w, indent*pre)
 191                 w.WriteString(syntaxStyle + "[]\x1b[0m")
 192             } else {
 193                 w.WriteString("\n")
 194                 writeSpaces(w, indent*level)
 195                 w.WriteString(syntaxStyle + "]\x1b[0m")
 196             }
 197             return nil
 198         }
 199 
 200         if i == 0 {
 201             writeSpaces(w, indent*pre)
 202             w.WriteString(syntaxStyle + "[\x1b[0m\n")
 203         } else {
 204             // this is a good spot to check for early-quit opportunities
 205             _, err = w.WriteString(syntaxStyle + ",\x1b[0m\n")
 206             if err != nil {
 207                 return errNoMoreOutput
 208             }
 209         }
 210 
 211         writeSpaces(w, indent*(level+1))
 212         if err := handleToken(w, d, t, level, level+1); err != nil {
 213             return err
 214         }
 215     }
 216 
 217     // make the compiler happy
 218     return nil
 219 }
 220 
 221 func handleBoolean(w *bufio.Writer, b bool, pre int) error {
 222     writeSpaces(w, indent*pre)
 223     if b {
 224         w.WriteString(boolStyle + "true\x1b[0m")
 225     } else {
 226         w.WriteString(boolStyle + "false\x1b[0m")
 227     }
 228     return nil
 229 }
 230 
 231 func handleKey(w *bufio.Writer, s string, pre int) error {
 232     writeSpaces(w, indent*pre)
 233     w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle)
 234     w.WriteString(s)
 235     w.WriteString(syntaxStyle + "\":\x1b[0m ")
 236     return nil
 237 }
 238 
 239 func handleNull(w *bufio.Writer, pre int) error {
 240     writeSpaces(w, indent*pre)
 241     w.WriteString(nullStyle + "null\x1b[0m")
 242     return nil
 243 }
 244 
 245 func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 246     writeSpaces(w, indent*pre)
 247     w.WriteString(numberStyle)
 248     w.WriteString(n.String())
 249     w.WriteString("\x1b[0m")
 250     return nil
 251 }
 252 
 253 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 254     for i := 0; true; i++ {
 255         t, err := d.Token()
 256         if err != nil {
 257             return err
 258         }
 259 
 260         if t == json.Delim('}') {
 261             if i == 0 {
 262                 writeSpaces(w, indent*pre)
 263                 w.WriteString(syntaxStyle + "{}\x1b[0m")
 264             } else {
 265                 w.WriteString("\n")
 266                 writeSpaces(w, indent*level)
 267                 w.WriteString(syntaxStyle + "}\x1b[0m")
 268             }
 269             return nil
 270         }
 271 
 272         if i == 0 {
 273             writeSpaces(w, indent*pre)
 274             w.WriteString(syntaxStyle + "{\x1b[0m\n")
 275         } else {
 276             // this is a good spot to check for early-quit opportunities
 277             _, err = w.WriteString(syntaxStyle + ",\x1b[0m\n")
 278             if err != nil {
 279                 return errNoMoreOutput
 280             }
 281         }
 282 
 283         // the stdlib's JSON parser is supposed to complain about non-string
 284         // keys anyway, but make sure just in case
 285         k, ok := t.(string)
 286         if !ok {
 287             return errors.New(`expected key to be a string`)
 288         }
 289         if err := handleKey(w, k, level+1); err != nil {
 290             return err
 291         }
 292 
 293         // handle value
 294         t, err = d.Token()
 295         if err != nil {
 296             return err
 297         }
 298         if err := handleToken(w, d, t, 0, level+1); err != nil {
 299             return err
 300         }
 301     }
 302 
 303     // make the compiler happy
 304     return nil
 305 }
 306 
 307 func handleString(w *bufio.Writer, s string, pre int) error {
 308     writeSpaces(w, indent*pre)
 309     w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle)
 310     w.WriteString(s)
 311     w.WriteString(syntaxStyle + "\"\x1b[0m")
 312     return nil
 313 }

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