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 }