cgram.c revision 1.10
1/* $NetBSD: cgram.c,v 1.10 2021/02/21 20:33:42 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.10 2021/02/21 20:33: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_isupper(char ch) 77{ 78 return isupper((unsigned char)ch) != 0; 79} 80 81static int 82imax(int a, int b) 83{ 84 return a > b ? a : b; 85} 86 87static int 88imin(int a, int b) 89{ 90 return a < b ? a : b; 91} 92 93//////////////////////////////////////////////////////////// 94 95struct string { 96 char *s; 97 size_t len; 98 size_t cap; 99}; 100 101struct stringarray { 102 struct string *v; 103 size_t num; 104}; 105 106static void 107string_init(struct string *s) 108{ 109 s->s = NULL; 110 s->len = 0; 111 s->cap = 0; 112} 113 114static void 115string_add(struct string *s, char ch) 116{ 117 if (s->len >= s->cap) { 118 s->cap = 2 * s->cap + 16; 119 s->s = realloc(s->s, s->cap); 120 if (s->s == NULL) 121 errx(1, "Out of memory"); 122 } 123 s->s[s->len++] = ch; 124} 125 126static void 127string_finish(struct string *s) 128{ 129 string_add(s, '\0'); 130 s->len--; 131} 132 133static void 134stringarray_init(struct stringarray *a) 135{ 136 a->v = NULL; 137 a->num = 0; 138} 139 140static void 141stringarray_cleanup(struct stringarray *a) 142{ 143 for (size_t i = 0; i < a->num; i++) 144 free(a->v[i].s); 145 free(a->v); 146} 147 148static void 149stringarray_add(struct stringarray *a, struct string *s) 150{ 151 size_t num = a->num++; 152 a->v = realloc(a->v, a->num * sizeof a->v[0]); 153 if (a->v == NULL) 154 errx(1, "Out of memory"); 155 a->v[num] = *s; 156} 157 158static void 159stringarray_dup(struct stringarray *dst, const struct stringarray *src) 160{ 161 assert(dst->num == 0); 162 for (size_t i = 0; i < src->num; i++) { 163 struct string str; 164 string_init(&str); 165 for (const char *p = src->v[i].s; *p != '\0'; p++) 166 string_add(&str, *p); 167 string_finish(&str); 168 stringarray_add(dst, &str); 169 } 170} 171 172//////////////////////////////////////////////////////////// 173 174static struct stringarray lines; 175static struct stringarray sollines; 176static bool hinting; 177static int extent_x; 178static int extent_y; 179static int offset_x; 180static int offset_y; 181static int cursor_x; 182static int cursor_y; 183 184static int 185cur_max_x(void) 186{ 187 return (int)lines.v[cursor_y].len; 188} 189 190static int 191cur_max_y(void) 192{ 193 return extent_y - 1; 194} 195 196static void 197readquote(void) 198{ 199 FILE *f = popen(_PATH_FORTUNE, "r"); 200 if (f == NULL) 201 err(1, "%s", _PATH_FORTUNE); 202 203 struct string line; 204 string_init(&line); 205 206 int ch; 207 while ((ch = fgetc(f)) != EOF) { 208 if (ch == '\n') { 209 string_finish(&line); 210 stringarray_add(&lines, &line); 211 string_init(&line); 212 } else if (ch == '\t') { 213 string_add(&line, ' '); 214 while (line.len % 8 != 0) 215 string_add(&line, ' '); 216 } else if (ch == '\b') { 217 if (line.len > 0) 218 line.len--; 219 } else { 220 string_add(&line, (char)ch); 221 } 222 } 223 224 stringarray_dup(&sollines, &lines); 225 226 extent_y = (int)lines.num; 227 for (int i = 0; i < extent_y; i++) 228 extent_x = imax(extent_x, (int)lines.v[i].len); 229 230 pclose(f); 231} 232 233static void 234encode(void) 235{ 236 int key[26]; 237 238 for (int i = 0; i < 26; i++) 239 key[i] = i; 240 241 for (int i = 26; i > 1; i--) { 242 int c = (int)(random() % i); 243 int t = key[i - 1]; 244 key[i - 1] = key[c]; 245 key[c] = t; 246 } 247 248 for (int y = 0; y < extent_y; y++) { 249 for (char *p = lines.v[y].s; *p != '\0'; p++) { 250 if (ch_islower(*p)) 251 *p = (char)('a' + key[*p - 'a']); 252 if (ch_isupper(*p)) 253 *p = (char)('A' + key[*p - 'A']); 254 } 255 } 256} 257 258static bool 259substitute(char ch) 260{ 261 assert(cursor_x >= 0 && cursor_x < extent_x); 262 assert(cursor_y >= 0 && cursor_y < extent_y); 263 if (cursor_x >= cur_max_x()) { 264 beep(); 265 return false; 266 } 267 268 char och = lines.v[cursor_y].s[cursor_x]; 269 if (!ch_isalpha(och)) { 270 beep(); 271 return false; 272 } 273 274 char loch = ch_tolower(och); 275 char uoch = ch_toupper(och); 276 char lch = ch_tolower(ch); 277 char uch = ch_toupper(ch); 278 279 for (int y = 0; y < (int)lines.num; y++) { 280 for (char *p = lines.v[y].s; *p != '\0'; p++) { 281 if (*p == loch) 282 *p = lch; 283 else if (*p == uoch) 284 *p = uch; 285 else if (*p == lch) 286 *p = loch; 287 else if (*p == uch) 288 *p = uoch; 289 } 290 } 291 return true; 292} 293 294//////////////////////////////////////////////////////////// 295 296static bool 297is_solved(void) 298{ 299 for (size_t i = 0; i < lines.num; i++) 300 if (strcmp(lines.v[i].s, sollines.v[i].s) != 0) 301 return false; 302 return true; 303} 304 305static void 306redraw(void) 307{ 308 erase(); 309 310 int max_y = imin(LINES - 1, extent_y - offset_y); 311 for (int y = 0; y < max_y; y++) { 312 move(y, 0); 313 314 int len = (int)lines.v[offset_y + y].len; 315 int max_x = imin(COLS - 1, len - offset_x); 316 const char *line = lines.v[offset_y + y].s; 317 const char *solline = sollines.v[offset_y + y].s; 318 319 for (int x = 0; x < max_x; x++) { 320 char ch = line[offset_x + x]; 321 bool bold = hinting && 322 ch == solline[offset_x + x] && 323 ch_isalpha(ch); 324 325 if (bold) 326 attron(A_BOLD); 327 addch(ch); 328 if (bold) 329 attroff(A_BOLD); 330 } 331 clrtoeol(); 332 } 333 334 move(LINES - 1, 0); 335 if (is_solved()) 336 addstr("*solved* "); 337 addstr("~ to quit, * to cheat, ^pnfb to move"); 338 339 move(cursor_y - offset_y, cursor_x - offset_x); 340 341 refresh(); 342} 343 344static void 345opencurses(void) 346{ 347 initscr(); 348 cbreak(); 349 noecho(); 350 keypad(stdscr, true); 351} 352 353static void 354closecurses(void) 355{ 356 endwin(); 357} 358 359//////////////////////////////////////////////////////////// 360 361static void 362saturate_cursor(void) 363{ 364 assert(cursor_y >= 0); 365 assert(cursor_y <= cur_max_y()); 366 367 assert(cursor_x >= 0); 368 cursor_x = imin(cursor_x, cur_max_x()); 369} 370 371static void 372scroll_into_view(void) 373{ 374 if (cursor_x < offset_x) 375 offset_x = cursor_x; 376 if (cursor_x > offset_x + COLS - 1) 377 offset_x = cursor_x - (COLS - 1); 378 379 if (cursor_y < offset_y) 380 offset_y = cursor_y; 381 if (cursor_y > offset_y + LINES - 2) 382 offset_y = cursor_y - (LINES - 2); 383} 384 385static void 386handle_char_input(int ch) 387{ 388 if (isascii(ch) && ch_isalpha((char)ch)) { 389 if (substitute((char)ch)) { 390 if (cursor_x < cur_max_x()) 391 cursor_x++; 392 if (cursor_x == cur_max_x() && 393 cursor_y < cur_max_y()) { 394 cursor_x = 0; 395 cursor_y++; 396 } 397 } 398 } else if (cursor_x < cur_max_x() && 399 ch == lines.v[cursor_y].s[cursor_x]) { 400 cursor_x++; 401 if (cursor_x == cur_max_x() && 402 cursor_y < cur_max_y()) { 403 cursor_x = 0; 404 cursor_y++; 405 } 406 } else { 407 beep(); 408 } 409} 410 411static bool 412handle_key(void) 413{ 414 int ch = getch(); 415 416 switch (ch) { 417 case 1: /* ^A */ 418 case KEY_HOME: 419 cursor_x = 0; 420 break; 421 case 2: /* ^B */ 422 case KEY_LEFT: 423 if (cursor_x > 0) { 424 cursor_x--; 425 } else if (cursor_y > 0) { 426 cursor_y--; 427 cursor_x = cur_max_x(); 428 } 429 break; 430 case 5: /* ^E */ 431 case KEY_END: 432 cursor_x = cur_max_x(); 433 break; 434 case 6: /* ^F */ 435 case KEY_RIGHT: 436 if (cursor_x < cur_max_x()) { 437 cursor_x++; 438 } else if (cursor_y < cur_max_y()) { 439 cursor_y++; 440 cursor_x = 0; 441 } 442 break; 443 case 12: /* ^L */ 444 clear(); 445 break; 446 case 14: /* ^N */ 447 case KEY_DOWN: 448 if (cursor_y < cur_max_y()) 449 cursor_y++; 450 break; 451 case 16: /* ^P */ 452 case KEY_UP: 453 if (cursor_y > 0) 454 cursor_y--; 455 break; 456 case '*': 457 hinting = !hinting; 458 break; 459 case '~': 460 return false; 461 default: 462 handle_char_input(ch); 463 break; 464 } 465 return true; 466} 467 468static void 469init(void) 470{ 471 stringarray_init(&lines); 472 stringarray_init(&sollines); 473 srandom((unsigned int)time(NULL)); 474 readquote(); 475 encode(); 476 opencurses(); 477} 478 479static void 480loop(void) 481{ 482 for (;;) { 483 redraw(); 484 if (!handle_key()) 485 break; 486 saturate_cursor(); 487 scroll_into_view(); 488 } 489} 490 491static void 492clean_up(void) 493{ 494 closecurses(); 495 stringarray_cleanup(&sollines); 496 stringarray_cleanup(&lines); 497} 498 499//////////////////////////////////////////////////////////// 500 501int 502main(void) 503{ 504 init(); 505 loop(); 506 clean_up(); 507} 508