|
|
@ -2,72 +2,59 @@ |
|
|
|
#include <limits.h> |
|
|
|
#include <locale.h> |
|
|
|
#include <ncurses.h> |
|
|
|
#include <stdbool.h> |
|
|
|
#include <stdlib.h> |
|
|
|
|
|
|
|
int main() { |
|
|
|
// Enable UTF-8 by explicitly initializing our locale before initializing
|
|
|
|
// ncurses.
|
|
|
|
setlocale(LC_ALL, ""); |
|
|
|
// Initialize ncurses
|
|
|
|
initscr(); |
|
|
|
// Allow ncurses to control newline translation. Fine to use with any modern
|
|
|
|
// terminal, and will let ncurses run faster.
|
|
|
|
nonl(); |
|
|
|
// Set interrupt keys (interrupt, break, quit...) to not flush. Helps keep
|
|
|
|
// ncurses state consistent, at the cost of less responsive terminal
|
|
|
|
// interrupt. (This will rarely happen.)
|
|
|
|
intrflush(stdscr, FALSE); |
|
|
|
// Receive keyboard input immediately, and receive shift, control, etc. as
|
|
|
|
// separate events, instead of combined with individual characters.
|
|
|
|
raw(); |
|
|
|
// Don't echo keyboard input
|
|
|
|
noecho(); |
|
|
|
// Also receive arrow keys, etc.
|
|
|
|
keypad(stdscr, TRUE); |
|
|
|
// Hide the terminal cursor
|
|
|
|
curs_set(0); |
|
|
|
typedef struct { |
|
|
|
chtype* buffer; |
|
|
|
int size_y; |
|
|
|
int size_x; |
|
|
|
chtype fill_char; |
|
|
|
} view_state; |
|
|
|
|
|
|
|
printw("Type any character to fill it in an alternating grid\n"); |
|
|
|
refresh(); |
|
|
|
// 'chtype' is the type of character that ncurses uses. It will be an
|
|
|
|
// ASCII-like value, if that's what the user hit on the keyboard, but
|
|
|
|
// 'chtype' is larger than an 8-bit number and could have something else in
|
|
|
|
// it (some Unicode character, a control character for the terminal, etc.)
|
|
|
|
chtype ch = getch(); |
|
|
|
// We get the dimensions that the terminal is currently set to, so we know
|
|
|
|
// how big of a buffer to allocate. We'll fill the buffer with some
|
|
|
|
// characters after we've allocated it.
|
|
|
|
int term_height = getmaxy(stdscr); |
|
|
|
int term_width = getmaxx(stdscr); |
|
|
|
assert(term_height >= 0 && term_width >= 0); |
|
|
|
// We use 'size_t' when we talk about the size of memory. We also sometimes
|
|
|
|
// use it when looping over indices in an array, but we won't do that this
|
|
|
|
// time, since we already have the terminal width and height as regular ints.
|
|
|
|
size_t term_cells = term_height * term_width; |
|
|
|
void init_view_state(view_state* vs) { |
|
|
|
vs->buffer = NULL; |
|
|
|
vs->size_y = 0; |
|
|
|
vs->size_x = 0; |
|
|
|
vs->fill_char = '?'; |
|
|
|
} |
|
|
|
|
|
|
|
void deinit_view_state(view_state* vs) { |
|
|
|
// Note that we don't have to check if the buffer was ever actually set to a
|
|
|
|
// non-null pointer: `free` does this for us.
|
|
|
|
free(vs->buffer); |
|
|
|
} |
|
|
|
|
|
|
|
// 'calloc' uses the C runtime library to give us a chunk of memory that we
|
|
|
|
// can use to do whatever we want. The first argument is the number of things
|
|
|
|
// we'll put into the memory, and the second argument is the size of the
|
|
|
|
// those things. The total amount of memory it gives us back will be (number
|
|
|
|
// of guys * size of guys).
|
|
|
|
//
|
|
|
|
// There is also another function you may have heard of -- malloc -- which
|
|
|
|
// does mostly the same thing. The main differences are that 1) malloc does
|
|
|
|
// not turn all of the memory into zeroes before giving it to us, and 2)
|
|
|
|
// malloc only takes one argument.
|
|
|
|
//
|
|
|
|
// Because malloc doesn't zero the memory for us, you have to make sure that
|
|
|
|
// you always clear (or write to it) yourself before using it. That wouldn't
|
|
|
|
// be a problem in our example, though.
|
|
|
|
//
|
|
|
|
// Because malloc only takes one argument, you have to do the multiplication
|
|
|
|
// yourself, and if you want to be safe about it, you have to check to make
|
|
|
|
// sure the multiplication won't overflow. calloc does that for us.
|
|
|
|
//
|
|
|
|
// sizeof is a special thing that returns the size of an expression or type
|
|
|
|
// *at compile time*.
|
|
|
|
chtype* buff = calloc(term_cells, sizeof(chtype)); |
|
|
|
void update_view_state(view_state* vs, int term_height, int term_width, |
|
|
|
chtype fill_char) { |
|
|
|
bool same_dimensions = vs->size_y == term_height && vs->size_x == term_width; |
|
|
|
bool same_fill_char = vs->fill_char == fill_char; |
|
|
|
// If nothing has changed, we don't have any work to do.
|
|
|
|
if (same_dimensions && same_fill_char) |
|
|
|
return; |
|
|
|
if (!same_dimensions) { |
|
|
|
// Note that this doesn't check for overflow. In theory that's unsafe, but
|
|
|
|
// really unlikely to happen here.
|
|
|
|
size_t term_cells = term_height * term_width; |
|
|
|
size_t new_mem_size = term_cells * sizeof(chtype); |
|
|
|
|
|
|
|
// 'realloc' is like malloc, but it lets you re-use a buffer instead of
|
|
|
|
// having to throw away an old one and create a new one. Oftentimes, the
|
|
|
|
// cost of 'realloc' is cheaper than 'malloc' for the C runtime, and it
|
|
|
|
// reduces memory fragmentation.
|
|
|
|
//
|
|
|
|
// It's called 'realloc', but you can also use it even you're starting out
|
|
|
|
// with a NULL pointer for your buffer.
|
|
|
|
vs->buffer = realloc(vs->buffer, new_mem_size); |
|
|
|
vs->size_y = term_height; |
|
|
|
vs->size_x = term_width; |
|
|
|
} |
|
|
|
if (!same_fill_char) { |
|
|
|
vs->fill_char = fill_char; |
|
|
|
} |
|
|
|
|
|
|
|
// (Re-)fill the buffer with the new data.
|
|
|
|
chtype* buff = vs->buffer; |
|
|
|
// For each row, in the buffer, fill it with an alternating pattern of spaces
|
|
|
|
// and the character the user typed.
|
|
|
|
for (int iy = 0; iy < term_height; ++iy) { |
|
|
@ -83,14 +70,16 @@ int main() { |
|
|
|
if ((iy + ix) % 2) { |
|
|
|
line[ix] = ' '; |
|
|
|
} else { |
|
|
|
line[ix] = ch; |
|
|
|
line[ix] = fill_char; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void draw_view_state(view_state* vs) { |
|
|
|
// Loop over each row in the buffer, and send the entire row to ncurses all
|
|
|
|
// at once. This is the fastest way to draw to the terminal with ncurses.
|
|
|
|
for (int i = 0; i < term_height; ++i) { |
|
|
|
for (int i = 0; i < vs->size_y; ++i) { |
|
|
|
// Move the cursor directly to the start of the row.
|
|
|
|
move(i, 0); |
|
|
|
// Send the entire line at once. If it's too long, it will be truncated
|
|
|
@ -101,19 +90,60 @@ int main() { |
|
|
|
// string. If we tried to use addchstr, it would keep trying to read until
|
|
|
|
// it got to the end of our buffer, and then past the end of our buffer
|
|
|
|
// into unknown memory, because we don't have a null terminator in it.
|
|
|
|
addchnstr(buff + i * term_width, term_width); |
|
|
|
addchnstr(vs->buffer + i * vs->size_x, vs->size_x); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
int main() { |
|
|
|
// Enable UTF-8 by explicitly initializing our locale before initializing
|
|
|
|
// ncurses.
|
|
|
|
setlocale(LC_ALL, ""); |
|
|
|
// Initialize ncurses
|
|
|
|
initscr(); |
|
|
|
// Allow ncurses to control newline translation. Fine to use with any modern
|
|
|
|
// terminal, and will let ncurses run faster.
|
|
|
|
nonl(); |
|
|
|
// Set interrupt keys (interrupt, break, quit...) to not flush. Helps keep
|
|
|
|
// ncurses state consistent, at the cost of less responsive terminal
|
|
|
|
// interrupt. (This will rarely happen.)
|
|
|
|
intrflush(stdscr, FALSE); |
|
|
|
// Receive keyboard input immediately, and receive shift, control, etc. as
|
|
|
|
// separate events, instead of combined with individual characters.
|
|
|
|
raw(); |
|
|
|
// Don't echo keyboard input
|
|
|
|
noecho(); |
|
|
|
// Also receive arrow keys, etc.
|
|
|
|
keypad(stdscr, TRUE); |
|
|
|
// Hide the terminal cursor
|
|
|
|
curs_set(0); |
|
|
|
|
|
|
|
// We don't need our buffer anymore. We call `free` to return it back to the
|
|
|
|
// operating system. If we don't do this, and we lose track of our `buff`
|
|
|
|
// pointer, the memory has leaked, and it can't be reclaimed by the OS until
|
|
|
|
// the program is terminated.
|
|
|
|
free(buff); |
|
|
|
view_state vs; |
|
|
|
init_view_state(&vs); |
|
|
|
|
|
|
|
// Refresh the terminal to make sure our changes get displayed immediately.
|
|
|
|
printw("Type any character to fill it in an alternating grid, or\ntype '"); |
|
|
|
attron(A_BOLD); |
|
|
|
printw("q"); |
|
|
|
attroff(A_BOLD); |
|
|
|
printw("' to quit\n"); |
|
|
|
refresh(); |
|
|
|
// Wair for the user's next input before terminating.
|
|
|
|
getch(); |
|
|
|
|
|
|
|
for (;;) { |
|
|
|
chtype ch = getch(); |
|
|
|
if (ch == 'q') |
|
|
|
break; |
|
|
|
// ncurses gives us the special value KEY_RESIZE if the user didn't
|
|
|
|
// actually type anything, but the terminal resized. If that happens to us,
|
|
|
|
// just re-use the fill character from last time.
|
|
|
|
if (ch == KEY_RESIZE) |
|
|
|
ch = vs.fill_char; |
|
|
|
int term_height = getmaxy(stdscr); |
|
|
|
int term_width = getmaxx(stdscr); |
|
|
|
assert(term_height >= 0 && term_width >= 0); |
|
|
|
update_view_state(&vs, term_height, term_width, ch); |
|
|
|
draw_view_state(&vs); |
|
|
|
refresh(); |
|
|
|
} |
|
|
|
deinit_view_state(&vs); |
|
|
|
endwin(); |
|
|
|
return 0; |
|
|
|
} |
|
|
|