/* The MIT License (MIT) Copyright © 2020-2025 pacman64 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* You can build this command-line app by running cc -Wall -s -O2 -o ./nn ./nn.c Building with COMPACT_OUTPUT defined makes `nn` output many fewer bytes, at the cost of using arguably worse colors. You can do that by running cc -Wall -s -O2 -D COMPACT_OUTPUT -o ./nh ./nh.c */ #include #include #include #include #include #include #ifdef _WIN32 #include #endif // #define COMPACT_OUTPUT // info is the message shown when this app is given any of its help options const char* info = "" "nn [options...] [filepaths...]\n" "\n" "\n" "Nice Numbers is an app which renders the plain text it's given to make long\n" "numbers much easier to read, by alternating 3-digit groups which are colored\n" "using ANSI-codes with unstyled ones.\n" "\n" "Unlike the common practice of inserting commas between 3-digit groups, this\n" "alternative doesn't widen the original text, keeping any alignments the same.\n" "\n" "All input is assumed to be UTF-8. When not given any filepaths, input is read\n" "from the standard input.\n" "\n" "\n" "Options, all of which can start with either 1 or 2 dashes:\n" "\n" "\n" " -blue use a blue-like color to alternate-style runs of digits\n" " -bold use a bold style/effect to alternate-style runs of digits\n" " -gray use a gray color to alternate-style runs of digits\n" " -green use a green color to alternate-style runs of digits\n" " -inverse invert/swap colors to alternate-style runs of digits\n" " -orange use an orange color to alternate-style runs of digits\n" " -purple use a purple color to alternate-style runs of digits\n" " -red use a red color to alternate-style runs of digits\n" "\n" " -h show this help message\n" " -help show this help message\n" "\n" " -highlight same as option -inverse\n" " -hilite same as option -inverse\n" ""; const char* line_memory_error_msg = "" "\x1b[31mcan't get memory for the line-scanner\x1b[0m\n"; // slice is a growable region of bytes in memory typedef struct slice { // ptr is the starting place of the region unsigned char* ptr; // len is how many bytes are currently being used size_t len; // cap is how many bytes the memory region has available size_t cap; } slice; // init_slice is the constructor for type slice void init_slice(slice* s, size_t cap) { s->ptr = malloc(cap); s->len = 0; s->cap = cap; } // advance updates a slice so it starts after the number of bytes given inline void advance(slice* src, size_t n) { src->ptr += n; src->len -= n; } inline void write_bytes(FILE* w, const unsigned char* src, size_t len) { fwrite(src, len, 1, w); } // find_digit returns the index of the first digit found, or a negative value // on failure int64_t find_digit(slice s) { for (size_t i = 0; i < s.len; i++) { const unsigned char b = s.ptr[i]; if ('0' <= b && b <= '9') { return i; } } return -1; } // find_non_digit returns the index of the first non-digit found, or a negative // value on failure int64_t find_non_digit(slice s) { for (size_t i = 0; i < s.len; i++) { const unsigned char b = s.ptr[i]; if (b < '0' || b > '9') { return i; } } return -1; } const unsigned char reset_style[] = "\x1b[0m"; // restyle_digits renders a run of digits as alternating styled/unstyled runs // of 3 digits, which greatly improves readability, and is the only purpose // of this app; string is assumed to be all decimal digits void restyle_digits(FILE* w, slice digits, slice style) { if (digits.len < 4) { // digit sequence is short, so emit it as is write_bytes(w, digits.ptr, digits.len); return; } // separate leading 0..2 digits which don't align with the 3-digit groups size_t lead = digits.len % 3; // emit leading digits unstyled, if there are any write_bytes(w, digits.ptr, lead); // the rest is guaranteed to have a length which is a multiple of 3 advance(&digits, lead); // start with the alternate style, unless there were no leading digits bool style_now = lead != 0; while (digits.len > 0) { if (style_now) { write_bytes(w, style.ptr, style.len); write_bytes(w, digits.ptr, 3); write_bytes(w, reset_style, sizeof(reset_style) - 1); } else { write_bytes(w, digits.ptr, 3); } advance(&digits, 3); // alternate between styled and unstyled 3-digit groups style_now = !style_now; } } // restyle_line renders the line given, using ANSI-styles to make any long // numbers in it more legible void restyle_line(FILE* w, slice line, slice alt_style) { while (!feof(w) && line.len > 0) { int64_t i = find_digit(line); if (i < 0) { // no (more) digits for sure write_bytes(w, line.ptr, line.len); return; } // some ANSI-style sequences use 4-digit numbers, which are long // enough for this app to mangle const unsigned char* p = line.ptr; bool is_ansi = i >= 2 && p[i - 2] == '\x1b' && p[i - 1] == '['; // emit line before current digit-run write_bytes(w, line.ptr, i); advance(&line, i); // see where the digit-run ends int64_t j = find_non_digit(line); if (j < 0) { // the digit-run goes until the end if (!is_ansi) { restyle_digits(w, line, alt_style); } else { write_bytes(w, line.ptr, line.len); } return; } // emit styled digit-run... maybe if (!is_ansi) { slice s; s.ptr = line.ptr; s.len = j; s.cap = j; restyle_digits(w, s, alt_style); } else { write_bytes(w, line.ptr, j); } // skip right past the end of the digit-run advance(&line, j); } } // default_digits_style makes it easy to change the built-in default style unsigned char default_digits_style[] = "\x1b[38;5;248m"; typedef struct handler_args { FILE* w; slice* line; slice style; } handler_args; bool bom_start(slice s) { const unsigned char* p = s.ptr; return s.len >= 3 && p[0] == 0xef && p[1] == 0xbb && p[2] == 0xbf; } // handle_lines loops over input lines, restyling all digit-runs as more // readable `nice numbers`, fulfilling the app's purpose bool handle_lines(handler_args args, FILE* src) { FILE* w = args.w; slice* line = args.line; slice trimmed; trimmed.cap = 0; for (size_t i = 0; !feof(w); i++) { int len = getline((char**)&line->ptr, &line->cap, src); if (len < 0) { break; } if (line->ptr == NULL) { fprintf(stderr, line_memory_error_msg); exit(1); } line->len = len; trimmed.ptr = line->ptr; trimmed.len = line->len; // get rid of leading UTF-8 BOM (byte-order mark) if 1st line has it if (i == 0 && bom_start(trimmed)) { trimmed.ptr += 3; trimmed.len -= 3; len = trimmed.len; } const unsigned char* p = trimmed.ptr; // get rid of trailing line-feeds and CRLF end-of-line byte-pairs if (len >= 2 && p[len - 2] == '\r' && p[len - 1] == '\n') { trimmed.len -= 2; } else if (len >= 1 && p[len - 1] == '\n') { trimmed.len--; } restyle_line(w, trimmed, args.style); putc('\n', w); } return true; } // handle_file handles data from the filename given; returns false only when // the file can't be opened bool handle_file(handler_args args, const char* path) { FILE* f = fopen(path, "rb"); if (f == NULL) { fprintf(stderr, "\x1b[31mcan't open file named %s\x1b[0m\n", path); return false; } const bool ok = handle_lines(args, f); fclose(f); return ok; } const char *style_names_aliases[] = { "b", "blue", "g", "green", "h", "inverse", "i", "inverse", "m", "magenta", "o", "orange", "p", "purple", "r", "red", "u", "underline", "hi", "inverse", "ma", "magenta", "or", "orange", "un", "underline", "inv", "inverse", "mag", "magenta", "grey", "gray", "highlight", "inverse", "highlighted", "inverse", "hilite", "inverse", "hilited", "inverse", "invert", "inverse", "inverted", "inverse", "underlined", "underline", "bb", "blueback", "gb", "greenback", "mb", "magentaback", "ob", "orangeback", "pb", "purpleback", "rb", "redback", "greyback", "grayback", }; #ifdef COMPACT_OUTPUT char *styles[] = { "blue", "\x1b[38;5;26m", "bold", "\x1b[1m", "gray", "\x1b[38;5;248m", "green", "\x1b[38;5;29m", "inverse", "\x1b[7m", "magenta", "\x1b[38;5;165m", "orange", "\x1b[38;5;166m", "purple", "\x1b[38;5;99m", "red", "\x1b[38;5;1m", "underline", "\x1b[4m", "blueback", "\x1b[48;5;26m\x1b[38;5;15m", "grayback", "\x1b[48;5;248m\x1b[38;5;15m", "greenback", "\x1b[48;5;29m\x1b[38;5;15m", "magentaback", "\x1b[48;5;165m\x1b[38;5;15m", "orangeback", "\x1b[48;5;166m\x1b[38;5;15m", "purpleback", "\x1b[48;5;99m\x1b[38;5;15m", "redback", "\x1b[48;5;1m\x1b[38;5;15m", }; #else char *styles[] = { "blue", "\x1b[38;2;0;95;215m", "bold", "\x1b[1m", "gray", "\x1b[38;2;168;168;168m", "green", "\x1b[38;2;0;135;95m", "inverse", "\x1b[7m", "magenta", "\x1b[38;2;215;0;255m", "orange", "\x1b[38;2;215;95;0m", "purple", "\x1b[38;2;135;95;255m", "red", "\x1b[38;2;204;0;0m", "underline", "\x1b[4m", "blueback", "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m", "grayback", "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m", "greenback", "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m", "magentaback", "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m", "orangeback", "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m", "purpleback", "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m", "redback", "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m", }; #endif bool change_style(const char* arg, slice* style) { // style-changing options must have 1 or 2 leading dashes if (arg[0] != '-') { return false; } // skip up to 2 leading dashes const char* s = arg + (arg[1] == '-' ? 2 : 1); // resolve style-name aliases const size_t n = sizeof(style_names_aliases) / sizeof(char*); for (size_t i = 0; i < n; i += 2) { if (strcmp(s, style_names_aliases[i]) == 0) { s = style_names_aliases[i + 1]; break; } } // try to find ANSI-code for the style-name given for (size_t i = 0; i < sizeof(styles) / sizeof(char *); i += 2) { if (strcmp(s, styles[i]) == 0) { style->ptr = (unsigned char*)styles[i + 1]; style->len = strlen(styles[i + 1]); return true; } } return false; } // run returns the number of errors int run(int argc, char** argv, FILE* w, slice* line) { size_t files = 0; size_t errors = 0; handler_args args; args.w = w; args.line = line; args.style.ptr = default_digits_style; args.style.len = strlen((char*)default_digits_style); for (size_t i = 1; i < (size_t)argc && !feof(w); i++) { const char* arg = argv[i]; // `-` means standard input if (arg[0] == '-' && arg[1] == 0) { if (!handle_lines(args, stdin)) { errors++; } files++; continue; } if (arg[0] == '-') { if (!change_style(arg, &args.style)) { char* fmt = "\x1b[31munsupported style named %s\x1b[0m\n"; fprintf(stderr, fmt, arg); errors++; } continue; } if (!handle_file(args, arg)) { errors++; } files++; } // use stdin when not given any filepaths if (files == 0) { if (!handle_lines(args, stdin)) { errors++; } } return errors; } // is_help_option simplifies control-flow for func main bool is_help_option(char* s) { return (s[0] == '-') && ( strcmp(s, "-h") == 0 || strcmp(s, "-help") == 0 || strcmp(s, "--h") == 0 || strcmp(s, "--help") == 0 ); } int main(int argc, char** argv) { #ifdef _WIN32 setmode(fileno(stdin), O_BINARY); // ensure output lines end in LF instead of CRLF on windows setmode(fileno(stdout), O_BINARY); setmode(fileno(stderr), O_BINARY); #endif // handle any of the help options, if given if (argc > 1 && is_help_option(argv[1])) { puts(info); return 0; } slice line; init_slice(&line, 32 * 1024); if (line.ptr == NULL) { fprintf(stderr, line_memory_error_msg); return 1; } const int res = run(argc, argv, stdout, &line) == 0 ? 0 : 1; free(line.ptr); return res; }