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