Skip to content

Commit 23da0d7

Browse files
committed
Improve art program
You can now easily compile this program with non-cosmocc toolchains. The glaring iconv() api usage mistake is now fixed. Restoring the terminal's state on exit now works better. We try our best to limit the terminal to 80x24 cells.
1 parent 4b2a00f commit 23da0d7

File tree

1 file changed

+223
-78
lines changed

1 file changed

+223
-78
lines changed

examples/art.c

Lines changed: 223 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -15,101 +15,241 @@
1515
#include <stdio.h>
1616
#include <stdlib.h>
1717
#include <string.h>
18+
#include <sys/ioctl.h>
1819
#include <termios.h>
1920
#include <time.h>
2021
#include <unistd.h>
2122

2223
/**
2324
* @fileoverview program for viewing bbs art files
25+
* @see https://github.com/blocktronics/artpacks
2426
* @see http://www.textfiles.com/art/
2527
*/
2628

29+
#define HELP \
30+
"Usage:\n\
31+
art [-b %d] [-f %s] [-t %s] FILE...\n\
32+
\n\
33+
Flags:\n\
34+
-b NUMBER specifies simulated modem baud rate, which defaults to\n\
35+
2400 since that was the most common modem speed in the\n\
36+
later half of the 1980s during the BBS golden age; you\n\
37+
could also say 300 for the slowest experience possible\n\
38+
or you could say 14.4k to get more of a 90's feel, and\n\
39+
there's also the infamous 56k to bring you back to y2k\n\
40+
-f CHARSET specifies charset of input bytes, where the default is\n\
41+
cp347 which means IBM Code Page 347 a.k.a. DOS\n\
42+
-t CHARSET specifies output charset used by your terminal, and it\n\
43+
defaults to utf8 a.k.a. thompson-pike encoding\n\
44+
\n\
45+
Supported charsets:\n\
46+
utf8, ascii, wchar_t, ucs2be, ucs2le, utf16be, utf16le, ucs4be,\n\
47+
ucs4le, utf16, ucs4, ucs2, eucjp, shiftjis, iso2022jp, gb18030, gbk,\n\
48+
gb2312, big5, euckr, iso88591, latin1, iso88592, iso88593, iso88594,\n\
49+
iso88595, iso88596, iso88597, iso88598, iso88599, iso885910,\n\
50+
iso885911, iso885913, iso885914, iso885915, iso885916, cp1250,\n\
51+
windows1250, cp1251, windows1251, cp1252, windows1252, cp1253,\n\
52+
windows1253, cp1254, windows1254, cp1255, windows1255, cp1256,\n\
53+
windows1256, cp1257, windows1257, cp1258, windows1258, koi8r, koi8u,\n\
54+
cp437, cp850, cp866, ibm1047, cp1047.\n\
55+
\n\
56+
See also:\n\
57+
http://www.textfiles.com/art/\n\
58+
https://github.com/blocktronics/artpacks\n\
59+
\n"
60+
2761
#define INBUFSZ 256
2862
#define OUBUFSZ (INBUFSZ * 6)
2963
#define SLIT(s) ((unsigned)s[3] << 24 | s[2] << 16 | s[1] << 8 | s[0])
3064

31-
volatile sig_atomic_t got_signal;
65+
// "When new technology comes out, people don't all buy it right away.
66+
// If what they have works, some will wait until it doesn't. A few
67+
// people do get the latest though. In 1984 2400 baud modems became
68+
// available, so some people had them, but many didn't. A BBS list
69+
// from 1986 shows operators were mostly 300 and 1200, but some were
70+
// using 2400. The next 5 years were the hayday of the 2400."
71+
//
72+
// https://forum.vcfed.org/index.php?threads/the-2400-baud-modem.44241/
73+
74+
int baud_rate = 2400; // -b 2400
75+
const char* from_charset = "CP437"; // -f CP437
76+
const char* to_charset = "UTF-8"; // -t UTF-8
77+
78+
volatile sig_atomic_t done;
3279

