Programming curses in Rust, Part 2

In the previous episode, we talked about ncurses and started exploring pancurses. Now we will introduce the rest of the functionality in pancurses that we need for mini watch:

  • positioning the cursor

  • reacting to a window resizing event

  • detecting a timeout

Positioning the cursor

For positioning the cursor, we use mv:

pub fn mv(&self, y: i32, x: i32)

For clearing the window, we use clear:

pub fn clear(&self) -> i32

Here's a little program that utilizes both:

use pancurses::{initscr, endwin, curs_set};
use pancurses;

fn main() {
    let window = initscr();
    window.mv(0, 0);
    window.printw("Hello Rust");
    window.refresh();

    window.getch();
    window.mv(10, 0);
    window.printw("hello again!");
    window.refresh();

    window.getch();
    window.mv(10, 20);
    window.printw("and again!");
    window.refresh();

    window.getch();
    window.clear();
    window.refresh();

    window.getch();
    window.mv(0, 20);
    window.printw("bye now!");
    window.refresh();

    window.getch();
    endwin();
}

Note the order of arguments in mv: it's mv(y, x), where y is the line, counting from the top, and x is the distance from the left edge of the window.

Reacting to a window resizing event

We will need to be able to detect a screen resize event and to react to it. The way it is done in pancurses (and ncurses) is to call method getch check the value it returns: if the window was resized, the value will be Input::KeyResize.

Here is a simple code that demonstrates this:

use pancurses::{initscr, endwin, curs_set, Input};
use pancurses;

fn main() {
    let window = initscr();
    window.printw("Hello Rust");
    pancurses::curs_set(0);
    window.refresh();

    loop {
        let ch = window.getch().unwrap();
        if ch == Input::KeyResize {
            window.clear();
            let width = window.get_max_x();
            let height = window.get_max_y();
            let x = height/2;
            let y = width/2;
            window.mv(x, y);
            window.printw(format!("resized: {}x{}", width, height));
            window.refresh();
        } else {
            break;
        }
    }

    endwin();
}

In this code, when we get a KeyResize event, we get the new window width and height and print a message in the middle of the window.

Detecting a timeout

We set the timeout by calling the timeout method. Then, when we call getch, it will return Some(code) if the user clicked on a key, or None if we timed out.

This sample keeps moving an "x" in the window, one step to the right every second:

use pancurses::{initscr, endwin, curs_set, Input};
use pancurses;

fn main() {
    let window = initscr();
    pancurses::curs_set(0);
    window.timeout(1000);
    window.refresh();

    let mut x = 0;
    loop {
        match window.getch() {
            Some(_) => break,
            None => { // timeout
                x = x + 1;
                if x == 20 {
                    x = 0;
                }
                window.clear();
                window.mv(5, x);
                window.printw("x");
                window.refresh();
            }
        }
    }

    endwin();
}

Get the code at 2023-10-22-watch.

In the next episode, we run a program in a subprocess and capture its standard output.