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?
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With