typetest.c (4765B)
1 #include <unistd.h> 2 #include <err.h> 3 #include <termio.h> 4 #include <stdarg.h> 5 #include <time.h> 6 #include <stdio.h> 7 #include <string.h> 8 #include <stdbool.h> 9 #include <stdint.h> 10 #include <stdlib.h> 11 12 #define LINTERP(a,b,v) ((a) * (v) + (b) * (1 - (v))) 13 14 enum { 15 C_EOF = 0x03, /* CTRL+C */ 16 C_EOT = 0x04, /* CTRL+D */ 17 C_DEL = 0x7f, /* BKSP */ 18 C_SKIP = 0x17, /* CTRL+W */ 19 }; 20 21 enum { 22 STYLE_END = -1, 23 24 STYLE_RESET = 0, 25 26 STYLE_BOLD = 1, 27 STYLE_FAINT = 2, 28 STYLE_ITALIC = 3, 29 STYLE_UNDERLINE = 4, 30 STYLE_BLINK = 5, 31 STYLE_INVERT = 7, 32 STYLE_CROSSED_OUT = 9, 33 34 STYLE_BLACK_FG = 30, 35 STYLE_RED_FG = 31, 36 STYLE_GREEN_FG = 32, 37 STYLE_YELLOW_FG = 33, 38 STYLE_BLUE_FG = 34, 39 STYLE_MAGENTA_FG = 35, 40 STYLE_CYAN_FG = 36, 41 STYLE_WHITE_FG = 37, 42 STYLE_GREY_FG = 90, 43 }; 44 45 static const int style_wrong[] = { STYLE_RED_FG, STYLE_BOLD, STYLE_END }; 46 static const int style_right[] = { STYLE_GREEN_FG, STYLE_END }; 47 static const int style_reset[] = { STYLE_RESET, STYLE_END }; 48 static const int style_preview[] = { STYLE_GREY_FG, STYLE_END }; 49 50 static struct termios old_termios; 51 52 static int typed = 0; 53 static float gcps; 54 55 void 56 print_style(const int *props) 57 { 58 const int *iter; 59 60 if (!props || *props == STYLE_END) return; 61 62 printf("\x1b["); 63 for (iter = props; *iter != STYLE_END; iter++) 64 printf("%i%c", *iter, (iter[1] == STYLE_END) ? 'm' : ';'); 65 } 66 67 void 68 time_sub(struct timespec *new, struct timespec *old) 69 { 70 new->tv_sec -= old->tv_sec; 71 new->tv_nsec -= old->tv_nsec; 72 73 if (new->tv_nsec < 0) { 74 new->tv_sec -= 1; 75 new->tv_nsec += 1e9; 76 } 77 } 78 79 int 80 type(const char *text, const char *preview) 81 { 82 int i, c, right, wrong, tlen; 83 struct timespec start = { 0 }, end; 84 float cps; 85 86 if (typed > 0) clock_gettime(CLOCK_REALTIME, &start); 87 88 tlen = strlen(text); 89 c = right = wrong = 0; 90 while (1) { 91 printf("\r\x1b[2K"); /* goto beginning of row and clear */ 92 printf("\n\x1b[2K\x1b[1A"); /* down a row and clear */ 93 94 switch (c) { 95 case C_EOF: 96 goto quit; 97 case C_EOT: 98 goto exit; 99 case C_DEL: 100 if (wrong) wrong--; 101 else if (right) right--; 102 break; 103 case C_SKIP: 104 right += wrong + 1; 105 if (right >= tlen) 106 goto exit; 107 wrong = 0; 108 break; 109 case u'\r': 110 case u'\n': 111 case u'\0': 112 break; 113 case ' ': 114 if (!wrong && right == tlen) 115 goto exit; 116 default: 117 if (!start.tv_sec) 118 clock_gettime(CLOCK_REALTIME, &start); 119 if (right + wrong == tlen) break; 120 if (c == text[right + wrong] && !wrong) right++; 121 else wrong++; 122 break; 123 } 124 125 print_style(style_right); 126 printf("%.*s", right, text); 127 print_style(style_wrong); 128 for (i = 0; i < wrong; i++) 129 putchar(text[right + i] == ' ' ? '_' : text[right + i]); 130 print_style(style_reset); 131 printf("%.*s", tlen - right - wrong, text + right + wrong); 132 133 print_style(style_preview); 134 printf("\n\r%s", preview); 135 print_style(style_reset); 136 137 printf("\x1b[1A\r"); 138 if (right + wrong) 139 printf("\x1b[%iC", right + wrong); 140 141 if (!*preview && !wrong && right == tlen) 142 goto exit; 143 144 c = getchar(); 145 if (c < 0) goto quit; 146 }; 147 148 exit: 149 printf("\r\x1b[2K"); /* goto beginning of line and clear */ 150 printf("\n\x1b[2K\x1b[1A"); /* down a row and clear */ 151 152 if (!start.tv_sec) return 1; 153 154 clock_gettime(CLOCK_REALTIME, &end); 155 time_sub(&end, &start); 156 157 typed += right; 158 159 end.tv_nsec /= 1000000; /* reduce precision to millis */ 160 cps = right / (end.tv_sec + end.tv_nsec / 1e3f); 161 if (!typed) gcps = cps; 162 else gcps = LINTERP(cps, gcps, right * 1.f / typed); 163 164 return 0; 165 166 quit: 167 return 1; 168 } 169 170 int 171 readline(FILE *f, char *buf, size_t size) 172 { 173 do { 174 if (!fgets(buf, size, f)) return 1; 175 176 for (ssize_t i = strlen(buf) - 1; i >= 0; i--) { 177 if (buf[i] == '\r' || buf[i] == '\n') 178 buf[i] = '\0'; 179 else 180 break; 181 } 182 if (strlen(buf) == 0) continue; 183 } while (!*buf); 184 185 return 0; 186 } 187 188 void 189 reset() 190 { 191 tcsetattr(0, TCSANOW, &old_termios); 192 } 193 194 int 195 main(int argc, const char **argv) 196 { 197 const char *path = NULL; 198 bool retry = true; 199 200 for (const char **arg = argv + 1; *arg; arg++) { 201 if (!strcmp(*arg, "-R")) { 202 retry = false; 203 } else if (!path) { 204 path = *arg; 205 } else { 206 break; 207 } 208 } 209 if (!path) errx(1, "missing file argument"); 210 211 FILE *file = fopen(path, "r"); 212 if (!file) err(1, "fopen"); 213 214 setvbuf(stdin, NULL, _IONBF, 0); 215 setvbuf(stdout, NULL, _IONBF, 0); 216 217 struct termios termios; 218 tcgetattr(0, &termios); 219 old_termios = termios; 220 221 cfmakeraw(&termios); 222 223 if (!tcsetattr(0, TCSANOW, &termios)) atexit(reset); 224 225 char line[256], next_line[256]; 226 if (readline(file, next_line, sizeof(next_line))) 227 errx(1, "Empty input file"); 228 229 for (bool eof = 0; !eof; ) { 230 strcpy(line, next_line); 231 eof |= readline(file, next_line, sizeof(next_line)); 232 if (type(line, eof ? "" : next_line)) break; 233 } 234 235 if (gcps) { 236 printf("CPM: %0.2f\n\r", gcps * 60); 237 printf("WPM: %0.2f\n\r", gcps * 12); 238 } 239 240 fclose(file); 241 }