Holiday snow on your terminal

0

I live in Minnesota and we haven’t gotten a lot of snow yet, so I figured I’d add a little “holiday cheer” by writing a program to add “gently falling snow” to my terminal instead. This program is also a good demonstration for how to update your terminal in character mode using the ncurses library.

Initialize the terminal

Before you can use any terminal-drawing routines from ncurses, your program will need to initialize the terminal. Most programs will use initscr() to set up the screen and set certain global values such as COLS for the number of columns and LINES for the number of lines, followed by cbreak() to disable buffering, and noecho() so characters typed by the user aren’t “echoed” on screen. These functions are defined in curses.h.

Some programs might also call intrflush(stdscr, FALSE) to disable “flushing” the screen output if the user presses an interrupt key. You can also use curs_set to control how the cursor should be displayed; use curs_set(0) to completely hide the cursor.

When the program is done, you must call endwin() to exit ncurses. Programs that simply do a thing and then end might wait for the user to press a key before returning to the command line. We can use the getch() function to return a single keystroke from the keyboard, which effectively waits for the user before taking action.

The basic outline for a program that uses ncurses to clear the screen and wait for the user to press a key looks like this:

#include <curses.h>

int
main()
{
  initscr();
  cbreak();
  noecho();

  intrflush(stdscr, FALSE);
  curs_set(0);

  getch();

  endwin();
  return 0;
}

Set up the screen

To write our “falling snow” program, we’ll first need to establish the “ground” on the terminal. The hline(ch,n) function in ncurses will draw a horizontal line of n characters at the current position on the screen. To draw the line at a specific location, we can use the move(y,x) function to set the position before calling hline(ch,n) to add the characters, or we can call mvhline(y,x,ch,n) to do both at the same time.

If we want to add text, such as a “Happy holidays” message, the addstr(s) function will add a string at the current screen position. We can use the similarly named mvaddstr(y,x,s) to add a string at a specific coordinate on the terminal.

Screen coordinates count from zero, so the top-left coordinate on the screen is 0,0 and the bottom-left position on the screen is LINES-1,0. We can use these coordinates to add a horizontal line and some text to the bottom of the screen:

  mvhline(LINES - 2, 0, ACS_HLINE, COLS);
  mvaddstr(LINES - 1, 0, "Let it snow!");
  refresh();

The refresh() function instructs ncurses to update the screen with the new text.

Gently falling snowflakes

To simulate the effect of a “gently falling snowflake” from the top of the screen, we’ll need to write a function called drop that draws an asterisk to represent a snowflake. Let’s define the function as drop(int row, int col) to start the snowflake at a specific coordinate. The function can continue to move the snowflake down the screen until it reaches some non-blank character, such as the border we drew with mvhline.

The addch(ch) function from ncurses adds a character at the current position on the screen. Like other functions that draw to the screen, we can use the mv variant of this function to first move to a specific location before adding the character. For example, to add an asterisk at row 5 and column 3, you would use mvaddch(5,3,'*').

We can use mvaddch again and again to reposition an asterisk. By adding a slight pause between iterations, we can create the effect of a single snowflake falling to the ground:

void
drop(int row, int col)
{
  /* drop a "snowflake" starting at row,col .. keep going until we run out
     of empty space */

  int y = row;

  mvaddch(y, col, '*');

  while (mvinch(y + 1, col) == ' ') {
    usleep(10000);                     /* 10000 microsecs = .01 secs */
    mvaddch(y, col, ' ');
    mvaddch(++y, col, '*');
    refresh();
  }
}

The usleep function is defined in unistd.h.

Randomize the snow

With our drop function to simulate a single snowflake, we can draw lots of snow falling one by one by using the same function across every column on the screen. But snow doesn’t fall from left to right; it’s random.

A simple way to randomize how the snow falls on our screen is to generate an array of all the column coordinates, then “shuffle” the array so all the coordinates appear in random order. Initializing the array is easy; that’s just assigning a number to each member in the array.

