Setup ncurses
See Also: NCURSES Programming HOWTO by Pradeep Padala
#include <locale.h>
#include <curses.h>
#include <stdlib.h>
#include <stdint.h>
void
init(void)
{
// enable support UTF-8
// confirm available locales with 'locale -a'
setlocale(LC_ALL, "C.utf8");
// newterm(getenv("TERM"), stdout, stdin);
// return stdscr;
initscr();
// Disables line buffering and erase/kill character-processing
cbreak();
// Characters typed by the user not echoed by getch
noecho();
// The nodelay option causes getch to be a non-blocking call.
// If no input is ready, getch returns ERR.
nodelay(stdscr, TRUE);
// Enable keypad keys (including arrow keys)
keypad(stdscr, TRUE);
// The number of milliseconds to wait after reading an escape character
// Nonzero delays will block thread while curses waits for escape code.
set_escdelay(0);
// Enable mouse event updates (button presses) and position updates.
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
// Except apparently sometimes for xterm.
// See: sylt/ncurses_mouse_movement.c
// And: xterm SET_ANY_EVENT_MOUSE
// https://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
printf("\033[?1003h\n");
fflush(stdout);
// The number of milliseconds to wait after a mouse down event.
// Nonzero delays will block thread while curses waits for mouse-up to
// resolve to click events. Zero disables click resolution.
mouseinterval(0);
// Clear screen
clear();
}
void
quit(void)
{
// Disable mouse movement events, as l = low
printf("\033[?1003l\n");
// restore terminal
endwin();
exit(0);
}
Update loop
See Also: Calculating Elapsed Time [gnu.org]
The target is 1/60 sec each frame. So we need to measure how long we actually take, subtract that from our target to calculate the remaining time, then sleep for that amount.
timespec frame_time;
timespec sleep_time;
timespec remaining_time;
timespec target_time = { 0, 16666666 };
To get the time we’ll use clock_gettime()
timespec start_time;
clock_gettime(CLOCK_REALTIME, &start_time);
To find the difference between two time samples, we’ll use:
timespec timespec_sub(timespec start, timespec end)
{
timespec temp;
if ((end.tv_nsec-start.tv_nsec)<0) {
temp.tv_sec = end.tv_sec-start.tv_sec-1;
temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec;
} else {
temp.tv_sec = end.tv_sec-start.tv_sec;
temp.tv_nsec = end.tv_nsec-start.tv_nsec;
}
return temp;
};
To sleep we’ll use nanosleep
// POSIX.1 specifies that nanosleep() should measure time against the
// CLOCK_REALTIME clock
nanosleep( &remaining_time, NULL );
Which looks something like this:
timespec frame_time;
timespec sleep_time;
timespec remaining_time;
timespec target_time = { 0, 16666666 };
while (1)
{
timespec start_time;
clock_gettime(CLOCK_REALTIME, &start_time);
// do stuff....
timespec end_time;
clock_gettime(CLOCK_REALTIME, &end_time);
frame_time = timespec_sub( start_time, end_time );
remaining_time = timespec_sub( frame_time, target_time );
nanosleep( &remaining_time, NULL );
}
Colors and Printing Text
See Also: About ncurses Colors by Jim Hall
Create the palette you will use.
The first 16 colors are the default terminal colors. We’ll leave those alone so we don’t mess up any user settings in the terminal.
Here for example we’ll use the 216 color Netscape palette - which is has the main feature of being ubiquitous.
We’ll define it here in standard 24bit rgb format.
#define COLOR_PALETTE_COUNT 256
uint32_t g_color_palette[COLOR_PALETTE_COUNT] =
{
// xterm default colors.
0x000000, // COLOR_BLACK
0xcd0000, // COLOR_BLUE
0x00cd00, // COLOR_GREEN
0xcdcd00, // COLOR_CYAN
0x0000cd, // COLOR_RED
0xcd00cd, // COLOR_MAGENTA
0x00cdcd, // COLOR_YELLOW
0xe5e5e5, // COLOR_WHITE
0x4d4d4d, // HI COLOR_BLACK
0xff0000, // HI COLOR_BLUE
0x00ff00, // HI COLOR_GREEN
0xffff00, // HI COLOR_CYAN
0x0000ff, // HI COLOR_RED
0xff00ff, // HI COLOR_MAGENTA
0x00ffff, // HI COLOR_YELLOW
0xffffff, // HI COLOR_WHITE
// Netscape palette
0x000000, 0x000033, 0x000066, 0x000099, 0x0000CC, 0x0000FF,
0x003300, 0x003333, 0x003366, 0x003399, 0x0033CC, 0x0033FF,
0x006600, 0x006633, 0x006666, 0x006699, 0x0066CC, 0x0066FF,
0x009900, 0x009933, 0x009966, 0x009999, 0x0099CC, 0x0099FF,
0x00CC00, 0x00CC33, 0x00CC66, 0x00CC99, 0x00CCCC, 0x00CCFF,
0x00FF00, 0x00FF33, 0x00FF66, 0x00FF99, 0x00FFCC, 0x00FFFF,
0x330000, 0x330033, 0x330066, 0x330099, 0x3300CC, 0x3300FF,
0x333300, 0x333333, 0x333366, 0x333399, 0x3333CC, 0x3333FF,
0x336600, 0x336633, 0x336666, 0x336699, 0x3366CC, 0x3366FF,
0x339900, 0x339933, 0x339966, 0x339999, 0x3399CC, 0x3399FF,
0x33CC00, 0x33CC33, 0x33CC66, 0x33CC99, 0x33CCCC, 0x33CCFF,
0x33FF00, 0x33FF33, 0x33FF66, 0x33FF99, 0x33FFCC, 0x33FFFF,
0x660000, 0x660033, 0x660066, 0x660099, 0x6600CC, 0x6600FF,
0x663300, 0x663333, 0x663366, 0x663399, 0x6633CC, 0x6633FF,
0x666600, 0x666633, 0x666666, 0x666699, 0x6666CC, 0x6666FF,
0x669900, 0x669933, 0x669966, 0x669999, 0x6699CC, 0x6699FF,
0x66CC00, 0x66CC33, 0x66CC66, 0x66CC99, 0x66CCCC, 0x66CCFF,
0x66FF00, 0x66FF33, 0x66FF66, 0x66FF99, 0x66FFCC, 0x66FFFF,
0x990000, 0x990033, 0x990066, 0x990099, 0x9900CC, 0x9900FF,
0x993300, 0x993333, 0x993366, 0x993399, 0x9933CC, 0x9933FF,
0x996600, 0x996633, 0x996666, 0x996699, 0x9966CC, 0x9966FF,
0x999900, 0x999933, 0x999966, 0x999999, 0x9999CC, 0x9999FF,
0x99CC00, 0x99CC33, 0x99CC66, 0x99CC99, 0x99CCCC, 0x99CCFF,
0x99FF00, 0x99FF33, 0x99FF66, 0x99FF99, 0x99FFCC, 0x99FFFF,
0xCC0000, 0xCC0033, 0xCC0066, 0xCC0099, 0xCC00CC, 0xCC00FF,
0xCC3300, 0xCC3333, 0xCC3366, 0xCC3399, 0xCC33CC, 0xCC33FF,
0xCC6600, 0xCC6633, 0xCC6666, 0xCC6699, 0xCC66CC, 0xCC66FF,
0xCC9900, 0xCC9933, 0xCC9966, 0xCC9999, 0xCC99CC, 0xCC99FF,
0xCCCC00, 0xCCCC33, 0xCCCC66, 0xCCCC99, 0xCCCCCC, 0xCCCCFF,
0xCCFF00, 0xCCFF33, 0xCCFF66, 0xCCFF99, 0xCCFFCC, 0xCCFFFF,
0xFF0000, 0xFF0033, 0xFF0066, 0xFF0099, 0xFF00CC, 0xFF00FF,
0xFF3300, 0xFF3333, 0xFF3366, 0xFF3399, 0xFF33CC, 0xFF33FF,
0xFF6600, 0xFF6633, 0xFF6666, 0xFF6699, 0xFF66CC, 0xFF66FF,
0xFF9900, 0xFF9933, 0xFF9966, 0xFF9999, 0xFF99CC, 0xFF99FF,
0xFFCC00, 0xFFCC33, 0xFFCC66, 0xFFCC99, 0xFFCCCC, 0xFFCCFF,
0xFFFF00, 0xFFFF33, 0xFFFF66, 0xFFFF99, 0xFFFFCC, 0xFFFFFF,
// Unused
0x000000, 0x000000, 0x000000, 0x000000,
0x000000, 0x000000, 0x000000, 0x000000,
0x000000, 0x000000, 0x000000, 0x000000,
0x000000, 0x000000, 0x000000, 0x000000,
0x000000, 0x000000, 0x000000, 0x000000,
0x000000, 0x000000, 0x000000, 0x000000,
};
We’ll need to convert the colors to ncurses color format, which is defined as a fraction of 1000 instead of a fraction of 256. (Why?)
void
init_color_rgb( int id, uint32_t rgb )
{
uint32_t b = ((rgb & 0xff) * 1000 ) / 255;
uint32_t g = (((rgb >> 8) & 0xff) * 1000 ) / 255;
uint32_t r = (((rgb >> 16) & 0xff) * 1000 ) / 255;
init_color(id, r, g, b);
// Black as default background color.
init_pair(id, id, COLOR_BLACK);
}
And we’ll add to the init function:
// Enable color support.
start_color();
// Initialize palette
for (int i=16;i<COLOR_PALETTE_COUNT;i++)
{
init_color_rgb(i,g_color_palette[i]);
}
Use move(y,x) to move cursor to character location relative to top/left.
Use printw to render unicode printf-style string and arguments at cursor position.
Use addwstr to render string with no arguments at cursor position.
use attron( COLOR_PAIR(n) ) to enable color n of palette for any rendered strings that follow.
COLS and LINES are ncurses globals which store the screen character resolution.
int line = 0;
attron( COLOR_PAIR(COLOR_WHITE) );
move(line++,0);
printw("character_resolution: %d x %d",COLS,LINES);
move(line++,0);
printw("time: %f",g_time);
move(line++,0);
printw("frame_counter: %d",g_frame_counter);
move(line++,0);
printw_timespec("sleep_time: ", sleep_time);
move(line++,0);
printw("mouse x:%d y:%d bstate:0x%08x", g_mouse_x, g_mouse_y, g_mouse_bstate);
for (int i=0;i<COLOR_PALETTE_COUNT;i++)
{
move(line+(i>>4),i&0x0f);
attron( COLOR_PAIR(i) );
addwstr(L"\u25fc");
}
line += COLOR_PALETTE_COUNT >> 4;
A helper function to print timespec:
void printw_timespec(const char* title, timespec time )
{
int64_t sec = time.tv_sec;
int64_t msec = time.tv_nsec / 1000000;
int64_t usec = (time.tv_nsec - (msec*1000000)) / 1000;
int64_t nsec = time.tv_nsec - (msec*1000000) - (usec*1000);
printw("%s%ds %dms %dus %dns",title,(int)(sec),(int)(msec),(int)(usec),(int)(nsec));
}
Events
Drain stdin every frame for character events.
void
drain_events(void)
{
int event_key = 0;
do
{
event_key = getch();
switch (event_key)
{
case KEY_LEFT:
break;
case KEY_RIGHT:
break;
case KEY_UP:
break;
case KEY_DOWN:
break;
case 'q':
quit();
break;
case KEY_MOUSE:
{
MEVENT event;
if (getmouse(&event) == OK)
{
g_mouse_x = event.x;
g_mouse_y = event.y;
g_mouse_bstate = event.bstate;
if ( g_mouse_bstate & BUTTON1_RELEASED )
{
}
if ( g_mouse_bstate & BUTTON1_PRESSED )
{
}
}
}
break;
case KEY_RESIZE:
{
}
break;
}
} while (event_key != ERR);
}
Drawing pixels
We’re going to draw in text on the terminal with Unicode Braille characters.
REFERENCE: asciimoo/drawille [github.com] - A python library for drawing in terminal with Unicode Braille characters
Convert each 2x4 block of pixels to a specific Braille pattern glyph:
U+2800 +
|---|---| |------|------|
| 0 | 1 | | 0x01 | 0x08 |
|---|---| |------|------|
| 2 | 3 | | 0x02 | 0x10 |
|---|---| ---> |------|------|
| 4 | 5 | | 0x04 | 0x20 |
|---|---| |------|------|
| 6 | 7 | | 0x40 | 0x80 |
|---|---| |------|------|
However, each glyph can only have one color. While there’s a lot of opportunity here to find the right color given 8 pixels, we’ll do something simple and just average them.
Allocate space for (COLS*2)*(ROWS*4) pixels.
g_color_buffer = (uint32_t*)malloc( (COLS*2) * (ROWS*4) * sizeof(uint32_t));
And on RESIZE event, reallocate the buffer:
case KEY_RESIZE:
{
g_color_buffer = (uint32_t*)realloc( g_color_buffer, (COLS*2) * (ROWS*4) * sizeof(uint32_t));
}
break;
Write whatever you like to g_color_buffer each frame, then call present()
void
present( void )
{
uint32_t last_color_index = 0;
for (int y=0;y<LINES;y++)
{
move(y,0);
for (int x=0;x<COLS;x++)
{
// extract colors
uint32_t color_0 = g_color_buffer[ (((y*4)+0)*(COLS*2))+((x*2)+0) ];
uint32_t color_1 = g_color_buffer[ (((y*4)+0)*(COLS*2))+((x*2)+1) ];
uint32_t color_2 = g_color_buffer[ (((y*4)+1)*(COLS*2))+((x*2)+0) ];
uint32_t color_3 = g_color_buffer[ (((y*4)+1)*(COLS*2))+((x*2)+1) ];
uint32_t color_4 = g_color_buffer[ (((y*4)+2)*(COLS*2))+((x*2)+0) ];
uint32_t color_5 = g_color_buffer[ (((y*4)+2)*(COLS*2))+((x*2)+1) ];
uint32_t color_6 = g_color_buffer[ (((y*4)+3)*(COLS*2))+((x*2)+0) ];
uint32_t color_7 = g_color_buffer[ (((y*4)+3)*(COLS*2))+((x*2)+1) ];
uint32_t is_color_0 = (uint32_t)((int32_t)(-color_0) >> 31);
uint32_t is_color_1 = (uint32_t)((int32_t)(-color_1) >> 31);
uint32_t is_color_2 = (uint32_t)((int32_t)(-color_2) >> 31);
uint32_t is_color_3 = (uint32_t)((int32_t)(-color_3) >> 31);
uint32_t is_color_4 = (uint32_t)((int32_t)(-color_4) >> 31);
uint32_t is_color_5 = (uint32_t)((int32_t)(-color_5) >> 31);
uint32_t is_color_6 = (uint32_t)((int32_t)(-color_6) >> 31);
uint32_t is_color_7 = (uint32_t)((int32_t)(-color_7) >> 31);
uint32_t color_count = (is_color_0 & 1)
+ (is_color_1 & 1)
+ (is_color_2 & 1)
+ (is_color_3 & 1)
+ (is_color_4 & 1)
+ (is_color_5 & 1)
+ (is_color_6 & 1)
+ (is_color_7 & 1);
if (color_count == 0)
{
addwstr( L"\u2800" );
continue;
}
uint32_t r0 = (color_0) & 0x0000ff;
uint32_t g0 = (color_0>>8) & 0x0000ff;
uint32_t b0 = (color_0>>16) & 0x0000ff;
uint32_t r1 = (color_1) & 0x0000ff;
uint32_t g1 = (color_1>>8) & 0x0000ff;
uint32_t b1 = (color_1>>16) & 0x0000ff;
uint32_t r2 = (color_2) & 0x0000ff;
uint32_t g2 = (color_2>>8) & 0x0000ff;
uint32_t b2 = (color_2>>16) & 0x0000ff;
uint32_t r3 = (color_3) & 0x0000ff;
uint32_t g3 = (color_3>>8) & 0x0000ff;
uint32_t b3 = (color_3>>16) & 0x0000ff;
uint32_t r4 = (color_4) & 0x0000ff;
uint32_t g4 = (color_4>>8) & 0x0000ff;
uint32_t b4 = (color_4>>16) & 0x0000ff;
uint32_t r5 = (color_5) & 0x0000ff;
uint32_t g5 = (color_5>>8) & 0x0000ff;
uint32_t b5 = (color_5>>16) & 0x0000ff;
uint32_t r6 = (color_6) & 0x0000ff;
uint32_t g6 = (color_6>>8) & 0x0000ff;
uint32_t b6 = (color_6>>16) & 0x0000ff;
uint32_t r7 = (color_7) & 0x0000ff;
uint32_t g7 = (color_7>>8) & 0x0000ff;
uint32_t b7 = (color_7>>16) & 0x0000ff;
// average colors
uint32_t r = (r0+r1+r2+r3+r4+r5+r6+r7) / color_count;
uint32_t g = (g0+g1+g2+g3+g4+g5+g6+g7) / color_count;
uint32_t b = (b0+b1+b2+b3+b4+b5+b6+b7) / color_count;
// convert to netscape palette color
uint32_t nr = (r+25)/51;
uint32_t ng = (g+25)/51;
uint32_t nb = (b+25)/51;
uint32_t color_index = 16+((36*nr)+(6*ng)+nb);
uint8_t glyph_index = (is_color_0 & 1)
| (is_color_1 & 8)
| (is_color_2 & 2)
| (is_color_3 & 16)
| (is_color_4 & 4)
| (is_color_5 & 32)
| (is_color_6 & 64)
| (is_color_7 & 128);
wchar_t str[2];
str[0] = L'\u2800'+glyph_index;
str[1] = 0;
if ( color_index != last_color_index )
{
attron( COLOR_PAIR(color_index) );
last_color_index = color_index;
}
addwstr(str);
}
}
}
Paid subscribers find example below.