/* showfont.c
 *
 * Copyright (C) 2004 Andy Goth <unununium@openverse.com>
 * http://ioioio.net/
 * This program is available under the GNU General Public License. */

#define _GNU_SOURCE
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* Usage information. */
#define USAGE_STR "Usage: showfont [\e[1mOPTIONS\e[0m]\n"                    \
"Fashionably display the current terminal font.\n"                           \
"\n"                                                                         \
"  -r, --range=\e[1mBEG\e[0m[,\e[1mEND\e[0m]    Display character[s] number "\
    "\e[1mBEG\e[0m [through \e[1mEND\e[0m].\n"                               \
"  -c, --columns=\e[1mNUM\e[0m        Display \e[1mNUM\e[0m characters "     \
    "per row.\n"                                                             \
"  -g, --grid=\e[1mPRI\e[0m[,\e[1mSEC\e[0m]     Group \e[1mPRI\e[0m "        \
    "characters [also group \e[1mSEC\e[0m groups].\n"                        \
"  -h, --hex-head[=\e[1mBOOL\e[0m]    Enable or disable hexadecimal row "    \
    "headers.\n"                                                             \
"  -d, --dec-head[=\e[1mBOOL\e[0m]    Enable or disable decimal row "        \
    "headers.\n"                                                             \
"  -n, --no-newline[=\e[1mBOOL\e[0m]  Enable or disable suppression of "     \
    "final newline.\n"                                                       \
"  -C, --colors[=\e[1mCOL1\e[0m,\e[1mCOL2\e[0m] Enable color [set column "   \
    "colors to \e[1mCOL1\e[0m, \e[1mCOL2\e[0m].\n"                           \
"  -H, --help               Print this message and exit.\n"                  \
"  -V, --version            Print version information and then exit.\n"      \
"\n"                                                                         \
"0: \e[1;30mgray\e[0m; 1: \e[1;31mred\e[0m; 2: \e[1;32mgreen\e[0m; "         \
    "3: \e[1;33myellow\e[0m; 4: \e[1;34mblue\e[0m; 5: \e[1;35mpink\e[0m; "   \
    "6: \e[1;36mcyan\e[0m; 7: \e[1;37mwhite\e[0m.\n"

/* Yeah, there are too many options, they control all the wrong things, it's
 * not very robust, and the result is inflexible and overcomplicated, but oh
 * well, so long as you stick to the paved surfaces everything's reasonably
 * pretty.  I learned that trick from Microsoft. :^) */

/* Version information. */
#define VERSION_STR "showfont 0.1, March 21, 2004\n"                         \
"Copyright 2004 Andy Goth <unununium@openverse.com>.\n"                      \
"Check http://ioioio.net/ for updates, if I ever release them. :^)\n"        \
"showfont is free software, covered by the GNU General Public License;\n"    \
"you are welcome to change and/or distribute it under certain conditions.\n" \
"There is absolutely no warranty for showfont.  See the GPL for details.\n"

/* An ill omen. */
#define ERROR_STR "Try `showfont --help' for more information.\n"

/* Why don't I just use printf()/fprintf()?  No reason. :^)  I suppose I was
 * just in a do-it-yourself mood. */

/* Prints a string. */
#define print_str(s) write(1, (s), sizeof(s))

/* Prints a string to stderr. */
#define error_str(s) write(2, (s), sizeof(s))

/* Prints a character. */
#define print_byte(c) ({char x = c; write(1, &x, 1);})

/* Converts the low-order four bits of an integer to a hexadecimal character. */
#define to_hex(v) ({int x = (v) % 16; (x) > 9 ? (x) + 'a' - 10 : (x) + '0';})

/* Evaluates to true if the argument, a boolean string, is true. */
#define is_true(s) (strcasecmp((s), "on"  ) == 0 || \
                    strcasecmp((s), "yes" ) == 0 || \
                    strcasecmp((s), "true") == 0 || \
                    strcasecmp((s), "1"   ) == 0)

/* Prints a nonnegative hexadecimal integer. */
static void print_hex(int v, int n)
{
    while (n > 0) {
        --n;
        print_byte(to_hex(v >> (4 * n)));
    }
}

/* Prints a nonnegative decimal integer. */
static void print_dec(int v, int n)
{
    char buf[n];

    while (n > 0) {
        /* Calculate digits in order of increasing significance. */
        --n;
        buf[n] = v % 10 + '0';
        v /= 10;
    }

    /* Then print in order of decreasing significance. */
    print_str(buf);
}

/* Prints any character from the current font. */
static void print_char(int c)
{
    if (c < 32 || c > 126) {
        /* Encode character as UTF-8. */
        char buf[3] = {0xef};
        buf[1] = 0x80 | ((c >> 6) &  7);
        buf[2] = 0x80 | ((c >> 0) & 63);
        print_str(buf);
    } else {
        /* Send character directly. */
        print_byte(c);
    }
}

