cgram.c revision 1.20
1/* $NetBSD: cgram.c,v 1.20 2021/04/25 20:19:19 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 <sys/cdefs.h> 33#if defined(__RCSID) && !defined(lint) 34__RCSID("$NetBSD: cgram.c,v 1.20 2021/04/25 20:19:19 rillig Exp $"); 35#endif 36 37#include <assert.h> 38#include <ctype.h> 39#include <curses.h> 40#include <err.h> 41#include <stdbool.h> 42#include <stdio.h> 43#include <stdlib.h> 44#include <string.h> 45#include <time.h> 46 47#include "pathnames.h" 48 49//////////////////////////////////////////////////////////// 50 51static char 52ch_toupper(char ch) 53{ 54 return (char)toupper((unsigned char)ch); 55} 56 57static char 58ch_tolower(char ch) 59{ 60 return (char)tolower((unsigned char)ch); 61} 62 63static bool 64ch_isalpha(char ch) 65{ 66 return isalpha((unsigned char)ch) != 0; 67} 68 69static bool 70ch_islower(char ch) 71{ 72 return islower((unsigned char)ch) != 0; 73} 74 75static bool 76ch_isspace(char ch) 77{ 78 return isspace((unsigned char)ch) != 0; 79} 80 81static bool 82ch_isupper(char ch) 83{ 84 return isupper((unsigned char)ch) != 0; 85} 86 87static int 88imax(int a, int b) 89{ 90 return a > b ? a : b; 91} 92 93static int 94imin(int a, int b) 95{ 96 return a < b ? a : b; 97} 98 99//////////////////////////////////////////////////////////// 100 101struct string { 102 char *s; 103 size_t len; 104 size_t cap; 105}; 106 107struct stringarray { 108 struct string *v; 109 size_t num; 110}; 111 112static void 113string_init(struct string *s) 114{ 115 s->s = NULL; 116 s->len = 0; 117 s->cap = 0; 118} 119 120static void 121string_add(struct string *s, char ch) 122{ 123 if (s->len >= s->cap) { 124 s->cap = 2 * s->cap + 16; 125 s->s = realloc(s->s, s->cap); 126 if (s->s == NULL) 127 errx(1, "Out of memory"); 128 } 129 s->s[s->len++] = ch; 130} 131 132static void 133string_finish(struct string *s) 134{ 135 string_add(s, '\0'); 136 s->len--; 137} 138 139static void 140stringarray_init(struct stringarray *a) 141{ 142 a->v = NULL; 143 a->num = 0; 144} 145 146static void 147stringarray_cleanup(struct stringarray *a) 148{ 149 for (size_t i = 0; i < a->num; i++) 150 free(a->v[i].s); 151 free(a->v); 152} 153 154static void 155stringarray_add(struct stringarray *a, struct string *s) 156{ 157 size_t num = a->num++; 158 a->v = realloc(a->v, a->num * sizeof a->v[0]); 159 if (a->v == NULL) 160 errx(1, "Out of memory"); 161 a->v[num] = *s; 162} 163 164static void 165stringarray_dup(struct stringarray *dst, const struct stringarray *src) 166{ 167 assert(dst->num == 0); 168 for (size_t i = 0; i < src->num; i++) { 169 struct string str; 170 string_init(&str); 171 for (const char *p = src->v[i].s; *p != '\0'; p++) 172 string_add(&str, *p); 173 string_finish(&str); 174 stringarray_add(dst, &str); 175 } 176} 177 178//////////////////////////////////////////////////////////// 179 180static struct stringarray lines; 181static struct stringarray sollines; 182static bool hinting; 183static int extent_x; 184static int extent_y; 185static int offset_x; 186static int offset_y; 187static int cursor_x; 188static int cursor_y; 189 190static int 191cur_max_x(void) 192{ 193 return (int)lines.v[cursor_y].len; 194} 195 196static int 197cur_max_y(void) 198{ 199 return extent_y - 1; 200} 201 202static char 203char_left_of_cursor(void) 204{ 205 if (cursor_x > 0) 206 return lines.v[cursor_y].s[cursor_x - 1]; 207 assert(cursor_y > 0); 208 return '\n'; /* eol of previous line */ 209} 210 211static char 212char_at_cursor(void) 213{ 214 if (cursor_x == cur_max_x()) 215 return '\n'; 216 return lines.v[cursor_y].s[cursor_x]; 217} 218 219static void 220getquote(FILE *f) 221{ 222 struct string line; 223 string_init(&line); 224 225 int ch; 226 while ((ch = fgetc(f)) != EOF) { 227 if (ch == '\n') { 228 string_finish(&line); 229 stringarray_add(&lines, &line); 230 string_init(&line); 231 } else if (ch == '\t') { 232 string_add(&line, ' '); 233 while (line.len % 8 != 0) 234 string_add(&line, ' '); 235 } else if (ch == '\b') { 236 if (line.len > 0) 237 line.len--; 238 } else { 239 string_add(&line, (char)ch); 240 } 241 } 242 243 stringarray_dup(&sollines, &lines); 244 245 extent_y = (int)lines.num; 246 for (int i = 0; i < extent_y; i++) 247 extent_x = imax(extent_x, (int)lines.v[i].len); 248} 249 250static void 251readfile(const char *name) 252{ 253 FILE *f = fopen(name, "r"); 254 if (f == NULL) 255 err(1, "%s", name); 256 257 getquote(f); 258 259 if (fclose(f) != 0) 260 err(1, "%s", name); 261} 262 263 264static void 265readquote(void) 266{ 267 FILE *f = popen(_PATH_FORTUNE, "r"); 268 if (f == NULL) 269 err(1, "%s", _PATH_FORTUNE); 270 271 getquote(f); 272 273 if (pclose(f) != 0) 274 exit(1); /* error message must come from child process */ 275} 276 277static void 278encode(void) 279{ 280 int key[26]; 281 282 for (int i = 0; i < 26; i++) 283 key[i] = i; 284 285 for (int i = 26; i > 1; i--) { 286 int c = (int)(random() % i); 287 int t = key[i - 1]; 288 key[i - 1] = key[c]; 289 key[c] = t; 290 } 291 292 for (int y = 0; y < extent_y; y++) { 293 for (char *p = lines.v[y].s; *p != '\0'; p++) { 294 if (ch_islower(*p)) 295 *p = (char)('a' + key[*p - 'a']); 296 if (ch_isupper(*p)) 297 *p = (char)('A' + key[*p - 'A']); 298 } 299 } 300} 301 302static void 303substitute(char a, char b) 304{ 305 char la = ch_tolower(a); 306 char ua = ch_toupper(a); 307 char lb = ch_tolower(b); 308 char ub = ch_toupper(b); 309 310 for (int y = 0; y < (int)lines.num; y++) { 311 for (char *p = lines.v[y].s; *p != '\0'; p++) { 312 if (*p == la) 313 *p = lb; 314 else if (*p == ua) 315 *p = ub; 316 else if (*p == lb) 317 *p = la; 318 else if (*p == ub) 319 *p = ua; 320 } 321 } 322} 323 324static bool 325is_solved(void) 326{ 327 for (size_t i = 0; i < lines.num; i++) 328 if (strcmp(lines.v[i].s, sollines.v[i].s) != 0) 329 return false; 330 return true; 331} 332 333//////////////////////////////////////////////////////////// 334 335static void 336redraw(void) 337{ 338 erase(); 339 340 int max_y = imin(LINES - 1, extent_y - offset_y); 341 for (int y = 0; y < max_y; y++) { 342 move(y, 0); 343 344 int len = (int)lines.v[offset_y + y].len; 345 int max_x = imin(COLS - 1, len - offset_x); 346 const char *line = lines.v[offset_y + y].s; 347 const char *solline = sollines.v[offset_y + y].s; 348 349 for (int x = 0; x < max_x; x++) { 350 char ch = line[offset_x + x]; 351 bool bold = hinting && 352 ch == solline[offset_x + x] && 353 ch_isalpha(ch); 354 355 if (bold) 356 attron(A_BOLD); 357 addch(ch); 358 if (bold) 359 attroff(A_BOLD); 360 } 361 clrtoeol(); 362 } 363 364 move(LINES - 1, 0); 365 addstr("~ to quit, * to cheat, ^pnfb to move"); 366 367 if (is_solved()) { 368 if (extent_y + 1 - offset_y < LINES - 2) 369 move(extent_y + 1 - offset_y, 0); 370 else 371 addch(' '); 372 attron(A_BOLD | A_STANDOUT); 373 addstr("*solved*"); 374 attroff(A_BOLD | A_STANDOUT); 375 } 376 377 move(cursor_y - offset_y, cursor_x - offset_x); 378 379 refresh(); 380} 381 382//////////////////////////////////////////////////////////// 383 384static void 385saturate_cursor(void) 386{ 387 cursor_y = imax(cursor_y, 0); 388 cursor_y = imin(cursor_y, cur_max_y()); 389 390 assert(cursor_x >= 0); 391 cursor_x = imin(cursor_x, cur_max_x()); 392} 393 394static void 395scroll_into_view(void) 396{ 397 if (cursor_x < offset_x) 398 offset_x = cursor_x; 399 if (cursor_x > offset_x + COLS - 1) 400 offset_x = cursor_x - (COLS - 1); 401 402 if (cursor_y < offset_y) 403 offset_y = cursor_y; 404 if (cursor_y > offset_y + LINES - 2) 405 offset_y = cursor_y - (LINES - 2); 406} 407 408static bool 409can_go_left(void) 410{ 411 return cursor_y > 0 || 412 (cursor_y == 0 && cursor_x > 0); 413} 414 415static bool 416can_go_right(void) 417{ 418 return cursor_y < cur_max_y() || 419 (cursor_y == cur_max_y() && cursor_x < cur_max_x()); 420} 421 422static void 423go_to_prev_line(void) 424{ 425 cursor_y--; 426 cursor_x = cur_max_x(); 427} 428 429static void 430go_to_next_line(void) 431{ 432 cursor_x = 0; 433 cursor_y++; 434} 435 436static void 437go_left(void) 438{ 439 if (cursor_x > 0) 440 cursor_x--; 441 else if (cursor_y > 0) 442 go_to_prev_line(); 443} 444 445static void 446go_right(void) 447{ 448 if (cursor_x < cur_max_x()) 449 cursor_x++; 450 else if (cursor_y < cur_max_y()) 451 go_to_next_line(); 452} 453 454static void 455go_to_prev_word(void) 456{ 457 while (can_go_left() && ch_isspace(char_left_of_cursor())) 458 go_left(); 459 460 while (can_go_left() && !ch_isspace(char_left_of_cursor())) 461 go_left(); 462} 463 464static void 465go_to_next_word(void) 466{ 467 while (can_go_right() && !ch_isspace(char_at_cursor())) 468 go_right(); 469 470 while (can_go_right() && ch_isspace(char_at_cursor())) 471 go_right(); 472} 473 474static bool 475can_substitute_here(int ch) 476{ 477 return isascii(ch) && 478 ch_isalpha((char)ch) && 479 cursor_x < cur_max_x() && 480 ch_isalpha(char_at_cursor()); 481} 482 483static void 484handle_char_input(int ch) 485{ 486 if (ch == char_at_cursor()) 487 go_right(); 488 else if (can_substitute_here(ch)) { 489 substitute(char_at_cursor(), (char)ch); 490 go_right(); 491 } else 492 beep(); 493} 494 495static bool 496handle_key(void) 497{ 498 int ch = getch(); 499 500 switch (ch) { 501 case 1: /* ^A */ 502 case KEY_HOME: 503 cursor_x = 0; 504 break; 505 case 2: /* ^B */ 506 case KEY_LEFT: 507 go_left(); 508 break; 509 case 5: /* ^E */ 510 case KEY_END: 511 cursor_x = cur_max_x(); 512 break; 513 case 6: /* ^F */ 514 case KEY_RIGHT: 515 go_right(); 516 break; 517 case '\t': 518 go_to_next_word(); 519 break; 520 case KEY_BTAB: 521 go_to_prev_word(); 522 break; 523 case '\n': 524 go_to_next_line(); 525 break; 526 case 12: /* ^L */ 527 clear(); 528 break; 529 case 14: /* ^N */ 530 case KEY_DOWN: 531 cursor_y++; 532 break; 533 case 16: /* ^P */ 534 case KEY_UP: 535 cursor_y--; 536 break; 537 case KEY_PPAGE: 538 cursor_y -= LINES - 2; 539 break; 540 case KEY_NPAGE: 541 cursor_y += LINES - 2; 542 break; 543 case '*': 544 hinting = !hinting; 545 break; 546 case '~': 547 return false; 548 case KEY_RESIZE: 549 break; 550 default: 551 handle_char_input(ch); 552 break; 553 } 554 return true; 555} 556 557static void 558init(const char *filename) 559{ 560 stringarray_init(&lines); 561 stringarray_init(&sollines); 562 srandom((unsigned int)time(NULL)); 563 if (filename != NULL) { 564 readfile(filename); 565 } else { 566 readquote(); 567 } 568 encode(); 569 570 initscr(); 571 cbreak(); 572 noecho(); 573 keypad(stdscr, true); 574} 575 576static void 577loop(void) 578{ 579 for (;;) { 580 redraw(); 581 if (!handle_key()) 582 break; 583 saturate_cursor(); 584 scroll_into_view(); 585 } 586} 587 588static void 589clean_up(void) 590{ 591 endwin(); 592 593 stringarray_cleanup(&sollines); 594 stringarray_cleanup(&lines); 595} 596 597 598static void __dead 599usage(void) 600{ 601 602 fprintf(stderr, "usage: %s [file]\n", getprogname()); 603 exit(1); 604} 605 606int 607main(int argc, char *argv[]) 608{ 609 610 setprogname(argv[0]); 611 if (argc != 1 && argc != 2) 612 usage(); 613 614 init(argc > 1 ? argv[1] : NULL); 615 loop(); 616 clean_up(); 617} 618