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