One way to randomize the array is to swap each element with another element chosen at random. The most effective way to do this is by “walking” backwards through the array and choosing another random member that comes before it:

void
randfill(int *array, int nsize)
{
  srand(time(NULL));                   /* pseudorandom but works anywhere */

  for (int i = 0; i < nsize; i++) {
    array[i] = i;
  }

  for (int i = nsize - 1; i > 0; i--) {
    swap(array, i, rand() % (i + 1));  /* pick a random number 0..i */
  }
}

The Linux kernel provides a getrandom system call that generates a series of random bytes, which effectively produces a random number. To allow you to compile this program on other systems, I’ve used the rand function from the C standard library. This produces pseudorandom values, which aren’t completely random, but good enough for a “falling snow” program—and it works everywhere.

I could have written the “swap” feature inside the second loop, but I thought the code would be a little more readable if I moved that into a separate function called swap that swaps two elements of an array:

void
swap(int *ary, int i, int j)
{
  /* swap two elements i and j */
  int tmp = ary[i];
  ary[i] = ary[j];
  ary[j] = tmp;
}

Let it snow

With these functions, we can now update the main program to draw the “ground” and some text on the terminal, then simulate gently falling snow. So the program doesn’t take forever to run, this only fills up the bottom five lines with “snow.” Or if you’ve defined a really small terminal window that’s less than ten lines, the program fills up about half of the screen.

#include <stdlib.h>
#include <time.h>

#include <curses.h>
#include <unistd.h>

#include <sys/param.h>

void
swap(int *ary, int i, int j)
{
  /* swap two elements i and j */
  int tmp = ary[i];
  ary[i] = ary[j];
  ary[j] = tmp;
}

void
randfill(int *array, int nsize)
{
  srand(time(NULL));                   /* pseudorandom but works anywhere */

  for (int i = 0; i < nsize; i++) {
    array[i] = i;
  }

  for (int i = nsize - 1; i > 0; i--) {
    swap(array, i, rand() % (i + 1));  /* pick a random number 0..i */
  }
}

void
drop(int row, int col)
{
  /* drop a "snowflake" starting at row,col .. keep going until we run out
     of empty space */

  int y = row;

  mvaddch(y, col, '*');

  while (mvinch(y + 1, col) == ' ') {
    usleep(10000);                     /* 10000 microsecs = .01 secs */
    mvaddch(y, col, ' ');
    mvaddch(++y, col, '*');
    refresh();
  }
}

int
main()
{
  int *colx;

  initscr();
  cbreak();
  noecho();

  intrflush(stdscr, FALSE);
  curs_set(0);                         /* hide cursor */

  /* allocate the x coords */

  colx = calloc(sizeof(int), COLS);

  if (colx == NULL) {                  /* error */
    endwin();
    return 1;
  }

  randfill(colx, COLS);

  /* set up the screen */

  mvhline(LINES - 2, 0, ACS_HLINE, COLS);
  mvaddstr(LINES - 1, 0, "Let it snow!");
  refresh();

  /* snow */

  for (int row = 0; row < MIN(5, LINES / 2); row++) {
    for (int c = 0; c < COLS; c++) {
      drop(0, colx[c]);
    }
  }

  /* done */

  mvaddstr(LINES - 1, 0, "press any key to quit . . .");
  getch();

  endwin();

  free(colx);
  return 0;
}

Save the program as snow.c, then compile and run it to simulate gently falling snow on your terminal:

$ gcc -Wall -o snow snow.c -lncurses
Screenshot of a program showing gently falling snow on a Linux terminal
Screenshot of our program showing gently falling snow on a Linux terminal.

Just in case you want to play with this little program without compiling it yourself, here’s a link to snow. Download it to your system and change the permissions to 555. If it’s in your path, launch it from your terminal session by typing snow. If it’s not in the path, prepend the path to the program name. Use ./snow if it’s in the current directory.