3380
void on_signal(int sig) {
34-
got_signal = 1;
81+
done = 1;
82+
(void)sig;
83+
}
84+
85+
void print(const char* s) {
86+
(void)!write(STDOUT_FILENO, s, strlen(s));
87+
}
88+
89+
int encode_character(char output[8], const char* codec, wchar_t character) {
90+
size_t inbytesleft = sizeof(wchar_t);
91+
size_t outbytesleft = 7;
92+
char* inbuf = (char*)&character;
93+
char* outbuf = output;
94+
iconv_t cd = iconv_open(codec, "wchar_t");
95+
if (cd == (iconv_t)-1)
96+
return -1;
97+
size_t result = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
98+
iconv_close(cd);
99+
if (result == (size_t)-1)
100+
return -1;
101+
*outbuf = '\0';
102+
return 7 - outbytesleft;
103+
}
104+
105+
void append_replacement_character(char** b) {
106+
int n = encode_character(*b, to_charset, 0xFFFD);
107+
if (n == -1)
108+
n = encode_character(*b, to_charset, '?');
109+
if (n != -1)
110+
*b += n;
111+
}
112+
113+
int compare_time(struct timespec a, struct timespec b) {
114+
int cmp;
115+
if (!(cmp = (a.tv_sec > b.tv_sec) - (a.tv_sec < b.tv_sec)))
116+
cmp = (a.tv_nsec > b.tv_nsec) - (a.tv_nsec < b.tv_nsec);
117+
return cmp;
118+
}
119+
120+
struct timespec add_time(struct timespec x, struct timespec y) {
121+
x.tv_sec += y.tv_sec;
122+
x.tv_nsec += y.tv_nsec;
123+
if (x.tv_nsec >= 1000000000) {
124+
x.tv_nsec -= 1000000000;
125+
x.tv_sec += 1;
126+
}
127+
return x;
128+
}
129+
130+
struct timespec subtract_time(struct timespec a, struct timespec b) {
131+
a.tv_sec -= b.tv_sec;
132+
if (a.tv_nsec < b.tv_nsec) {
133+
a.tv_nsec += 1000000000;
134+
a.tv_sec--;
135+
}
136+
a.tv_nsec -= b.tv_nsec;
137+
return a;
138+
}
139+
140+
struct timespec fromnanos(long long x) {
141+
struct timespec ts;
142+
ts.tv_sec = x / 1000000000;
143+
ts.tv_nsec = x % 1000000000;
144+
return ts;
35145
}
36146