/* This function needs no introduction. */
int main(int argc, char** argv)
{
    /* Defaults for command-line options. */
    int first_char  =   0, last_char   = 255, num_cols    =  64;
    int pri_grid    =   4, sec_grid    =   8, hex_headers =   0;
    int dec_headers =   0, no_newline  =   0, colors      =   0;
    int color_1     =   1, color_2     =   4;

    /* getopt_long parameters describing command-line options. */
    char* short_opts = "r:c:g:h::d::n::C::HV";
    struct option long_opts[] = {
       {"range"      , required_argument, NULL, 'r'},
       {"columns"    , required_argument, NULL, 'c'},
       {"grid"       , required_argument, NULL, 'g'},
       {"hex-head"   , optional_argument, NULL, 'h'},
       {"dec-head"   , optional_argument, NULL, 'd'},
       {"no-newline" , optional_argument, NULL, 'n'},
       {"colors"     , optional_argument, NULL, 'C'},
       {"help"       , no_argument      , NULL, 'H'},
       {"version"    , no_argument      , NULL, 'V'},
       {NULL         , 0                , NULL,  0 }
    };

    int c;
    char* p;

    /* Scan the command line. */
    while ((c = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
        switch (c) {
        case 'r':
            first_char = atoi(optarg);
            p = strchr(optarg, ',');
            last_char = p == NULL ? first_char : atoi(p + 1);
            if (first_char <   0       ) first_char =   0;
            if (first_char > 511       ) first_char = 511;
            if (last_char  < first_char) last_char  = first_char;
            if (last_char  > 511       ) last_char  = 511;
            break;
        case 'c':
            num_cols = atoi(optarg);
            break;
        case 'g':
            pri_grid = atoi(optarg);
            p = strchr(optarg, ',');
            if (p != NULL) {
                sec_grid = atoi(p + 1);
            } else {
                sec_grid = 0;
            }
            break;
        case 'h': hex_headers = optarg == NULL ? 1 : is_true(optarg); break;
        case 'd': dec_headers = optarg == NULL ? 1 : is_true(optarg); break;
        case 'n': no_newline  = optarg == NULL ? 1 : is_true(optarg); break;
        case 'C': 
            colors = 1;
            if (optarg != NULL) {
                color_1 = atoi(optarg);
                p = strchr(optarg, ',');
                color_2 = p == NULL ? color_1 : atoi(p + 1);
            }
            break;
        case 'H': print_str(USAGE_STR);   exit(EXIT_SUCCESS);
        case 'V': print_str(VERSION_STR); exit(EXIT_SUCCESS);
        case '?':
        case ':': error_str(ERROR_STR);   exit(EXIT_FAILURE);
        }
    }
    
    /* Enable Unicode mode. */
    print_str("\e%G");

    /* Plot the selected character range in a nice colored grid arrangement. */
    for (c = first_char; c <= last_char; ++c) {
        if ((c - first_char) % num_cols == 0) {
            /* If this is the first character of the row... */

            if (c != first_char) {
                /* ... but not the first row, go to the next line. */
                if (colors) print_str("\e[0m");
                print_byte('\n');
            }

            if (hex_headers || dec_headers) {
                /* Print the left parens of the row header. */
                if (colors) print_str("\e[30;1m");
                print_byte('(');
                if (colors) print_str("\e[0m");

                if (hex_headers) {
                    /* Print the hex header. */
                    print_hex(c, 3);
                    if (dec_headers) {
                        /* Print the divider between the hex and dec headers. */
                        if (colors) print_str("\e[30;1m");
                        print_byte('/');
                        if (colors) print_str("\e[0m");
                    }
                }

                if (dec_headers) {
                    /* Print the dec header. */
                    print_dec(c, 3);
                }

                /* Print the right parens of the row header. */
                if (colors) print_str("\e[30;1m");
                print_byte(')');
                if (colors) print_str("\e[0m");
                print_str("  ");
            }
            if (colors) {
                print_str("\e[3");
                print_dec(color_1, 1);
                print_str(";1m");
            }
        } else if (pri_grid > 0 &&
        (c - first_char) % num_cols % pri_grid == 0) {
            /* If a grid line belongs here... */
            if (colors) {
                if (sec_grid > 0 &&
                (c - first_char) % num_cols % (pri_grid * sec_grid) == 0) {
                    print_str("\e[0m");
                }
                if ((c - first_char) % num_cols % (pri_grid * 2) == 0) {
                    print_str("\e[3");
                    print_dec(color_1, 1);
                    print_str(";1m");
                } else {
                    print_str("\e[3");
                    print_dec(color_2, 1);
                    print_str(";1m");
                }
            }
            print_byte(' ');
            if (sec_grid > 0 &&
            (c - first_char) % num_cols % (pri_grid * sec_grid) == 0 &&
            (c - first_char) % num_cols > 0) {
                print_byte(' ');
            }
        }

        print_char(c);
    }
    if (colors)      print_str("\e[0m");
    if (!no_newline) print_byte('\n');

    /* Disable Unicode mode.  (Hopefully it wasn't enabled before??) */
    print_str("\e%@");

    return EXIT_SUCCESS;
}

/* vim: set ts=4 sts=4 sw=4 et: */

