diff --git a/main.c b/main.c index f9dcb43..acdae78 100644 --- a/main.c +++ b/main.c @@ -2,72 +2,59 @@ #include #include #include +#include #include -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; }