cgram.c revision 1.13
1101172Srwatson/* $NetBSD: cgram.c,v 1.13 2021/02/22 17:36:42 rillig Exp $ */ 2101172Srwatson 3101172Srwatson/*- 4101172Srwatson * Copyright (c) 2013, 2021 The NetBSD Foundation, Inc. 5101172Srwatson * All rights reserved. 6101172Srwatson * 7101172Srwatson * This code is derived from software contributed to The NetBSD Foundation 8101172Srwatson * by David A. Holland and Roland Illig. 9101172Srwatson * 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.13 2021/02/22 17:36:42 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 220readquote(void) 221{ 222 FILE *f = popen(_PATH_FORTUNE, "r"); 223 if (f == NULL) 224 err(1, "%s", _PATH_FORTUNE); 225 226 struct string line; 227 string_init(&line); 228 229 int ch; 230 while ((ch = fgetc(f)) != EOF) { 231 if (ch == '\n') { 232 string_finish(&line); 233 stringarray_add(&lines, &line); 234 string_init(&line); 235 } else if (ch == '\t') { 236 string_add(&line, ' '); 237 while (line.len % 8 != 0) 238 string_add(&line, ' '); 239 } else if (ch == '\b') { 240 if (line.len > 0) 241 line.len--; 242 } else { 243 string_add(&line, (char)ch); 244 } 245 } 246 247 stringarray_dup(&sollines, &lines); 248 249 extent_y = (int)lines.num; 250 for (int i = 0; i < extent_y; i++) 251 extent_x = imax(extent_x, (int)lines.v[i].len); 252 253 if (pclose(f) != 0) 254 exit(1); /* error message must come from child process */ 255} 256 257static void 258encode(void) 259{ 260 int key[26]; 261 262 for (int i = 0; i < 26; i++) 263 key[i] = i; 264 265 for (int i = 26; i > 1; i--) { 266 int c = (int)(random() % i); 267 int t = key[i - 1]; 268 key[i - 1] = key[c]; 269 key[c] = t; 270 } 271 272 for (int y = 0; y < extent_y; y++) { 273 for (char *p = lines.v[y].s; *p != '\0'; p++) { 274 if (ch_islower(*p)) 275 *p = (char)('a' + key[*p - 'a']); 276 if (ch_isupper(*p)) 277 *p = (char)('A' + key[*p - 'A']); 278 } 279 } 280} 281 282static bool 283substitute(char ch) 284{ 285 assert(cursor_x >= 0 && cursor_x < extent_x); 286 assert(cursor_y >= 0 && cursor_y < extent_y); 287 if (cursor_x >= cur_max_x()) { 288 beep(); 289 return false; 290 } 291 292 char och = char_at_cursor(); 293 if (!ch_isalpha(och)) { 294 beep(); 295 return false; 296 } 297 298 char loch = ch_tolower(och); 299 char uoch = ch_toupper(och); 300 char lch = ch_tolower(ch); 301 char uch = ch_toupper(ch); 302 303 for (int y = 0; y < (int)lines.num; y++) { 304 for (char *p = lines.v[y].s; *p != '\0'; p++) { 305 if (*p == loch) 306 *p = lch; 307 else if (*p == uoch) 308 *p = uch; 309 else if (*p == lch) 310 *p = loch; 311 else if (*p == uch) 312 *p = uoch; 313 } 314 } 315 return true; 316} 317 318static bool 319is_solved(void) 320{ 321 for (size_t i = 0; i < lines.num; i++) 322 if (strcmp(lines.v[i].s, sollines.v[i].s) != 0) 323 return false; 324 return true; 325} 326 327//////////////////////////////////////////////////////////// 328 329static void 330redraw(void) 331{ 332 erase(); 333 334 int max_y = imin(LINES - 1, extent_y - offset_y); 335 for (int y = 0; y < max_y; y++) { 336 move(y, 0); 337 338 int len = (int)lines.v[offset_y + y].len; 339 int max_x = imin(COLS - 1, len - offset_x); 340 const char *line = lines.v[offset_y + y].s; 341 const char *solline = sollines.v[offset_y + y].s; 342 343 for (int x = 0; x < max_x; x++) { 344 char ch = line[offset_x + x]; 345 bool bold = hinting && 346 ch == solline[offset_x + x] && 347 ch_isalpha(ch); 348 349 if (bold) 350 attron(A_BOLD); 351 addch(ch); 352 if (bold) 353 attroff(A_BOLD); 354 } 355 clrtoeol(); 356 } 357 358 move(LINES - 1, 0); 359 if (is_solved()) 360 addstr("*solved* "); 361 addstr("~ to quit, * to cheat, ^pnfb to move"); 362 363 move(cursor_y - offset_y, cursor_x - offset_x); 364 365 refresh(); 366} 367 368//////////////////////////////////////////////////////////// 369 370static void 371saturate_cursor(void) 372{ 373 cursor_y = imax(cursor_y, 0); 374 cursor_y = imin(cursor_y, cur_max_y()); 375 376 assert(cursor_x >= 0); 377 cursor_x = imin(cursor_x, cur_max_x()); 378} 379 380static void 381scroll_into_view(void) 382{ 383 if (cursor_x < offset_x) 384 offset_x = cursor_x; 385 if (cursor_x > offset_x + COLS - 1) 386 offset_x = cursor_x - (COLS - 1); 387 388 if (cursor_y < offset_y) 389 offset_y = cursor_y; 390 if (cursor_y > offset_y + LINES - 2) 391 offset_y = cursor_y - (LINES - 2); 392} 393 394static bool 395can_go_left(void) 396{ 397 return cursor_y > 0 || 398 (cursor_y == 0 && cursor_x > 0); 399} 400 401static bool 402can_go_right(void) 403{ 404 return cursor_y < cur_max_y() || 405 (cursor_y == cur_max_y() && cursor_x < cur_max_x()); 406} 407 408static void 409go_to_prev_line(void) 410{ 411 cursor_y--; 412 cursor_x = cur_max_x(); 413} 414 415static void 416go_to_next_line(void) 417{ 418 cursor_x = 0; 419 cursor_y++; 420} 421 422static void 423go_left(void) 424{ 425 if (cursor_x > 0) 426 cursor_x--; 427 else if (cursor_y > 0) 428 go_to_prev_line(); 429} 430 431static void 432go_right(void) 433{ 434 if (cursor_x < cur_max_x()) 435 cursor_x++; 436 else if (cursor_y < cur_max_y()) 437 go_to_next_line(); 438} 439 440static void 441go_to_prev_word(void) 442{ 443 while (can_go_left() && ch_isspace(char_left_of_cursor())) 444 go_left(); 445 446 while (can_go_left() && !ch_isspace(char_left_of_cursor())) 447 go_left(); 448} 449 450static void 451go_to_next_word(void) 452{ 453 while (can_go_right() && !ch_isspace(char_at_cursor())) 454 go_right(); 455 456 while (can_go_right() && ch_isspace(char_at_cursor())) 457 go_right(); 458} 459 460static void 461handle_char_input(int ch) 462{ 463 if (isascii(ch) && ch_isalpha((char)ch)) { 464 if (substitute((char)ch)) { 465 if (cursor_x < cur_max_x()) 466 cursor_x++; 467 if (cursor_x == cur_max_x()) 468 go_to_next_line(); 469 } 470 } else if (ch == char_at_cursor()) 471 go_right(); 472 else 473 beep(); 474} 475 476static bool 477handle_key(void) 478{ 479 int ch = getch(); 480 481 switch (ch) { 482 case 1: /* ^A */ 483 case KEY_HOME: 484 cursor_x = 0; 485 break; 486 case 2: /* ^B */ 487 case KEY_LEFT: 488 go_left(); 489 break; 490 case 5: /* ^E */ 491 case KEY_END: 492 cursor_x = cur_max_x(); 493 break; 494 case 6: /* ^F */ 495 case KEY_RIGHT: 496 go_right(); 497 break; 498 case '\t': 499 go_to_next_word(); 500 break; 501 case KEY_BTAB: 502 go_to_prev_word(); 503 break; 504 case '\n': 505 go_to_next_line(); 506 break; 507 case 12: /* ^L */ 508 clear(); 509 break; 510 case 14: /* ^N */ 511 case KEY_DOWN: 512 cursor_y++; 513 break; 514 case 16: /* ^P */ 515 case KEY_UP: 516 cursor_y--; 517 break; 518 case KEY_PPAGE: 519 cursor_y -= LINES - 2; 520 break; 521 case KEY_NPAGE: 522 cursor_y += LINES - 2; 523 break; 524 case '*': 525 hinting = !hinting; 526 break; 527 case '~': 528 return false; 529 default: 530 handle_char_input(ch); 531 break; 532 } 533 return true; 534} 535 536static void 537init(void) 538{ 539 stringarray_init(&lines); 540 stringarray_init(&sollines); 541 srandom((unsigned int)time(NULL)); 542 readquote(); 543 encode(); 544 545 initscr(); 546 cbreak(); 547 noecho(); 548 keypad(stdscr, true); 549} 550 551static void 552loop(void) 553{ 554 for (;;) { 555 redraw(); 556 if (!handle_key()) 557 break; 558 saturate_cursor(); 559 scroll_into_view(); 560 } 561} 562 563static void 564clean_up(void) 565{ 566 endwin(); 567 568 stringarray_cleanup(&sollines); 569 stringarray_cleanup(&lines); 570} 571 572//////////////////////////////////////////////////////////// 573 574int 575main(void) 576{ 577 init(); 578 loop(); 579 clean_up(); 580} 581