1 /* $NetBSD: cgram.c,v 1.30 2023/05/10 12:30:27 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.30 2023/05/10 12:30:27 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
ch_islower(char ch)51 ch_islower(char ch)
52 {
53 return ch >= 'a' && ch <= 'z';
54 }
55
56 static bool
ch_isupper(char ch)57 ch_isupper(char ch)
58 {
59 return ch >= 'A' && ch <= 'Z';
60 }
61
62 static bool
ch_isalpha(char ch)63 ch_isalpha(char ch)
64 {
65 return ch_islower(ch) || ch_isupper(ch);
66 }
67
68 static char
ch_toupper(char ch)69 ch_toupper(char ch)
70 {
71 return ch_islower(ch) ? (char)(ch - 'a' + 'A') : ch;
72 }
73
74 static char
ch_tolower(char ch)75 ch_tolower(char ch)
76 {
77 return ch_isupper(ch) ? (char)(ch - 'A' + 'a') : ch;
78 }
79
80 static int
imax(int a,int b)81 imax(int a, int b)
82 {
83 return a > b ? a : b;
84 }
85
86 static int
imin(int a,int b)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
string_init(struct string * s)106 string_init(struct string *s)
107 {
108 s->s = NULL;
109 s->len = 0;
110 s->cap = 0;
111 }
112
113 static void
string_add(struct string * s,char ch)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
string_finish(struct string * s)126 string_finish(struct string *s)
127 {
128 string_add(s, '\0');
129 s->len--;
130 }
131
132 static void
stringarray_init(struct stringarray * a)133 stringarray_init(struct stringarray *a)
134 {
135 a->v = NULL;
136 a->num = 0;
137 }
138
139 static void
stringarray_done(struct stringarray * a)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
stringarray_add(struct stringarray * a,struct string * s)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
stringarray_dup(struct stringarray * dst,const struct stringarray * src)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
cur_max_x(void)183 cur_max_x(void)
184 {
185 return (int)lines.v[cursor_y].len;
186 }
187
188 static int
cur_max_y(void)189 cur_max_y(void)
190 {
191 return extent_y - 1;
192 }
193
194 static char
char_left_of_cursor(void)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
char_at_cursor(void)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
getquote(FILE * f)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
readfile(const char * name)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
readquote(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
encode(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
substitute(char a,char b)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
is_solved(void)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
redraw(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
saturate_cursor(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
scroll_into_view(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
can_go_left(void)400 can_go_left(void)
401 {
402 return cursor_y > 0 ||
403 (cursor_y == 0 && cursor_x > 0);
404 }
405
406 static bool
can_go_right(void)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
go_to_prev_line(void)414 go_to_prev_line(void)
415 {
416 cursor_y--;
417 cursor_x = cur_max_x();
418 }
419
420 static void
go_to_next_line(void)421 go_to_next_line(void)
422 {
423 cursor_x = 0;
424 cursor_y++;
425 }
426
427 static void
go_left(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
go_right(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
go_to_prev_word(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
go_to_next_word(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
can_substitute_here(int ch)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
handle_char_input(int ch)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
handle_key(void)487 handle_key(void)
488 {
489 int ch = getch();
490
491 #define CTRL(letter) (letter - 64)
492 switch (ch) {
493 case CTRL('A'):
494 case KEY_BEG:
495 case KEY_HOME:
496 cursor_x = 0;
497 break;
498 case CTRL('B'):
499 case KEY_LEFT:
500 go_left();
501 break;
502 case CTRL('E'):
503 case KEY_END:
504 cursor_x = cur_max_x();
505 break;
506 case CTRL('F'):
507 case KEY_RIGHT:
508 go_right();
509 break;
510 case '\t':
511 go_to_next_word();
512 break;
513 case KEY_BTAB:
514 go_to_prev_word();
515 break;
516 case '\n':
517 go_to_next_line();
518 break;
519 case CTRL('L'):
520 clear();
521 break;
522 case CTRL('N'):
523 case KEY_DOWN:
524 cursor_y++;
525 break;
526 case CTRL('P'):
527 case KEY_UP:
528 cursor_y--;
529 break;
530 case KEY_PPAGE:
531 cursor_y -= LINES - 2;
532 break;
533 case KEY_NPAGE:
534 cursor_y += LINES - 2;
535 break;
536 case '*':
537 hinting = !hinting;
538 break;
539 case '~':
540 return false;
541 case KEY_RESIZE:
542 break;
543 default:
544 handle_char_input(ch);
545 break;
546 }
547 return true;
548 }
549
550 static void
init(const char * filename)551 init(const char *filename)
552 {
553 stringarray_init(&lines);
554 stringarray_init(&sollines);
555 srandom((unsigned int)time(NULL));
556 if (filename != NULL) {
557 readfile(filename);
558 } else {
559 readquote();
560 }
561 encode();
562
563 initscr();
564 cbreak();
565 noecho();
566 keypad(stdscr, true);
567 }
568
569 static void
loop(void)570 loop(void)
571 {
572 for (;;) {
573 redraw();
574 if (!handle_key())
575 break;
576 saturate_cursor();
577 scroll_into_view();
578 }
579 }
580
581 static void
done(void)582 done(void)
583 {
584 move(LINES - 1, 0);
585 clrtoeol();
586 refresh();
587
588 endwin();
589
590 stringarray_done(&sollines);
591 stringarray_done(&lines);
592 }
593
594
595 static void __dead
usage(void)596 usage(void)
597 {
598
599 fprintf(stderr, "usage: %s [file]\n", getprogname());
600 exit(1);
601 }
602
603 int
main(int argc,char * argv[])604 main(int argc, char *argv[])
605 {
606
607 setprogname(argv[0]);
608 if (argc != 1 && argc != 2)
609 usage();
610
611 init(argc > 1 ? argv[1] : NULL);
612 loop();
613 done();
614 return 0;
615 }
616