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