cgram.c revision 1.23
1/* $NetBSD: cgram.c,v 1.23 2021/05/01 20:29:23 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.23 2021/05/01 20:29:23 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_done(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] || !ch_isalpha(ch)); 352 353 if (bold) 354 attron(A_BOLD); 355 addch(ch); 356 if (bold) 357 attroff(A_BOLD); 358 } 359 clrtoeol(); 360 } 361 362 move(LINES - 1, 0); 363 addstr("~ to quit, * to cheat, ^pnfb to move"); 364 365 if (is_solved()) { 366 if (extent_y + 1 - offset_y < LINES - 2) 367 move(extent_y + 1 - offset_y, 0); 368 else 369 addch(' '); 370 attron(A_BOLD | A_STANDOUT); 371 addstr("*solved*"); 372 attroff(A_BOLD | A_STANDOUT); 373 } 374 375 move(cursor_y - offset_y, cursor_x - offset_x); 376 377 refresh(); 378} 379 380//////////////////////////////////////////////////////////// 381 382static void 383saturate_cursor(void) 384{ 385 cursor_y = imax(cursor_y, 0); 386 cursor_y = imin(cursor_y, cur_max_y()); 387 388 assert(cursor_x >= 0); 389 cursor_x = imin(cursor_x, cur_max_x()); 390} 391 392static void 393scroll_into_view(void) 394{ 395 if (cursor_x < offset_x) 396 offset_x = cursor_x; 397 if (cursor_x > offset_x + COLS - 1) 398 offset_x = cursor_x - (COLS - 1); 399 400 if (cursor_y < offset_y) 401 offset_y = cursor_y; 402 if (cursor_y > offset_y + LINES - 2) 403 offset_y = cursor_y - (LINES - 2); 404} 405 406static bool 407can_go_left(void) 408{ 409 return cursor_y > 0 || 410 (cursor_y == 0 && cursor_x > 0); 411} 412 413static bool 414can_go_right(void) 415{ 416 return cursor_y < cur_max_y() || 417 (cursor_y == cur_max_y() && cursor_x < cur_max_x()); 418} 419 420static void 421go_to_prev_line(void) 422{ 423 cursor_y--; 424 cursor_x = cur_max_x(); 425} 426 427static void 428go_to_next_line(void) 429{ 430 cursor_x = 0; 431 cursor_y++; 432} 433 434static void 435go_left(void) 436{ 437 if (cursor_x > 0) 438 cursor_x--; 439 else if (cursor_y > 0) 440 go_to_prev_line(); 441} 442 443static void 444go_right(void) 445{ 446 if (cursor_x < cur_max_x()) 447 cursor_x++; 448 else if (cursor_y < cur_max_y()) 449 go_to_next_line(); 450} 451 452static void 453go_to_prev_word(void) 454{ 455 while (can_go_left() && ch_isspace(char_left_of_cursor())) 456 go_left(); 457 458 while (can_go_left() && !ch_isspace(char_left_of_cursor())) 459 go_left(); 460} 461 462static void 463go_to_next_word(void) 464{ 465 while (can_go_right() && !ch_isspace(char_at_cursor())) 466 go_right(); 467 468 while (can_go_right() && ch_isspace(char_at_cursor())) 469 go_right(); 470} 471 472static bool 473can_substitute_here(int ch) 474{ 475 return isascii(ch) && 476 ch_isalpha((char)ch) && 477 cursor_x < cur_max_x() && 478 ch_isalpha(char_at_cursor()); 479} 480 481static void 482handle_char_input(int ch) 483{ 484 if (ch == char_at_cursor()) 485 go_right(); 486 else if (can_substitute_here(ch)) { 487 substitute(char_at_cursor(), (char)ch); 488 go_right(); 489 } else 490 beep(); 491} 492 493static bool 494handle_key(void) 495{ 496 int ch = getch(); 497 498 switch (ch) { 499 case 1: /* ^A */ 500 case KEY_HOME: 501 cursor_x = 0; 502 break; 503 case 2: /* ^B */ 504 case KEY_LEFT: 505 go_left(); 506 break; 507 case 5: /* ^E */ 508 case KEY_END: 509 cursor_x = cur_max_x(); 510 break; 511 case 6: /* ^F */ 512 case KEY_RIGHT: 513 go_right(); 514 break; 515 case '\t': 516 go_to_next_word(); 517 break; 518 case KEY_BTAB: 519 go_to_prev_word(); 520 break; 521 case '\n': 522 go_to_next_line(); 523 break; 524 case 12: /* ^L */ 525 clear(); 526 break; 527 case 14: /* ^N */ 528 case KEY_DOWN: 529 cursor_y++; 530 break; 531 case 16: /* ^P */ 532 case KEY_UP: 533 cursor_y--; 534 break; 535 case KEY_PPAGE: 536 cursor_y -= LINES - 2; 537 break; 538 case KEY_NPAGE: 539 cursor_y += LINES - 2; 540 break; 541 case '*': 542 hinting = !hinting; 543 break; 544 case '~': 545 return false; 546 case KEY_RESIZE: 547 break; 548 default: 549 handle_char_input(ch); 550 break; 551 } 552 return true; 553} 554 555static void 556init(const char *filename) 557{ 558 stringarray_init(&lines); 559 stringarray_init(&sollines); 560 srandom((unsigned int)time(NULL)); 561 if (filename != NULL) { 562 readfile(filename); 563 } else { 564 readquote(); 565 } 566 encode(); 567 568 initscr(); 569 cbreak(); 570 noecho(); 571 keypad(stdscr, true); 572} 573 574static void 575loop(void) 576{ 577 for (;;) { 578 redraw(); 579 if (!handle_key()) 580 break; 581 saturate_cursor(); 582 scroll_into_view(); 583 } 584} 585 586static void 587done(void) 588{ 589 endwin(); 590 591 stringarray_done(&sollines); 592 stringarray_done(&lines); 593} 594 595 596static void __dead 597usage(void) 598{ 599 600 fprintf(stderr, "usage: %s [file]\n", getprogname()); 601 exit(1); 602} 603 604int 605main(int argc, char *argv[]) 606{ 607 608 setprogname(argv[0]); 609 if (argc != 1 && argc != 2) 610 usage(); 611 612 init(argc > 1 ? argv[1] : NULL); 613 loop(); 614 done(); 615} 616