1 /* $NetBSD: cgram.c,v 1.17 2021/02/26 15:18:40 rillig Exp $ */ 2 3 /*- 4 * Copyright (c) 2013, 2021 The NetBSD Foundation, Inc. 5 * All rights reserved. 6 * 7 * This code is derived from software contributed to The NetBSD Foundation 8 * by David A. Holland and Roland Illig. 9 * 10 * Redistribution and use in source and binary forms, with or without 11 * modification, are permitted provided that the following conditions 12 * are met: 13 * 1. Redistributions of source code must retain the above copyright 14 * notice, this list of conditions and the following disclaimer. 15 * 2. Redistributions in binary form must reproduce the above copyright 16 * notice, this list of conditions and the following disclaimer in the 17 * documentation and/or other materials provided with the distribution. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS 20 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 21 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 22 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS 23 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 * POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 #include <assert.h> 33 #include <ctype.h> 34 #include <curses.h> 35 #include <err.h> 36 #include <stdbool.h> 37 #include <stdio.h> 38 #include <stdlib.h> 39 #include <string.h> 40 #include <time.h> 41 42 #include "pathnames.h" 43 44 //////////////////////////////////////////////////////////// 45 46 static char 47 ch_toupper(char ch) 48 { 49 return (char)toupper((unsigned char)ch); 50 } 51 52 static char 53 ch_tolower(char ch) 54 { 55 return (char)tolower((unsigned char)ch); 56 } 57 58 static bool 59 ch_isalpha(char ch) 60 { 61 return isalpha((unsigned char)ch) != 0; 62 } 63 64 static bool 65 ch_islower(char ch) 66 { 67 return islower((unsigned char)ch) != 0; 68 } 69 70 static bool 71 ch_isspace(char ch) 72 { 73 return isspace((unsigned char)ch) != 0; 74 } 75 76 static bool 77 ch_isupper(char ch) 78 { 79 return isupper((unsigned char)ch) != 0; 80 } 81 82 static int 83 imax(int a, int b) 84 { 85 return a > b ? a : b; 86 } 87 88 static int 89 imin(int a, int b) 90 { 91 return a < b ? a : b; 92 } 93 94 //////////////////////////////////////////////////////////// 95 96 struct string { 97 char *s; 98 size_t len; 99 size_t cap; 100 }; 101 102 struct stringarray { 103 struct string *v; 104 size_t num; 105 }; 106 107 static void 108 string_init(struct string *s) 109 { 110 s->s = NULL; 111 s->len = 0; 112 s->cap = 0; 113 } 114 115 static void 116 string_add(struct string *s, char ch) 117 { 118 if (s->len >= s->cap) { 119 s->cap = 2 * s->cap + 16; 120 s->s = realloc(s->s, s->cap); 121 if (s->s == NULL) 122 errx(1, "Out of memory"); 123 } 124 s->s[s->len++] = ch; 125 } 126 127 static void 128 string_finish(struct string *s) 129 { 130 string_add(s, '\0'); 131 s->len--; 132 } 133 134 static void 135 stringarray_init(struct stringarray *a) 136 { 137 a->v = NULL; 138 a->num = 0; 139 } 140 141 static void 142 stringarray_cleanup(struct stringarray *a) 143 { 144 for (size_t i = 0; i < a->num; i++) 145 free(a->v[i].s); 146 free(a->v); 147 } 148 149 static void 150 stringarray_add(struct stringarray *a, struct string *s) 151 { 152 size_t num = a->num++; 153 a->v = realloc(a->v, a->num * sizeof a->v[0]); 154 if (a->v == NULL) 155 errx(1, "Out of memory"); 156 a->v[num] = *s; 157 } 158 159 static void 160 stringarray_dup(struct stringarray *dst, const struct stringarray *src) 161 { 162 assert(dst->num == 0); 163 for (size_t i = 0; i < src->num; i++) { 164 struct string str; 165 string_init(&str); 166 for (const char *p = src->v[i].s; *p != '\0'; p++) 167 string_add(&str, *p); 168 string_finish(&str); 169 stringarray_add(dst, &str); 170 } 171 } 172 173 //////////////////////////////////////////////////////////// 174 175 static struct stringarray lines; 176 static struct stringarray sollines; 177 static bool hinting; 178 static int extent_x; 179 static int extent_y; 180 static int offset_x; 181 static int offset_y; 182 static int cursor_x; 183 static int cursor_y; 184 185 static int 186 cur_max_x(void) 187 { 188 return (int)lines.v[cursor_y].len; 189 } 190 191 static int 192 cur_max_y(void) 193 { 194 return extent_y - 1; 195 } 196 197 static char 198 char_left_of_cursor(void) 199 { 200 if (cursor_x > 0) 201 return lines.v[cursor_y].s[cursor_x - 1]; 202 assert(cursor_y > 0); 203 return '\n'; /* eol of previous line */ 204 } 205 206 static char 207 char_at_cursor(void) 208 { 209 if (cursor_x == cur_max_x()) 210 return '\n'; 211 return lines.v[cursor_y].s[cursor_x]; 212 } 213 214 static void 215 readquote(void) 216 { 217 FILE *f = popen(_PATH_FORTUNE, "r"); 218 if (f == NULL) 219 err(1, "%s", _PATH_FORTUNE); 220 221 struct string line; 222 string_init(&line); 223 224 int ch; 225 while ((ch = fgetc(f)) != EOF) { 226 if (ch == '\n') { 227 string_finish(&line); 228 stringarray_add(&lines, &line); 229 string_init(&line); 230 } else if (ch == '\t') { 231 string_add(&line, ' '); 232 while (line.len % 8 != 0) 233 string_add(&line, ' '); 234 } else if (ch == '\b') { 235 if (line.len > 0) 236 line.len--; 237 } else { 238 string_add(&line, (char)ch); 239 } 240 } 241 242 stringarray_dup(&sollines, &lines); 243 244 extent_y = (int)lines.num; 245 for (int i = 0; i < extent_y; i++) 246 extent_x = imax(extent_x, (int)lines.v[i].len); 247 248 if (pclose(f) != 0) 249 exit(1); /* error message must come from child process */ 250 } 251 252 static void 253 encode(void) 254 { 255 int key[26]; 256 257 for (int i = 0; i < 26; i++) 258 key[i] = i; 259 260 for (int i = 26; i > 1; i--) { 261 int c = (int)(random() % i); 262 int t = key[i - 1]; 263 key[i - 1] = key[c]; 264 key[c] = t; 265 } 266 267 for (int y = 0; y < extent_y; y++) { 268 for (char *p = lines.v[y].s; *p != '\0'; p++) { 269 if (ch_islower(*p)) 270 *p = (char)('a' + key[*p - 'a']); 271 if (ch_isupper(*p)) 272 *p = (char)('A' + key[*p - 'A']); 273 } 274 } 275 } 276 277 static void 278 substitute(char a, char b) 279 { 280 char la = ch_tolower(a); 281 char ua = ch_toupper(a); 282 char lb = ch_tolower(b); 283 char ub = ch_toupper(b); 284 285 for (int y = 0; y < (int)lines.num; y++) { 286 for (char *p = lines.v[y].s; *p != '\0'; p++) { 287 if (*p == la) 288 *p = lb; 289 else if (*p == ua) 290 *p = ub; 291 else if (*p == lb) 292 *p = la; 293 else if (*p == ub) 294 *p = ua; 295 } 296 } 297 } 298 299 static bool 300 is_solved(void) 301 { 302 for (size_t i = 0; i < lines.num; i++) 303 if (strcmp(lines.v[i].s, sollines.v[i].s) != 0) 304 return false; 305 return true; 306 } 307 308 //////////////////////////////////////////////////////////// 309 310 static void 311 redraw(void) 312 { 313 erase(); 314 315 int max_y = imin(LINES - 1, extent_y - offset_y); 316 for (int y = 0; y < max_y; y++) { 317 move(y, 0); 318 319 int len = (int)lines.v[offset_y + y].len; 320 int max_x = imin(COLS - 1, len - offset_x); 321 const char *line = lines.v[offset_y + y].s; 322 const char *solline = sollines.v[offset_y + y].s; 323 324 for (int x = 0; x < max_x; x++) { 325 char ch = line[offset_x + x]; 326 bool bold = hinting && 327 ch == solline[offset_x + x] && 328 ch_isalpha(ch); 329 330 if (bold) 331 attron(A_BOLD); 332 addch(ch); 333 if (bold) 334 attroff(A_BOLD); 335 } 336 clrtoeol(); 337 } 338 339 move(LINES - 1, 0); 340 addstr("~ to quit, * to cheat, ^pnfb to move"); 341 342 if (is_solved()) { 343 if (extent_y + 1 - offset_y < LINES - 2) 344 move(extent_y + 1 - offset_y, 0); 345 else 346 addch(' '); 347 attron(A_BOLD | A_STANDOUT); 348 addstr("*solved*"); 349 attroff(A_BOLD | A_STANDOUT); 350 } 351 352 move(cursor_y - offset_y, cursor_x - offset_x); 353 354 refresh(); 355 } 356 357 //////////////////////////////////////////////////////////// 358 359 static void 360 saturate_cursor(void) 361 { 362 cursor_y = imax(cursor_y, 0); 363 cursor_y = imin(cursor_y, cur_max_y()); 364 365 assert(cursor_x >= 0); 366 cursor_x = imin(cursor_x, cur_max_x()); 367 } 368 369 static void 370 scroll_into_view(void) 371 { 372 if (cursor_x < offset_x) 373 offset_x = cursor_x; 374 if (cursor_x > offset_x + COLS - 1) 375 offset_x = cursor_x - (COLS - 1); 376 377 if (cursor_y < offset_y) 378 offset_y = cursor_y; 379 if (cursor_y > offset_y + LINES - 2) 380 offset_y = cursor_y - (LINES - 2); 381 } 382 383 static bool 384 can_go_left(void) 385 { 386 return cursor_y > 0 || 387 (cursor_y == 0 && cursor_x > 0); 388 } 389 390 static bool 391 can_go_right(void) 392 { 393 return cursor_y < cur_max_y() || 394 (cursor_y == cur_max_y() && cursor_x < cur_max_x()); 395 } 396 397 static void 398 go_to_prev_line(void) 399 { 400 cursor_y--; 401 cursor_x = cur_max_x(); 402 } 403 404 static void 405 go_to_next_line(void) 406 { 407 cursor_x = 0; 408 cursor_y++; 409 } 410 411 static void 412 go_left(void) 413 { 414 if (cursor_x > 0) 415 cursor_x--; 416 else if (cursor_y > 0) 417 go_to_prev_line(); 418 } 419 420 static void 421 go_right(void) 422 { 423 if (cursor_x < cur_max_x()) 424 cursor_x++; 425 else if (cursor_y < cur_max_y()) 426 go_to_next_line(); 427 } 428 429 static void 430 go_to_prev_word(void) 431 { 432 while (can_go_left() && ch_isspace(char_left_of_cursor())) 433 go_left(); 434 435 while (can_go_left() && !ch_isspace(char_left_of_cursor())) 436 go_left(); 437 } 438 439 static void 440 go_to_next_word(void) 441 { 442 while (can_go_right() && !ch_isspace(char_at_cursor())) 443 go_right(); 444 445 while (can_go_right() && ch_isspace(char_at_cursor())) 446 go_right(); 447 } 448 449 static bool 450 can_substitute_here(int ch) 451 { 452 return isascii(ch) && 453 ch_isalpha((char)ch) && 454 cursor_x < cur_max_x() && 455 ch_isalpha(char_at_cursor()); 456 } 457 458 static void 459 handle_char_input(int ch) 460 { 461 if (ch == char_at_cursor()) 462 go_right(); 463 else if (can_substitute_here(ch)) { 464 substitute(char_at_cursor(), (char)ch); 465 go_right(); 466 } else 467 beep(); 468 } 469 470 static bool 471 handle_key(void) 472 { 473 int ch = getch(); 474 475 switch (ch) { 476 case 1: /* ^A */ 477 case KEY_HOME: 478 cursor_x = 0; 479 break; 480 case 2: /* ^B */ 481 case KEY_LEFT: 482 go_left(); 483 break; 484 case 5: /* ^E */ 485 case KEY_END: 486 cursor_x = cur_max_x(); 487 break; 488 case 6: /* ^F */ 489 case KEY_RIGHT: 490 go_right(); 491 break; 492 case '\t': 493 go_to_next_word(); 494 break; 495 case KEY_BTAB: 496 go_to_prev_word(); 497 break; 498 case '\n': 499 go_to_next_line(); 500 break; 501 case 12: /* ^L */ 502 clear(); 503 break; 504 case 14: /* ^N */ 505 case KEY_DOWN: 506 cursor_y++; 507 break; 508 case 16: /* ^P */ 509 case KEY_UP: 510 cursor_y--; 511 break; 512 case KEY_PPAGE: 513 cursor_y -= LINES - 2; 514 break; 515 case KEY_NPAGE: 516 cursor_y += LINES - 2; 517 break; 518 case '*': 519 hinting = !hinting; 520 break; 521 case '~': 522 return false; 523 case KEY_RESIZE: 524 break; 525 default: 526 handle_char_input(ch); 527 break; 528 } 529 return true; 530 } 531 532 static void 533 init(void) 534 { 535 stringarray_init(&lines); 536 stringarray_init(&sollines); 537 srandom((unsigned int)time(NULL)); 538 readquote(); 539 encode(); 540 541 initscr(); 542 cbreak(); 543 noecho(); 544 keypad(stdscr, true); 545 } 546 547 static void 548 loop(void) 549 { 550 for (;;) { 551 redraw(); 552 if (!handle_key()) 553 break; 554 saturate_cursor(); 555 scroll_into_view(); 556 } 557 } 558 559 static void 560 clean_up(void) 561 { 562 endwin(); 563 564 stringarray_cleanup(&sollines); 565 stringarray_cleanup(&lines); 566 } 567 568 //////////////////////////////////////////////////////////// 569 570 int 571 main(void) 572 { 573 init(); 574 loop(); 575 clean_up(); 576 } 577