Download
482 lines 16.4 KiBCModified 2019-01-06 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
/**
  ASCII Table Utility by Bruce Hill

  A simple utility that uses ncurses to display the ASCII table and lets you
  manually output ASCII characters one byte at a time. Use arrow keys or
  h/j/k/l to move, 'enter' to add a character to the output, '/' to search,
  and 'q' or escape to quit and print the output.

  Licensed under the MIT License (see LICENSE file for details)
*/

#include <curses.h>
#include <ncurses.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

typedef enum {BROWSE_MODE, INSERT_MODE, SEARCH_MODE} Mode;

#define DELETE_KEY 127
#define ESCAPE_KEY 27

// The descriptive names of the ASCII characters
const char *NAMES[128][2] = {
    {"NUL", "null"}, // 0
    {"SOH", "start of heading"}, // 1
    {"STX", "start of text"}, // 2
    {"ETX", "end of text"}, // 3
    {"EOT", "end of transmission"}, // 4
    {"ENQ", "enquiry"}, // 5
    {"ACK", "acknowledge"}, // 6
    {"BEL", "bell"}, // 7
    {"BS", "backspace"}, // 8
    {"TAB", "horizontal tab"}, // 9
    {"LF", "NL line feed, new line"}, // 10
    {"VT", "vertical tab"}, // 11
    {"FF", "NP form feed, new page"}, // 12
    {"CR", "carriage return"}, // 13
    {"SO", "shift out"}, // 14
    {"SI", "shift in"}, // 15
    {"DLE", "data link escape"}, // 16
    {"DC1", "device control 1"}, // 17
    {"DC2", "device control 2"}, // 18
    {"DC3", "device control 3"}, // 19
    {"DC4", "device control 4"}, // 20
    {"NAK", "negative acknowledge"}, // 21
    {"SYN", "synchonous idle"}, // 22
    {"ETB", "end of transmission block"}, // 23
    {"CAN", "cancel"}, // 24
    {"EM", "end of medium"}, // 25
    {"SUB", "substitute"}, // 26
    {"ESC", "escape"}, // 27
    {"FS", "file separator"}, // 28
    {"GS", "group separator"}, // 29
    {"RS", "record separator"}, // 30
    {"US", "unit separator"}, // 31
    {" ", "space"}, // 32
    [127] = {"DEL", "delete"}
};

static int new_colorpair(int fg, int bg)
{
    static int next_colornum = 1;
    init_pair(next_colornum, fg, bg);
    return COLOR_PAIR(next_colornum++);
}

int DECIMAL_COLORS, HEX_COLORS, CHAR_COLORS, DESCRIPTION_SPECIAL_COLORS,
    DESCRIPTION_LETTER_COLORS, DESCRIPTION_NUMBER_COLORS, DESCRIPTION_SYMBOL_COLORS,
    OUTPUT_CHAR_COLORS, OUTPUT_ESCAPE_COLORS, OUTPUT_LABEL_COLORS, BROWSE_MODE_COLORS,
    INSERT_MODE_COLORS, SEARCH_MODE_COLORS;

const int COL_WIDTH = 42;
int W = 0;
int H = 0;

static inline int min(int a, int b) {
    return a < b ? a : b;
}

static inline int max(int a, int b) {
    return a >= b ? a : b;
}

// Shift the cursor
static inline void shift(int dy, int dx) {
    int x, y;
    getyx(stdscr, y, x);
    move(y+dy, x+dx);
}

// Same as printw, except it doesn't do text wrapping, and it sets attributes
#define printwattrs(_attrs, ...) \
    { char _buf[W+1];\
    chtype _bufch[W+1];\
    sprintf(_buf, __VA_ARGS__);\
    for (int i = 0; i == 0 || _buf[i-1]; i++) _bufch[i] = _buf[i] | _attrs;\
    addchstr(_bufch);\
    shift(0, strlen(_buf));}


