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