diff options
Diffstat (limited to 'read-quickly.c')
-rw-r--r-- | read-quickly.c | 482 |
1 files changed, 482 insertions, 0 deletions
diff --git a/read-quickly.c b/read-quickly.c new file mode 100644 index 0000000..acd52a6 --- /dev/null +++ b/read-quickly.c @@ -0,0 +1,482 @@ +/* See LICENSE file for copyright and license details. */ +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <signal.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <termios.h> +#include <unistd.h> +#include <wchar.h> + + + +/** + * The default word rate. + */ +#ifndef DEFAULT_RATE +# define DEFAULT_RATE 120 /* 2 hz */ +#endif + +/** + * Delta-value of rate increment and rate decrement. + */ +#ifndef RATE_DELTA +# define RATE_DELTA 10 /* 10/min */ +#endif + + + +/** + * The a word. + */ +struct word { + /** + * The word. + */ + const char *word; + + /** + * Should reverse video be applied? + */ + int reverse_video; +}; + +/** + * The name of the process. + */ +static const char *argv0; + +/** + * Have the terminal been resized? + */ +static volatile sig_atomic_t caught_sigwinch = 1; + +/** + * Has the timer expired? + */ +static volatile sig_atomic_t caught_sigalrm = 0; + +/** + * The width of the terminal. + */ +static size_t width = 80; + +/** + * The height of the terminal. + */ +static size_t height = 30; + +/** + * The number of words. + */ +static size_t word_count = 0; + +/** + * All loaded words. Refer to `word_count` + * for the number of contained words. + */ +static struct word *words; + + + +/** + * Signal handler for SIGWINCH. + * Invoked when the terminal resizes. + */ +static void +sigwinch(int signo) +{ + caught_sigwinch = 1; + (void) signo; +} + + +/** + * Signal handler for SIGALRM. + * Invoked when the timer expires. + */ +static void +sigalrm(int signo) +{ + caught_sigalrm = 1; + (void) signo; +} + + +/** + * Get the size of the terminal. + */ +static void +get_terminal_size(void) +{ + struct winsize winsize; + if (caught_sigwinch) { + caught_sigwinch = 0; + while (ioctl(STDOUT_FILENO, (unsigned long)TIOCGWINSZ, &winsize) < 0) + if (errno != EINTR) + return; + height = winsize.ws_row; + width = winsize.ws_col; + } +} + + +/** + * Get the selected word rate by reading + * the environment variable READ_QUICKLY_RATE. + * + * @return The rate in words per minute. + */ +static long +get_word_rate(void) +{ + char *s; + long r; + + errno = 0; + s = getenv("READ_QUICKLY_RATE"); + if (!s || !*s || !isdigit(*s)) + return DEFAULT_RATE; + + r = strtol(s, &s, 10); + if (r <= 0) + return DEFAULT_RATE; + while (*s == ' ') + s++; + + if (!*s) r *= 1; + else if (!strcasecmp(s, "wpm")) r *= 1; + else if (!strcasecmp(s, "w/m")) r *= 1; + else if (!strcasecmp(s, "/m")) r *= 1; + else if (!strcasecmp(s, "wpmin")) r *= 1; + else if (!strcasecmp(s, "w/min")) r *= 1; + else if (!strcasecmp(s, "/min")) r *= 1; + else if (!strcasecmp(s, "wps")) r *= 60; + else if (!strcasecmp(s, "w/s")) r *= 60; + else if (!strcasecmp(s, "/s")) r *= 60; + else if (!strcasecmp(s, "wpsec")) r *= 60; + else if (!strcasecmp(s, "w/sec")) r *= 60; + else if (!strcasecmp(s, "/sec")) r *= 60; + else if (!strcasecmp(s, "hz")) r *= 60; + else + return DEFAULT_RATE; + + return r; +} + + +/** + * Count the number of character in a string. + * + * @param s The string. + * @return The number of characters in `s`. + */ +static size_t +display_len(const char *s) +{ + size_t r = 0; + wchar_t wc; + int len, w; + for (; *s; s += len) { + len = mbtowc(&wc, s, SIZE_MAX); + if (len <= 0) + break; + w = wcwidth(wc); + if (w < 0) + break; + r += (size_t)w; + } + return r; +} + + +/** + * Load the file and do some preparsing. + * + * @param fd The file descriptor to the file, -1 to clean up instead. + * @return 0 on success, -1 on error. + */ +static int +load_file(int fd) +{ + static char *buffer = NULL; + size_t ptr = 0; + size_t size = 0; + void *new; + int saved_errno; + char *s; + char *end; + size_t i; + ssize_t n; + + if (fd == -1) + return free(buffer), 0; + + /* Load file. */ + for (;;) { + if (ptr == size) { + size = size ? size << 1 : 8 << 10; + new = realloc(buffer, size); + if (!new) + goto fail; + buffer = new; + } + n = read(fd, buffer + ptr, size - ptr); + if (n <= 0) { + if (!n) + break; + if (errno == EINTR) + continue; + goto fail; + } + ptr += (size_t)n; + } + if (!buffer) + return 0; + new = realloc(buffer, ptr + 2); + if (!new) + goto fail; + buffer = new; + buffer[ptr++] = '\0'; + buffer[ptr++] = '\0'; + + /* Split words. */ + size = 0; + for (s = buffer; *s; s = &end[1]) { + if (word_count == size) { + size = size ? size << 1 : 512; + new = realloc(words, size * sizeof(*words)); + if (!new) + goto fail; + words = new; + } + while (isspace(*s)) + s++; + end = strpbrk(s, " \f\n\r\t\v"); + if (!end) + end = strchr(s, '\0'); + *end = '\0'; + words[word_count].word = s; + words[word_count].reverse_video = 0; + word_count++; + } + + /* Figure out which words should have reverse video. */ + for (i = 1; i < word_count; i++) + if (!strcmp(words[i].word, words[i - 1].word)) + words[i].reverse_video = words[i - 1].reverse_video ^ 1; + + return 0; + +fail: + saved_errno = errno; + free(buffer); + buffer = NULL; + errno = saved_errno; + return -1; +} + + +/** + * Display a file word by word. + * + * @param ttyfd File descriptor for reading from the terminal. + * @param rate The number of words per minute to display. + * @return 0 on success, -1 on error. + */ +static int +display_file(int ttyfd, long rate) +{ +#define SET_RATE \ + (interval.it_value.tv_usec = 60000000L / rate, \ + interval.it_value.tv_sec = interval.it_value.tv_usec / 1000000L, \ + interval.it_value.tv_usec %= 1000000L) + + ssize_t n; + int timer_set = 1; + char c; + size_t i; + struct itimerval interval; + + memset(&interval, 0, sizeof(interval)); + + SET_RATE; + for (i = 0; i < word_count; i++) { + if (setitimer(ITIMER_REAL, &interval, NULL)) + goto fail; + rewait: + n = read(ttyfd, &c, sizeof(c)); + if (n < 0) { + if (errno != EINTR) + goto fail; + c = 0; + } else if (n == 0) { + break; + } + switch (c) { + case '+': /* plus */ + case '-': /* hyphen */ + rate += c == '+' ? RATE_DELTA : -RATE_DELTA; + rate = rate <= 0 ? 1 : rate; + SET_RATE; + goto rewait; + case 'p': /* P */ + if (timer_set) + memset(&interval, 0, sizeof(interval)); + else + SET_RATE; + if (setitimer(ITIMER_REAL, &interval, NULL)) + goto fail; + timer_set ^= 1; + goto rewait; + case 'q': /* Q */ + goto done; + case 'B': /* down */ + case 'C': /* right */ + break; + case 'A': /* up */ + case 'D': /* left */ + i = i < 2 ? 0 : i - 2; + break; + case 0: + if (!caught_sigalrm) + goto rewait; + caught_sigalrm = 0; + break; + default: + goto rewait; + } + + get_terminal_size(); + if (fprintf(stdout, "\033[H\033[2J\033[%zu;%zuH%s%s%s", + (height + 1) / 2, + (width - display_len(words[i].word)) / 2 + 1, + words[i].reverse_video ? "\033[7m" : "", + words[i].word, + words[i].reverse_video ? "\033[27m" : "") < 0) + goto fail; + if (fflush(stdout)) + goto fail; + } + + if (setitimer(ITIMER_REAL, &interval, NULL)) + goto fail; + (void) read(ttyfd, &c, sizeof(c)); + +done: + return 0; + +fail: + return -1; +} + + +int +main(int argc, char *argv[]) +{ + long rate = get_word_rate(); + int fd = -1, ttyfd = -1, tty_configured = 0; + struct termios stty, saved_stty; + struct stat _attr; + struct sigaction sa; + + /* Parse arguments. */ + argv0 = argv ? (argc--, *argv++) : "read-quickly"; + if (argc && argv[0][0] == '-') { + if (argv[0][1] == '-' && !argv[0][2]) { + argc--; + argv++; + } else if (argv[0][1]) { + goto usage; + } + } + if (argc > 1) + goto usage; + + /* Check that we have a stdout. */ + if (fstat(STDOUT_FILENO, &_attr)) + if (errno == EBADF) + goto fail; + + /* Open file. */ + if (argc && strcmp(*argv, "-")) { + fd = open(*argv, O_RDONLY); + if (fd < 0) + goto fail; + } else { + fd = STDIN_FILENO; + } + + /* Load file. */ + if (load_file(fd)) + goto fail; + + /* We do not need the file anymore. */ + close(fd); + fd = -1; + + /* Get a readable file descriptor for the controlling terminal. */ + ttyfd = open("/dev/tty", O_RDONLY); + if (ttyfd < 0) + goto fail; + + /* Configure terminal. */ + if (fprintf(stdout, "\033[?1049h\033[?25l") < 0) + goto fail; + if (fflush(stdout)) + goto fail; + if (tcgetattr(ttyfd, &stty)) + goto fail; + saved_stty = stty; + stty.c_lflag &= (tcflag_t)~(ICANON | ECHO | ISIG); + if (tcsetattr(ttyfd, TCSAFLUSH, &stty)) + goto fail; + tty_configured = 1; + + /* Display file. */ + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigalrm; + sigaction(SIGALRM, &sa, NULL); + sa.sa_handler = sigwinch; + sigaction(SIGWINCH, &sa, NULL); + if (display_file(ttyfd, rate)) + goto fail; + + /* Restore terminal configurations. */ + tcsetattr(ttyfd, TCSAFLUSH, &saved_stty); + fprintf(stdout, "\033[?25h\033[?1049l"); + fflush(stdout); + tty_configured = 0; + + free(words); + load_file(-1); + close(ttyfd); + return 0; + +fail: + perror(argv0); + free(words); + load_file(-1); + if (tty_configured) { + tcsetattr(ttyfd, TCSAFLUSH, &saved_stty); + fprintf(stdout, "\033[?25h\033[?1049l"); + fflush(stdout); + } + if (fd >= 0) + close(fd); + if (ttyfd >= 0) + close(ttyfd); + return 1; + +usage: + fprintf(stderr, "usage: %s [file].\n", argv0); + return 1; +} |