2

The code is a bit long, but that's only because I commented everything so it's very easy to read. Basically this is a simple text-based selection menu that I'm working on. You need to be on Linux and have a C++11 compiler to run this properly. Here's the code (fully functional example, ready to compile):

#include <string>
#include <vector>
#include <iostream>
#include <unistd.h>
#include <sys/ioctl.h>
#include "raw_terminal.h" // for setting the terminal to raw mode
using namespace std;

/* Simple escape sequences to control the cursor and colors on the screen */
#define CLI_HIDE_CUR            "\033[?25l"
#define CLI_SHOW_CUR            "\033[?25h"
#define CLI_SAVE_CUR_POS        "\033[s"
#define CLI_REST_CUR_POS        "\033[u"
#define CLI_CLR_LAST_LINE       "\033[A\033[2K"
#define CLI_MV_CUR_UP           "\033[A"
#define CLI_MV_CUR_DN           "\033[B"
#define CLI_DEFAULT_COLOR       "\033[0m"
#define CLI_FGROUND_BLACK       "\033[0;30m"
#define CLI_FGROUND_RED         "\033[0;31m"
#define CLI_FGROUND_GREEN       "\033[0;32m"
#define CLI_FGROUND_BROWN       "\033[0;33m"
#define CLI_FGROUND_BLUE        "\033[0;34m"
#define CLI_FGROUND_MAGENTA     "\033[0;35m"
#define CLI_FGROUND_CYAN        "\033[0;36m"
#define CLI_FGROUND_LIGHTGREY   "\033[0;37m"
#define CLI_BOLD                "\033[0;1m"
#define CLI_BGROUND_BLACK       "\033[7;30m"
#define CLI_BGROUND_RED         "\033[7;31m"
#define CLI_BGROUND_GREEN       "\033[7;32m"
#define CLI_BGROUND_BROWN       "\033[7;33m"
#define CLI_BGROUND_BLUE        "\033[7;34m"
#define CLI_BGROUND_MAGENTA     "\033[7;35m"
#define CLI_BGROUND_CYAN        "\033[7;36m"
#define CLI_BGROUND_LIGHTGREY   "\033[7;37m"


/* Centers a string in a 'width' character wide terminal
 by appending spaces before and after the string */
auto centerText(string& text, int width) -> void;

/* Creates a selection menu on the screen */
auto selectionMenu(std::vector<std::string> items) -> int;

int main() {
    std::vector<string> items;
    items.push_back("Menu item one");
    items.push_back("Menu item two");
    items.push_back("Menu item three");

    int selection = selectionMenu(items);
    if (selection == -1) return 0;
    cout << "You selected: " << items[selection] << endl;

    cin.get();

    return 0;
}

auto centerText(string& text, int width) -> void {
    size_t len = text.length();
    if (width <= len+1) return;
    for (int i=0; i<(width - len)/2; i++) text.insert(text.begin(), ' ');
    for (int i=0; i<(width - len)/2+len%2; i++) text.push_back(' ');
    return;
}

