2024-10-14 00:43:39 +00:00
|
|
|
|
#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 <errno.h>
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include <getopt.h>
|
|
|
|
|
#include <iconv.h>
|
|
|
|
|
#include <signal.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
2024-10-15 18:39:16 +00:00
|
|
|
|
#include <sys/ioctl.h>
|
2024-10-14 00:43:39 +00:00
|
|
|
|
#include <termios.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @fileoverview program for viewing bbs art files
|
2024-10-15 18:39:16 +00:00
|
|
|
|
* @see https://github.com/blocktronics/artpacks
|
2024-10-14 00:43:39 +00:00
|
|
|
|
* @see http://www.textfiles.com/art/
|
|
|
|
|
*/
|
|
|
|
|
|
2024-10-15 18:39:16 +00:00
|
|
|
|
#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"
|
|
|
|
|
|
2024-10-14 00:43:39 +00:00
|
|
|
|
#define INBUFSZ 256
|
|
|
|
|
#define OUBUFSZ (INBUFSZ * 6)
|
|
|
|
|
#define SLIT(s) ((unsigned)s[3] << 24 | s[2] << 16 | s[1] << 8 | s[0])
|
|
|
|
|
|
2024-10-15 18:39:16 +00:00
|
|
|
|
// "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;
|
2024-10-14 00:43:39 +00:00
|
|
|
|
|
|
|
|
|
void on_signal(int sig) {
|
2024-10-15 18:39:16 +00:00
|
|
|
|
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;
|
2024-10-14 00:43:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-10-15 18:39:16 +00:00
|
|
|
|
void process_file(const char* path, int fd, iconv_t cd) {
|
|
|
|
|
size_t carry = 0;
|
|
|
|
|
struct timespec next;
|
2024-10-14 00:43:39 +00:00
|
|
|
|
char input_buffer[INBUFSZ];
|
2024-10-15 18:39:16 +00:00
|
|
|
|
|
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &next);
|
2024-10-14 00:43:39 +00:00
|
|
|
|
|
|
|
|
|
for (;;) {
|
|
|
|
|
|
|
|
|
|
// read from file
|
2024-10-15 18:39:16 +00:00
|
|
|
|
ssize_t bytes_read = read(fd, input_buffer + carry, INBUFSZ - carry);
|
|
|
|
|
if (!bytes_read)
|
|
|
|
|
return;
|
2024-10-14 00:43:39 +00:00
|
|
|
|
if (bytes_read == -1) {
|
|
|
|
|
perror(path);
|
2024-10-15 18:39:16 +00:00
|
|
|
|
done = 1;
|
|
|
|
|
return;
|
2024-10-14 00:43:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// modernize character set
|
2024-10-15 18:39:16 +00:00
|
|
|
|
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 <20> 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;
|
|
|
|
|
}
|
2024-10-14 00:43:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// write to terminal
|
2024-10-15 18:39:16 +00:00
|
|
|
|
for (char* p = output_buffer; p < output_ptr; p++) {
|
|
|
|
|
if (done)
|
2024-10-14 00:43:39 +00:00
|
|
|
|
return;
|
|
|
|
|
|
2024-10-15 18:39:16 +00:00
|
|
|
|
(void)!write(STDOUT_FILENO, p, 1);
|
2024-10-14 00:43:39 +00:00
|
|
|
|
|
|
|
|
|
// allow arrow keys to change baud rate
|
2024-10-15 18:39:16 +00:00
|
|
|
|
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;
|
2024-10-14 00:43:39 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2024-10-15 18:39:16 +00:00
|
|
|
|
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);
|
|
|
|
|
}
|
2024-10-14 00:43:39 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-15 18:39:16 +00:00
|
|
|
|
int main(int argc, char* argv[]) {
|
2024-10-14 00:43:39 +00:00
|
|
|
|
int opt;
|
|
|
|
|
while ((opt = getopt(argc, argv, "hb:f:t:")) != -1) {
|
|
|
|
|
switch (opt) {
|
|
|
|
|
case 'b': {
|
2024-10-15 18:39:16 +00:00
|
|
|
|
char* endptr;
|
2024-10-14 00:43:39 +00:00
|
|
|
|
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':
|
2024-10-15 18:39:16 +00:00
|
|
|
|
fprintf(stderr, HELP, baud_rate, from_charset, to_charset);
|
2024-10-14 00:43:39 +00:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-15 18:39:16 +00:00
|
|
|
|
// create character transcoder
|
2024-10-14 00:43:39 +00:00
|
|
|
|
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
|
2024-10-15 18:39:16 +00:00
|
|
|
|
for (int i = optind; i < argc && !done; i++) {
|
|
|
|
|
|
|
|
|
|
// open file
|
2024-10-14 00:43:39 +00:00
|
|
|
|
int fd = open(argv[i], O_RDONLY);
|
|
|
|
|
if (fd == -1) {
|
|
|
|
|
perror(argv[i]);
|
2024-10-15 18:39:16 +00:00
|
|
|
|
break;
|
2024-10-14 00:43:39 +00:00
|
|
|
|
}
|
2024-10-15 18:39:16 +00:00
|
|
|
|
|
|
|
|
|
// 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);
|
2024-10-14 00:43:39 +00:00
|
|
|
|
close(fd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cleanup
|
|
|
|
|
iconv_close(cd);
|
|
|
|
|
|
2024-10-15 18:39:16 +00:00
|
|
|
|
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
|
2024-10-14 00:43:39 +00:00
|
|
|
|
|
|
|
|
|
// restore terminal
|
|
|
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &t);
|
|
|
|
|
}
|