/* See LICENSE file for copyright and license details. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * 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 RQ_RATE. * * @return The rate in words per minute. */ static long get_word_rate(void) { char *s; long r; errno = 0; s = getenv("RQ_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++) : "rq"; 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; }