cgram.c revision 1.19
1/* $NetBSD: cgram.c,v 1.19 2021/04/25 20:14:29 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.19 2021/04/25 20:14:29 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
220getquote(FILE *f)
221{
222	struct string line;
223	string_init(&line);
224
225	int ch;
226	while ((ch = fgetc(f)) != EOF) {
227		if (ch == '\n') {
228			string_finish(&line);
229			stringarray_add(&lines, &line);
230			string_init(&line);
231		} else if (ch == '\t') {
232			string_add(&line, ' ');
233			while (line.len % 8 != 0)
234				string_add(&line, ' ');
235		} else if (ch == '\b') {
236			if (line.len > 0)
237				line.len--;
238		} else {
239			string_add(&line, (char)ch);
240		}
241	}
242
243	stringarray_dup(&sollines, &lines);
244
245	extent_y = (int)lines.num;
246	for (int i = 0; i < extent_y; i++)
247		extent_x = imax(extent_x, (int)lines.v[i].len);
248}
249
250static void
251readfile(const char *name)
252{
253	FILE *f = fopen(name, "r");
254	if (f == NULL)
255		err(1, "%s", name);
256
257	getquote(f);
258
259	if (fclose(f) != 0)
260		err(1, "%s", name);
261}
262
263
264static void
265readquote(void)
266{
267	FILE *f = popen(_PATH_FORTUNE, "r");
268	if (f == NULL)
269		err(1, "%s", _PATH_FORTUNE);
270
271	getquote(f);
272
273	if (pclose(f) != 0)
274		exit(1); /* error message must come from child process */
275}
276
277static void
278encode(void)
279{
280	int key[26];
281
282	for (int i = 0; i < 26; i++)
283		key[i] = i;
284
285	for (int i = 26; i > 1; i--) {
286		int c = (int)(random() % i);
287		int t = key[i - 1];
288		key[i - 1] = key[c];
289		key[c] = t;
290	}
291
292	for (int y = 0; y < extent_y; y++) {
293		for (char *p = lines.v[y].s; *p != '\0'; p++) {
294			if (ch_islower(*p))
295				*p = (char)('a' + key[*p - 'a']);
296			if (ch_isupper(*p))
297				*p = (char)('A' + key[*p - 'A']);
298		}
299	}
300}
301
302static void
303substitute(char a, char b)
304{
305	char la = ch_tolower(a);
306	char ua = ch_toupper(a);
307	char lb = ch_tolower(b);
308	char ub = ch_toupper(b);
309
310	for (int y = 0; y < (int)lines.num; y++) {
311		for (char *p = lines.v[y].s; *p != '\0'; p++) {
312			if (*p == la)
313				*p = lb;
314			else if (*p == ua)
315				*p = ub;
316			else if (*p == lb)
317				*p = la;
318			else if (*p == ub)
319				*p = ua;
320		}
321	}
322}
323
324static bool
325is_solved(void)
326{
327	for (size_t i = 0; i < lines.num; i++)
328		if (strcmp(lines.v[i].s, sollines.v[i].s) != 0)
329			return false;
330	return true;
331}
332
333////////////////////////////////////////////////////////////
334
335static void
336redraw(void)
337{
338	erase();
339
340	int max_y = imin(LINES - 1, extent_y - offset_y);
341	for (int y = 0; y < max_y; y++) {
342		move(y, 0);
343
344		int len = (int)lines.v[offset_y + y].len;
345		int max_x = imin(COLS - 1, len - offset_x);
346		const char *line = lines.v[offset_y + y].s;
347		const char *solline = sollines.v[offset_y + y].s;
348
349		for (int x = 0; x < max_x; x++) {
350			char ch = line[offset_x + x];
351			bool bold = hinting &&
352			    ch == solline[offset_x + x] &&
353			    ch_isalpha(ch);
354
355			if (bold)
356				attron(A_BOLD);
357			addch(ch);
358			if (bold)
359				attroff(A_BOLD);
360		}
361		clrtoeol();
362	}
363
364	move(LINES - 1, 0);
365	addstr("~ to quit, * to cheat, ^pnfb to move");
366
367	if (is_solved()) {
368		if (extent_y + 1 - offset_y < LINES - 2)
369			move(extent_y + 1 - offset_y, 0);
370		else
371			addch(' ');
372		attron(A_BOLD | A_STANDOUT);
373		addstr("*solved*");
374		attroff(A_BOLD | A_STANDOUT);
375	}
376
377	move(cursor_y - offset_y, cursor_x - offset_x);
378
379	refresh();
380}
381
382////////////////////////////////////////////////////////////
383
384static void
385saturate_cursor(void)
386{
387	cursor_y = imax(cursor_y, 0);
388	cursor_y = imin(cursor_y, cur_max_y());
389
390	assert(cursor_x >= 0);
391	cursor_x = imin(cursor_x, cur_max_x());
392}
393
394static void
395scroll_into_view(void)
396{
397	if (cursor_x < offset_x)
398		offset_x = cursor_x;
399	if (cursor_x > offset_x + COLS - 1)
400		offset_x = cursor_x - (COLS - 1);
401
402	if (cursor_y < offset_y)
403		offset_y = cursor_y;
404	if (cursor_y > offset_y + LINES - 2)
405		offset_y = cursor_y - (LINES - 2);
406}
407
408static bool
409can_go_left(void)
410{
411	return cursor_y > 0 ||
412	    (cursor_y == 0 && cursor_x > 0);
413}
414
415static bool
416can_go_right(void)
417{
418	return cursor_y < cur_max_y() ||
419	    (cursor_y == cur_max_y() && cursor_x < cur_max_x());
420}
421
422static void
423go_to_prev_line(void)
424{
425	cursor_y--;
426	cursor_x = cur_max_x();
427}
428
429static void
430go_to_next_line(void)
431{
432	cursor_x = 0;
433	cursor_y++;
434}
435
436static void
437go_left(void)
438{
439	if (cursor_x > 0)
440		cursor_x--;
441	else if (cursor_y > 0)
442		go_to_prev_line();
443}
444
445static void
446go_right(void)
447{
448	if (cursor_x < cur_max_x())
449		cursor_x++;
450	else if (cursor_y < cur_max_y())
451		go_to_next_line();
452}
453
454static void
455go_to_prev_word(void)
456{
457	while (can_go_left() && ch_isspace(char_left_of_cursor()))
458		go_left();
459
460	while (can_go_left() && !ch_isspace(char_left_of_cursor()))
461		go_left();
462}
463
464static void
465go_to_next_word(void)
466{
467	while (can_go_right() && !ch_isspace(char_at_cursor()))
468		go_right();
469
470	while (can_go_right() && ch_isspace(char_at_cursor()))
471		go_right();
472}
473
474static bool
475can_substitute_here(int ch)
476{
477	return isascii(ch) &&
478	    ch_isalpha((char)ch) &&
479	    cursor_x < cur_max_x() &&
480	    ch_isalpha(char_at_cursor());
481}
482
483static void
484handle_char_input(int ch)
485{
486	if (ch == char_at_cursor())
487		go_right();
488	else if (can_substitute_here(ch)) {
489		substitute(char_at_cursor(), (char)ch);
490		go_right();
491	} else
492		beep();
493}
494
495static bool
496handle_key(void)
497{
498	int ch = getch();
499
500	switch (ch) {
501	case 1:			/* ^A */
502	case KEY_HOME:
503		cursor_x = 0;
504		break;
505	case 2:			/* ^B */
506	case KEY_LEFT:
507		go_left();
508		break;
509	case 5:			/* ^E */
510	case KEY_END:
511		cursor_x = cur_max_x();
512		break;
513	case 6:			/* ^F */
514	case KEY_RIGHT:
515		go_right();
516		break;
517	case '\t':
518		go_to_next_word();
519		break;
520	case KEY_BTAB:
521		go_to_prev_word();
522		break;
523	case '\n':
524		go_to_next_line();
525		break;
526	case 12:		/* ^L */
527		clear();
528		break;
529	case 14:		/* ^N */
530	case KEY_DOWN:
531		cursor_y++;
532		break;
533	case 16:		/* ^P */
534	case KEY_UP:
535		cursor_y--;
536		break;
537	case KEY_PPAGE:
538		cursor_y -= LINES - 2;
539		break;
540	case KEY_NPAGE:
541		cursor_y += LINES - 2;
542		break;
543	case '*':
544		hinting = !hinting;
545		break;
546	case '~':
547		return false;
548	case KEY_RESIZE:
549		break;
550	default:
551		handle_char_input(ch);
552		break;
553	}
554	return true;
555}
556
557static void
558init(const char *filename)
559{
560	stringarray_init(&lines);
561	stringarray_init(&sollines);
562	srandom((unsigned int)time(NULL));
563	if (filename != NULL) {
564	    readfile(filename);
565	} else {
566	    readquote();
567	}
568	encode();
569
570	initscr();
571	cbreak();
572	noecho();
573	keypad(stdscr, true);
574}
575
576static void
577loop(void)
578{
579	for (;;) {
580		redraw();
581		if (!handle_key())
582			break;
583		saturate_cursor();
584		scroll_into_view();
585	}
586}
587
588static void
589clean_up(void)
590{
591	endwin();
592
593	stringarray_cleanup(&sollines);
594	stringarray_cleanup(&lines);
595}
596
597////////////////////////////////////////////////////////////
598
599int
600main(int argc, char *argv[])
601{
602	init(argc > 1 ? argv[1] : NULL);
603	loop();
604	clean_up();
605}
606