File: nn.c 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 You can build this command-line app by running 27 28 cc -Wall -s -O2 -march=native -mtune=native -flto -o ./nn ./nn.c 29 30 Building with COMPACT_OUTPUT defined makes `nn` output many fewer bytes, at 31 the cost of using arguably worse colors. You can do that by running 32 33 cc -s -O2 -march=native -mtune=native -flto -D COMPACT_OUTPUT -o ./nh ./nh.c 34 */ 35 36 #include <stdbool.h> 37 #include <stddef.h> 38 #include <stdint.h> 39 #include <stdio.h> 40 #include <stdlib.h> 41 #include <string.h> 42 #include <unistd.h> 43 44 #ifdef _WIN32 45 #include <fcntl.h> 46 #include <windows.h> 47 #endif 48 49 #ifdef RED_ERRORS 50 #define ERROR_STYLE "\x1b[38;2;204;0;0m" 51 #ifdef __APPLE__ 52 #define ERROR_STYLE "\x1b[31m" 53 #endif 54 #define RESET_STYLE "\x1b[0m" 55 #else 56 #define ERROR_STYLE 57 #define RESET_STYLE 58 #endif 59 60 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n") 61 62 #define BAD_ALLOC 2 63 64 // #define COMPACT_OUTPUT 65 66 // EMIT_CONST emits string constants without their final null byte 67 #define EMIT_CONST(w, x) fwrite(x, 1, sizeof(x) - 1, w) 68 69 const char* info = "" 70 "nn [options...] [filepaths...]\n" 71 "\n" 72 "\n" 73 "Nice Numbers is an app which renders the plain text it's given to make long\n" 74 "numbers much easier to read, by alternating 3-digit groups which are colored\n" 75 "using ANSI-codes with unstyled ones.\n" 76 "\n" 77 "Unlike the common practice of inserting commas between 3-digit groups, this\n" 78 "alternative doesn't widen the original text, keeping any alignments the same.\n" 79 "\n" 80 "All input is assumed to be UTF-8. When not given any filepaths, input is read\n" 81 "from the standard input.\n" 82 "\n" 83 "\n" 84 "Options, all of which can start with either 1 or 2 dashes:\n" 85 "\n" 86 "\n" 87 " -blue use a blue-like color to alternate-style runs of digits\n" 88 " -bold use a bold style/effect to alternate-style runs of digits\n" 89 " -gray use a gray color to alternate-style runs of digits\n" 90 " -green use a green color to alternate-style runs of digits\n" 91 " -inverse invert/swap colors to alternate-style runs of digits\n" 92 " -orange use an orange color to alternate-style runs of digits\n" 93 " -purple use a purple color to alternate-style runs of digits\n" 94 " -red use a red color to alternate-style runs of digits\n" 95 "\n" 96 " -h show this help message\n" 97 " -help show this help message\n" 98 "\n" 99 " -highlight same as option -inverse\n" 100 " -hilite same as option -inverse\n" 101 ""; 102 103 // span is a region of bytes in memory 104 typedef struct span { 105 // ptr is the starting place of the region 106 unsigned char* ptr; 107 108 // len is how many bytes are in the region 109 size_t len; 110 } span; 111 112 // advance updates a span so it starts after the number of bytes given 113 static inline void advance(span* src, size_t n) { 114 src->ptr += n; 115 src->len -= n; 116 } 117 118 // slice is a growable region of bytes in memory 119 typedef struct slice { 120 // ptr is the starting place of the region 121 unsigned char* ptr; 122 123 // cap is how many bytes the memory region has available 124 size_t cap; 125 } slice; 126 127 // find_digit returns the index of the first digit found, or a negative value 128 // on failure 129 static inline int64_t find_digit(span s) { 130 for (size_t i = 0; i < s.len; i++) { 131 const unsigned char b = s.ptr[i]; 132 if ('0' <= b && b <= '9') { 133 return i; 134 } 135 } 136 return -1; 137 } 138 139 // find_non_digit returns the index of the first non-digit found, or a negative 140 // value on failure 141 static inline int64_t find_non_digit(span s) { 142 for (size_t i = 0; i < s.len; i++) { 143 const unsigned char b = s.ptr[i]; 144 if (b < '0' || b > '9') { 145 return i; 146 } 147 } 148 return -1; 149 } 150 151 // restyle_digits renders a run of digits as alternating styled/unstyled runs 152 // of 3 digits, which greatly improves readability, and is the only purpose 153 // of this app; string is assumed to be all decimal digits 154 void restyle_digits(FILE* w, span digits, span style) { 155 if (digits.len < 4) { 156 // digit sequence is short, so emit it as is 157 fwrite(digits.ptr, 1, digits.len, w); 158 return; 159 } 160 161 // separate leading 0..2 digits which don't align with the 3-digit groups 162 size_t lead = digits.len % 3; 163 // emit leading digits unstyled, if there are any 164 fwrite(digits.ptr, 1, lead, w); 165 // the rest is guaranteed to have a length which is a multiple of 3 166 advance(&digits, lead); 167 168 // start with the alternate style, unless there were no leading digits 169 bool style_now = lead != 0; 170 171 while (digits.len > 0) { 172 if (style_now) { 173 fwrite(style.ptr, 1, style.len, w); 174 fwrite(digits.ptr, 1, 3, w); 175 EMIT_CONST(w, "\x1b[0m"); 176 } else { 177 fwrite(digits.ptr, 1, 3, w); 178 } 179 180 advance(&digits, 3); 181 // alternate between styled and unstyled 3-digit groups 182 style_now = !style_now; 183 } 184 } 185 186 // restyle_line renders the line given, using ANSI-styles to make any long 187 // numbers in it more legible 188 void restyle_line(FILE* w, unsigned char* s, size_t len, span style) { 189 span line; 190 line.ptr = s; 191 line.len = len; 192 193 while (!feof(w) && line.len > 0) { 194 int64_t i = find_digit(line); 195 if (i < 0) { 196 // no (more) digits for sure 197 fwrite(line.ptr, 1, line.len, w); 198 return; 199 } 200 201 // some ANSI-style sequences use 4-digit numbers, which are long 202 // enough for this app to mangle 203 bool is_ansi = i >= 2 && s[i - 2] == '\x1b' && s[i - 1] == '['; 204 205 // emit line before current digit-run 206 fwrite(line.ptr, 1, i, w); 207 208 advance(&line, i); 209 210 // see where the digit-run ends 211 int64_t j = find_non_digit(line); 212 if (j < 0) { 213 // the digit-run goes until the end 214 if (!is_ansi) { 215 restyle_digits(w, line, style); 216 } else { 217 fwrite(line.ptr, 1, line.len, w); 218 } 219 return; 220 } 221 222 // emit styled digit-run... maybe 223 if (!is_ansi) { 224 span chunk; 225 chunk.ptr = line.ptr; 226 chunk.len = j; 227 restyle_digits(w, chunk, style); 228 } else { 229 fwrite(line.ptr, 1, j, w); 230 } 231 232 // skip right past the end of the digit-run 233 advance(&line, j); 234 } 235 } 236 237 // default_digits_style makes it easy to change the built-in default style 238 #ifdef COMPACT_OUTPUT 239 unsigned char default_digits_style[] = "\x1b[38;5;248m"; 240 #else 241 unsigned char default_digits_style[] = "\x1b[38;2;168;168;168m"; 242 #endif 243 244 typedef struct handler_args { 245 FILE* w; 246 slice* line; 247 span style; 248 } handler_args; 249 250 bool starts_with_bom(const unsigned char* p, size_t len) { 251 return len >= 3 && p[0] == 0xef && p[1] == 0xbb && p[2] == 0xbf; 252 } 253 254 // handle_lines loops over input lines, restyling all digit-runs as more 255 // readable `nice numbers`, fulfilling the app's purpose 256 void handle_lines(handler_args args, FILE* src, bool live_lines) { 257 FILE* w = args.w; 258 slice* line = args.line; 259 260 for (size_t i = 0; !feof(w); i++) { 261 ssize_t len = getline((char**)&line->ptr, &line->cap, src); 262 if (line->ptr == NULL) { 263 fprintf(stderr, "\n"); 264 fprintf(stderr, ERROR_LINE("out of memory")); 265 exit(BAD_ALLOC); 266 } 267 268 if (len < 0) { 269 break; 270 } 271 272 unsigned char* ptr = line->ptr; 273 274 // get rid of leading UTF-8 BOM (byte-order mark) if 1st line has it 275 if (i == 0 && starts_with_bom(ptr, len)) { 276 ptr += 3; 277 len -= 3; 278 } 279 280 // replace trailing carriage-returns with line-feeds 281 if (len >= 1 && ptr[len - 1] == '\r') { 282 ptr[len - 1] = '\n'; 283 } 284 285 // get rid of carriage-returns preceding line-feeds 286 if (len >= 2 && ptr[len - 2] == '\r' && ptr[len - 1] == '\n') { 287 ptr[len - 2] = '\n'; 288 len--; 289 } 290 291 restyle_line(w, ptr, len, args.style); 292 if (len < 1 || ptr[len - 1] != '\n') { 293 fputc('\n', w); 294 } 295 } 296 297 if (!live_lines) { 298 fflush(w); 299 } 300 } 301 302 // handle_file handles data from the filename given; returns false only when 303 // the file can't be opened 304 bool handle_file(handler_args args, const char* path, bool live_lines) { 305 FILE* f = fopen(path, "rb"); 306 if (f == NULL) { 307 fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path); 308 return false; 309 } 310 311 handle_lines(args, f, live_lines); 312 fclose(f); 313 return true; 314 } 315 316 const char* style_names_aliases[] = { 317 "b", "blue", 318 "g", "green", 319 "h", "inverse", 320 "i", "inverse", 321 "m", "magenta", 322 "o", "orange", 323 "p", "purple", 324 "r", "red", 325 "u", "underline", 326 327 "hi", "inverse", 328 "ma", "magenta", 329 "or", "orange", 330 "un", "underline", 331 332 "inv", "inverse", 333 "mag", "magenta", 334 335 "grey", "gray", 336 "highlight", "inverse", 337 "highlighted", "inverse", 338 "hilite", "inverse", 339 "hilited", "inverse", 340 "invert", "inverse", 341 "inverted", "inverse", 342 "underlined", "underline", 343 344 "bb", "blueback", 345 "gb", "greenback", 346 "mb", "magentaback", 347 "ob", "orangeback", 348 "pb", "purpleback", 349 "rb", "redback", 350 351 "greyback", "grayback", 352 }; 353 354 #ifdef COMPACT_OUTPUT 355 char* styles[] = { 356 "blue", "\x1b[38;5;26m", 357 "bold", "\x1b[1m", 358 "gray", "\x1b[38;5;248m", 359 "green", "\x1b[38;5;29m", 360 "inverse", "\x1b[7m", 361 "magenta", "\x1b[38;5;165m", 362 "orange", "\x1b[38;5;166m", 363 "purple", "\x1b[38;5;99m", 364 "red", "\x1b[38;5;1m", 365 "underline", "\x1b[4m", 366 367 "blueback", "\x1b[48;5;26m\x1b[38;5;15m", 368 "grayback", "\x1b[48;5;248m\x1b[38;5;15m", 369 "greenback", "\x1b[48;5;29m\x1b[38;5;15m", 370 "magentaback", "\x1b[48;5;165m\x1b[38;5;15m", 371 "orangeback", "\x1b[48;5;166m\x1b[38;5;15m", 372 "purpleback", "\x1b[48;5;99m\x1b[38;5;15m", 373 "redback", "\x1b[48;5;1m\x1b[38;5;15m", 374 }; 375 #else 376 char* styles[] = { 377 "blue", "\x1b[38;2;0;95;215m", 378 "bold", "\x1b[1m", 379 "gray", "\x1b[38;2;168;168;168m", 380 "green", "\x1b[38;2;0;135;95m", 381 "inverse", "\x1b[7m", 382 "magenta", "\x1b[38;2;215;0;255m", 383 "orange", "\x1b[38;2;215;95;0m", 384 "purple", "\x1b[38;2;135;95;255m", 385 "red", "\x1b[38;2;204;0;0m", 386 "underline", "\x1b[4m", 387 388 "blueback", "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m", 389 "grayback", "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m", 390 "greenback", "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m", 391 "magentaback", "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m", 392 "orangeback", "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m", 393 "purpleback", "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m", 394 "redback", "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m", 395 }; 396 #endif 397 398 bool change_style(const char* arg, span* style) { 399 // style-changing options must have 1 or 2 leading dashes 400 if (arg[0] != '-') { 401 return false; 402 } 403 404 // skip up to 2 leading dashes 405 const char* s = arg + (arg[1] == '-' ? 2 : 1); 406 407 // resolve style-name aliases 408 const size_t n = sizeof(style_names_aliases) / sizeof(char*); 409 for (size_t i = 0; i < n; i += 2) { 410 if (strcmp(s, style_names_aliases[i]) == 0) { 411 s = style_names_aliases[i + 1]; 412 break; 413 } 414 } 415 416 // try to find ANSI-code for the style-name given 417 for (size_t i = 0; i < sizeof(styles) / sizeof(char*); i += 2) { 418 if (strcmp(s, styles[i]) == 0) { 419 style->ptr = (unsigned char*)styles[i + 1]; 420 style->len = strlen(styles[i + 1]); 421 return true; 422 } 423 } 424 425 return false; 426 } 427 428 // run returns the number of errors 429 int run(char** args, size_t nargs, FILE* w, bool live_lines) { 430 size_t dashes = 0; 431 for (int i = 0; i < nargs; i++) { 432 if (strcmp(args[i], "-") == 0) { 433 dashes++; 434 } 435 } 436 437 if (dashes > 1) { 438 const char* m = "can't use the standard input (dash) more than once"; 439 fprintf(stderr, ERROR_LINE("%s"), m); 440 return 1; 441 } 442 443 size_t files = 0; 444 size_t errors = 0; 445 446 slice line; 447 line.cap = 32 * 1024; 448 line.ptr = malloc(line.cap); 449 450 if (line.ptr == NULL) { 451 fprintf(stderr, ERROR_LINE("out of memory")); 452 exit(BAD_ALLOC); 453 } 454 455 handler_args ha; 456 ha.w = w; 457 ha.line = &line; 458 ha.style.ptr = default_digits_style; 459 ha.style.len = strlen((char*)default_digits_style); 460 461 bool options = true; 462 463 for (size_t i = 0; i < nargs && !feof(w); i++) { 464 const char* arg = args[i]; 465 466 // `--` means no more options 467 if (arg[0] == '-' && arg[1] == '-' && arg[2] == 0) { 468 options = false; 469 continue; 470 } 471 472 // `-` means standard input 473 if (arg[0] == '-' && arg[1] == 0) { 474 handle_lines(ha, stdin, live_lines); 475 files++; 476 continue; 477 } 478 479 if (options && arg[0] == '-') { 480 if (!change_style(arg, &ha.style)) { 481 fprintf(stderr, ERROR_LINE("unsupported style named %s"), arg); 482 errors++; 483 } 484 continue; 485 } 486 487 if (!handle_file(ha, arg, live_lines)) { 488 errors++; 489 } 490 files++; 491 } 492 493 // use stdin when not given any filepaths 494 if (files == 0 && !feof(w)) { 495 handle_lines(ha, stdin, live_lines); 496 } 497 498 free(line.ptr); 499 return errors; 500 } 501 502 int main(int argc, char** argv) { 503 #ifdef _WIN32 504 setmode(fileno(stdin), O_BINARY); 505 // ensure output lines end in LF instead of CRLF on windows 506 setmode(fileno(stdout), O_BINARY); 507 setmode(fileno(stderr), O_BINARY); 508 #endif 509 510 if (argc > 1) { 511 if ( 512 strcmp(argv[1], "-h") == 0 || 513 strcmp(argv[1], "-help") == 0 || 514 strcmp(argv[1], "--h") == 0 || 515 strcmp(argv[1], "--help") == 0 516 ) { 517 fprintf(stdout, "%s", info); 518 return 0; 519 } 520 } 521 522 size_t nargs = argc - 1; 523 char** args = argv + 1; 524 bool buffered = false; 525 526 if (nargs > 0) { 527 if ( 528 strcmp(args[0], "-b") == 0 || 529 strcmp(args[0], "--b") == 0 || 530 strcmp(args[0], "-buffered") == 0 || 531 strcmp(args[0], "--buffered") == 0 532 ) { 533 buffered = true; 534 nargs--; 535 args++; 536 } 537 } 538 539 if (nargs > 0 && strcmp(args[0], "--") == 0) { 540 nargs--; 541 args++; 542 } 543 544 const int fd = fileno(stdout); 545 const bool live_lines = !buffered && lseek(fd, 0, SEEK_CUR) != 0; 546 if (live_lines) { 547 setvbuf(stdout, NULL, _IOLBF, 0); 548 } else { 549 setvbuf(stdout, NULL, _IOFBF, 0); 550 } 551 return run(args, nargs, stdout, live_lines) == 0 ? 0 : 1; 552 }