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 } 120 } 121 122 if err := run(os.Stdout, args, style); err != nil && err != io.EOF { 123 os.Stderr.WriteString(err.Error()) 124 os.Stderr.WriteString("\n") 125 os.Exit(1) 126 } 127 } 128 129 func run(w io.Writer, args []string, style string) error { 130 bw := bufio.NewWriter(w) 131 defer bw.Flush() 132 133 if len(args) == 0 { 134 return restyle(bw, os.Stdin, style) 135 } 136 137 for _, name := range args { 138 if err := handleFile(bw, name, style); err != nil { 139 return err 140 } 141 } 142 return nil 143 } 144 145 func handleFile(w *bufio.Writer, name string, style string) error { 146 if name == `` || name == `-` { 147 return restyle(w, os.Stdin, style) 148 } 149 150 f, err := os.Open(name) 151 if err != nil { 152 return errors.New(`can't read from file named "` + name + `"`) 153 } 154 defer f.Close() 155 156 return restyle(w, f, style) 157 } 158 159 func restyle(w *bufio.Writer, r io.Reader, style string) error { 160 const gb = 1024 * 1024 * 1024 161 sc := bufio.NewScanner(r) 162 sc.Buffer(nil, 8*gb) 163 164 for i := 0; sc.Scan(); i++ { 165 s := sc.Bytes() 166 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) { 167 s = s[3:] 168 } 169 170 restyleLine(w, s, style) 171 w.WriteByte('\n') 172 if err := w.Flush(); err != nil { 173 // a write error may be the consequence of stdout being closed, 174 // perhaps by another app along a pipe 175 return io.EOF 176 } 177 } 178 return sc.Err() 179 } 180 181 func lookupStyle(name string) (style string, ok bool) { 182 if alias, ok := styleAliases[name]; ok { 183 name = alias 184 } 185 186 style, ok = styles[name] 187 return style, ok 188 } 189 190 var styleAliases = map[string]string{ 191 `b`: `blue`, 192 `g`: `green`, 193 `m`: `magenta`, 194 `o`: `orange`, 195 `p`: `purple`, 196 `r`: `red`, 197 `u`: `underline`, 198 199 `bolded`: `bold`, 200 `h`: `inverse`, 201 `hi`: `inverse`, 202 `highlight`: `inverse`, 203 `highlighted`: `inverse`, 204 `hilite`: `inverse`, 205 `hilited`: `inverse`, 206 `inv`: `inverse`, 207 `invert`: `inverse`, 208 `inverted`: `inverse`, 209 `underlined`: `underline`, 210 211 `bb`: `blueback`, 212 `bg`: `greenback`, 213 `bm`: `magentaback`, 214 `bo`: `orangeback`, 215 `bp`: `purpleback`, 216 `br`: `redback`, 217 218 `gb`: `greenback`, 219 `mb`: `magentaback`, 220 `ob`: `orangeback`, 221 `pb`: `purpleback`, 222 `rb`: `redback`, 223 224 `bblue`: `blueback`, 225 `bgray`: `grayback`, 226 `bgreen`: `greenback`, 227 `bmagenta`: `magentaback`, 228 `borange`: `orangeback`, 229 `bpurple`: `purpleback`, 230 `bred`: `redback`, 231 232 `backblue`: `blueback`, 233 `backgray`: `grayback`, 234 `backgreen`: `greenback`, 235 `backmagenta`: `magentaback`, 236 `backorange`: `orangeback`, 237 `backpurple`: `purpleback`, 238 `backred`: `redback`, 239 } 240 241 // styles turns style-names into the ANSI-code sequences used for the 242 // alternate groups of digits 243 var styles = map[string]string{ 244 `blue`: "\x1b[38;2;0;95;215m", 245 `bold`: "\x1b[1m", 246 `gray`: "\x1b[38;2;168;168;168m", 247 `green`: "\x1b[38;2;0;135;95m", 248 `inverse`: "\x1b[7m", 249 `magenta`: "\x1b[38;2;215;0;255m", 250 `orange`: "\x1b[38;2;215;95;0m", 251 `plain`: "\x1b[0m", 252 `red`: "\x1b[38;2;204;0;0m", 253 `underline`: "\x1b[4m", 254 255 // `blue`: "\x1b[38;5;26m", 256 // `bold`: "\x1b[1m", 257 // `gray`: "\x1b[38;5;248m", 258 // `green`: "\x1b[38;5;29m", 259 // `inverse`: "\x1b[7m", 260 // `magenta`: "\x1b[38;5;99m", 261 // `orange`: "\x1b[38;5;166m", 262 // `plain`: "\x1b[0m", 263 // `red`: "\x1b[31m", 264 // `underline`: "\x1b[4m", 265 266 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m", 267 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m", 268 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m", 269 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m", 270 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m", 271 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m", 272 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m", 273 } 274 275 // restyleLine renders the line given, using ANSI-styles to make any long 276 // numbers in it more legible; this func doesn't emit a line-feed, which 277 // is up to its caller 278 func restyleLine(w *bufio.Writer, line []byte, style string) { 279 for len(line) > 0 { 280 i := indexDigit(line) 281 if i < 0 { 282 // no (more) digits to style for sure 283 w.Write(line) 284 return 285 } 286 287 // emit line before current digit-run 288 w.Write(line[:i]) 289 // advance to the start of the current digit-run 290 line = line[i:] 291 292 // see where the digit-run ends 293 j := indexNonDigit(line) 294 if j < 0 { 295 // the digit-run goes until the end 296 restyleDigits(w, line, style) 297 return 298 } 299 300 // emit styled digit-run 301 restyleDigits(w, line[:j], style) 302 // skip right past the end of the digit-run 303 line = line[j:] 304 } 305 } 306 307 // indexDigit finds the index of the first digit in a string, or -1 when the 308 // string has no decimal digits 309 func indexDigit(s []byte) int { 310 for i := 0; i < len(s); i++ { 311 switch s[i] { 312 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 313 return i 314 } 315 } 316 317 // empty slice, or a slice without any digits 318 return -1 319 } 320 321 // indexNonDigit finds the index of the first non-digit in a string, or -1 322 // when the string is all decimal digits 323 func indexNonDigit(s []byte) int { 324 for i := 0; i < len(s); i++ { 325 switch s[i] { 326 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 327 continue 328 default: 329 return i 330 } 331 } 332 333 // empty slice, or a slice which only has digits 334 return -1 335 } 336 337 // restyleDigits renders a run of digits as alternating styled/unstyled runs 338 // of 3 digits, which greatly improves readability, and is the only purpose 339 // of this app; string is assumed to be all decimal digits 340 func restyleDigits(w *bufio.Writer, digits []byte, altStyle string) { 341 if len(digits) < 4 { 342 // digit sequence is short, so emit it as is 343 w.Write(digits) 344 return 345 } 346 347 // separate leading 0..2 digits which don't align with the 3-digit groups 348 i := len(digits) % 3 349 // emit leading digits unstyled, if there are any 350 w.Write(digits[:i]) 351 // the rest is guaranteed to have a length which is a multiple of 3 352 digits = digits[i:] 353 354 // start by styling, unless there were no leading digits 355 style := i != 0 356 357 for len(digits) > 0 { 358 if style { 359 w.WriteString(altStyle) 360 w.Write(digits[:3]) 361 w.Write([]byte{'\x1b', '[', '0', 'm'}) 362 } else { 363 w.Write(digits[:3]) 364 } 365 366 // advance to the next triple: the start of this func is supposed 367 // to guarantee this step always works 368 digits = digits[3:] 369 370 // alternate between styled and unstyled 3-digit groups 371 style = !style 372 } 373 }