File: 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 /* 26 Single-file source-code for nn: this version has no http(s) support. Even 27 the unit-tests from the original nn are omitted. 28 29 To compile a smaller-sized command-line app, you can use the `go` command as 30 follows: 31 32 go build -ldflags "-s -w" -trimpath nn.go 33 */ 34 35 package main 36 37 import ( 38 "bufio" 39 "errors" 40 "io" 41 "os" 42 "strings" 43 ) 44 45 // Note: the code is avoiding using the fmt package to save hundreds of 46 // kilobytes on the resulting executable, which is a noticeable difference. 47 48 const info = ` 49 nn [options...] [file...] 50 51 52 Nice Numbers is an app which renders the UTF-8 text it's given to make long 53 numbers much easier to read. It does so by alternating 3-digit groups which 54 are colored using ANSI-codes with plain/unstyled 3-digit groups. 55 56 Unlike the common practice of inserting commas between 3-digit groups, this 57 trick doesn't widen the original text, keeping alignments across lines the 58 same. 59 60 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line 61 feeds. 62 63 All (optional) leading options start with either single or double-dash, 64 and most of them change the style/color used. Some of the options are, 65 shown in their single-dash form: 66 67 -h show this help message 68 -help show this help message 69 70 -b use a blue color 71 -blue use a blue color 72 -bold bold-style digits 73 -g use a green color 74 -gray use a gray color (default) 75 -green use a green color 76 -hi use a highlighting/inverse style 77 -m use a magenta color 78 -magenta use a magenta color 79 -o use an orange color 80 -orange use an orange color 81 -r use a red color 82 -red use a red color 83 -u underline digits 84 -underline underline digits 85 ` 86 87 // errNoMoreOutput is a dummy error whose message is ignored, and which 88 // causes the app to quit immediately and successfully 89 var errNoMoreOutput = errors.New(`no more output`) 90 91 func main() { 92 args := os.Args[1:] 93 94 if len(args) > 0 { 95 switch args[0] { 96 case `-h`, `--h`, `-help`, `--help`: 97 os.Stdout.WriteString(info[1:]) 98 return 99 } 100 } 101 102 options := true 103 if len(args) > 0 && args[0] == `--` { 104 options = false 105 args = args[1:] 106 } 107 108 style, _ := lookupStyle(`gray`) 109 110 // if the first argument is 1 or 2 dashes followed by a supported 111 // style-name, change the style used 112 if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) { 113 name := args[0] 114 name = strings.TrimPrefix(name, `-`) 115 name = strings.TrimPrefix(name, `-`) 116 args = args[1:] 117 118 // check if the `dedashed` argument is a supported style-name 119 if s, ok := lookupStyle(name); ok { 120 style = s 121 } else { 122 os.Stderr.WriteString(`invalid style name `) 123 os.Stderr.WriteString(name) 124 os.Stderr.WriteString("\n") 125 os.Exit(1) 126 } 127 } 128 129 if err := run(os.Stdout, args, style); isActualError(err) { 130 os.Stderr.WriteString(err.Error()) 131 os.Stderr.WriteString("\n") 132 os.Exit(1) 133 } 134 } 135 136 func run(w io.Writer, args []string, style string) error { 137 bw := bufio.NewWriter(w) 138 defer bw.Flush() 139 140 if len(args) == 0 { 141 return restyle(bw, os.Stdin, style) 142 } 143 144 for _, name := range args { 145 if err := handleFile(bw, name, style); err != nil { 146 return err 147 } 148 } 149 return nil 150 } 151 152 func handleFile(w *bufio.Writer, name string, style string) error { 153 if name == `` || name == `-` { 154 return restyle(w, os.Stdin, style) 155 } 156 157 f, err := os.Open(name) 158 if err != nil { 159 return errors.New(`can't read from file named "` + name + `"`) 160 } 161 defer f.Close() 162 163 return restyle(w, f, style) 164 } 165 166 // isActualError is to figure out whether not to ignore an error, and thus 167 // show it as an error message 168 func isActualError(err error) bool { 169 return err != nil && err != io.EOF && err != errNoMoreOutput 170 } 171 172 func restyle(w *bufio.Writer, r io.Reader, style string) error { 173 const gb = 1024 * 1024 * 1024 174 sc := bufio.NewScanner(r) 175 sc.Buffer(nil, 8*gb) 176 177 for i := 0; sc.Scan(); i++ { 178 s := sc.Bytes() 179 if i == 0 && len(s) > 2 && s[0] == 0xef && s[1] == 0xbb && s[2] == 0xbf { 180 s = s[3:] 181 } 182 183 restyleLine(w, s, style) 184 w.WriteByte('\n') 185 if err := w.Flush(); err != nil { 186 // a write error may be the consequence of stdout being closed, 187 // perhaps by another app along a pipe 188 return errNoMoreOutput 189 } 190 } 191 return sc.Err() 192 } 193 194 func lookupStyle(name string) (style string, ok bool) { 195 if alias, ok := styleAliases[name]; ok { 196 name = alias 197 } 198 199 style, ok = styles[name] 200 return style, ok 201 } 202 203 var styleAliases = map[string]string{ 204 `b`: `blue`, 205 `g`: `green`, 206 `m`: `magenta`, 207 `o`: `orange`, 208 `p`: `purple`, 209 `r`: `red`, 210 `u`: `underline`, 211 212 `bolded`: `bold`, 213 `h`: `inverse`, 214 `hi`: `inverse`, 215 `highlight`: `inverse`, 216 `highlighted`: `inverse`, 217 `hilite`: `inverse`, 218 `hilited`: `inverse`, 219 `inv`: `inverse`, 220 `invert`: `inverse`, 221 `inverted`: `inverse`, 222 `underlined`: `underline`, 223 224 `bb`: `blueback`, 225 `bg`: `greenback`, 226 `bm`: `magentaback`, 227 `bo`: `orangeback`, 228 `bp`: `purpleback`, 229 `br`: `redback`, 230 231 `gb`: `greenback`, 232 `mb`: `magentaback`, 233 `ob`: `orangeback`, 234 `pb`: `purpleback`, 235 `rb`: `redback`, 236 237 `bblue`: `blueback`, 238 `bgray`: `grayback`, 239 `bgreen`: `greenback`, 240 `bmagenta`: `magentaback`, 241 `borange`: `orangeback`, 242 `bpurple`: `purpleback`, 243 `bred`: `redback`, 244 245 `backblue`: `blueback`, 246 `backgray`: `grayback`, 247 `backgreen`: `greenback`, 248 `backmagenta`: `magentaback`, 249 `backorange`: `orangeback`, 250 `backpurple`: `purpleback`, 251 `backred`: `redback`, 252 } 253 254 // styles turns style-names into the ANSI-code sequences used for the 255 // alternate groups of digits 256 var styles = map[string]string{ 257 `blue`: "\x1b[38;2;0;95;215m", 258 `bold`: "\x1b[1m", 259 `gray`: "\x1b[38;2;168;168;168m", 260 `green`: "\x1b[38;2;0;135;95m", 261 `inverse`: "\x1b[7m", 262 `magenta`: "\x1b[38;2;215;0;255m", 263 `orange`: "\x1b[38;2;215;95;0m", 264 `plain`: "\x1b[0m", 265 `red`: "\x1b[38;2;204;0;0m", 266 `underline`: "\x1b[4m", 267 268 // `blue`: "\x1b[38;5;26m", 269 // `bold`: "\x1b[1m", 270 // `gray`: "\x1b[38;5;248m", 271 // `green`: "\x1b[38;5;29m", 272 // `inverse`: "\x1b[7m", 273 // `magenta`: "\x1b[38;5;99m", 274 // `orange`: "\x1b[38;5;166m", 275 // `plain`: "\x1b[0m", 276 // `red`: "\x1b[31m", 277 // `underline`: "\x1b[4m", 278 279 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m", 280 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m", 281 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m", 282 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m", 283 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m", 284 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m", 285 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m", 286 } 287 288 // restyleLine renders the line given, using ANSI-styles to make any long 289 // numbers in it more legible; this func doesn't emit a line-feed, which 290 // is up to its caller 291 func restyleLine(w *bufio.Writer, line []byte, style string) { 292 for len(line) > 0 { 293 i := indexDigit(line) 294 if i < 0 { 295 // no (more) digits to style for sure 296 w.Write(line) 297 return 298 } 299 300 // emit line before current digit-run 301 w.Write(line[:i]) 302 // advance to the start of the current digit-run 303 line = line[i:] 304 305 // see where the digit-run ends 306 j := indexNonDigit(line) 307 if j < 0 { 308 // the digit-run goes until the end 309 restyleDigits(w, line, style) 310 return 311 } 312 313 // emit styled digit-run 314 restyleDigits(w, line[:j], style) 315 // skip right past the end of the digit-run 316 line = line[j:] 317 } 318 } 319 320 // indexDigit finds the index of the first digit in a string, or -1 when the 321 // string has no decimal digits 322 func indexDigit(s []byte) int { 323 for i := 0; i < len(s); i++ { 324 switch s[i] { 325 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 326 return i 327 } 328 } 329 330 // empty slice, or a slice without any digits 331 return -1 332 } 333 334 // indexNonDigit finds the index of the first non-digit in a string, or -1 335 // when the string is all decimal digits 336 func indexNonDigit(s []byte) int { 337 for i := 0; i < len(s); i++ { 338 switch s[i] { 339 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 340 continue 341 default: 342 return i 343 } 344 } 345 346 // empty slice, or a slice which only has digits 347 return -1 348 } 349 350 // restyleDigits renders a run of digits as alternating styled/unstyled runs 351 // of 3 digits, which greatly improves readability, and is the only purpose 352 // of this app; string is assumed to be all decimal digits 353 func restyleDigits(w *bufio.Writer, digits []byte, altStyle string) { 354 if len(digits) < 4 { 355 // digit sequence is short, so emit it as is 356 w.Write(digits) 357 return 358 } 359 360 // separate leading 0..2 digits which don't align with the 3-digit groups 361 i := len(digits) % 3 362 // emit leading digits unstyled, if there are any 363 w.Write(digits[:i]) 364 // the rest is guaranteed to have a length which is a multiple of 3 365 digits = digits[i:] 366 367 // start by styling, unless there were no leading digits 368 style := i != 0 369 370 for len(digits) > 0 { 371 if style { 372 w.WriteString(altStyle) 373 w.Write(digits[:3]) 374 w.Write([]byte{'\x1b', '[', '0', 'm'}) 375 } else { 376 w.Write(digits[:3]) 377 } 378 379 // advance to the next triple: the start of this func is supposed 380 // to guarantee this step always works 381 digits = digits[3:] 382 383 // alternate between styled and unstyled 3-digit groups 384 style = !style 385 } 386 }