nix

configuration files that power my machines
Log | Files | Refs | README | LICENSE

speedread (6780B)


      1 #!/usr/bin/env perl
      2 #
      3 # speedread:  A simple terminal-based open source spritz-alike
      4 #
      5 # Show input text as a per-word RSVP (rapid serial visual presentation)
      6 # aligned on optimal reading points.  This kind of input mode allows
      7 # reading text at a much more rapid pace than usual as the eye can
      8 # stay fixed on a single place.
      9 #
     10 # (c) Petr Baudis <pasky@ucw.cz>  2014
     11 # MIT licence
     12 #
     13 # Usage: cat file.txt | speedread [-w WORDSPERMINUTE] [-r RESUMEPOINT] [-m]
     14 #
     15 # The default of 250 words per minut is very timid, designed so that
     16 # you get used to this.  Be sure to try cranking this up, 500wpm
     17 # should still be fairly easy to follow even for beginners.
     18 #
     19 # speedread can join short words together if you specify the -m switch.
     20 # It did not work well for pasky so far, though.
     21 #
     22 # speedread is slightly interactive, with these controls accepted:
     23 #
     24 #   [ - slow down by 10%
     25 #   ] - speed up by 10%
     26 #   space - pause (and show the last two lines)
     27 
     28 use warnings;
     29 use strict;
     30 use autodie;
     31 use v5.14;
     32 
     33 my $wpm = 250;
     34 my $resume = 0;
     35 my $multiword = 0;
     36 
     37 
     38 use utf8;
     39 binmode(STDIN, ":encoding(UTF-8)");
     40 binmode(STDOUT, ":encoding(UTF-8)");
     41 
     42 use Term::ANSIColor;
     43 use POSIX qw(ceil floor);
     44 use Time::HiRes qw(time sleep gettimeofday tv_interval);
     45 
     46 my $offset = 0;
     47 
     48 use Getopt::Long;
     49 GetOptions("wpm|w=i" => \$wpm,
     50 	   "resume|r=i" => \$resume,
     51 	   "multiword|m" => \$multiword,
     52 	   "offset|o=i" => \$offset);
     53 
     54 my $wordtime = 0.9; # relative to wpm
     55 my $lentime = 0.04; # * sqrt(length $word), relative to wpm
     56 my $commatime = 2; # relative to wpm
     57 my $fstoptime = 3; # relative to wpm
     58 my $multitime = 1.2; # relative to wpm
     59 my $firsttime = 0.2; # [s]
     60 my $ORPloc = 0.35;
     61 my $ORPmax = 0.2;
     62 my $ORPvisualpos = 20 + $offset;
     63 my $cursorpos = 64 + $offset;
     64 my $paused = 0;
     65 my $current_word;
     66 my $current_orp;
     67 my $next_word_time = 0;
     68 my $next_input_time = 0;
     69 my $skipped = 0;
     70 
     71 my @lastlines;
     72 my $tty = rawinput->new();
     73 $| = 1;
     74 
     75 my $wordcounter = 0;
     76 my $lettercounter = 0;
     77 my $t0 = [gettimeofday];
     78 sub print_stats {
     79 	my $elapsed = tv_interval($t0, [gettimeofday]);
     80 	my $truewpm = $wordcounter / $elapsed * 60;
     81 	printf("\n %.2fs, %d words, %d letters, %s%.2f%s true wpm\n",
     82 		$elapsed, $wordcounter, $lettercounter,
     83 		color('bold green'), $truewpm, color('reset'));
     84 }
     85 $SIG{INT} = sub {
     86 	print_stats;
     87 	my $resume_word = $wordcounter + $resume;
     88 	say " To resume from this point run with argument -r $resume_word";
     89 	exit;
     90 };
     91 
     92 main();
     93 
     94 # ORP: Optical Recognition Point (the red-colored alignment pilot),
     95 # the way Spritz probably does it.
     96 sub find_ORP {
     97 	my ($word, $ORPloc) = @_;
     98 
     99 	return 4 if (length($word) > 13);
    100 	return (0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3)[length($word)];
    101 }
    102 
    103 sub show_guide {
    104 	# Top visual guide
    105 	say(" "x$ORPvisualpos . color('red') . "v" . color('reset') . "\033[K");
    106 }
    107 
    108 sub show_word {
    109 	my ($word, $i) = @_;
    110 
    111 	my $pivotch = substr($word, $i, 1);
    112 	$pivotch = "ยท" if $pivotch eq ' ';
    113 
    114 	my $rpad = $cursorpos - ($ORPvisualpos - $i) - length($word);
    115 	$rpad = 0 if $rpad < 0;
    116 	print("\r\033[K"
    117 		. " " x ($ORPvisualpos - $i)
    118 		. color("bold")
    119 		. substr($word, 0, $i)
    120 		. color("red")
    121 		. $pivotch
    122 		. color("reset")
    123 		. color("bold")
    124 		. substr($word, $i+1)
    125 		. color("reset")
    126 		. " " x $rpad
    127 		. "$wpm wpm"
    128 		. ($paused ? "  ".color("yellow")."PAUSED".color("reset") : ""));
    129 }
    130 
    131 sub word_time {
    132 	my ($word) = @_;
    133 
    134 	my $time = $wordtime;
    135 	if ($word =~ /[.?!]\W*$/) {
    136 		$time = $fstoptime;
    137 	} elsif ($word =~ /[:;,]\W*$/) {
    138 		$time = $commatime;
    139 	} elsif ($word =~ / /) {
    140 		$time = $multitime;
    141 	}
    142 	$time += sqrt(length($word)) * $lentime;
    143 	$time *= 60 / $wpm;
    144 
    145 	# Give user some time to focus on the first word, even with high wpm.
    146 	$time = $firsttime if ($wordcounter == 0 and $time < $firsttime);
    147 
    148 	return $time;
    149 }
    150 
    151 sub print_context {
    152 	my ($wn) = @_;
    153 	# One line up and to its beginning
    154 	print "\r\033[K\033[A\033[K";
    155 	# First line of context
    156 	say $lastlines[1] if $lastlines[1];
    157 	# In second line of context, highlight our word
    158 	my $line0 = $lastlines[0];
    159 	my $c0 = color('yellow');
    160 	my $c1 = color('reset');
    161 	$line0 =~ s/^((?:.*?(?:-|\s)+){$wn})(.*?)(-|\s)/$1$c0$2$c1$3/;
    162 	say $line0;
    163 }
    164 
    165 sub process_keys {
    166 	my ($word, $i, $wn) = @_;
    167 	while ($tty->key_pressed()) {
    168 		my $ch = $tty->getch();
    169 		if ($ch eq '[') {
    170 			$wpm = int($wpm * 0.9);
    171 
    172 		} elsif ($ch eq ']') {
    173 			$wpm = int($wpm * 1.1);
    174 
    175 		} elsif ($ch eq ' ') {
    176 			$paused = not $paused;
    177 			if ($paused) {
    178 				# Print context.
    179 				print_context($wn);
    180 				show_guide();
    181 				show_word($word, $i);
    182 			}
    183 			else {
    184 				$next_word_time = time();
    185 			}
    186 		}
    187 	}
    188 }
    189 
    190 sub main {
    191 	show_guide();
    192 
    193 	$next_word_time = time();
    194 	$next_input_time = time();
    195 
    196 	while (<>) {
    197 		chomp;
    198 
    199 		unshift @lastlines, $_;
    200 		pop @lastlines if @lastlines > 2;
    201 
    202 		my (@words) = grep { /./ } split /(?:-|\s)+/;
    203 
    204 		if ($multiword) {
    205 			# Join adjecent short words
    206 			for (my $i = 0; $i < $#words - 1; $i++) {
    207 				if (length($words[$i]) <= 3 and length($words[$i+1]) <= 3) {
    208 					$words[$i] .= ' ' . $words[$i+1];
    209 					splice(@words, $i+1, 1);
    210 				}
    211 			}
    212 		}
    213 
    214 		my $wn = 0;
    215 		while (scalar(@words) > 0) {
    216 			if ($skipped < $resume) {
    217 				$skipped++;
    218 				shift @words;
    219 				next;
    220 			}
    221 
    222 			my $current_time = time();
    223 
    224 			if ($next_word_time <= $current_time and !$paused) {
    225 				$current_word = shift @words;
    226 				$current_orp = find_ORP($current_word, $ORPloc);
    227 				$next_word_time += word_time($current_word);
    228 				$wordcounter++;
    229 				$lettercounter += length($current_word);
    230 				$wn++;
    231 			}
    232 
    233 			if ($next_input_time <= $current_time) {
    234 				process_keys($current_word, $current_orp, $wn);
    235 				$next_input_time += 0.05; # checking for input 20 times / second seems to give a reasonably responsive UI
    236 			}
    237 
    238 			# redrawing the word on each "frame" gives a more responsive UI
    239 			# (we don't have to wait for the word to change to display changed stats like wpm)
    240 			show_word($current_word, $current_orp);
    241 
    242 			my $sleep_time = ($next_word_time < $next_input_time and !$paused) ? $next_word_time-$current_time : $next_input_time-$current_time;
    243 			sleep($sleep_time) if ($sleep_time > 0);
    244 		}
    245 	}
    246 
    247 	print_stats();
    248 	sleep(1);
    249 }
    250 
    251 
    252 package rawinput;
    253 
    254 # An ad-hoc interface to interactive terminal input.  Term::Screen *should*
    255 # have been a natural choice here, unfortunately it is fixated at reading
    256 # from stdin instead of /dev/tty. Tough.
    257 
    258 sub new {
    259 	my $class = shift;
    260 	my $self;
    261 	open $self, '/dev/tty';
    262 	bless $self, $class;
    263 	stty('min 1', '-icanon', '-echo');
    264 	return $self;
    265 }
    266 
    267 sub DESTROY {
    268 	stty('cooked', 'echo');
    269 }
    270 
    271 sub stty {
    272 	my $self = shift;
    273 	eval { system('stty', $^O eq 'darwin' ? '-f' : '-F', '/dev/tty', @_); };
    274 }
    275 
    276 sub key_pressed {
    277 	my $self = shift;
    278 	my $readfields = '';
    279 	vec($readfields, fileno($self), 1) = 1;
    280 	my $ready = 0;
    281 	eval { $ready = select($readfields, undef, undef, 0); };
    282 	return $ready;
    283 }
    284 
    285 sub getch {
    286 	my $self = shift;
    287 	getc($self);
    288 }