cgram.c revision 1.15
1/* $NetBSD: cgram.c,v 1.15 2021/02/26 14:55:37 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.15 2021/02/26 14:55:37 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 void 283substitute(char a, char b) 284{ 285 char la = ch_tolower(a); 286 char ua = ch_toupper(a); 287 char lb = ch_tolower(b); 288 char ub = ch_toupper(b); 289 290 for (int y = 0; y < (int)lines.num; y++) { 291 for (char *p = lines.v[y].s; *p != '\0'; p++) { 292 if (*p == la) 293 *p = lb; 294 else if (*p == ua) 295 *p = ub; 296 else if (*p == lb) 297 *p = la; 298 else if (*p == ub) 299 *p = ua; 300 } 301 } 302} 303 304static bool 305is_solved(void) 306{ 307 for (size_t i = 0; i < lines.num; i++) 308 if (strcmp(lines.v[i].s, sollines.v[i].s) != 0) 309 return false; 310 return true; 311} 312 313//////////////////////////////////////////////////////////// 314 315static void 316redraw(void) 317{ 318 erase(); 319 320 int max_y = imin(LINES - 1, extent_y - offset_y); 321 for (int y = 0; y < max_y; y++) { 322 move(y, 0); 323 324 int len = (int)lines.v[offset_y + y].len; 325 int max_x = imin(COLS - 1, len - offset_x); 326 const char *line = lines.v[offset_y + y].s; 327 const char *solline = sollines.v[offset_y + y].s; 328 329 for (int x = 0; x < max_x; x++) { 330 char ch = line[offset_x + x]; 331 bool bold = hinting && 332 ch == solline[offset_x + x] && 333 ch_isalpha(ch); 334 335 if (bold) 336 attron(A_BOLD); 337 addch(ch); 338 if (bold) 339 attroff(A_BOLD); 340 } 341 clrtoeol(); 342 } 343 344 move(LINES - 1, 0); 345 if (is_solved()) { 346 attron(A_BOLD | A_STANDOUT); 347 addstr("*solved*"); 348 attroff(A_BOLD | A_STANDOUT); 349 addch(' '); 350 } 351 addstr("~ to quit, * to cheat, ^pnfb to move"); 352 353 move(cursor_y - offset_y, cursor_x - offset_x); 354 355 refresh(); 356} 357 358//////////////////////////////////////////////////////////// 359 360static void 361saturate_cursor(void) 362{ 363 cursor_y = imax(cursor_y, 0); 364 cursor_y = imin(cursor_y, cur_max_y()); 365 366 assert(cursor_x >= 0); 367 cursor_x = imin(cursor_x, cur_max_x()); 368} 369 370static void 371scroll_into_view(void) 372{ 373 if (cursor_x < offset_x) 374 offset_x = cursor_x; 375 if (cursor_x > offset_x + COLS - 1) 376 offset_x = cursor_x - (COLS - 1); 377 378 if (cursor_y < offset_y) 379 offset_y = cursor_y; 380 if (cursor_y > offset_y + LINES - 2) 381 offset_y = cursor_y - (LINES - 2); 382} 383 384static bool 385can_go_left(void) 386{ 387 return cursor_y > 0 || 388 (cursor_y == 0 && cursor_x > 0); 389} 390 391static bool 392can_go_right(void) 393{ 394 return cursor_y < cur_max_y() || 395 (cursor_y == cur_max_y() && cursor_x < cur_max_x()); 396} 397 398static void 399go_to_prev_line(void) 400{ 401 cursor_y--; 402 cursor_x = cur_max_x(); 403} 404 405static void 406go_to_next_line(void) 407{ 408 cursor_x = 0; 409 cursor_y++; 410} 411 412static void 413go_left(void) 414{ 415 if (cursor_x > 0) 416 cursor_x--; 417 else if (cursor_y > 0) 418 go_to_prev_line(); 419} 420 421static void 422go_right(void) 423{ 424 if (cursor_x < cur_max_x()) 425 cursor_x++; 426 else if (cursor_y < cur_max_y()) 427 go_to_next_line(); 428} 429 430static void 431go_to_prev_word(void) 432{ 433 while (can_go_left() && ch_isspace(char_left_of_cursor())) 434 go_left(); 435 436 while (can_go_left() && !ch_isspace(char_left_of_cursor())) 437 go_left(); 438} 439 440static void 441go_to_next_word(void) 442{ 443 while (can_go_right() && !ch_isspace(char_at_cursor())) 444 go_right(); 445 446 while (can_go_right() && ch_isspace(char_at_cursor())) 447 go_right(); 448} 449 450static bool 451can_substitute_here(int ch) 452{ 453 return isascii(ch) && 454 ch_isalpha((char)ch) && 455 cursor_x < cur_max_x() && 456 ch_isalpha(char_at_cursor()); 457} 458 459static void 460handle_char_input(int ch) 461{ 462 if (ch == char_at_cursor()) 463 go_right(); 464 else if (can_substitute_here(ch)) { 465 substitute(char_at_cursor(), (char)ch); 466 go_right(); 467 } else 468 beep(); 469} 470 471static bool 472handle_key(void) 473{ 474 int ch = getch(); 475 476 switch (ch) { 477 case 1: /* ^A */ 478 case KEY_HOME: 479 cursor_x = 0; 480 break; 481 case 2: /* ^B */ 482 case KEY_LEFT: 483 go_left(); 484 break; 485 case 5: /* ^E */ 486 case KEY_END: 487 cursor_x = cur_max_x(); 488 break; 489 case 6: /* ^F */ 490 case KEY_RIGHT: 491 go_right(); 492 break; 493 case '\t': 494 go_to_next_word(); 495 break; 496 case KEY_BTAB: 497 go_to_prev_word(); 498 break; 499 case '\n': 500 go_to_next_line(); 501 break; 502 case 12: /* ^L */ 503 clear(); 504 break; 505 case 14: /* ^N */ 506 case KEY_DOWN: 507 cursor_y++; 508 break; 509 case 16: /* ^P */ 510 case KEY_UP: 511 cursor_y--; 512 break; 513 case KEY_PPAGE: 514 cursor_y -= LINES - 2; 515 break; 516 case KEY_NPAGE: 517 cursor_y += LINES - 2; 518 break; 519 case '*': 520 hinting = !hinting; 521 break; 522 case '~': 523 return false; 524 default: 525 handle_char_input(ch); 526 break; 527 } 528 return true; 529} 530 531static void 532init(void) 533{ 534 stringarray_init(&lines); 535 stringarray_init(&sollines); 536 srandom((unsigned int)time(NULL)); 537 readquote(); 538 encode(); 539 540 initscr(); 541 cbreak(); 542 noecho(); 543 keypad(stdscr, true); 544} 545 546static void 547loop(void) 548{ 549 for (;;) { 550 redraw(); 551 if (!handle_key()) 552 break; 553 saturate_cursor(); 554 scroll_into_view(); 555 } 556} 557 558static void 559clean_up(void) 560{ 561 endwin(); 562 563 stringarray_cleanup(&sollines); 564 stringarray_cleanup(&lines); 565} 566 567//////////////////////////////////////////////////////////// 568 569int 570main(void) 571{ 572 init(); 573 loop(); 574 clean_up(); 575} 576