cgram.c revision 1.29 1 /* $NetBSD: cgram.c,v 1.29 2022/06/12 14:59:44 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 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.29 2022/06/12 14:59:44 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 static bool
51 ch_islower(char ch)
52 {
53 return ch >= 'a' && ch <= 'z';
54 }
55
56 static bool
57 ch_isupper(char ch)
58 {
59 return ch >= 'A' && ch <= 'Z';
60 }
61
62 static bool
63 ch_isalpha(char ch)
64 {
65 return ch_islower(ch) || ch_isupper(ch);
66 }
67
68 static char
69 ch_toupper(char ch)
70 {
71 return ch_islower(ch) ? (char)(ch - 'a' + 'A') : ch;
72 }
73
74 static char
75 ch_tolower(char ch)
76 {
77 return ch_isupper(ch) ? (char)(ch - 'A' + 'a') : ch;
78 }
79
80 static int
81 imax(int a, int b)
82 {
83 return a > b ? a : b;
84 }
85
86 static int
87 imin(int a, int b)
88 {
89 return a < b ? a : b;
90 }
91
92 ////////////////////////////////////////////////////////////
93
94 struct string {
95 char *s;
96 size_t len;
97 size_t cap;
98 };
99
100 struct stringarray {
101 struct string *v;
102 size_t num;
103 };
104
105 static void
106 string_init(struct string *s)
107 {
108 s->s = NULL;
109 s->len = 0;
110 s->cap = 0;
111 }
112
113 static void
114 string_add(struct string *s, char ch)
115 {
116 if (s->len >= s->cap) {
117 s->cap = 2 * s->cap + 16;
118 s->s = realloc(s->s, s->cap);
119 if (s->s == NULL)
120 errx(1, "Out of memory");
121 }
122 s->s[s->len++] = ch;
123 }
124
125 static void
126 string_finish(struct string *s)
127 {
128 string_add(s, '\0');
129 s->len--;
130 }
131
132 static void
133 stringarray_init(struct stringarray *a)
134 {
135 a->v = NULL;
136 a->num = 0;
137 }
138
139 static void
140 stringarray_done(struct stringarray *a)
141 {
142 for (size_t i = 0; i < a->num; i++)
143 free(a->v[i].s);
144 free(a->v);
145 }
146
147 static void
148 stringarray_add(struct stringarray *a, struct string *s)
149 {
150 size_t num = a->num++;
151 if (reallocarr(&a->v, a->num, sizeof(a->v[0])) != 0)
152 errx(1, "Out of memory");
153 a->v[num] = *s;
154 }
155
156 static void
157 stringarray_dup(struct stringarray *dst, const struct stringarray *src)
158 {
159 assert(dst->num == 0);
160 for (size_t i = 0; i < src->num; i++) {
161 struct string str;
162 string_init(&str);
163 for (const char *p = src->v[i].s; *p != '\0'; p++)
164 string_add(&str, *p);
165 string_finish(&str);
166 stringarray_add(dst, &str);
167 }
168 }
169
170 ////////////////////////////////////////////////////////////
171
172 static struct stringarray lines;
173 static struct stringarray sollines;
174 static bool hinting;
175 static int extent_x;
176 static int extent_y;
177 static int offset_x;
178 static int offset_y;
179 static int cursor_x;
180 static int cursor_y;
181
182 static int
183 cur_max_x(void)
184 {
185 return (int)lines.v[cursor_y].len;
186 }
187
188 static int
189 cur_max_y(void)
190 {
191 return extent_y - 1;
192 }
193
194 static char
195 char_left_of_cursor(void)
196 {
197 if (cursor_x > 0)
198 return lines.v[cursor_y].s[cursor_x - 1];
199 assert(cursor_y > 0);
200 return '\n'; /* eol of previous line */
201 }
202
203 static char
204 char_at_cursor(void)
205 {
206 if (cursor_x == cur_max_x())
207 return '\n';
208 return lines.v[cursor_y].s[cursor_x];
209 }
210
211 static void
212 getquote(FILE *f)
213 {
214 struct string line;
215 string_init(&line);
216
217 int ch;
218 while ((ch = fgetc(f)) != EOF) {
219 if (ch == '\n') {
220 string_finish(&line);
221 stringarray_add(&lines, &line);
222 string_init(&line);
223 } else if (ch == '\t') {
224 string_add(&line, ' ');
225 while (line.len % 8 != 0)
226 string_add(&line, ' ');
227 } else if (ch == '\b') {
228 if (line.len > 0)
229 line.len--;
230 } else {
231 string_add(&line, (char)ch);
232 }
233 }
234
235 stringarray_dup(&sollines, &lines);
236
237 extent_y = (int)lines.num;
238 for (int i = 0; i < extent_y; i++)
239 extent_x = imax(extent_x, (int)lines.v[i].len);
240 }
241
242 static void
243 readfile(const char *name)
244 {
245 FILE *f = fopen(name, "r");
246 if (f == NULL)
247 err(1, "%s", name);
248
249 getquote(f);
250
251 if (fclose(f) != 0)
252 err(1, "%s", name);
253 }
254
255
256 static void
257 readquote(void)
258 {
259 FILE *f = popen(_PATH_FORTUNE, "r");
260 if (f == NULL)
261 err(1, "%s", _PATH_FORTUNE);
262
263 getquote(f);
264
265 if (pclose(f) != 0)
266 exit(1); /* error message must come from child process */
267 }
268
269 static void
270 encode(void)
271 {
272 int key[26];
273
274 for (int i = 0; i < 26; i++)
275 key[i] = i;
276
277 for (int i = 26; i > 1; i--) {
278 int c = (int)(random() % i);
279 int t = key[i - 1];
280 key[i - 1] = key[c];
281 key[c] = t;
282 }
283
284 for (int y = 0; y < extent_y; y++) {
285 for (char *p = lines.v[y].s; *p != '\0'; p++) {
286 if (ch_islower(*p))
287 *p = (char)('a' + key[*p - 'a']);
288 if (ch_isupper(*p))
289 *p = (char)('A' + key[*p - 'A']);
290 }
291 }
292 }
293
294 static void
295 substitute(char a, char b)
296 {
297 char la = ch_tolower(a);
298 char ua = ch_toupper(a);
299 char lb = ch_tolower(b);
300 char ub = ch_toupper(b);
301
302 for (int y = 0; y < (int)lines.num; y++) {
303 for (char *p = lines.v[y].s; *p != '\0'; p++) {
304 if (*p == la)
305 *p = lb;
306 else if (*p == ua)
307 *p = ub;
308 else if (*p == lb)
309 *p = la;
310 else if (*p == ub)
311 *p = ua;
312 }
313 }
314 }
315
316 static bool
317 is_solved(void)
318 {
319 for (size_t i = 0; i < lines.num; i++)
320 if (strcmp(lines.v[i].s, sollines.v[i].s) != 0)
321 return false;
322 return true;
323 }
324
325 ////////////////////////////////////////////////////////////
326
327 static void
328 redraw(void)
329 {
330 erase();
331
332 int max_y = imin(LINES - 1, extent_y - offset_y);
333 for (int y = 0; y < max_y; y++) {
334 move(y, 0);
335
336 int len = (int)lines.v[offset_y + y].len;
337 int max_x = imin(COLS - 1, len - offset_x);
338 const char *line = lines.v[offset_y + y].s;
339 const char *solline = sollines.v[offset_y + y].s;
340
341 for (int x = 0; x < max_x; x++) {
342 char ch = line[offset_x + x];
343 bool bold = hinting &&
344 (ch == solline[offset_x + x] || !ch_isalpha(ch));
345
346 if (bold)
347 attron(A_BOLD);
348 addch(ch);
349 if (bold)
350 attroff(A_BOLD);
351 }
352 clrtoeol();
353 }
354
355 move(LINES - 1, 0);
356 addstr("~ to quit, * to cheat, ^pnfb to move");
357
358 if (is_solved()) {
359 if (extent_y + 1 - offset_y < LINES - 2)
360 move(extent_y + 1 - offset_y, 0);
361 else
362 addch(' ');
363 attron(A_BOLD | A_STANDOUT);
364 addstr("*solved*");
365 attroff(A_BOLD | A_STANDOUT);
366 }
367
368 move(cursor_y - offset_y, cursor_x - offset_x);
369
370 refresh();
371 }
372
373 ////////////////////////////////////////////////////////////
374
375 static void
376 saturate_cursor(void)
377 {
378 cursor_y = imax(cursor_y, 0);
379 cursor_y = imin(cursor_y, cur_max_y());
380
381 assert(cursor_x >= 0);
382 cursor_x = imin(cursor_x, cur_max_x());
383 }
384
385 static void
386 scroll_into_view(void)
387 {
388 if (cursor_x < offset_x)
389 offset_x = cursor_x;
390 if (cursor_x > offset_x + COLS - 1)
391 offset_x = cursor_x - (COLS - 1);
392
393 if (cursor_y < offset_y)
394 offset_y = cursor_y;
395 if (cursor_y > offset_y + LINES - 2)
396 offset_y = cursor_y - (LINES - 2);
397 }
398
399 static bool
400 can_go_left(void)
401 {
402 return cursor_y > 0 ||
403 (cursor_y == 0 && cursor_x > 0);
404 }
405
406 static bool
407 can_go_right(void)
408 {
409 return cursor_y < cur_max_y() ||
410 (cursor_y == cur_max_y() && cursor_x < cur_max_x());
411 }
412
413 static void
414 go_to_prev_line(void)
415 {
416 cursor_y--;
417 cursor_x = cur_max_x();
418 }
419
420 static void
421 go_to_next_line(void)
422 {
423 cursor_x = 0;
424 cursor_y++;
425 }
426
427 static void
428 go_left(void)
429 {
430 if (cursor_x > 0)
431 cursor_x--;
432 else if (cursor_y > 0)
433 go_to_prev_line();
434 }
435
436 static void
437 go_right(void)
438 {
439 if (cursor_x < cur_max_x())
440 cursor_x++;
441 else if (cursor_y < cur_max_y())
442 go_to_next_line();
443 }
444
445 static void
446 go_to_prev_word(void)
447 {
448 while (can_go_left() && !ch_isalpha(char_left_of_cursor()))
449 go_left();
450
451 while (can_go_left() && ch_isalpha(char_left_of_cursor()))
452 go_left();
453 }
454
455 static void
456 go_to_next_word(void)
457 {
458 while (can_go_right() && ch_isalpha(char_at_cursor()))
459 go_right();
460
461 while (can_go_right() && !ch_isalpha(char_at_cursor()))
462 go_right();
463 }
464
465 static bool
466 can_substitute_here(int ch)
467 {
468 return isascii(ch) &&
469 ch_isalpha((char)ch) &&
470 cursor_x < cur_max_x() &&
471 ch_isalpha(char_at_cursor());
472 }
473
474 static void
475 handle_char_input(int ch)
476 {
477 if (ch == char_at_cursor())
478 go_right();
479 else if (can_substitute_here(ch)) {
480 substitute(char_at_cursor(), (char)ch);
481 go_right();
482 } else
483 beep();
484 }
485
486 static bool
487 handle_key(void)
488 {
489 int ch = getch();
490
491 switch (ch) {
492 case 1: /* ^A */
493 case KEY_BEG:
494 case KEY_HOME:
495 cursor_x = 0;
496 break;
497 case 2: /* ^B */
498 case KEY_LEFT:
499 go_left();
500 break;
501 case 5: /* ^E */
502 case KEY_END:
503 cursor_x = cur_max_x();
504 break;
505 case 6: /* ^F */
506 case KEY_RIGHT:
507 go_right();
508 break;
509 case '\t':
510 go_to_next_word();
511 break;
512 case KEY_BTAB:
513 go_to_prev_word();
514 break;
515 case '\n':
516 go_to_next_line();
517 break;
518 case 12: /* ^L */
519 clear();
520 break;
521 case 14: /* ^N */
522 case KEY_DOWN:
523 cursor_y++;
524 break;
525 case 16: /* ^P */
526 case KEY_UP:
527 cursor_y--;
528 break;
529 case KEY_PPAGE:
530 cursor_y -= LINES - 2;
531 break;
532 case KEY_NPAGE:
533 cursor_y += LINES - 2;
534 break;
535 case '*':
536 hinting = !hinting;
537 break;
538 case '~':
539 return false;
540 case KEY_RESIZE:
541 break;
542 default:
543 handle_char_input(ch);
544 break;
545 }
546 return true;
547 }
548
549 static void
550 init(const char *filename)
551 {
552 stringarray_init(&lines);
553 stringarray_init(&sollines);
554 srandom((unsigned int)time(NULL));
555 if (filename != NULL) {
556 readfile(filename);
557 } else {
558 readquote();
559 }
560 encode();
561
562 initscr();
563 cbreak();
564 noecho();
565 keypad(stdscr, true);
566 }
567
568 static void
569 loop(void)
570 {
571 for (;;) {
572 redraw();
573 if (!handle_key())
574 break;
575 saturate_cursor();
576 scroll_into_view();
577 }
578 }
579
580 static void
581 done(void)
582 {
583 move(LINES - 1, 0);
584 clrtoeol();
585 refresh();
586
587 endwin();
588
589 stringarray_done(&sollines);
590 stringarray_done(&lines);
591 }
592
593
594 static void __dead
595 usage(void)
596 {
597
598 fprintf(stderr, "usage: %s [file]\n", getprogname());
599 exit(1);
600 }
601
602 int
603 main(int argc, char *argv[])
604 {
605
606 setprogname(argv[0]);
607 if (argc != 1 && argc != 2)
608 usage();
609
610 init(argc > 1 ? argv[1] : NULL);
611 loop();
612 done();
613 return 0;
614 }
615