auto selectionMenu(std::vector<std::string> items) -> int {
    /* This stuff is required to get the width of the terminal
     in order to center the text on the screen with centerText() */
    struct winsize w;
    ioctl(0, TIOCGWINSZ, &w);

    /* Hide the cursor, initialize some variables */
    cout << CLI_HIDE_CUR << CLI_DEFAULT_COLOR << endl;
    int selection = 0, prevSelection, key;

    /* Center the menu items on the screen */
    for (const auto& s : items) centerText(s, w.ws_col);

    /* Print out the menu items */
    for (const auto& s : items) cout << s << endl;

    /* Highlight the first item */
    for (int i=0; i<items.size(); i++) cout << CLI_MV_CUR_UP;
    cout << CLI_BGROUND_BROWN << items[selection] << endl;

    /* Configure stuff so that we're able to retrieve raw keystrokes from stdin */
    raw_terminal::setRawTerminal();

    /* If the enter key is down, wait until it's released.
     This prevents the user from accidentally selecting
     an item after hitting enter in a previous menu. */
    while (getchar() == '\n') { usleep(1000); }

    /* Main loop */
    while (1) {
        key = getchar();

        /* We're only interested in escape sequences (starting with '\033') */
        if (key == '\033') {

            /* If nothing comes after the escape character, then esc was pressed, so we quit. */
            if (getchar() == -1) {
                selection = -1;
                goto MENU_END;
            }

            /* Get the next character in the received sequence */
            key = getchar();

            /* up arrow */
            if (key == 65) {
                prevSelection = selection;
                selection--;
            }

            /* down arrow */
            else if (key == 66) {
                prevSelection = selection;
                selection++;
            }

            /* If (first item - 1) or (last item + 1) is selected, loop around */
            if (selection < 0) selection = items.size()+selection;
            if (selection > items.size()-1) selection -= items.size();

            /* Draw the previously selected line with the default colors */
            cout << CLI_MV_CUR_UP << CLI_DEFAULT_COLOR << items[prevSelection] << endl;
            cout << CLI_MV_CUR_UP;

            /* Move the cursor to the new selection */
            if (selection < prevSelection) for (int i=0; i<(prevSelection-selection); i++) cout << CLI_MV_CUR_UP;
            if (selection > prevSelection) for (int i=0; i<(selection-prevSelection); i++) cout << CLI_MV_CUR_DN;

            /* Draw the newly selected line with the highlighting color */
            cout << CLI_BGROUND_BROWN << items[selection] << endl;

        }

        /* If the retrieved key is not an escape sequence, check whether it's the enter key.
         If so, break the main loop and return the selected item's number. */
        else if (key == '\n') break;
    }

MENU_END:
    cout << CLI_DEFAULT_COLOR;

    /* Position the cursor below the menu to continue */
    for (int i=0; i<items.size()-selection; i++) cout << CLI_MV_CUR_DN;

    /* Unhide the cursor, and set the terminal back to normal mode */
    cout << CLI_SHOW_CUR << endl;
    raw_terminal::restoreTerminal();

    /* Return selected item's number */
    return selection;
}

And this is raw_terminal.h :

#ifndef _RAW_TERMINAL_H_
#define _RAW_TERMINAL_H_

#include <cstring>
#include <iostream>
#include <termios.h>

class raw_terminal {
public:
    static void setRawTerminal() {
        /* set the terminal to raw mode */
        if (isRaw) return;
        tcgetattr(fileno(stdin), &orig_term_attr);
        memcpy(&new_term_attr, &orig_term_attr, sizeof(struct termios));
        new_term_attr.c_lflag &= ~(ECHO|ICANON);
        new_term_attr.c_cc[VTIME] = 0;
        new_term_attr.c_cc[VMIN] = 0;
        tcsetattr(fileno(stdin), TCSANOW, &new_term_attr);
        isRaw = true;
    }
    static void restoreTerminal() {
        tcsetattr(fileno(stdin), TCSANOW, &orig_term_attr);
        isRaw = false;
    }
private:
    static struct termios orig_term_attr;
    static struct termios new_term_attr;
    static bool isRaw;
};

struct termios raw_terminal::orig_term_attr;
struct termios raw_terminal::new_term_attr;
bool raw_terminal::isRaw = false;

#endif

It works just fine if you're only using the up/down arrows. But if you press either left or right (which shouldn't do anything) it totally messes up the screen, duplicating menu items and stuff. The problem is not in the code I guess, because it completely ignores the left and right keys. It's the terminal that does something when those are pressed I guess. So how could I prevent that from happening?

Any help would be appreciated. Thanks!

notadam
  • 2,754
  • 2
  • 19
  • 35

1 Answers1

1

Wow, this kinda shocked me. I solved the problem by including the left and right arrows in the program like so:

if (key == 65 || key == 68) // ...
if (key == 66 || key == 67) // ...

This way the left and right keys alter the selection normally. I originally wanted to ignore those keys and only use up/down, but this is still better than having the screen go crazy.

notadam
  • 2,754
  • 2
  • 19
  • 35