cgram.c revision 1.27
1/* $NetBSD: cgram.c,v 1.27 2022/03/28 20:00: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 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.27 2022/03/28 20:00: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
50static bool
51ch_islower(char ch)
52{
53	return ch >= 'a' && ch <= 'z';
54}
55
56static bool
57ch_isupper(char ch)
58{
59	return ch >= 'A' && ch <= 'Z';
60}
61
62static bool
63ch_isalpha(char ch)
64{
65	return ch_islower(ch) || ch_isupper(ch);
66}
67
68static char
69ch_toupper(char ch)
70{
71	return ch_islower(ch) ? (char)(ch - 'a' + 'A') : ch;
72}
73
74static char
75ch_tolower(char ch)
76{
77	return ch_isupper(ch) ? (char)(ch - 'A' + 'a') : ch;
78}
79
80static int
81imax(int a, int b)
82{
83	return a > b ? a : b;
84}
85
86static int
87imin(int a, int b)
88{
89	return a < b ? a : b;
90}
91
92////////////////////////////////////////////////////////////
93
94struct string {
95	char *s;
96	size_t len;
97	size_t cap;
98};
99
100struct stringarray {
101	struct string *v;
102	size_t num;
103};
104
105static void
106string_init(struct string *s)
107{
108	s->s = NULL;
109	s->len = 0;
110	s->cap = 0;
111}
112
113static void
114string_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
125static void
126string_finish(struct string *s)
127{
128	string_add(s, '\0');
129	s->len--;
130}
131
132static void
133stringarray_init(struct stringarray *a)
134{
135	a->v = NULL;
136	a->num = 0;
137}
138
139static void
140stringarray_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
147static void
148stringarray_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
156static void
157stringarray_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
172static struct stringarray lines;
173static struct stringarray sollines;
174static bool hinting;
175static int extent_x;
176static int extent_y;
177static int offset_x;
178static int offset_y;
179static int cursor_x;
180static int cursor_y;
181
182static int
183cur_max_x(void)
184{
185	return (int)lines.v[cursor_y].len;
186}
187
188static int
189cur_max_y(void)
190{
191	return extent_y - 1;
192}
193
194static char
195char_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
203static char
204char_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
211static void
212getquote(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
242static void
243readfile(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
256static void
257readquote(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
269static void
270encode(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
294static void
295substitute(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
316static bool
317is_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
327static void
328redraw(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
375static void
376saturate_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
385static void
386scroll_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
399static bool
400can_go_left(void)
401{
402	return cursor_y > 0 ||
403	    (cursor_y == 0 && cursor_x > 0);
404}
405
406static bool
407can_go_right(void)
408{
409	return cursor_y < cur_max_y() ||
410	    (cursor_y == cur_max_y() && cursor_x < cur_max_x());
411}
412
413static void
414go_to_prev_line(void)
415{
416	cursor_y--;
417	cursor_x = cur_max_x();
418}
419
420static void
421go_to_next_line(void)
422{
423	cursor_x = 0;
424	cursor_y++;
425}
426
427static void
428go_left(void)
429{
430	if (cursor_x > 0)
431		cursor_x--;
432	else if (cursor_y > 0)
433		go_to_prev_line();
434}
435
436static void
437go_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
445static void
446go_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
455static void
456go_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
465static bool
466can_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
474static void
475handle_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
486static bool
487handle_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
548static void
549init(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
567static void
568loop(void)
569{
570	for (;;) {
571		redraw();
572		if (!handle_key())
573			break;
574		saturate_cursor();
575		scroll_into_view();
576	}
577}
578
579static void
580done(void)
581{
582	endwin();
583
584	stringarray_done(&sollines);
585	stringarray_done(&lines);
586}
587
588
589static void __dead
590usage(void)
591{
592
593	fprintf(stderr, "usage: %s [file]\n", getprogname());
594	exit(1);
595}
596
597int
598main(int argc, char *argv[])
599{
600
601	setprogname(argv[0]);
602	if (argc != 1 && argc != 2)
603		usage();
604
605	init(argc > 1 ? argv[1] : NULL);
606	loop();
607	done();
608	return 0;
609}
610