1 /*
2  *  Dvorak 7min 1.6.1, a Dvorak typing tutor
3  *  Copyright (C) 1998-2003  Ragnar Hojland Espinosa
4  * 			     <ragnar@ragnar-hojland.com>
5  *
6  *  This program is free software; you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation; either version 2 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program; if not, write to the Free Software
18  *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19  */
20 
21 /*
22  * It's horrible code. I agree.
23  * I wanted it. I needed it. I now have it.
24  * I hope it ends my frustration learning dvorak.
25  * Otherwise I'm gonna kill someone. Soon.
26  * Do I sound desesperate?
27  *
28  * Try truning on the nastiness option.
29  * You can use the space bar or the enter key interchangeably.
30  *
31  * As far as 1.1, the code doesn't look that bad now thanks to Smoke,
32  * so mental SANity loss is reduced from 1d100 to merely 1d10.
33  *
34  *
35  * Smoke/CRAP here - I just hacked 1.3 up and it's pretty late
36  *                   a great excuse for messing up the code even more :)
37  *
38  * Hm, 1.5 and I'm pondering on sumbitting it to the Ofuscated C contest...
39  * It'll take a few versions more and some extra "features", and who knows.
40  * This baby is fun :)
41  *
42  */
43 
44 #include <string.h>
45 #include <stdlib.h>
46 #include <ctype.h>
47 #include <time.h>
48 #include <curses.h>
49 #include <getopt.h>
50 #include <errno.h>
51 #include <sys/stat.h>
52 
53 #include "lessons.h"
54 
55 /* these three represent the visualization of the key on screen */
56 #define PRESSED_NOT       0
57 #define PRESSED_REQUESTED 1
58 #define PRESSED_BY_USER   2
59 
60 /* Update cp[sm] every LATENCY tenths of second */
61 #define LATENCY 1
62 
63 #ifdef NOT_SO_PRETTY
64 int tryToBePretty = 0;
65 #else
66 int tryToBePretty = 1;
67 #endif
68 
69 #ifdef BEEPS_ARENT_IRRITATING
70 int beepsArentIrritating = 1;
71 #else
72 int beepsArentIrritating = 0;
73 #endif
74 
75 #ifdef FLASHES_ARENT_IRRITATING
76 int flashesArentIrritating = 1;
77 #else
78 int flashesArentIrritating = 0;
79 #endif
80 
81 #ifdef NASTY_AS_USUAL
82 int nastiness = 1;
83 #else
84 int nastiness = 0;
85 #endif
86 
87 #ifdef NO_COLORS_PLEASE
88 int colorSupport = 0;
89 #else
90 int colorSupport = 1;
91 #endif
92 
93 #ifdef NO_KEYS
94 int hideKeys = 1;
95 #else
96 int hideKeys = 0;
97 #endif
98 
99 int max_editable_lines = 6;
100 int right_margin = 70;
101 
102 const char *postmortem = "undefined postmortem";
103 
104 /* all typable characters */
105 char typables[] = "`1234567890[]',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"
106                   "~!@#$%^&*(){}\"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ\n ";
107 
108 char map[] =
109 {
110       '`', '~', '1', '!', '2', '@', '3', '#', '4', '$', '5', '%', '6', '^',
111       '7', '&', '8', '*', '9', '(', '0', ')', '[', '{', ']', '}', 0,
112    3, '\'', '"', ',', '?', '.', '?', 'P', 'Y', 'F', 'G', 'C', 'R', 'L',
113       '/', '?', '=', '+',  0,
114    5, 'A', 'O', 'E', 'U', 'I', 'D', 'H', 'T', 'N', 'S', '-', '_',
115       '\\', '|', 0,
116    2, '<', '>', ';', ':', 'Q', 'J', 'K', 'X', 'B', 'M', 'W', 'V', 'Z', 0,
117    0
118 };
119 
120 
121 char colormap[] =
122 {
123       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
124       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
125    3, -5, -5, -4, -4, -3, -3, -2, -2, 2, 2, 3, 4, 5,
126       0, 0, 0, 0,  0,
127    5, -5, -4, -3, -2, -2, 2, 2, 3, 4, 5, 0, 0,
128       0, 0, 0,
129    2, 0, 0, -5, -5, -4, -3, -2, -2, 2, 2, 3, 4, 5, 0,
130    0
131 };
132 
133 
134 int helpInterval;
135 int helpLength;
136 time_t nextHelp;
137 
myGetch(char shouldBe)138 int myGetch(char shouldBe)
139 {
140    int deviation;
141    int ch;
142 
143    ch = getch();
144 
145    if (ch != ERR) {
146       helpInterval = 10000 + (random() % 10000);
147       deviation = random() % 300;
148       nextHelp = time(0) + (helpInterval + deviation) / 1000;
149       helpLength = 1;
150       return ch;
151    }
152 
153    if (time(0) < nextHelp) {
154      return ch;
155    }
156 
157    if ((helpLength--) == 0) {
158       helpInterval = random() % 1000;
159       helpLength = random() % 8;
160    }
161    deviation = random() % 300;
162    nextHelp = time(0) + (helpInterval + deviation) / 1000;
163 
164    if ((random() % 26) == 0) {
165      return KEY_BACKSPACE;
166    } else {
167      return shouldBe;
168    }
169 }
170 
171 
172 /* calculate typing speed in a given interval */
calcSpeed(time_t timeStart,time_t timeFinish,int hits)173 inline float calcSpeed (time_t timeStart, time_t timeFinish, int hits)
174 {
175     // Use WPS/WPM as better standard, so also divide by 5
176     return (float) hits / 5 / (timeFinish - timeStart);
177 }
178 
179 
180 /* Import an ASCII text to use as a lesson. Allocates memory for *buf.
181    return length of read text on success, -1 on error
182 
183    tabstops are skipped, as are blanks on the beginning and end of
184    lines */
importText(char const * const fileName,char ** buf,unsigned int length)185 int importText (char const * const fileName, char** buf, unsigned int length)
186 {
187    FILE* file;
188    int c;
189    int i = 0;
190    int lines = 0;
191    int column;
192    struct stat st;
193 
194    file = fopen (fileName, "r");
195    if (!file) {
196       postmortem = strerror(errno);
197       return -1;
198    }
199 
200    fstat (fileno(file), &st);
201    if (st.st_size > length) {
202       length = st.st_size;
203    }
204 #if defined(LIMIT_LENGTH) && (LIMIT_LENGTH > 0)
205    if (length > LIMIT_LENGTH) {
206       length = LIMIT_LENGTH;
207    }
208 #endif
209 
210    *buf = (char*) malloc (length+1+1);
211    if (!*buf) {
212       postmortem = "memory allocation failed";
213       fclose (file);
214       return -1;
215    }
216 
217    column = 0;
218    (*buf[i++]) = '\x2';
219 
220    while (1) {
221       c = fgetc (file);
222       if (errno) {
223 	 postmortem = strerror(errno);
224 	 fclose (file);
225 	 return -1;
226       }
227       if (c == EOF) {
228 	 fclose (file);
229 	 (*buf)[i] = '\x1';
230 	 break;
231       }
232 
233       if (index(typables, c) == NULL) {
234 	 continue;
235       }
236 
237       if (c == '\n' || column > right_margin) {
238 	 if (column > right_margin) {
239 	    (*buf)[i++] = '\n';
240 	 }
241 
242 	 while (i > 0) {
243 	    if ((*buf)[i-1] == ' ') {
244 	       --i;
245 	    } else {
246 	       break;
247 	    }
248 	 }
249 	 column = 0;
250 	 ++lines;
251 
252 	 if (c == ' ') {
253 	    continue;
254 	 }
255       }
256 
257       if (lines == max_editable_lines) {
258 	 lines = 0;
259 	 (*buf)[i++] = '\x1';
260       } else {
261 	 (*buf)[i++] = (char) c;
262 	 ++column;
263       }
264 
265       if (i >= length - 1) {
266 	 fclose (file);
267 	 (*buf)[i] = '\x1';
268 	 break;
269       }
270    }
271 
272    /* strip unwanted whitespace from the end */
273    while (i > 0) {
274       if ((*buf)[i-1] == ' ' || (*buf)[i-1] == '\n' || (*buf)[i-1] == '\x1') {
275 	 --i;
276       } else {
277 	 break;
278       }
279    }
280 
281    (*buf)[i] = '\0';
282    return i;
283 }
284 
boxed(int x,int y,char u,char d,char pressed,int key)285 void boxed(int x, int y, char u, char d, char pressed, int key)
286 {
287    if (pressed == PRESSED_REQUESTED) {
288       attron (A_REVERSE);
289    }
290    if (pressed == PRESSED_BY_USER) {
291       attron (A_BOLD);
292    }
293    if (colorSupport) {
294       attron (COLOR_PAIR (7-abs(colormap[key])));
295    }
296 
297    if (tryToBePretty) {
298       move (y+0, x); addch (ACS_ULCORNER); addch (ACS_HLINE); addch (ACS_HLINE); addch (ACS_HLINE); addch (ACS_URCORNER);
299       move (y+1, x); addch (ACS_VLINE); addch (u); addch (' '); addch (' '); addch (ACS_VLINE);
300       move (y+2, x); addch (ACS_VLINE); addch (' '); addch (' '); addch (d); addch (ACS_VLINE);
301       move (y+3, x); addch (ACS_LLCORNER); addch (ACS_HLINE); addch (ACS_HLINE); addch (ACS_HLINE); addch (ACS_LRCORNER);
302    } else {
303       mvprintw (y+0, x, ".---.");
304       mvprintw (y+1, x, "|%c  |", u);
305       mvprintw (y+2, x, "|  %c|", d);
306       mvprintw (y+3, x, "`---'");
307    }
308 
309    if (colorSupport) {
310       attron (COLOR_PAIR(7));
311    }
312    if (pressed == PRESSED_REQUESTED) {
313       attroff (A_REVERSE);
314    }
315    if (pressed == PRESSED_BY_USER) {
316       attroff (A_BOLD);
317    }
318 }
319 
show_layout(char pressedKey,char pressed)320 void show_layout(char pressedKey, char pressed)
321 {
322     int key = 0;
323     int x = 0, y = 0;
324 
325     if (hideKeys) {
326        return;
327     }
328 
329     while (map[key]) {
330         while (map[key]) {
331             char u = map[key];
332             char d;
333             if (isupper(u)) {
334                         /* hack, to fix the keymap i wrote erroneusly.. */
335                 d = u;
336                 u = tolower (d);
337             } else {
338                 ++key;
339                 d = map[key];
340             }
341 
342             if ((u == pressedKey) || (d == pressedKey)) {
343 	       boxed (x, y, d, u, pressed, key);
344             } else {
345 	       if (pressed != PRESSED_REQUESTED) {
346 		  boxed (x, y, d, u, PRESSED_NOT, key);
347 	       }
348             }
349 
350             x += 6;
351             ++key;
352         }
353         ++key;
354         y += 4;
355         x = map[key];
356         ++key;
357     }
358     refresh();
359 }
360 
myaddnstr(char const * text,int lenght)361 void myaddnstr (char const * text, int lenght)
362 {
363    char *buf = malloc (lenght);
364    char *obuf = buf;
365    int i = lenght;
366 
367    while (--i) {
368       if (*text != '\x2') {
369 	 *(buf++) = *(text);
370       }
371       ++text;
372    }
373 
374    addnstr (obuf, lenght);
375    free (obuf);
376 }
377 
do_text(char * const text)378 void do_text (char * const text)
379 {
380     int hits, misses;
381     int this_page_size;
382 
383     time_t timeStart, timeFinish, timeCurrent;
384     float speed;
385 
386     char *p = text;
387     char *i;
388     char *this_page_start;
389 
390     curs_set (FALSE);
391     show_layout(' ', PRESSED_NOT);
392     mvaddstr (LINES - 1, 0, "[ ] Press a key to start");
393 
394     i = (char*) index (text, 0x1);
395     this_page_start = text;
396     this_page_size  = i ? i - this_page_start : strlen(this_page_start);
397 
398     move ((hideKeys ? 0 : 17), 0);
399 
400     myaddnstr (text, this_page_size);
401     hits = 0; misses = 0;
402     refresh();
403     noecho();
404     move (LINES - 1, 1);
405     getch();
406 
407     halfdelay (LATENCY);
408 
409     move (LINES - 1, 0);
410     clrtoeol();
411     timeStart = 0;
412     nextHelp = time(0) + (helpInterval / 1000);
413 
414     while (*p) {
415        int ch = 0;
416 
417        if (*p == 0x2) {
418 	  ++p;
419 	  if (!timeStart) {
420 	     timeStart = time(0);
421 	     hits = 0;
422 	  }
423        }
424 
425        if (*p > 27) {
426 	  show_layout(*p, PRESSED_REQUESTED);
427        }
428 
429        do {
430 	  ch = myGetch (*p);
431 	  if (timeStart) {
432 	     float ratio = hits - misses;
433 	     ratio = (ratio < 0) ? 0 : (100.0 * ratio / hits);
434 	     timeCurrent = time(0);
435 	     speed = calcSpeed (timeStart, timeCurrent, hits + misses);
436 	     // Use WPS/WPM as better standards
437 	     mvprintw (LINES - 1, 0, "WPS %.2f  WPM %.2f Hits: %d Misses: %d Seconds: %d Ratio: %.2f%%", speed, speed * 60, hits, misses, time(0) - timeStart, ratio);
438 	     clrtoeol();
439 	  }
440        } while (ch == ERR);
441 
442        show_layout(ch, PRESSED_BY_USER);
443 
444         if (ch == *p || (*p == '\n' && ch == ' ')) {
445             ++p;
446             ++hits;
447         } else {
448 	    /* any other possibilities? */
449             if ( (ch == 8 || ch == KEY_BACKSPACE || ch == KEY_DC || ch == 127)
450                  && p > this_page_start) {
451                 --p;
452 	       if (*p == '\x2') {
453 		  --p;
454 	       }
455             } else if (ch == 27 || ch == KEY_END) {
456                 break;
457             } else {
458                 if (beepsArentIrritating) {
459 		   beep();
460 		}
461 		if (flashesArentIrritating) {
462 		   flash();
463 		}
464 
465                 ++misses;
466                 if (nastiness && p > this_page_start) {
467 		   --hits;
468                    --p;
469 		   if (*p == '\x2') {
470 		      --p;
471 		   }
472                 }
473             }
474         }
475 
476 	move ((hideKeys ? 0 : 17), 0);
477         i = this_page_start;
478         attron (A_BOLD);
479         while (i < p) {
480 	   if (*i != 0x2) {
481 	      addch(*i);
482 	   }
483 	   ++i;
484         }
485         if (*i == '\x2') {
486 	   ++i;
487 	}
488         attroff (A_BOLD);
489         attron (A_REVERSE);
490         if (*i != 0x2) {
491  	   addch(*i);
492         }
493         ++i;
494         attroff (A_REVERSE);
495         while (*i && i < this_page_start + this_page_size) {
496 	   if (*i != 0x2) {
497 	      addch(*i);
498 	   }
499 	   ++i;
500         }
501 
502         if (*p == 0x1) {
503 	   char *next;
504 	   ++p;
505 	   next = (char*) index (p, 0x1);
506 	   this_page_start = p;
507 	   this_page_size  = next ? next - this_page_start : strlen(this_page_start);
508 	   move ((hideKeys ? 0 : 17), 0);
509 	   clrtobot();
510 	   myaddnstr (this_page_start, this_page_size);
511         }
512     }
513 
514     timeFinish = time(0);
515     speed = calcSpeed(timeStart, timeFinish, hits + misses);
516     clear();
517     cbreak();
518     if (!timeStart) {
519        mvprintw (0, 0, "You haven't done enough to get a good benchmark.");
520     } else {
521        float ratio = hits - misses;
522        ratio = (ratio < 0) ? 0 : (100.0 * ratio / hits);
523        mvprintw (0, 0, "Elapsed time: %d seconds", timeFinish - timeStart);
524        mvprintw (1, 0, "Total: %d  Misses: %d  Ratio: %.2f%%",
525                  hits + misses, misses,
526 		 (float)100*(hits) / (hits + misses));
527        // Use WPM/WPS as better standard
528        mvprintw (2, 0, "WPS: %.2f  WPM: %.2f", speed, speed * 60);
529     }
530 
531    mvprintw (4, 0, "[ ] Press ESCAPE to continue.");
532    move (4, 1);
533    refresh();
534 
535    while (getch() != 27);
536 
537    curs_set (TRUE);
538    echo();
539    clear();
540    refresh();
541 }
542 
menuInteractive()543 void menuInteractive()
544 {
545     int i, num;
546     char buf[255];
547 
548     while (1) {
549         mvprintw (2, 0, "Dvorak7Min, Ragnar Hojland Espinosa, 1998-2003" );
550         mvprintw (3, 0, "enhanced by Smoke of CRAP, 1999 and Nopik, 2003");
551         for (i = 0; lessons[i*2]; ++i) {
552             mvprintw (i/2 + 5, (i%2) * 40, "%2d. %s", i+1, lessons[i*2]);
553         }
554         move (21, 0); clrtobot();
555         mvprintw (21, 0, "Type a lesson number ([N]astiness [H]ide keyboard [Q]uit)? ");
556         refresh();
557         echo();
558         getnstr (buf, sizeof(buf));
559         buf[0] = toupper(buf[0]);
560         if (buf[0] == 'N') {
561             nastiness = !nastiness;
562             if (nastiness)
563                 mvprintw (23, 0, "Nastiness is now turned ON. Press any key.");
564             else
565                 mvprintw (23, 0, "Nastiness is now turned OFF. Press any key.");
566             getch();
567             continue;
568         }
569 
570         if (buf[0] == 'H') {
571             hideKeys = !hideKeys;
572             if (hideKeys)
573                 mvprintw (23, 0, "Keyboard layout is now OFF. Press any key.");
574             else
575                 mvprintw (23, 0, "Keyboard layout is now ON. Press any key.");
576             getch();
577             continue;
578         }
579 
580         if (buf[0] == 'Q' || buf[0] == 27) {
581             clear();
582             move (0,0);
583             refresh();
584             return;
585         }
586         num = atoi (buf);
587         if (!buf[0] || num < 1 || num > i) {
588             mvprintw (23, 0, "Invalid lesson number. Press any key.");
589             getch();
590             continue;
591         }
592 
593         clear();
594         refresh();
595         do_text(lessons[(num-1)*2+1]);
596     }
597 }
598 
599 
initColors()600 int initColors()
601 {
602    start_color();
603 
604    if (!has_colors()) {
605       colorSupport = 0;
606       return -1;
607    }
608 
609    init_pair( 1, COLOR_BLUE , COLOR_BLACK );
610    init_pair( 2, COLOR_GREEN , COLOR_BLACK );
611    init_pair( 3, COLOR_CYAN , COLOR_BLACK );
612    init_pair( 4, COLOR_RED , COLOR_BLACK );
613    init_pair( 5, COLOR_MAGENTA , COLOR_BLACK );
614    init_pair( 6, COLOR_YELLOW , COLOR_BLACK );
615    init_pair( 7, COLOR_WHITE , COLOR_BLACK );
616 
617    return 0;
618 }
619 
620 
initApp()621 void initApp()
622 {
623    initscr();
624 
625    right_margin = COLS - 10;
626    max_editable_lines = LINES - 18 - 1;
627    helpInterval = 10000 + (random() % 10000);
628    helpLength = 1;
629 
630    if (colorSupport) {
631      initColors();
632    }
633 }
634 
635 
636 
637 
closeApp()638 void closeApp()
639 {
640     move (0, 0);
641     clear();
642     refresh();
643     endwin();
644 }
645 
playFile(char * filename)646 int playFile(char *filename)
647 {
648    char *buffer;
649    int r;
650    int len = 0;
651 
652 #ifdef LIMIT_LENGTH
653    len = LIMIT_LENGTH;
654 #endif
655 
656    r = importText (filename, &buffer, len);
657    if (r <= 0) {
658       return -1;
659    }
660 
661    do_text (buffer);
662    return 0;
663 }
664 
showHelp()665 void showHelp()
666 {
667    printf("Usage: dvorak7min [OPTION]... [FILE]...\n\n"
668 	  "  -b, --bell                   beep on error\n"
669           "  -f, --flash                  flash the screen on error\n"
670 	  "  -u, --ugly                   use low ascii for artwork\n"
671 	  "  -n, --nastiness              set nastiness on by default\n"
672 	  "  -k, --hidekeys               hides keyboard layout\n"
673 	  "      --help                   display this help and exit\n"
674 	  "      --version                output version information and exit\n"
675 	  "\n");
676 }
677 
showVersionInfo()678 void showVersionInfo()
679 {
680     printf ("dvorak 7min tutor hack 1.6 - FREE Software with NO Warranty\n"
681             "(C) 1998-2001 Ragnar Hojland Espinosa <ragnar@ragnar-hojland.com>\n"
682             "Lessons are (C) 1995 Dan Wood <danwood@karelia.com>\n"
683             "Enhancements in 1999 by Smoke of Crap aka Tijs van Bakel <smoke@casema.net>\n"
684 						"and in 2003 by Nopik aka Kamil Burzynski <K.Burzynski@adbglobal.com>\n"
685 	    "\n");
686 }
687 
main(int argc,char * argv[])688 int main ( int argc, char *argv[] )
689 {
690    struct option long_opts[] = {
691 	 { "help",      0, 0, 'H' },
692 	 { "version",   0, 0, 'V' },
693 	 { "nastiness", 0, 0, 'n' },
694 	 { "bell",      0, 0, 'b' },
695 	 { "flash",     0, 0, 'f' },
696 	 { "ugly",      0, 0, 'u' },
697          { "hidekeys",  0, 0, 'k' },
698 	 { 0, 0, 0, 0 }
699    };
700 
701    int opt_index = 0;
702    int c;
703 
704    while (1) {
705       c = getopt_long(argc, argv, "HVnbfuk", long_opts, &opt_index);
706       if (c == -1) {
707 	 break;
708       }
709 
710       switch (c) {
711        case 'H':
712 	 showHelp();
713 	 return 1;
714        case 'V':
715 	 showVersionInfo();
716 	 return 1;
717        case 'b':
718 	 beepsArentIrritating = 1;
719 	 break;
720        case 'f':
721 	 flashesArentIrritating = 1;
722 	 break;
723        case 'n':
724 	 nastiness = 1;
725 	 break;
726        case 'u':
727 	 tryToBePretty = 0;
728 	 break;
729        case 'k':
730 	 hideKeys = 1;
731 	 break;
732       }
733    }
734 
735    initApp();
736 
737    if (optind < argc) {
738       while (optind < argc) {
739 	 if (playFile(argv[optind]) < 0) {
740 	    closeApp();
741 	    printf ("%s: %s: %s\n", argv[0], argv[optind], postmortem);
742 	    return 1;
743 	 }
744 	 ++optind;
745       }
746    } else {
747       menuInteractive();
748    }
749 
750    closeApp();
751    showVersionInfo();
752 
753    return 0;
754 }
755