37-
void process_file(const char *path, int fd, iconv_t cd, int baud_rate) {
147+
void process_file(const char* path, int fd, iconv_t cd) {
148+
size_t carry = 0;
149+
struct timespec next;
38150
char input_buffer[INBUFSZ];
39-
char output_buffer[OUBUFSZ];
40-
size_t input_left, output_left;
41-
char *input_ptr, *output_ptr;
42-
struct timespec next = timespec_mono();
151+
152+
clock_gettime(CLOCK_MONOTONIC, &next);
43153

44154
for (;;) {
45155

46156
// read from file
47-
ssize_t bytes_read = read(fd, input_buffer, INBUFSZ);
157+
ssize_t bytes_read = read(fd, input_buffer + carry, INBUFSZ - carry);
158+
if (!bytes_read)
159+
return;
48160
if (bytes_read == -1) {
49161
perror(path);
50-
exit(1);
162+
done = 1;
163+
return;
51164
}
52-
if (!bytes_read)
53-
break;
54165

55166
// modernize character set
56-
input_ptr = input_buffer;
57-
input_left = bytes_read;
58-
output_ptr = output_buffer;
59-
output_left = OUBUFSZ;
60-
if (iconv(cd, &input_ptr, &input_left, &output_ptr, &output_left) ==
61-
(size_t)-1) {
62-
perror(path);
63-
exit(1);
167+
char* input_ptr = input_buffer;
168+
size_t input_left = carry + bytes_read;
169+
char output_buffer[OUBUFSZ];
170+
char* output_ptr = output_buffer;
171+
size_t output_left = OUBUFSZ;
172+
size_t ir = iconv(cd, &input_ptr, &input_left, &output_ptr, &output_left);
173+
carry = 0;
174+
if (ir == (size_t)-1) {
175+
if (errno == EINVAL) {
176+
// incomplete multibyte sequence encountered
177+
memmove(input_buffer, input_ptr, input_left);
178+
carry = input_left;
179+
} else if (errno == EILSEQ && input_left) {
180+
// EILSEQ means either
181+
// 1. illegal input sequence encountered
182+
// 2. code not encodable in output codec
183+
//
184+
// so we skip one byte of input, and insert � or ? in the output
185+
// this isn't the most desirable behavior, but it is the best we
186+
// can do, since we don't know specifics about the codecs in use
187+
//
188+
// unlike glibc cosmo's iconv implementation may handle case (2)
189+
// automatically by inserting an asterisk in place of a sequence
190+
++input_ptr;
191+
--input_left;
192+
memmove(input_buffer, input_ptr, input_left);
193+
carry = input_left;
194+
if (output_left >= 8)
195+
append_replacement_character(&output_ptr);
196+
} else {
197+
perror(path);
198+
done = 1;
199+
return;
200+
}
64201
}
65202

66203
// write to terminal
67-
for (char *p = output_buffer; p < output_ptr; p++) {
68-
if (got_signal)
204+
for (char* p = output_buffer; p < output_ptr; p++) {
205+
if (done)
69206
return;
70207

71-
write(STDOUT_FILENO, p, 1);
208+
(void)!write(STDOUT_FILENO, p, 1);
72209

73210
// allow arrow keys to change baud rate
74-
char key[4] = {0};
75-
if (read(STDIN_FILENO, key, sizeof(key)) > 0) {
76-
if (SLIT(key) == SLIT("\e[A") || // up
77-
SLIT(key) == SLIT("\e[C")) { // right
78-
baud_rate *= 1.4;
79-
} else if (SLIT(key) == SLIT("\e[B") || // down
80-
SLIT(key) == SLIT("\e[D")) { // left
81-
baud_rate *= 0.6;
211+
int have;
212+
if (ioctl(STDIN_FILENO, FIONREAD, &have)) {
213+
perror("ioctl");
214+
done = 1;
215+
return;
216+
}
217+
if (have > 0) {
218+
char key[4] = {0};
219+
if (read(STDIN_FILENO, key, sizeof(key)) > 0) {
220+
if (SLIT(key) == SLIT("\33[A") || // up
221+
SLIT(key) == SLIT("\33[C")) { // right
222+
baud_rate *= 1.4;
223+
} else if (SLIT(key) == SLIT("\33[B") || // down
224+
SLIT(key) == SLIT("\33[D")) { // left
225+
baud_rate *= 0.6;
226+
}
227+
if (baud_rate < 3)
228+
baud_rate = 3;
229+
if (baud_rate > 1000000000)
230+
baud_rate = 1000000000;
82231
}
83232
}
84233

85234
// insert artificial delay for one byte. we divide by 10 to convert
86235
// bits to bytes, because that is how many bits 8-N-1 encoding used
87-
next = timespec_add(next, timespec_fromnanos(1e9 / (baud_rate / 10.)));
88-
usleep(timespec_tomicros(timespec_subz(next, timespec_mono())));
236+
struct timespec now;
237+
clock_gettime(CLOCK_MONOTONIC, &now);
238+
next = add_time(next, fromnanos(1e9 / (baud_rate / 10.)));
239+
if (compare_time(next, now) > 0) {
240+
struct timespec sleep = subtract_time(next, now);
241+
nanosleep(&sleep, 0);
242+
}
89243
}
90244
}
91245
}
92246

93-
int main(int argc, char *argv[]) {
94-
95-
// "When new technology comes out, people don't all buy it right away.
96-
// If what they have works, some will wait until it doesn't. A few
97-
// people do get the latest though. In 1984 2400 baud modems became
98-
// available, so some people had them, but many didn't. A BBS list
99-
// from 1986 shows operators were mostly 300 and 1200, but some were
100-
// using 2400. The next 5 years were the hayday of the 2400."
101-
//
102-
// https://forum.vcfed.org/index.php?threads/the-2400-baud-modem.44241/
103-
104-
int baud_rate = 2400; // -b 2400
105-
const char *from_charset = "CP437"; // -f CP437
106-
const char *to_charset = "UTF-8"; // -t UTF-8
107-
247+
int main(int argc, char* argv[]) {
108248
int opt;
109249
while ((opt = getopt(argc, argv, "hb:f:t:")) != -1) {
110250
switch (opt) {
111251
case 'b': {
112-
char *endptr;
252+
char* endptr;
113253
double rate = strtod(optarg, &endptr);
114254
if (*endptr == 'k') {
115255
rate *= 1e3;
@@ -132,25 +272,7 @@ int main(int argc, char *argv[]) {
132272
to_charset = optarg;
133273
break;
134274
case 'h':
135-
fprintf(stderr, "\
136-
Usage:\n\
137-
%s [-b BAUD] [-f CP437] [-t UTF-8] FILE...\n\
138-
\n\
139-
Supported charsets:\n\
140-
utf8, wchart, ucs2be, ucs2le, utf16be, utf16le, ucs4be, ucs4le,\n\
141-
ascii, utf16, ucs4, ucs2, eucjp, shiftjis, iso2022jp, gb18030, gbk,\n\
142-
gb2312, big5, euckr, iso88591, latin1, iso88592, iso88593, iso88594,\n\
143-
iso88595, iso88596, iso88597, iso88598, iso88599, iso885910,\n\
144-
iso885911, iso885913, iso885914, iso885915, iso885916, cp1250,\n\
145-
windows1250, cp1251, windows1251, cp1252, windows1252, cp1253,\n\
146-
windows1253, cp1254, windows1254, cp1255, windows1255, cp1256,\n\
147-
windows1256, cp1257, windows1257, cp1258, windows1258, koi8r, koi8u,\n\
148-
cp437, cp850, cp866, ibm1047, cp1047.\n\
149-
\n\
150-
See also:\n\
151-
http://www.textfiles.com/art/\n\
152-
\n",
153-
argv[0]);
275+
fprintf(stderr, HELP, baud_rate, from_charset, to_charset);
154276
exit(0);
155277
default:
156278
fprintf(stderr, "protip: pass the -h flag for help\n");
@@ -162,6 +284,7 @@ See also:\n\
162284
exit(1);
163285
}
164286

287+
// create character transcoder
165288
iconv_t cd = iconv_open(to_charset, from_charset);
166289
if (cd == (iconv_t)-1) {
167290
fprintf(stderr, "error: conversion from %s to %s not supported\n",
@@ -180,28 +303,50 @@ See also:\n\
180303
tcsetattr(STDIN_FILENO, TCSANOW, &t2);
181304
}
182305

183-
// make stdin nonblocking
184-
fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);
185-
186-
// hide cursor
187-
write(STDOUT_FILENO, "\e[?25l", 6);
188-
189306
// Process each file specified on the command line
190-
for (int i = optind; i < argc && !got_signal; i++) {
307+
for (int i = optind; i < argc && !done; i++) {
308+
309+
// open file
191310
int fd = open(argv[i], O_RDONLY);
192311
if (fd == -1) {
193312
perror(argv[i]);
194-
continue;
313+
break;
195314
}
196-
process_file(argv[i], fd, cd, baud_rate);
315+
316+
// wait between files
317+
if (i > optind)
318+
sleep(1);
319+
320+
print("\33[?25l"); // hide cursor
321+
print("\33[H"); // move cursor to top-left
322+
print("\33[J"); // erase display forward
323+
print("\33[1;24r"); // set scrolling region to first 24 lines
324+
print("\33[?7h"); // enable auto-wrap mode
325+
print("\33[?3l"); // 80 column mode (deccolm) vt100
326+
print("\33[H"); // move cursor to top-left, again
327+
328+
// get busy
329+
process_file(argv[i], fd, cd);
197330
close(fd);
198331
}
199332

200333
// cleanup
201334
iconv_close(cd);
202335

203-
// show cursor
204-
write(STDOUT_FILENO, "\e[?25h", 6);
336+
print("\33[s"); // save cursor position
337+
print("\33[?25h"); // show cursor
338+
print("\33[0m"); // reset text attributes (color, bold, etc.)
339+
print("\33[?1049l"); // exit alternate screen mode
340+
print("\33(B"); // exit line drawing and other alt charset modes
341+
print("\33[r"); // reset scrolling region
342+
print("\33[?2004l"); // turn off bracketed paste mode
343+
print("\33[4l"); // exit insert mode
344+
print("\33[?1l\33>"); // exit application keypad mode
345+
print("\33[?7h"); // reset text wrapping mode
346+
print("\33[?12l"); // reset cursor blinking mode
347+
print("\33[?6l"); // reset origin mode
348+
print("\33[20l"); // reset auto newline mode
349+
print("\33[u"); // restore cursor position
205350

206351
// restore terminal
207352
tcsetattr(STDIN_FILENO, TCSANOW, &t);

0 commit comments

Comments
 (0)