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