From 1ac1104535cd5da1493a3c0642747b19f0e87163 Mon Sep 17 00:00:00 2001 From: Martchus Date: Fri, 28 May 2021 19:15:09 +0200 Subject: [PATCH] Add workaround for Windows-specific console problems * Allow disabling the hack for creating a console for a GUI application via `ENABLE_CONSOLE=0` to workaround downsides of this hack (pipes not working, possibly spawns an additional console) * Set the console's character set to UTF-8 by default because this actually seems to work now and non-ASCII characters are displayed correctly. There is still an opt-out via `ENABLE_CP_UTF8=0`. * Note that with mintty it just works anyways so using that terminal emulator is still the best workaround. --- CMakeLists.txt | 6 --- application/argumentparser.cpp | 44 +++++++++------- application/argumentparserprivate.h | 4 ++ application/commandlineutils.cpp | 81 ++++++++++++++++------------- 4 files changed, 76 insertions(+), 59 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 913f971..c9186c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -171,12 +171,6 @@ else () message(WARNING "The use of std::filesystem has been disabled. Bash completion for files and directories will not work.") endif () -# configure forcing UTF-8 code page under Windows -option(FORCE_UTF8_CODEPAGE "forces use of UTF-8 code page under Windows via ApplicationUtilities::startConsole()" OFF) -if (FORCE_UTF8_CODEPAGE) - list(APPEND META_PRIVATE_COMPILE_DEFINITIONS ${META_PROJECT_VARNAME}_FORCE_UTF8_CODEPAGE) -endif () - # configure whether escape codes should be enabled by default option(ENABLE_ESCAPE_CODES_BY_DEAULT "enables usage of escape codes by default" ON) if (ENABLE_ESCAPE_CODES_BY_DEAULT) diff --git a/application/argumentparser.cpp b/application/argumentparser.cpp index 4fea820..201a0be 100644 --- a/application/argumentparser.cpp +++ b/application/argumentparser.cpp @@ -32,6 +32,27 @@ using namespace CppUtilities::EscapeCodes; */ namespace CppUtilities { +/*! + * \brief Returns whether the specified env variable is set to a non-zero and non-white-space-only value. + */ +std::optional isEnvVariableSet(const char *variableName) +{ + const char *envValue = std::getenv(variableName); + if (!envValue) { + return std::nullopt; + } + for (; *envValue; ++envValue) { + switch (*envValue) { + case '0': + case ' ': + break; + default: + return true; + } + } + return false; +} + /*! * \brief The ArgumentDenotationType enum specifies the type of a given argument denotation. */ @@ -1728,27 +1749,14 @@ NoColorArgument::NoColorArgument() { setCombinable(true); - // set the environmentvariable: note that this is not directly used and just assigned for printing help + // set the environment variable (not directly used and just assigned for printing help) setEnvironmentVariable("ENABLE_ESCAPE_CODES"); - // default-initialize EscapeCodes::enabled from environment variable - const char *envValue = getenv(environmentVariable()); - if (!envValue) { - return; + // initialize EscapeCodes::enabled from environment variable + const auto escapeCodesEnabled = isEnvVariableSet(environmentVariable()); + if (escapeCodesEnabled.has_value()) { + EscapeCodes::enabled = escapeCodesEnabled.value(); } - for (; *envValue; ++envValue) { - switch (*envValue) { - case '0': - case ' ': - break; - default: - // enable escape codes if ENABLE_ESCAPE_CODES contains anything else than spaces or zeros - EscapeCodes::enabled = true; - return; - } - } - // disable escape codes if ENABLE_ESCAPE_CODES is empty or only contains spaces and zeros - EscapeCodes::enabled = false; } /*! diff --git a/application/argumentparserprivate.h b/application/argumentparserprivate.h index 6a24bb7..0725135 100644 --- a/application/argumentparserprivate.h +++ b/application/argumentparserprivate.h @@ -4,6 +4,8 @@ #include "./argumentparser.h" #include "./commandlineutils.h" +#include + namespace CppUtilities { class CPP_UTILITIES_EXPORT ArgumentReader { @@ -56,6 +58,8 @@ inline Wrapper::Wrapper(const char *str, Indentation currentIndentation) { } +std::optional isEnvVariableSet(const char *variableName); + } // namespace CppUtilities #endif // APPLICATION_UTILITIES_ARGUMENTPARSER_PRIVATE_H diff --git a/application/commandlineutils.cpp b/application/commandlineutils.cpp index 0402546..eef1d05 100644 --- a/application/commandlineutils.cpp +++ b/application/commandlineutils.cpp @@ -1,4 +1,5 @@ #include "./commandlineutils.h" +#include "./argumentparserprivate.h" #include #include @@ -73,52 +74,62 @@ void stopConsole() fclose(stdout); fclose(stdin); fclose(stderr); - if (auto *consoleWindow = GetConsoleWindow()) { + if (auto *const consoleWindow = GetConsoleWindow()) { PostMessage(consoleWindow, WM_KEYUP, VK_RETURN, 0); FreeConsole(); } } /*! - * \brief Starts the console and sets the console output code page to UTF-8 if this is configured. + * \brief Ensure the process has a console attached and sets its output code page to UTF-8. * \remarks - * - only available under Windows - * - used to start a console from a GUI application - * - closes the console automatically when the application exists + * - Only available (and required) under Windows where otherwise stdout/stderr is not printed to the console (at + * least when using `cmd.exe`). + * - Used to start a console from a GUI application. Does *not* create a new console if the process already has one. + * - Closes the console automatically when the application exits. + * - It breaks redirecting stdout/stderr so this can be opted-out by setting the enviornment + * variable `ENABLE_CONSOLE=0` and/or `ENABLE_CP_UTF8=0`. + * \sa + * - https://docs.microsoft.com/en-us/windows/console/AttachConsole + * - https://docs.microsoft.com/en-us/windows/console/AllocConsole + * - https://docs.microsoft.com/en-us/windows/console/SetConsoleCP + * - https://docs.microsoft.com/en-us/windows/console/SetConsoleOutputCP */ void startConsole() { - if (!AttachConsole(ATTACH_PARENT_PROCESS) && !AllocConsole()) { - return; + // attach to the parent process' console or allocate a new console if that's not possible + const auto consoleEnabled = isEnvVariableSet("ENABLE_CONSOLE"); + if ((!consoleEnabled.has_value() || consoleEnabled.value()) && (AttachConsole(ATTACH_PARENT_PROCESS) || AllocConsole())) { + // redirect stdout + auto stdHandle = reinterpret_cast(GetStdHandle(STD_OUTPUT_HANDLE)); + auto conHandle = _open_osfhandle(stdHandle, _O_TEXT); + auto fp = _fdopen(conHandle, "w"); + *stdout = *fp; + setvbuf(stdout, nullptr, _IONBF, 0); + // redirect stdin + stdHandle = reinterpret_cast(GetStdHandle(STD_INPUT_HANDLE)); + conHandle = _open_osfhandle(stdHandle, _O_TEXT); + fp = _fdopen(conHandle, "r"); + *stdin = *fp; + setvbuf(stdin, nullptr, _IONBF, 0); + // redirect stderr + stdHandle = reinterpret_cast(GetStdHandle(STD_ERROR_HANDLE)); + conHandle = _open_osfhandle(stdHandle, _O_TEXT); + fp = _fdopen(conHandle, "w"); + *stderr = *fp; + setvbuf(stderr, nullptr, _IONBF, 0); + // sync + ios::sync_with_stdio(true); + // ensure the console prompt is shown again when app terminates + atexit(stopConsole); + } + + // set console character set to UTF-8 + const auto utf8Enabled = isEnvVariableSet("ENABLE_CP_UTF8"); + if (!utf8Enabled.has_value() || utf8Enabled.value()) { + SetConsoleCP(CP_UTF8); + SetConsoleOutputCP(CP_UTF8); } - // redirect stdout - auto stdHandle = reinterpret_cast(GetStdHandle(STD_OUTPUT_HANDLE)); - auto conHandle = _open_osfhandle(stdHandle, _O_TEXT); - auto fp = _fdopen(conHandle, "w"); - *stdout = *fp; - setvbuf(stdout, nullptr, _IONBF, 0); - // redirect stdin - stdHandle = reinterpret_cast(GetStdHandle(STD_INPUT_HANDLE)); - conHandle = _open_osfhandle(stdHandle, _O_TEXT); - fp = _fdopen(conHandle, "r"); - *stdin = *fp; - setvbuf(stdin, nullptr, _IONBF, 0); - // redirect stderr - stdHandle = reinterpret_cast(GetStdHandle(STD_ERROR_HANDLE)); - conHandle = _open_osfhandle(stdHandle, _O_TEXT); - fp = _fdopen(conHandle, "w"); - *stderr = *fp; - setvbuf(stderr, nullptr, _IONBF, 0); -#ifdef CPP_UTILITIES_FORCE_UTF8_CODEPAGE - // set console to handle UTF-8 IO correctly - // however, this doesn't work as intended and is therefore disabled by default - SetConsoleCP(CP_UTF8); - SetConsoleOutputCP(CP_UTF8); -#endif - // sync - ios::sync_with_stdio(true); - // ensure the console prompt is shown again when app terminates - atexit(stopConsole); } /*!