--- /dev/null
+/*
+ * Copyright (c) 2023, Matthew Mondor
+ * ALL RIGHTS RESERVED.
+ *
+ * Interactive curses user input test 4
+ * Based on the input-interrupting interval timer of test 3.
+ * Unlike test3, this one directly uses the terminfo(5) "variable names" as
+ * called by X/Open ("long names" in the NetBSD documentation). After
+ * setupterm(3), which curses(3) initialization implicitly also uses, these
+ * are actually macros that can directly be used, pointing to a string in an
+ * array, or NULL. This replaces the need to use termcap(3)-style tgetstr(3)
+ * for each capability.
+ *
+ * Uses curses(3) for initialization and to decode user input sequences but
+ * relies on terminfo(3) for the animation to avoid unnecessary overhead.
+ * Runs some actual animation, until the user requests to exit.
+ * Note that on NetBSD and Linux curses endwin(3) was enough to properly
+ * restore the terminal state on exit, but not on OSX. savetty(3)/resetty(3)
+ * using attempts did not solve the problem, so despite the redundancy
+ * resorting to explicit termios use was the solution.
+ * tiparm(3) also seemed to be missing in OSX curses(3), but it has the
+ * equivalent older tparm(3). For that to work on NetBSD without needing to
+ * supply all parameters per X/Open, TPARM_VARARGS was defined to provide OSX
+ * and Linux ncurses default behavior.
+ *
+ * $ cc -Wall -O0 -g -o test4 test4.c -lcurses -lterminfo
+ * GNU: $ cc -Wall -O0 -g -o test4 test4.c -lcurses
+ */
+
+#include <assert.h>
+#include <curses.h>
+#include <err.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#define TPARM_VARARGS
+#include <term.h>
+#include <termios.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/time.h>
+
+
+#define TERM_COLUMNS 79
+#define TERM_LINES 24
+#define NOBJECTS 46
+
+
+#define TI_CHECK(s) ti_check("" #s "", s)
+
+
+/* Animation object */
+
+enum aobject_type {
+ AOT_AVATAR = 0,
+ AOT_STAR,
+ AOT_BUG
+};
+
+typedef struct aobject {
+ int t, x, y, col;
+ char c;
+} aobject_t;
+
+
+int main(void);
+static void cleanup(void);
+static void sighandler(int);
+static void ti_check(const char *, const char *);
+static void ti_goto(int, int);
+static void ti_bgcolor(int);
+static void anim_init(void);
+static void anim_refresh_full(void);
+static void anim_refresh_update(void);
+
+
+/* To explicitly save/restore tty(4) state despite curses(3) */
+static struct termios old_tios;
+
+/* Clock */
+static bool refresh_expired = false;
+
+/* Animation world */
+static char map[TERM_LINES][TERM_COLUMNS];
+static aobject_t objects[NOBJECTS];
+static aobject_t avatar = { AOT_AVATAR,
+ TERM_COLUMNS / 2, TERM_LINES / 2,
+ 0, '?' };
+
+
+int
+main(void)
+{
+ struct sigaction act;
+ struct itimerval itv;
+ WINDOW *w;
+ int c, uc, i;
+
+ /* Setup signal handler */
+ act.sa_handler = sighandler;
+ act.sa_flags = 0; /* ~SA_RESTART */
+ (void)sigemptyset(&act.sa_mask);
+ (void)sigaction(SIGALRM, &act, NULL);
+ (void)sigaction(SIGINT, &act, NULL);
+ (void)sigaction(SIGTERM, &act, NULL);
+ (void)sigaction(SIGHUP, &act, NULL);
+
+ /* Setup interval timer */
+ timerclear(&itv.it_interval);
+ timerclear(&itv.it_value);
+ itv.it_interval.tv_sec = 0;
+ itv.it_interval.tv_usec = 66666;
+ itv.it_value.tv_sec = itv.it_interval.tv_sec;
+ itv.it_value.tv_usec = itv.it_interval.tv_usec;
+ if (setitimer(ITIMER_REAL, &itv, NULL) == -1)
+ goto err;
+
+ /* Setup curses */
+ (void)tcgetattr(STDIN_FILENO, &old_tios);
+ w = initscr();
+ (void)savetty();
+ if (COLS < TERM_COLUMNS || LINES < TERM_LINES) {
+ (void)endwin();
+ (void)fprintf(stderr,
+ "The terminal must support at least %d columns by %d"
+ " lines for this program to work.\n",
+ TERM_COLUMNS, TERM_LINES);
+ exit(EXIT_FAILURE);
+ }
+ (void)raw();
+ (void)noecho();
+ (void)keypad(w, TRUE);
+ (void)timeout(-1);
+ (void)clear();
+ (void)refresh();
+
+ /* Query terminfo */
+ assert(setupterm(NULL, STDIN_FILENO, &i) != -1);
+ assert(i == 1);
+ TI_CHECK(clear_screen);
+ TI_CHECK(exit_attribute_mode);
+ TI_CHECK(cursor_address);
+ TI_CHECK(enter_reverse_mode);
+ TI_CHECK(cursor_invisible);
+ TI_CHECK(cursor_normal);
+ TI_CHECK(set_a_foreground);
+ TI_CHECK(set_a_background);
+
+ /* Exit cleanup hook to restore normal terminal */
+ (void)atexit(cleanup);
+
+ /* Setup animation world */
+ anim_init();
+
+ /* Initialize screen */
+ (void)putp(clear_screen);
+ (void)putp(cursor_invisible);
+ (void)fflush(stdout);
+
+ /* Animate until user requests to quit with q */
+ anim_refresh_full();
+ refresh_expired = false;
+ uc = -1;
+ for (;;) {
+cont:
+ if ((c = wgetch(w)) != ERR)
+ uc = c;
+ if (refresh_expired) {
+ refresh_expired = false;
+ int x = -1, y = -1;
+
+ /* Process user input if any */
+ /* XXX Should probably be in a function */
+ switch (uc) {
+ case -1:
+ break;
+ case 'q':
+ goto end;
+ break;
+ case 12:
+ anim_refresh_full();
+ uc = -1;
+ goto cont;
+ break;
+
+ case KEY_UP:
+ case '8': /* FALLTHROUGH */
+ case 'i': /* FALLTHROUGH */
+ case 'a': /* FALLTHROUGH */
+ if (avatar.y > 0) {
+ y = avatar.y - 1;
+ x = avatar.x;
+ }
+ break;
+
+ case KEY_DOWN:
+ case '2': /* FALLTHROUGH */
+ case 'k': /* FALLTHROUGH */
+ case 'm': /* FALLTHROUGH */
+ case 'z': /* FALLTHROUGH */
+ if (avatar.y < TERM_LINES - 1) {
+ y = avatar.y + 1;
+ x = avatar.x;
+ }
+ break;
+
+ case KEY_LEFT:
+ case '4': /* FALLTHROUGH */
+ case 'j': /* FALLTHROUGH */
+ case ',': /* FALLTHROUGH */
+ if (avatar.x > 0) {
+ x = avatar.x - 1;
+ y = avatar.y;
+ }
+ break;
+
+ case KEY_RIGHT:
+ case '6': /* FALLTHROUGH */
+ case 'l': /* FALLTHROUGH */
+ case '.': /* FALLTHROUGH */
+ if (avatar.x < TERM_COLUMNS - 1) {
+ x = avatar.x + 1;
+ y = avatar.y;
+ }
+ break;
+ }
+ /* Update avatar if necessary */
+ if (x != -1 && y != -1) {
+ /* XXX */
+ avatar.x = x;
+ avatar.y = y;
+ }
+
+ /* Animate */
+ anim_refresh_update();
+ uc = -1;
+ }
+ }
+end:
+
+ exit(EXIT_SUCCESS);
+
+err:
+ err(EXIT_FAILURE, "main()");
+}
+
+static void
+cleanup(void)
+{
+
+ (void)putp(cursor_normal);
+ (void)putp(exit_attribute_mode);
+ (void)endwin();
+ (void)tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_tios);
+}
+
+static void
+sighandler(int sig)
+{
+
+ switch (sig) {
+ case SIGALRM:
+ refresh_expired = true;
+ break;
+ case SIGINT:
+ case SIGTERM: /* FALLTHROUGH */
+ case SIGHUP: /* FALLTHROUGH */
+ exit(EXIT_SUCCESS);
+ break;
+ }
+}
+
+/* Verify required terminfo sequence capability */
+static void
+ti_check(const char *name, const char *seq)
+{
+
+ if (name == NULL)
+ err(EXIT_FAILURE,
+ "Failed to query terminfo for required capability '%s'",
+ name);
+}
+
+static void
+ti_goto(int x, int y)
+{
+ const char *s;
+
+ s = tparm(cursor_address, y, x);
+ (void)putp(s);
+}
+
+static void
+ti_bgcolor(int c)
+{
+ const char *s;
+
+ s = tparm(set_a_background, c);
+ (void)putp(s);
+}
+
+static void
+anim_init(void)
+{
+ int x, y, i, col;
+
+ for (y = 0; y < TERM_LINES; y++) {
+ for (x = 0; x < TERM_COLUMNS; x++) {
+ map[y][x] = ' ';
+ }
+ }
+
+ srandom(13);
+ col = 0;
+ for (i = 0; i < NOBJECTS; i++) {
+ aobject_t *o = &objects[i];
+
+ if (i <= 26) {
+ o->t = AOT_BUG;
+ o->c = 'A' + i;
+ o->col = ++col % 7;
+ } else {
+ o->t = AOT_STAR;
+ o->c = '.';
+ o->col = 0;
+ }
+ o->x = random() % (TERM_COLUMNS - 1);
+ o->y = random() % (TERM_LINES - 1);
+ }
+
+ /* XXX Avatar */
+}
+
+static void
+anim_refresh_full(void)
+{
+ int x, y, i;
+
+ (void)putp(clear_screen);
+ ti_bgcolor(0);
+ for (y = 0; y < TERM_LINES; y++) {
+ for (x = 0; x < TERM_COLUMNS; x++) {
+ (void)putchar(map[y][x]);
+ }
+ (void)fwrite("\r\n", 2, 1, stdout);
+ }
+ for (i = 0; i < NOBJECTS; i++) {
+ aobject_t *o = &objects[i];
+
+ ti_goto(o->x, o->y);
+ ti_bgcolor(o->col);
+ putchar(o->c);
+ }
+
+ /* Avatar */
+ ti_goto(avatar.x, avatar.y);
+ (void)putp(exit_attribute_mode);
+ (void)putp(enter_reverse_mode);
+ (void)putchar(avatar.c);
+ (void)putp(exit_attribute_mode);
+
+ (void)fflush(stdout);
+}
+
+static void
+anim_refresh_update(void)
+{
+ int i;
+
+ for (i = 0; i < NOBJECTS; i++) {
+ aobject_t *o = &objects[i];
+ int x = o->x, y = o->y, d = random() % 4;
+
+ if (o->t == AOT_STAR) {
+ if (x > 0)
+ x--;
+ else
+ x = TERM_COLUMNS - 1;
+ } else {
+ switch (d) {
+ case 0: /* Up */
+ if (y > 0)
+ y--;
+ break;
+ case 1: /* Down */
+ if (y < TERM_LINES - 1)
+ y++;
+ break;
+ case 2: /* Left */
+ if (x > 0)
+ x--;
+ break;
+ case 3: /* Right */
+ if (x < TERM_COLUMNS - 1)
+ x++;
+ break;
+ }
+ }
+
+ /* Erase from old pos */
+ map[o->y][o->x] = ' ';
+ ti_goto(o->x, o->y);
+ ti_bgcolor(0);
+ (void)putchar(' ');
+
+ /* Move/redraw at new pos */
+ map[y][x] = o->c;
+ ti_goto(x, y);
+ ti_bgcolor(o->col);
+ (void)putchar(o->c);
+ o->x = x;
+ o->y = y;
+ }
+
+ /* Avatar */
+ ti_goto(avatar.x, avatar.y);
+ (void)putp(exit_attribute_mode);
+ (void)putp(enter_reverse_mode);
+ (void)putchar(avatar.c);
+ (void)putp(exit_attribute_mode);
+
+ (void)fflush(stdout);
+}