cgram.c revision 1.28 1 /* $NetBSD: cgram.c,v 1.28 2022/05/14 14:20:10 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.28 2022/05/14 14:20:10 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_HOME:
494 cursor_x = 0;
495 break;
496 case 2: /* ^B */
497 case KEY_LEFT:
498 go_left();
499 break;
500 case 5: /* ^E */
501 case KEY_END:
502 cursor_x = cur_max_x();
503 break;
504 case 6: /* ^F */
505 case KEY_RIGHT:
506 go_right();
507 break;
508 case '\t':
509 go_to_next_word();
510 break;
511 case KEY_BTAB:
512 go_to_prev_word();
513 break;
514 case '\n':
515 go_to_next_line();
516 break;
517 case 12: /* ^L */
518 clear();
519 break;
520 case 14: /* ^N */
521 case KEY_DOWN:
522 cursor_y++;
523 break;
524 case 16: /* ^P */
525 case KEY_UP:
526 cursor_y--;
527 break;
528 case KEY_PPAGE:
529 cursor_y -= LINES - 2;
530 break;
531 case KEY_NPAGE:
532 cursor_y += LINES - 2;
533 break;
534 case '*':
535 hinting = !hinting;
536 break;
537 case '~':
538 return false;
539 case KEY_RESIZE:
540 break;
541 default:
542 handle_char_input(ch);
543 break;
544 }
545 return true;
546 }
547
548 static void
549 init(const char *filename)
550 {
551 stringarray_init(&lines);
552 stringarray_init(&sollines);
553 srandom((unsigned int)time(NULL));
554 if (filename != NULL) {
555 readfile(filename);
556 } else {
557 readquote();
558 }
559 encode();
560
561 initscr();
562 cbreak();
563 noecho();
564 keypad(stdscr, true);
565 }
566
567 static void
568 loop(void)
569 {
570 for (;;) {
571 redraw();
572 if (!handle_key())
573 break;
574 saturate_cursor();
575 scroll_into_view();
576 }
577 }
578
579 static void
580 done(void)
581 {
582 move(LINES - 1, 0);
583 clrtoeol();
584 refresh();
585
586 endwin();
587
588 stringarray_done(&sollines);
589 stringarray_done(&lines);
590 }
591
592
593 static void __dead
594 usage(void)
595 {
596
597 fprintf(stderr, "usage: %s [file]\n", getprogname());
598 exit(1);
599 }
600
601 int
602 main(int argc, char *argv[])
603 {
604
605 setprogname(argv[0]);
606 if (argc != 1 && argc != 2)
607 usage();
608
609 init(argc > 1 ? argv[1] : NULL);
610 loop();
611 done();
612 return 0;
613 }
614