aboutsummaryrefslogblamecommitdiffstats
path: root/read-quickly.c
blob: acd52a6e8741a15f8b5e82d3be83e0498d5f21b9 (plain) (tree)
1
2
3
4
5
6
7
8
9






                                                         
                   
                   
                   

                   
                   
                    
                    
                   
                  





                         


                                      
 






                                                    


   














                                           



                           





                                                 




                                                








                              










                                          





                                     

                   
 
                            
                     


 



                                  

                  
 
                           
                     

 



                                

                       

                               







                                                                                     


 

                                        
                                              


                                         

                   
 
                


                  
                                        


                                      
                              

                                    
















                                                   






                                    
   

                                             


                                               

                          

                     










                                               



                 
   
                                        
   

                                                                        
   

                 
 
                                   





                        




                                       



                                  
                                                          
                                                    

                                          


                                                       





                                           
                 

                                 
                    
                         



                                       
                             



                             
                                           
                                         
                                                      
                                                                    

                                          




                                                
                         












                                                                                
 

                            

                      











                                                                 

                                  










                                                                           
 
                                               
 
                 
                                          

                                                            


                                               

                                           






                                      

                                                                    


                                    



                                                                       

                                                                    

                                       



                                     


                                    
                                              

                              


                                            



                                    
 
                                    








                                                                          

         

                                                    

                                          
     


                 



                  

                            
 
                                    
                                                    
                                        
                          
                            

                              
                                                          

                                                       
                               


                                        

                 






                                          

                        

                                           

                                  

                                  

         
                        

                          

                                              

                  
 

                                                                          

                          

                                 





                                                        

                                                          

                                               

                           
                           






                                       






                                                 

                      




                      

                      











                                                         

                                                      
 
/* 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;
}