#if 0 /*─────────────────────────────────────────────────────────────────╗ │ To the extent possible under law, Justine Tunney has waived │ │ all copyright and related or neighboring rights to this file, │ │ as it is written in the following disclaimers: │ │ • http://unlicense.org/ │ │ • http://creativecommons.org/publicdomain/zero/1.0/ │ ╚─────────────────────────────────────────────────────────────────*/ #endif #include #include #include #include #include #include #include #include #include #include #include #include /** * @fileoverview program for viewing bbs art files * @see https://github.com/blocktronics/artpacks * @see http://www.textfiles.com/art/ */ #define HELP \ "Usage:\n\ art [-b %d] [-f %s] [-t %s] FILE...\n\ \n\ Flags:\n\ -b NUMBER specifies simulated modem baud rate, which defaults to\n\ 2400 since that was the most common modem speed in the\n\ later half of the 1980s during the BBS golden age; you\n\ could also say 300 for the slowest experience possible\n\ or you could say 14.4k to get more of a 90's feel, and\n\ there's also the infamous 56k to bring you back to y2k\n\ -f CHARSET specifies charset of input bytes, where the default is\n\ cp347 which means IBM Code Page 347 a.k.a. DOS\n\ -t CHARSET specifies output charset used by your terminal, and it\n\ defaults to utf8 a.k.a. thompson-pike encoding\n\ \n\ Supported charsets:\n\ utf8, ascii, wchar_t, ucs2be, ucs2le, utf16be, utf16le, ucs4be,\n\ ucs4le, utf16, ucs4, ucs2, eucjp, shiftjis, iso2022jp, gb18030, gbk,\n\ gb2312, big5, euckr, iso88591, latin1, iso88592, iso88593, iso88594,\n\ iso88595, iso88596, iso88597, iso88598, iso88599, iso885910,\n\ iso885911, iso885913, iso885914, iso885915, iso885916, cp1250,\n\ windows1250, cp1251, windows1251, cp1252, windows1252, cp1253,\n\ windows1253, cp1254, windows1254, cp1255, windows1255, cp1256,\n\ windows1256, cp1257, windows1257, cp1258, windows1258, koi8r, koi8u,\n\ cp437, cp850, cp866, ibm1047, cp1047.\n\ \n\ See also:\n\ http://www.textfiles.com/art/\n\ https://github.com/blocktronics/artpacks\n\ \n" #define INBUFSZ 256 #define OUBUFSZ (INBUFSZ * 6) #define SLIT(s) ((unsigned)s[3] << 24 | s[2] << 16 | s[1] << 8 | s[0]) // "When new technology comes out, people don't all buy it right away. // If what they have works, some will wait until it doesn't. A few // people do get the latest though. In 1984 2400 baud modems became // available, so some people had them, but many didn't. A BBS list // from 1986 shows operators were mostly 300 and 1200, but some were // using 2400. The next 5 years were the hayday of the 2400." // // https://forum.vcfed.org/index.php?threads/the-2400-baud-modem.44241/ int baud_rate = 2400; // -b 2400 const char* from_charset = "CP437"; // -f CP437 const char* to_charset = "UTF-8"; // -t UTF-8 volatile sig_atomic_t done; void on_signal(int sig) { done = 1; (void)sig; } void print(const char* s) { (void)!write(STDOUT_FILENO, s, strlen(s)); } int encode_character(char output[8], const char* codec, wchar_t character) { size_t inbytesleft = sizeof(wchar_t); size_t outbytesleft = 7; char* inbuf = (char*)&character; char* outbuf = output; iconv_t cd = iconv_open(codec, "wchar_t"); if (cd == (iconv_t)-1) return -1; size_t result = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); iconv_close(cd); if (result == (size_t)-1) return -1; *outbuf = '\0'; return 7 - outbytesleft; } void append_replacement_character(char** b) { int n = encode_character(*b, to_charset, 0xFFFD); if (n == -1) n = encode_character(*b, to_charset, '?'); if (n != -1) *b += n; } int compare_time(struct timespec a, struct timespec b) { int cmp; if (!(cmp = (a.tv_sec > b.tv_sec) - (a.tv_sec < b.tv_sec))) cmp = (a.tv_nsec > b.tv_nsec) - (a.tv_nsec < b.tv_nsec); return cmp; } struct timespec add_time(struct timespec x, struct timespec y) { x.tv_sec += y.tv_sec; x.tv_nsec += y.tv_nsec; if (x.tv_nsec >= 1000000000) { x.tv_nsec -= 1000000000; x.tv_sec += 1; } return x; } struct timespec subtract_time(struct timespec a, struct timespec b) { a.tv_sec -= b.tv_sec; if (a.tv_nsec < b.tv_nsec) { a.tv_nsec += 1000000000; a.tv_sec--; } a.tv_nsec -= b.tv_nsec; return a; } struct timespec fromnanos(long long x) { struct timespec ts; ts.tv_sec = x / 1000000000; ts.tv_nsec = x % 1000000000; return ts; } void process_file(const char* path, int fd, iconv_t cd) { size_t carry = 0; struct timespec next; char input_buffer[INBUFSZ]; clock_gettime(CLOCK_MONOTONIC, &next); for (;;) { // read from file ssize_t bytes_read = read(fd, input_buffer + carry, INBUFSZ - carry); if (!bytes_read) return; if (bytes_read == -1) { perror(path); done = 1; return; } // modernize character set char* input_ptr = input_buffer; size_t input_left = carry + bytes_read; char output_buffer[OUBUFSZ]; char* output_ptr = output_buffer; size_t output_left = OUBUFSZ; size_t ir = iconv(cd, &input_ptr, &input_left, &output_ptr, &output_left); carry = 0; if (ir == (size_t)-1) { if (errno == EINVAL) { // incomplete multibyte sequence encountered memmove(input_buffer, input_ptr, input_left); carry = input_left; } else if (errno == EILSEQ && input_left) { // EILSEQ means either // 1. illegal input sequence encountered // 2. code not encodable in output codec // // so we skip one byte of input, and insert � or ? in the output // this isn't the most desirable behavior, but it is the best we // can do, since we don't know specifics about the codecs in use // // unlike glibc cosmo's iconv implementation may handle case (2) // automatically by inserting an asterisk in place of a sequence ++input_ptr; --input_left; memmove(input_buffer, input_ptr, input_left); carry = input_left; if (output_left >= 8) append_replacement_character(&output_ptr); } else { perror(path); done = 1; return; } } // write to terminal for (char* p = output_buffer; p < output_ptr; p++) { if (done) return; (void)!write(STDOUT_FILENO, p, 1); // allow arrow keys to change baud rate int have; if (ioctl(STDIN_FILENO, FIONREAD, &have)) { perror("ioctl"); done = 1; return; } if (have > 0) { char key[4] = {0}; if (read(STDIN_FILENO, key, sizeof(key)) > 0) { if (SLIT(key) == SLIT("\33[A") || // up SLIT(key) == SLIT("\33[C")) { // right baud_rate *= 1.4; } else if (SLIT(key) == SLIT("\33[B") || // down SLIT(key) == SLIT("\33[D")) { // left baud_rate *= 0.6; } if (baud_rate < 3) baud_rate = 3; if (baud_rate > 1000000000) baud_rate = 1000000000; } } // insert artificial delay for one byte. we divide by 10 to convert // bits to bytes, because that is how many bits 8-N-1 encoding used struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); next = add_time(next, fromnanos(1e9 / (baud_rate / 10.))); if (compare_time(next, now) > 0) { struct timespec sleep = subtract_time(next, now); nanosleep(&sleep, 0); } } } } int main(int argc, char* argv[]) { int opt; while ((opt = getopt(argc, argv, "hb:f:t:")) != -1) { switch (opt) { case 'b': { char* endptr; double rate = strtod(optarg, &endptr); if (*endptr == 'k') { rate *= 1e3; ++endptr; } else if (*endptr == 'm') { rate *= 1e6; ++endptr; } if (*endptr || baud_rate <= 0) { fprintf(stderr, "%s: invalid baud rate: %s\n", argv[0], optarg); exit(1); } baud_rate = rate; break; } case 'f': from_charset = optarg; break; case 't': to_charset = optarg; break; case 'h': fprintf(stderr, HELP, baud_rate, from_charset, to_charset); exit(0); default: fprintf(stderr, "protip: pass the -h flag for help\n"); exit(1); } } if (optind == argc) { fprintf(stderr, "%s: missing operand\n", argv[0]); exit(1); } // create character transcoder iconv_t cd = iconv_open(to_charset, from_charset); if (cd == (iconv_t)-1) { fprintf(stderr, "error: conversion from %s to %s not supported\n", from_charset, to_charset); exit(1); } // catch ctrl-c signal(SIGINT, on_signal); // don't wait until newline to read() keystrokes struct termios t; if (!tcgetattr(STDIN_FILENO, &t)) { struct termios t2 = t; t2.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &t2); } // Process each file specified on the command line for (int i = optind; i < argc && !done; i++) { // open file int fd = open(argv[i], O_RDONLY); if (fd == -1) { perror(argv[i]); break; } // wait between files if (i > optind) sleep(1); print("\33[?25l"); // hide cursor print("\33[H"); // move cursor to top-left print("\33[J"); // erase display forward print("\33[1;24r"); // set scrolling region to first 24 lines print("\33[?7h"); // enable auto-wrap mode print("\33[?3l"); // 80 column mode (deccolm) vt100 print("\33[H"); // move cursor to top-left, again // get busy process_file(argv[i], fd, cd); close(fd); } // cleanup iconv_close(cd); print("\33[s"); // save cursor position print("\33[?25h"); // show cursor print("\33[0m"); // reset text attributes (color, bold, etc.) print("\33[?1049l"); // exit alternate screen mode print("\33(B"); // exit line drawing and other alt charset modes print("\33[r"); // reset scrolling region print("\33[?2004l"); // turn off bracketed paste mode print("\33[4l"); // exit insert mode print("\33[?1l\33>"); // exit application keypad mode print("\33[?7h"); // reset text wrapping mode print("\33[?12l"); // reset cursor blinking mode print("\33[?6l"); // reset origin mode print("\33[20l"); // reset auto newline mode print("\33[u"); // restore cursor position // restore terminal tcsetattr(STDIN_FILENO, TCSANOW, &t); }