11

I'm using a console in my multithreaded application. Right now, it only accepts output (printf and the like) and so far I have no issues. However, I want to be able to support console input as well, and this is where my life gets complicated.

To forewarn, I'm very unfamiliar with the more complicated nuances of working with console input and output. My experience in the subject doesn't go much further than printf/cout, scanf/cin, and using SetConsoleTextAttribute() to change the color (on windows).

I would prefer to keep my program as cross-compatible as possible, but I'm not opposed to having to write platform-specific code, as long as I can find viable alternatives for other platforms.

Conceptually, I'd like the console to run on it's own thread, so that it can lock up while waiting with cin without freezing the whole program or one of the other threads. Any thread could send console output to this thread which would output it in a clean manner (probably using a thread-safe queue), and any input that the console reads would send the command off to the appropriate thread.

My first problem is that while I'm typing some input, any output will show up in the middle of what I'm typing. The solution I would like to handle this would be to reserve the bottom line of the console for input, and have output go to the second last line, pushing the input line down. How can I do this?

Haydn V. Harach
  • 1,265
  • 1
  • 18
  • 36

3 Answers3

2

You really don't want to go down the road of trying to reserve part of the console for input while writing to the rest of the console. At least, not if you're just writing scrolling text. It's possible, but fraught with error and way more trouble than it's worth. See Async Console Output for a few hints of the problems.

Certainly, it's not possible to do this using just conio.h.

You could allocate two console screen buffers, with one being for input and one for program output. When your program is running normally, the output screen buffer is selected and you see the output scrolling on the screen. But when your program is waiting for user input, you swap screen buffers so that the output is still going, but in the other screen buffer.

You end up having to format the output yourself and call WriteConsoleOutput, passing it the handle of the screen buffer you want to write to. It gets complicated in a real hurry, and it's very difficult to get right. If it's even possible. I know I've spent way too much time on it in the past, and there were always odd problems.

I won't say that what you want to do isn't possible. I will say, however, that you're going to have a tough time with it.

Community
  • 1
  • 1
Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • The idea is that multiple threads are writing to the console output while another thread is waiting for input. I just want to avoid clashes. The problem is that "when your program is running normally" and "when your program is waiting for user input" are both happening at the same time, all the time. Right now I'm looking into pdcurses. – Haydn V. Harach Mar 11 '14 at 04:16
  • @HaydnV.Harach: You could have the output thread always writing to the console. You could then swap output buffers when the user presses a key, and leave the output suspended (hidden, really) until the user finishes his input, or until some timeout expires (so if the user walks away without pressing Enter, output will resume). It's perhaps an imperfect solution, but it could work. – Jim Mischel Mar 12 '14 at 16:21
  • I'm okay with going through the extra work to get it done properly. Right now I'm using pdcurses, but I've hit a couple of stumbling blocks, namely, how to get text to scroll (right now it writes until it hits the bottom of the window, then gives up), and how to get two "windows" one the screen at once, each one receiving output (one for actual output messages, and one for echoed input). – Haydn V. Harach Mar 20 '14 at 05:02
  • Downvoter? It's customary to leave a comment explaining what's wrong with the answer. – Jim Mischel Oct 11 '16 at 16:54
2

Wellp, I solved it using pdcurses. In case someone else wants to do something similar, here's how I did it. First, I initialize the console thusly:

Console::Console(bool makeConsole)
{
    if (makeConsole == false)
        return;

    if (self)
        throw ("You only need one console - do not make another!\n");
    self = this;

#ifdef WIN32
    AllocConsole();
#endif
    initscr();

    inputLine = newwin(1, COLS, LINES - 1, 0);
    outputLines = newwin(LINES - 1, COLS, 0, 0);

    if (has_colors())
    {
        start_color();
        for (int i = 1; i <= COLOR_WHITE; ++i)
        {
            init_pair(i, i, COLOR_BLACK);
        }
    }
    else
        wprintw(outputLines, "Terminal cannot print colors.\n");

    scrollok(outputLines, TRUE);
    scrollok(inputLine, TRUE);

    leaveok(inputLine, TRUE);
    nodelay(inputLine, TRUE);
    cbreak();
    noecho();
    keypad(inputLine, TRUE);

    initCommands();

    hello("Starting %s.\n", APP_NAME);
    hellomore("Version %i.%i.%i.\n\n", APP_MAJORVER, APP_MINORVER, APP_REVISION);
}

Next, This is the function responsible for handling output. It's actually very simple, I don't need to do anything special to keep it thread-safe. I might simply not have encountered any issues with it, but an easy fix would be to slap a mutex on it.

void Console::sendFormattedMsg(short prefixColor, const char* prefix, short color, const char* format, ...)
{
    if (!self)
        return;

    va_list args;
    va_start(args, format);

    if (has_colors())
    {
        if (prefix)
        {
            wattron(outputLines, A_BOLD | COLOR_PAIR(prefixColor));
            wprintw(outputLines, prefix);
        }

        if (color == COLOR_WHITE)
            wattroff(outputLines, A_BOLD);
        wattron(outputLines, COLOR_PAIR(color));
        vwprintw(outputLines, format, args);

        wattroff(outputLines, A_BOLD | COLOR_PAIR(color));
    }
    else
    {
        wprintw(outputLines, prefix);
        vwprintw(outputLines, format, args);
    }

    wrefresh(outputLines);
    va_end(args);
}

And finally, input. This one required quite a bit of fine-tuning.

void Console::inputLoop(void)
{
    static string input;

    wattron(inputLine, A_BOLD | COLOR_PAIR(COLOR_WHITE));
    wprintw(inputLine, "\n> ");
    wattroff(inputLine, A_BOLD | COLOR_PAIR(COLOR_WHITE));

    wprintw(inputLine, input.c_str());
    wrefresh(inputLine);

    char c = wgetch(inputLine);
    if (c == ERR)
        return;

    switch (c)
    {
    case '\n':
        if (input.size() > 0)
        {
            sendFormattedMsg(COLOR_WHITE, "> ", COLOR_WHITE, input.c_str());
            cprint("\n");

            executeCommand(&input[0]);
            input.clear();
        }
        break;

    case 8:
    case 127:
        if (input.size() > 0) input.pop_back();
        break;

    default:
        input += c;
        break;
    }
}

This is run every frame from the same thread that handles window messages. I disabled wgetch()'s blocking behavior using nodelay(), eliminating the need to have console input running in it's own thread. I also disable echoing and echo the input manually. Enabling scrolling on the input window allows me to clear it's contents using a simple "\n", replacing it with updated contents if the user has typed anything. It supports everything one would expect from a simple, multi-threaded terminal capable to typing input as well as receiving output from multiple threads.

Haydn V. Harach
  • 1,265
  • 1
  • 18
  • 36
1

To disable echoing characters check this out: Reading a password from std::cin

Maybe combine that with this guy's blog post on non-blocking Win32 console io.

You might also find this stuff useful: conio.h, pdcurses

Community
  • 1
  • 1
erapert
  • 1,383
  • 11
  • 15
  • I'm already using conio.h (press any key to continue is very handy to have). I do want characters typed for input commands to be echoed, I just don't want output message to happen to in the middle of them. – Haydn V. Harach Mar 10 '14 at 23:43
  • check the first link, it shows how to disable echoing the pressed characters. – erapert Mar 11 '14 at 00:28