Going from raw to cooked terminal mode sometimes fails to work on OSX--but seems to work fine on Linux
Thanks so much for this library--it plays a key role in an open-source typing test game that I have been working on. This game uses raw mode during the actual typing test (which allows it to respond instantly to players' input), but cooked mode during other parts. Thus, there's lots of transitioning between raw and cooked mode (and back).
These transitions seem to work fine on Linux and Windows, but on OSX, the game sometimes hangs when the player enters cooked input following a typing test. I created a very simplified version of the game code in order to make this issue easier to debug. (It only needs the cpp-terminal library to run, so feel free to try cloning it and running it on your end; you'd just need to adjust my CMakeLists.txt file to account for your own cpp-terminal setup. I know that hardcoding my path to the library isn't good practice.)
The raw_to_cooked_terminal_test.cpp code runs fine on Linux, and initially, it seemed to run fine on OSX. (I haven't tested it on Windows yet.) However, after 5 or 6 calls of the example's type_phrase function within OSX, I didn't get any response when entering 200, followed by Return within the main loop. Eventually, the program did call type_phrase, but Ctrl+JCtrl+JCtrl+JCtrl+JCtrl+JCtrl+J appeared on the screen, indicating that the game was having trouble processing my initial cooked entries.
I imagine that I'm missing something very simple here; I have very little OSX experience, though, so any help with debugging this code would be greatly appreciated. (I'm also considering updating the code so that all user input is carried out in raw mode; this would be easy for this simplified program, but a bit more challenging (though still doable) for the actual game.)
Finally, for reference, here's a copy of the raw_to_cooked_terminal_test.cpp game code: (Apologies for the indentations--they could definitely be improved.)
//Raw to cooked terminal test:
// A simple typing test script for testing raw-to-cooked
// transitions using the cpp-terminal library
// on Linux and Mac
// Released under the MIT license
#include <iostream>
#include <chrono>
#include "cpp-terminal/terminal.hpp"
#include "cpp-terminal/color.hpp"
#include "cpp-terminal/cursor.hpp"
#include "cpp-terminal/exception.hpp"
#include "cpp-terminal/input.hpp"
#include "cpp-terminal/iostream.hpp"
#include "cpp-terminal/key.hpp"
#include "cpp-terminal/options.hpp"
#include "cpp-terminal/screen.hpp"
#include "cpp-terminal/terminal.hpp"
#include "cpp-terminal/tty.hpp"
#include "cpp-terminal/version.hpp"
// Defining colors that represent correct and incorrect output:
// (This code is based on the Hello World example for
// cpp-terminal at https://github.com/jupyter-xeus/cpp-terminal .)
std::string correct_output_color_code = Term::color_fg(
Term::Color::Name::Green);
std::string incorrect_output_color_code = Term::color_fg(
Term::Color::Name::Red);
std::string default_output_color_code = Term::color_fg(
Term::Color::Name::Default);
std::string print_color_code = default_output_color_code; // This
// variable will get updated within typing tests to reflect either
// correct or incorrect output.
void type_phrase(std::string gameplay_option)
{
std::string phrase;
if (gameplay_option == "100")
{phrase = "It's SO over.";}
else if (gameplay_option == "200")
{phrase = "We're SO back!";}
else
{Term::cout << "Unknown phrase." << std::endl;
return;}
int phrase_length = phrase.size();
std::string user_string;
// // Clearing the console and displaying the verse to type:
Term::cout << Term::clear_screen() << Term::terminal.clear()
<< Term::cursor_move(
1, 1) << phrase << std::endl;
// Defining a string that will allow the cursor to get moved
// directly below the phrase: (This assumes that the phrase
// is only one line long, which is indeed the case within
// this example.)
std::string cursor_reposition_code = Term::cursor_move(
2, 1);
Term::cout << cursor_reposition_code << "\033[J" << std::endl;
Term::terminal.setOptions(Term::Option::Raw);
auto start_time = std::chrono::high_resolution_clock::now();
while (user_string != phrase)
{
Term::Event event = Term::read_event();
switch (event.type())
{
case Term::Event::Type::Key:
{
Term::Key key(event);
std::string char_to_add = "";
std::string keyname = key.name();
if (keyname == "Space")
{
char_to_add = " ";
}
else if (keyname == "Backspace") // We'll need to remove the
// last character from our string.
{
user_string = user_string.substr(
0, user_string.length() - 1);
}
else
{
char_to_add = keyname;
}
user_string += char_to_add;
/* Determining how to color the output: (If the output is correct
so far, it will be colored green; if there is a mistake, it will
instead be colored red.*/
if (user_string == phrase.substr(0, user_string.length()))
{print_color_code = correct_output_color_code;}
else
{print_color_code = incorrect_output_color_code;}
Term::cout << cursor_reposition_code << "\033[J" <<
print_color_code << user_string
<< default_output_color_code <<std::endl;
break;
}
default:
break;
}
}
auto end_time = std::chrono::high_resolution_clock::now();
auto test_seconds = std::chrono::duration<double>(
end_time - start_time).count();
double wpm = (phrase_length / test_seconds) * 12; /* To calculate WPM,
we begin with characters per second, then multiply by 60 (to go
from seconds to minutes) and divide by 5 (to go from characters
to words using the latter's standard definition).*/
Term::cout << "You typed the phrase at "
<< wpm << " words per minute." << std::endl;
}
int main() {
std::string gameplay_option;
while (gameplay_option != "quit")
{
Term::cout << "Please enter 100 or 200 to choose which phrase \
to type, followed by Enter. Your typing test will then begin; \
once you enter the text correctly, the test will finish \
automatically. You may also enter 'quit' to quit." << std::endl;
Term::terminal.setOptions(Term::Option::Cooked);
gameplay_option.clear();
Term::cin >> gameplay_option;
if (gameplay_option != "quit")
{type_phrase(gameplay_option);}
}
Term::cout << "Finished running test." << std::endl;
}
Note: I realized that it would also be advantageous to have Linux and Windows process all inputs in raw mode; that way, users could get instant feedback for certain entries (such as 'y' to confirm an operation). Therefore, I went ahead and replaced my existing Term::cin calls with code that would call one of the two functions below. The updated code seems to work fine on both Linux and Mac; I imagine it will work well on Windows also, but I'll make sure to test it.
cooked_input_within_raw_mode() simulates a 'cooked' environment within raw mode; it allows users to modify their input (via backspace) before submitting it via enter. Meanwhile, get_single_keypress() retrieves, then responds to a single key entry. (These functions, like my actual typing test game, are released under the MIT license.)
std::string cooked_input_within_raw_mode(
std::string prompt = "", bool multiline=false)
{/* This function aims to provide 'cooked-like' input within a
raw mode cpp-terminal session. It does so by allowing users to add
to a string until they press enter.
If the input may be more than one line long, set multiline to true.
The function will then clear the console and display only the
prompt, thus making multi-line input easier to handle.*/
int starting_result_row = 2; // Defining the row at which
// the player's input should be typed. (Will only be used
// for multiline results, as otherwise, it would likely
// overwrite existing information on the screen.)
std::string user_string;
// Defining a string that will allow the cursor to get moved
// directly below the prompt:
std::string cursor_reposition_code = Term::cursor_move(
starting_result_row, 1);
if (multiline == true)
{
int prompt_length = prompt.size();
// Determining on which row to begin printing the user's
// input: (See similar code within run_test() for more
// details.)
Term::Screen term_size{Term::screen_size()};
starting_result_row = (prompt_length - 1) / (
term_size.columns()) + 2;
// Clearing the console and displaying the prompt:
// (Note that, unlike within run_test(), clear() isn't
// called here, as the usuer might want to be able to scroll
// up to see previous input.
Term::cout << Term::clear_screen()
<< Term::cursor_move(
1, 1) << prompt << std::endl;
// Calling cursor_move to determine the cursor reposition
// code that will bring the cursor right under the verse
// after each keypress:
cursor_reposition_code = Term::cursor_move(
starting_result_row, 1);
}
else
{//Printing the prompt, then adding two newlines (including that
// printed by std::endl) to give
// the player more space to enter his/her response:
Term::cout << prompt << "\n" << std::endl;
// If multiline isn't active, the cursor reposition code
// will be set as "\r", the carriage return call, which
// will move the cursor back to the start of the line.
cursor_reposition_code = "\r";
}
std::string keyname;
while (keyname != "Enter") // May also need to add 'Return'
// or 'Ctrl+J' here also for OSX compatibility)
{
Term::Event event = Term::read_event();
switch (event.type())
{
case Term::Event::Type::Key:
{
Term::Key key(event);
std::string char_to_add = "";
keyname = key.name();
if (keyname == "Space")
{
char_to_add = " ";
}
else if (keyname == "Backspace") // We'll need to remove the
// last character from our string.
{
user_string = user_string.substr(
0, user_string.length() - 1);
}
else if (keyname == "Enter")
{break;}
else
{
char_to_add = keyname;
}
user_string += char_to_add;
// See run_test documentation for more information about
// "\033[J". This code repositions the cursor (either
// to the beginning of the current line if multiline
// is false or to the beginning of the line below the
// prompt if multiline is true); clears out everything
// past this point; then shows what the user has entered
// so far.
Term::cout << cursor_reposition_code << "\033[J" <<
user_string << std::flush; // Using std::flush rather
// than std::endl so that we can begin our new
// entry from the same line (which will be useful in
// non-multiline mode).
}
default:
{break;}
}
}
//For debugging:
// Term::cout << "The string you entered was:\n"
// << user_string << std::endl;
return std::move(user_string);
}
std::string get_single_keypress(std::vector<std::string>
valid_keypresses = {})
/* This function retrieves a single keypress, then responds
immediately to it.
The valid_keypresses argument allows the caller to specify
which keypresses should be considered valid entries. If
the default option is kept, any keypress will be considered
valid. */
{
std::string single_key;
bool valid_keypress_entered = false;
while (valid_keypress_entered == false)
{
Term::Event event = Term::read_event();
switch (event.type())
{
case Term::Event::Type::Key:
{
Term::Key key(event);
single_key = key.name();
if (valid_keypresses.size() > 0)
{
for (std::string valid_keypress: valid_keypresses)
{
if (single_key == valid_keypress)
{valid_keypress_entered = true;}
}
if (valid_keypress_entered == false)
{Term::cout << single_key << " isn't a valid entry. Please \
try again." << std::endl;}
}
else // In this case, we'll assume the keypress
// to be valid.
{
valid_keypress_entered = true;}
}
default:
{break;}
}
}
return single_key;
}
Thanks for the bug report. We need to debug it to see what the problem is on macOS.
I am happy the library is useful to your project.
Hi, thx for the bug report. I will try to investigate the problem. However on macos it is a bit difficult for me because I don't have a mac in hand. For last developments I used a VM withvquite an old version of the OS