/* $NetBSD: cgram.c,v 1.11 2021/02/21 22:21:56 rillig Exp $ */ /*- * Copyright (c) 2013, 2021 The NetBSD Foundation, Inc. * All rights reserved. * * This code is derived from software contributed to The NetBSD Foundation * by David A. Holland and Roland Illig. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include #if defined(__RCSID) && !defined(lint) __RCSID("$NetBSD: cgram.c,v 1.11 2021/02/21 22:21:56 rillig Exp $"); #endif #include #include #include #include #include #include #include #include #include #include "pathnames.h" //////////////////////////////////////////////////////////// static char ch_toupper(char ch) { return (char)toupper((unsigned char)ch); } static char ch_tolower(char ch) { return (char)tolower((unsigned char)ch); } static bool ch_isalpha(char ch) { return isalpha((unsigned char)ch) != 0; } static bool ch_islower(char ch) { return islower((unsigned char)ch) != 0; } static bool ch_isupper(char ch) { return isupper((unsigned char)ch) != 0; } static int imax(int a, int b) { return a > b ? a : b; } static int imin(int a, int b) { return a < b ? a : b; } //////////////////////////////////////////////////////////// struct string { char *s; size_t len; size_t cap; }; struct stringarray { struct string *v; size_t num; }; static void string_init(struct string *s) { s->s = NULL; s->len = 0; s->cap = 0; } static void string_add(struct string *s, char ch) { if (s->len >= s->cap) { s->cap = 2 * s->cap + 16; s->s = realloc(s->s, s->cap); if (s->s == NULL) errx(1, "Out of memory"); } s->s[s->len++] = ch; } static void string_finish(struct string *s) { string_add(s, '\0'); s->len--; } static void stringarray_init(struct stringarray *a) { a->v = NULL; a->num = 0; } static void stringarray_cleanup(struct stringarray *a) { for (size_t i = 0; i < a->num; i++) free(a->v[i].s); free(a->v); } static void stringarray_add(struct stringarray *a, struct string *s) { size_t num = a->num++; a->v = realloc(a->v, a->num * sizeof a->v[0]); if (a->v == NULL) errx(1, "Out of memory"); a->v[num] = *s; } static void stringarray_dup(struct stringarray *dst, const struct stringarray *src) { assert(dst->num == 0); for (size_t i = 0; i < src->num; i++) { struct string str; string_init(&str); for (const char *p = src->v[i].s; *p != '\0'; p++) string_add(&str, *p); string_finish(&str); stringarray_add(dst, &str); } } //////////////////////////////////////////////////////////// static struct stringarray lines; static struct stringarray sollines; static bool hinting; static int extent_x; static int extent_y; static int offset_x; static int offset_y; static int cursor_x; static int cursor_y; static int cur_max_x(void) { return (int)lines.v[cursor_y].len; } static int cur_max_y(void) { return extent_y - 1; } static void readquote(void) { FILE *f = popen(_PATH_FORTUNE, "r"); if (f == NULL) err(1, "%s", _PATH_FORTUNE); struct string line; string_init(&line); int ch; while ((ch = fgetc(f)) != EOF) { if (ch == '\n') { string_finish(&line); stringarray_add(&lines, &line); string_init(&line); } else if (ch == '\t') { string_add(&line, ' '); while (line.len % 8 != 0) string_add(&line, ' '); } else if (ch == '\b') { if (line.len > 0) line.len--; } else { string_add(&line, (char)ch); } } stringarray_dup(&sollines, &lines); extent_y = (int)lines.num; for (int i = 0; i < extent_y; i++) extent_x = imax(extent_x, (int)lines.v[i].len); pclose(f); } static void encode(void) { int key[26]; for (int i = 0; i < 26; i++) key[i] = i; for (int i = 26; i > 1; i--) { int c = (int)(random() % i); int t = key[i - 1]; key[i - 1] = key[c]; key[c] = t; } for (int y = 0; y < extent_y; y++) { for (char *p = lines.v[y].s; *p != '\0'; p++) { if (ch_islower(*p)) *p = (char)('a' + key[*p - 'a']); if (ch_isupper(*p)) *p = (char)('A' + key[*p - 'A']); } } } static bool substitute(char ch) { assert(cursor_x >= 0 && cursor_x < extent_x); assert(cursor_y >= 0 && cursor_y < extent_y); if (cursor_x >= cur_max_x()) { beep(); return false; } char och = lines.v[cursor_y].s[cursor_x]; if (!ch_isalpha(och)) { beep(); return false; } char loch = ch_tolower(och); char uoch = ch_toupper(och); char lch = ch_tolower(ch); char uch = ch_toupper(ch); for (int y = 0; y < (int)lines.num; y++) { for (char *p = lines.v[y].s; *p != '\0'; p++) { if (*p == loch) *p = lch; else if (*p == uoch) *p = uch; else if (*p == lch) *p = loch; else if (*p == uch) *p = uoch; } } return true; } //////////////////////////////////////////////////////////// static bool is_solved(void) { for (size_t i = 0; i < lines.num; i++) if (strcmp(lines.v[i].s, sollines.v[i].s) != 0) return false; return true; } static void redraw(void) { erase(); int max_y = imin(LINES - 1, extent_y - offset_y); for (int y = 0; y < max_y; y++) { move(y, 0); int len = (int)lines.v[offset_y + y].len; int max_x = imin(COLS - 1, len - offset_x); const char *line = lines.v[offset_y + y].s; const char *solline = sollines.v[offset_y + y].s; for (int x = 0; x < max_x; x++) { char ch = line[offset_x + x]; bool bold = hinting && ch == solline[offset_x + x] && ch_isalpha(ch); if (bold) attron(A_BOLD); addch(ch); if (bold) attroff(A_BOLD); } clrtoeol(); } move(LINES - 1, 0); if (is_solved()) addstr("*solved* "); addstr("~ to quit, * to cheat, ^pnfb to move"); move(cursor_y - offset_y, cursor_x - offset_x); refresh(); } static void opencurses(void) { initscr(); cbreak(); noecho(); keypad(stdscr, true); } static void closecurses(void) { endwin(); } //////////////////////////////////////////////////////////// static void saturate_cursor(void) { cursor_y = imax(cursor_y, 0); cursor_y = imin(cursor_y, cur_max_y()); assert(cursor_x >= 0); cursor_x = imin(cursor_x, cur_max_x()); } static void scroll_into_view(void) { if (cursor_x < offset_x) offset_x = cursor_x; if (cursor_x > offset_x + COLS - 1) offset_x = cursor_x - (COLS - 1); if (cursor_y < offset_y) offset_y = cursor_y; if (cursor_y > offset_y + LINES - 2) offset_y = cursor_y - (LINES - 2); } static void handle_char_input(int ch) { if (isascii(ch) && ch_isalpha((char)ch)) { if (substitute((char)ch)) { if (cursor_x < cur_max_x()) cursor_x++; if (cursor_x == cur_max_x() && cursor_y < cur_max_y()) { cursor_x = 0; cursor_y++; } } } else if (cursor_x < cur_max_x() && ch == lines.v[cursor_y].s[cursor_x]) { cursor_x++; if (cursor_x == cur_max_x() && cursor_y < cur_max_y()) { cursor_x = 0; cursor_y++; } } else { beep(); } } static bool handle_key(void) { int ch = getch(); switch (ch) { case 1: /* ^A */ case KEY_HOME: cursor_x = 0; break; case 2: /* ^B */ case KEY_LEFT: if (cursor_x > 0) { cursor_x--; } else if (cursor_y > 0) { cursor_y--; cursor_x = cur_max_x(); } break; case 5: /* ^E */ case KEY_END: cursor_x = cur_max_x(); break; case 6: /* ^F */ case KEY_RIGHT: if (cursor_x < cur_max_x()) { cursor_x++; } else if (cursor_y < cur_max_y()) { cursor_y++; cursor_x = 0; } break; case 12: /* ^L */ clear(); break; case 14: /* ^N */ case KEY_DOWN: cursor_y++; break; case 16: /* ^P */ case KEY_UP: cursor_y--; break; case KEY_PPAGE: cursor_y -= LINES - 2; break; case KEY_NPAGE: cursor_y += LINES - 2; break; case '*': hinting = !hinting; break; case '~': return false; default: handle_char_input(ch); break; } return true; } static void init(void) { stringarray_init(&lines); stringarray_init(&sollines); srandom((unsigned int)time(NULL)); readquote(); encode(); opencurses(); } static void loop(void) { for (;;) { redraw(); if (!handle_key()) break; saturate_cursor(); scroll_into_view(); } } static void clean_up(void) { closecurses(); stringarray_cleanup(&sollines); stringarray_cleanup(&lines); } //////////////////////////////////////////////////////////// int main(void) { init(); loop(); clean_up(); }