// Draw the information for one character
static inline void draw_entry(int i, int scrollx, int attrs)
{
    int x = (i / (H-2)) * COL_WIDTH - scrollx;
    if (x > W || x < 0) return;
    int y = (i % (H-2)) + 1;
    move(y, x);
    printwattrs(DECIMAL_COLORS | attrs, "%3d ", i);
    printwattrs(HEX_COLORS | attrs, " 0x%02X ", i);
    if (NAMES[i][0]) {
        printwattrs(CHAR_COLORS | attrs, " %-3s ", NAMES[i][0]);
    } else {
        printwattrs(CHAR_COLORS | attrs, " %c   ", i);
    }
    if (NAMES[i][1]) {
        printwattrs(DESCRIPTION_SPECIAL_COLORS | attrs, " %-25s", NAMES[i][1]);
    } else if (('a' <= i && i <= 'z') || ('A' <= i && i <= 'Z')) {
        printwattrs(DESCRIPTION_LETTER_COLORS | attrs, " the letter %c             ", i);
    } else if ('0' <= i && i <= '9') {
        printwattrs(DESCRIPTION_NUMBER_COLORS | attrs, " the number %c             ", i);
    } else {
        printwattrs(DESCRIPTION_SYMBOL_COLORS | A_BOLD | attrs, " the %c symbol             ", i);
    }
}

// Redraw the parts of the ASCII table necessary for the selected item
static void redraw(int selected)
{
    static int last_selected = -1, last_scrollx = -1, last_W = -1, last_H = -1;
    int selectedx = (selected / (H-2)) * COL_WIDTH;
    int scrollx = (selectedx + COL_WIDTH < W) ? 0 : (((selectedx + COL_WIDTH) - W)/COL_WIDTH+1)*COL_WIDTH;
    if (last_selected == -1 || last_scrollx != scrollx || last_W != W || last_H != H) {
        // Redraw all the entries
        erase();
        for (int i = 0; i < 128; i++) {
            draw_entry(i, scrollx, i == selected ? A_REVERSE : 0);
        }
        last_scrollx = scrollx;
        last_H = H;
        last_W = W;
    } else if (selected != last_selected) {
        // Otherwise, we can lazily just redraw 2 entries
        draw_entry(selected, scrollx, A_REVERSE);
        draw_entry(last_selected, scrollx, 0);
    }
    last_selected = selected;
}

// Find the ascii character starting at `start`, going in `direction`, matching
// `searchbuff`, and return `fallback` if nothing matches.
static int find_ascii_match(int start, int fallback, int direction, char *searchbuf)
{
    if (searchbuf[0] == '\0') return fallback;
    char tmp[COL_WIDTH+6];
    for (int i = 0; i <= 127; i++) {
        int c = (start + i*direction) % 128; // relative to starting point
        if (searchbuf[0] == c && searchbuf[1] == '\0')
            return c;
        // Only do full match for search strings longer than 1
        if (searchbuf[1]) {
            if (NAMES[c][1]) {
                sprintf(tmp, "%d 0x%02X 0x%02x %s %s", c, c, c, NAMES[c][0] ? NAMES[c][0] : "", NAMES[c][1]);
            } else if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')) {
                sprintf(tmp, "%d 0x%02X 0x%02x %s the letter %c", c, c, c, NAMES[c][0] ? NAMES[c][0] : "", c);
            } else if ('0' <= c && c <= '9') {
                sprintf(tmp, "%d 0x%02X 0x%02x %s the number %c", c, c, c, NAMES[c][0] ? NAMES[c][0] : "", c);
            } else {
                sprintf(tmp, "%d 0x%02X 0x%02x %s the %c character", c, c, c, NAMES[c][0] ? NAMES[c][0] : "", c);
            }
            if (strstr(tmp, searchbuf) != NULL)
                return c;
        }
    }
    return fallback;
}

// To prevent wasted work, wait until 200ms has elapsed with no additional
// resizes before doing anything, then set the correct W/H values.
static void handle_resize()
{
    timeout(0);
    do {
        // Wait 200ms
        usleep(2e5);
        // Peek if there's another resize event
        int k = getch();
        if (k == KEY_RESIZE) continue;
        if (k > 0) ungetch(k);
    } while (0);
    // Back to blocking input
    timeout(-1);
    W = getmaxx(stdscr);
    H = getmaxy(stdscr);
}

