-
Notifications
You must be signed in to change notification settings - Fork 2
/
ReadR.py
executable file
·343 lines (307 loc) · 12 KB
/
ReadR.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#!/usr/bin/env python3
#TODO speed isn't accurate, context showing on pause, maybe add focus letter.
from time import sleep
import sys
import os
import tty
import termios
import threading
import argparse
bookmarks_file = os.path.dirname(os.path.realpath(__file__))+'/bookmarks'
OFFSET=22 #Half the width of the phrase display area
BEGINNING_FILE_MARKER = '________BEGINNING_OF_FILE________'
END_FILE_MARKER = '___________END_OF_FILE___________'
#Useful terminal definitions
NONE='\033[00m'
RED='\033[01;31m'
GREEN='\033[01;32m'
YELLOW='\033[01;33m'
PURPLE='\033[01;35m'
CYAN='\033[01;36m'
WHITE='\033[01;37m'
BOLD='\033[1m'
UNDERLINE='\033[4m'
#Alter these for color and style alterations
BORDER = NONE
FOCUSLINE = GREEN
CONTROLS = PURPLE
TEXT = WHITE+BOLD
QUOTE = CYAN
STATUSLINE = NONE
def get_args():
parser=argparse.ArgumentParser(description='User-friendly colorful command line speed reader, focused on comprehension, written in python.')
parser.add_argument('FILE', type=argparse.FileType('r'), action='store', help='The text file to read')
parser.add_argument('-w', '--wpm', type=int, action='store', default=350, help='The words per minute at startup. Default 350')
parser.add_argument('-p', '--phrase-length', type=int, action='store', default=7, help='The max number of summed characters (non-whitespace) that triggers clumping of short phrases, like "I am a". Default 7')
parser.add_argument('-s', '--steady-speed', action='store_true', help="Do not give special calculation to a phrase's display time, but show each phrase strictly for 60/wpm seconds")
parser.add_argument('-c', '--color-quotes', action='store_true', help='Colors the text inside quotation marks ("...") differently')
parser.add_argument('-m', '--minimalist', action='store_true', help='Remove the interface. Keep only the flashing words. Note that this does not affect any of the controls, or color.')
return parser.parse_args()
def apply_args(args):
global wpm, chars_in_phrase, highlight_speech, minimalist, steady_speed, file_name, line_pause, sentence_pause
wpm = args.wpm #Approximate words per minute, though exact delays on words will vary with commonality or length.
chars_in_phrase = args.phrase_length #If neighboring words contain these or fewer characters, they will be lumped into one printed phrase, such as 'in the' or 'I was in'. spaces don't count
highlight_speech = args.color_quotes #Changes color of the printed text if inside quotes, eg "...".
minimalist = args.minimalist #Removes the pretty interface
steady_speed = args.steady_speed #Gives all phrases equal time.
file_name = args.FILE.name.split('/')[-1]
line_pause=200/wpm #How long (extra) to pause when an empty line (usually indicates a new paragraph or speaker) is encountered
sentence_pause=80/wpm #How long (extra) to pause at the end of a sentence (at . ." ? ?")
#Inputs the whole file to an array, for easy navigation
def load_words():
global ALL_WORDS
for line in f:
line = line.replace(' ',' ').strip()
if line == '':
if nl<2:
ALL_WORDS.append('<NEWLINE>')
nl+=1
else:
nl=0
ALL_WORDS+=line.split()
#Prepares words from the ALL_WORDS array for imminent display.
def fill_word_queue(words):
global word_queue, word_num
if word_num<0:
word_num=0
word_queue.append(BEGINNING_FILE_MARKER)
elif word_num>=len(ALL_WORDS)-1:
word_num=len(ALL_WORDS)-1
word_queue.append(END_FILE_MARKER)
else:
try:
for i in range(words):
word_queue.append(ALL_WORDS[word_num])
word_num+=1
except IndexError:
word_num=len(ALL_WORDS)-1
word_queue.append(END_FILE_MARKER)
#Uses the word queue to decide how much to put in a phrase (which is printed at once on the screen)
def build_phrase():
global word_queue, chars_in_phrase, quotes, highlight_speech
#First, make sure there are enough words loaded
while len(word_queue) < 10:
fill_word_queue(10)
phrase=str(word_queue.pop(0).strip())
#If the next word(s) are short, combine them into one phrase
try:
while phrase!='<NEWLINE>' and len(phrase)+len(word_queue[0])<chars_in_phrase and \
word_queue[0]!='<NEWLINE>' and not phrase.endswith('.') and not phrase.endswith('?') and not phrase.endswith('"'):
phrase+=' '+word_queue.pop(0).strip()
except IndexError:
1;
if highlight_speech:
#The previously displayed phrase was the end of a quote, so turn off highlighting now.
if quotes==2:
quotes=0
#Check for starting or ending quotations
if phrase.startswith('"'):
quotes=1
if phrase.endswith('"') or (len(phrase)>=2 and phrase[-2]=='"'):
quotes=2
return phrase
#Returns the length of time (in seconds) for the phrase to display
def get_delay(phrase):
if steady_speed:
return 60/wpm
if phrase=='<NEWLINE>':
return line_pause
words = len(phrase.split(' '))
chars = len(phrase)
time = words/(wpm*wpm_calib) * 60 #All words equal, this is the time the phrase should be displayed for (in seconds).
time *= .8**(words-1) #Cut off a bit of time for phrases like 'I was a'. This allows for some extra time on bigger words (next line)
time *= 1.07**(chars) #Add a little bit of time for the longer words/phrases.
if phrase.endswith('.') or phrase.endswith('?') or phrase.endswith(';') or phrase.endswith('"'):
time += sentence_pause
return time
#Adds padding and pretty formatting to the phrase margins
def phrase_format(o, p):
global STATUSLINE, QUOTE, TEXT, BORDER, quotes
phrase=p
if phrase=='<NEWLINE>':
phrase=' '*2*(OFFSET+1)
#center the phrase with spaces on either side. Note that it does not currently center on a vowel in the phrase, as some software does.
else:
i=0
while i <= (o-len(p)/2):
phrase=' '+phrase+' '
i+=1
if len(phrase)%2==1:
phrase+=' '
#Additional formatting, including different colored quotes
if quotes!=0:
phrase=QUOTE+phrase+NONE
else:
phrase=TEXT+phrase+NONE
#Add the context of the application format, with borders, and status on the sides
if not minimalist:
phrase=BORDER+'|'+phrase+'|'+STATUSLINE+UNDERLINE+' WPM: '+str(int(actual_wpm+.5))+', WordNum: '+str(word_num-5)
if word_num>5:
for i in range(0,6-len(str(word_num-5))):
phrase+=' '
if not minimalist:
phrase+=BORDER+'|'
return phrase
#Listens for key presses in the terminal
def get_character():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
#Parses key presses for commands
def command_listener():
global wpm, paused, word_num, bump
sleep(.5)
while True:
char=str(get_character()).lower()
if char=='+':
wpm+=5
elif char=='-':
wpm-=5
elif char=='p':
paused=not paused
elif char=='q':
write_bookmarks()
os._exit(0)
elif char=='.':
del word_queue[:]
word_num+=45
if paused:
bump=True
elif char==',':
del word_queue[:]
word_num-=55
if paused:
bump=True
elif char=='>':
del word_queue[:]
word_num+=195
if paused:
bump=True
elif char=='<':
del word_queue[:]
word_num-=205
if paused:
bump=True
calib_wpm(3)
#print('Unknown character:',char)
#Formats the interface
def setup():
global phrase, BORDER, FOCUSLINE, CONTROLS
if not minimalist:
os.system('clear')
# sys.stdout.write(BORDER+CONTROLS+'Q:quit | P:pause | +/-:wpm up/down | >/<:Move 200 words forward/backward | ./,:Move only 50 words\n')
sys.stdout.write(BORDER+UNDERLINE+' ReadR_v_1_2_2_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\n')
sys.stdout.write(BORDER+'| '+FOCUSLINE+' | '+BORDER+' |'+CONTROLS+'-/+ wpm down/up\n')
sys.stdout.write(BORDER+'| '+FOCUSLINE+' | '+BORDER+' |'+CONTROLS+',/. 50 words back/forward\n')
sys.stdout.write(BORDER+'| '+FOCUSLINE+' | '+BORDER+' |'+CONTROLS+'</> 200 words back/forward\n')
sys.stdout.write(BORDER+'| '+FOCUSLINE+' | '+BORDER+' |'+CONTROLS+'P pause\n')
sys.stdout.write(BORDER+'| '+FOCUSLINE+' | '+BORDER+' |'+CONTROLS+'Q quit\n')
sys.stdout.write(BORDER+'| '+FOCUSLINE+' | '+BORDER+' |___________________________\n')
sys.stdout.write(BORDER+phrase)
#Each iteration improves the accuracy of wpm_calib, by scanning through the document with the current settings, and recording the "time" it took. Its proportional error compared to the desired wpm is used to improve the wpm_calib. 4 times seems to be enough to get within .5 wpm of desired.
def calib_wpm(iterations):
global wpm_calib, word_num, actual_wpm, word_queue, calibrating, quotes
calibrating=True
save_word_queue=[]
save_word_queue+=word_queue
del word_queue[:]
save_word_num=word_num
save_quotes=quotes
# word_num=0
time_elapse=0
phrase=''
count=0
while count < 200 and phrase != END_FILE_MARKER:
phrase=build_phrase()
if phrase != '<NEWLINE>':
count+=len(phrase.split())
delay=get_delay(phrase)
time_elapse+=delay
actual_wpm=count/time_elapse*60
wpm_calib=wpm_calib*(wpm/actual_wpm)
#print('WPM:',wpm,'Actual:',actual_wpm)
#print('Recalibrated WPM: ',wpm_calib)
word_num=save_word_num
quotes=save_quotes
del word_queue[:]
word_queue+=save_word_queue
if iterations <= 1 or abs(wpm-actual_wpm) < 1:
calibrating=False
return
else:
calib_wpm(iterations - 1)
#Clears the line, and prints the new phrase.
def print_phrase():
global phrase
sys.stdout.write('\r') #Move cursor to beginning of line
sys.stdout.write(phrase)
sys.stdout.flush() #Causes the change to have effect, since there was no line break.
def write_bookmarks():
global word_num, bookmarks
try:
bookmarks[file_name] = word_num
writer = open(bookmarks_file, 'w')
for i in range(0,len(bookmarks)):
writer.write(sorted(bookmarks.keys())[i]+'\t'+str(sorted(bookmarks.values())[i])+'\n')
writer.close()
except PermissionError:
print()
print()
print('You do not have write permission for bookmarks file \''+bookmarks_file+'\'. You most allow write access to save bookmarks')
def load_bookmarks():
global bookmarks, word_num;
try:
bookmarks={}
for line in open(bookmarks_file, 'r'):
bookmarks[line.split()[0]] = int(line.split()[1])
if line.split()[0]==file_name:
word_num = int(line.split()[1])-25
except PermissionError:
print('Was unable to read the bookmarks file')
except FileNotFoundError:
print('Bookmarks file does not exist. It will be created on exit.')
#MAIN
if __name__ == '__main__':
ALL_WORDS=[] #Loads every word in the file for easy navigation later
word_queue=[] #The lineup of words to be printed on the screen, with the word at [0] being next.
phrase='' #The current phrase being displayed, with every edit and format
line_num=0 #The current line in the file
word_num=0 #Word counter
time_elapse=0 #Cumulative time delay
quotes=0 #Whether the reading is inside quotes, for the highlight_speech option. 0 is outside, 1 is inside, 2 is inside, but on the last phrase.
wpm_calib=1.56 #Assists with keeping the average wpm close to the above value. This will vary based on wpm and the format and style of the work.
paused=False
args = get_args()
apply_args(args)
threading.Thread(target=command_listener).start()
load_bookmarks()
f=args.FILE
load_words()
quotes=0
calib_wpm(6)
i=0
print('Press Q to quit...')
print('')
setup()
bump=False
calibrating=False
while True:
while paused:
sleep(.1)
if bump:
break
if not paused:
sleep(.1)
while calibrating:
sleep(.05)
bump=False
phrase=build_phrase()
delay=get_delay(phrase)
phrase=phrase_format(OFFSET, phrase)
threading.Thread(target=print_phrase).start() #This saves a slight amount of time error in maintaining wpm.
sleep(delay)