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