int main(int argc, char *argv[])
{	
    if (argc > 1 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0)) {
        printf("ASCII table viewer/editor, Usage: ascii [N...]\n");
        return 0;
    }

    // Instead of initscr(), set up the terminal this way to play nicely with
    // the unix pipeline:
    {
        char* term_type = getenv("TERM");
        if (term_type == NULL || *term_type == '\0') {
          term_type = "unknown";
        }
        FILE* term_in = fopen("/dev/tty", "r");
        if (term_in == NULL) {
            perror("fopen(/dev/tty)");
            exit(EXIT_FAILURE);
        }
        SCREEN *screen = newterm(term_type, stderr, term_in);
        set_term(screen);
    }

    W = getmaxx(stdscr);
    H = getmaxy(stdscr);
    noecho();
    set_escdelay(5);
    keypad(stdscr, 1);
    curs_set(2);
    start_color();

	DECIMAL_COLORS = new_colorpair(COLOR_YELLOW, COLOR_BLACK) | A_BOLD;
	HEX_COLORS = new_colorpair(COLOR_GREEN, COLOR_BLACK) | A_BOLD;
	CHAR_COLORS = new_colorpair(COLOR_MAGENTA, COLOR_BLACK) | A_BOLD;
	DESCRIPTION_SPECIAL_COLORS = new_colorpair(COLOR_RED, COLOR_BLACK) | A_BOLD;
	DESCRIPTION_LETTER_COLORS = new_colorpair(COLOR_BLUE, COLOR_BLACK) | A_BOLD;
	DESCRIPTION_NUMBER_COLORS = new_colorpair(COLOR_CYAN, COLOR_BLACK) | A_BOLD;
	DESCRIPTION_SYMBOL_COLORS = new_colorpair(COLOR_WHITE, COLOR_BLACK) | A_BOLD;
	OUTPUT_CHAR_COLORS = new_colorpair(COLOR_BLACK, COLOR_WHITE) | A_BOLD;
	OUTPUT_ESCAPE_COLORS = new_colorpair(COLOR_RED, COLOR_WHITE) | A_BOLD;
    OUTPUT_LABEL_COLORS = new_colorpair(COLOR_BLACK, COLOR_YELLOW);
    BROWSE_MODE_COLORS = new_colorpair(COLOR_BLACK, COLOR_YELLOW);
    INSERT_MODE_COLORS = new_colorpair(COLOR_WHITE, COLOR_RED) | A_BOLD;
    SEARCH_MODE_COLORS = new_colorpair(COLOR_BLACK, COLOR_BLUE);

    size_t buf_chunk = 1024;
    size_t buf_size = buf_chunk, buf_i = 0;
    char *outbuf = calloc(buf_size, sizeof(char));

    { // Read stdin if anything was piped in
        struct pollfd desc;
        desc.fd = STDIN_FILENO;
        desc.events = POLLIN;
        int ret = poll(&desc, 1, 50);
        if (ret > 0) {
            size_t consumed;
            while ((consumed = fread(&outbuf[buf_i], 1, buf_chunk, stdin))) {
                buf_i += consumed;
                if (consumed == buf_chunk) {
                    buf_chunk *= 2;
                    buf_size += buf_chunk;
                    outbuf = realloc(outbuf, buf_size);
                }
            }
        }
    }

    char searchbuf[1024] = {'\0'};
    size_t searchlen = 0;

    // If any command line args, treat them as bytes to append to
    // the output buffer
    for (int i = 1; i < argc; i++) {
        long c = strtol(argv[i], NULL, 0);
        outbuf[buf_i++] = (char)c;
        if (buf_i >= buf_size) {
            buf_size *= 2;
            outbuf = realloc(outbuf, buf_size);
        }
    }

    Mode mode = BROWSE_MODE;
    int prev_selected = 0, selected = 0;
    while (1) {
        redraw(selected);
        // Title bar
        move(0,0);
        char *title;
        int title_colors;
        switch (mode) {
            case INSERT_MODE:
                title = "ASCII TABLE (typing, 'esc' to browse)";
                title_colors = INSERT_MODE_COLORS;
                break;
            case SEARCH_MODE:
                title = "ASCII TABLE (searching, 'esc' to browse)";
                title_colors = SEARCH_MODE_COLORS;
                break;
            case BROWSE_MODE:
                title = "ASCII TABLE (browsing, 'esc' to exit)";
                title_colors = BROWSE_MODE_COLORS;
                break;
        }
        int spacer = W/2-strlen(title)/2;
        printwattrs(title_colors, "%*s%s%*s", spacer, "", title, spacer+1, "");

        if (mode == SEARCH_MODE) {
            attron(SEARCH_MODE_COLORS);
            mvprintw(H-1, 0, " Search:");
            attroff(SEARCH_MODE_COLORS);
            addch(' ');
            printwattrs(OUTPUT_CHAR_COLORS, "%s", searchbuf);
        } else {
            // Bar at the bottom of the screen showing the output text:
            // left-truncate output so the end of it fits on screen:
            char *visible = &outbuf[buf_i];
            for (int space = W-strlen(" Output: ")-1-5; space > 0 && visible > outbuf; visible--) {
                int chlen = (' ' <= *visible && *visible <= '~') ? 1 : 4; // printable range
                if (chlen > space) break;
                else space -= chlen;
            }
            attron(mode == INSERT_MODE ? INSERT_MODE_COLORS : OUTPUT_LABEL_COLORS);
            mvprintw(H-1, 0, " Output:");
            attroff(mode == INSERT_MODE ? INSERT_MODE_COLORS : OUTPUT_LABEL_COLORS);
            addch(' ');
            for (char *c = visible; c < &outbuf[buf_i]; c++) {
                if (' ' <= *c && *c <= '~') { // printable range
                    printwattrs(OUTPUT_CHAR_COLORS, "%c", *c);
                } else {
                    printwattrs(OUTPUT_ESCAPE_COLORS, "\\x%02X", *c);
                }
            }
        }
        int y, x;
        getyx(stdscr, y, x);
        // Clear to end of line
        for (int x2 = x; x2 < W; x2++) mvaddch(y, x2, ' ');
        move(y, x);

        refresh();

        int key;
      next_input:
        key = getch();
        switch (mode) {
            case INSERT_MODE:
                switch (key) {
                    case ESCAPE_KEY:
                        mode = BROWSE_MODE;
                        break;
                    case DELETE_KEY:
                        if (buf_i > 0) buf_i--;
                        break;
                    case KEY_RESIZE:
                        handle_resize();
                        break;
                    default:
                        if (0 <= key && key < 127) {
                            outbuf[buf_i++] = (char)key;
                            if (buf_i >= buf_size) {
                                buf_size *= 2;
                                outbuf = realloc(outbuf, buf_size);
                            }
                            break;
                        } else goto next_input; // skip redraw
                }
                break;


            case SEARCH_MODE:
                switch (key) {
                    case ESCAPE_KEY: // Escape
                        selected = prev_selected;
                        searchbuf[0] = '\0';
                        searchlen = 0;
                        mode = BROWSE_MODE;
                        break;

                    case KEY_ENTER: case '\n':
                        mode = BROWSE_MODE;
                        break;

                    case DELETE_KEY:
                        if (searchlen > 0) {
                            searchbuf[searchlen--] = '\0';
                            selected = find_ascii_match(prev_selected, prev_selected, 1, searchbuf);
                        }
                        break;

                    case KEY_RESIZE:
                        handle_resize();
                        break;

                    default:
                        if (0 <= key && key < 127) {
                            if (searchlen < sizeof(searchbuf)-1) {
                                searchbuf[searchlen++] = (char)key;
                                searchbuf[searchlen] = '\0';
                            }
                            selected = find_ascii_match(prev_selected, prev_selected, 1, searchbuf);
                            break;
                        } else goto next_input; // skip redraw
                }
                break;


            case BROWSE_MODE:
                switch (key) {
                    case 'j': case KEY_DOWN:
                        selected++;
                        break;

                    case 'J':
                        selected += 10;
                        break;

                    case 'k': case KEY_UP:
                        selected--;
                        break;

                    case 'K':
                        selected -= 10;
                        break;

                    case 'l': case 'L': case KEY_RIGHT: case KEY_SRIGHT:
                        selected += H-2;
                        break;

                    case 'h': case 'H': case KEY_LEFT: case KEY_SLEFT:
                        selected -= H-2;
                        break;

                    case 'i': case 'a':
                        mode = INSERT_MODE;
                        break;

                    case '/':
                        mode = SEARCH_MODE;
                        prev_selected = selected;
                        searchbuf[0] = '\0';
                        searchlen = 0;
                        break;

                    case 'n':
                        selected = find_ascii_match(selected+1, selected, 1, searchbuf);
                        break;

                    case 'N':
                        selected = find_ascii_match(selected-1, selected, -1, searchbuf);
                        break;

                    case KEY_RESIZE:
                        handle_resize();
                        break;

                    case KEY_ENTER: case '\n':
                        outbuf[buf_i++] = (char)selected;
                        if (buf_i >= buf_size) {
                            buf_size *= 2;
                            outbuf = realloc(outbuf, buf_size);
                        }
                        break;

                    case KEY_BACKSPACE: case KEY_DC: case DELETE_KEY:
                        if (buf_i > 0) buf_i--;
                        break;

                    case 'q': case 'Q': case KEY_CANCEL: case KEY_CLOSE: case KEY_EXIT: case ESCAPE_KEY:
                        endwin();
                        write(STDOUT_FILENO, outbuf, buf_i);
                        return 0;

                    default:
                        goto next_input; // skip redraw
                }
                break;
        }
        if (selected < 0) selected = 0;
        if (selected > 127) selected = 127;
    }

	endwin();
	return 0;
}