From b6c53ac70772791c3cf0011da1154dda54d0a9dd Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Tue, 11 Jun 2024 11:40:09 +0800 Subject: [PATCH] fix: fix console helper - add annotations for console helper for better user experience. - change build macro to allow using console helper in non-Windows platform. because console color macros is universal and should not be limited in Windows platform. --- src/ConsoleHelper.cpp | 139 ++++++++++++++++++++++++------------------ src/ConsoleHelper.hpp | 85 +++++++++++++++----------- testbench/main.cpp | 2 +- 3 files changed, 127 insertions(+), 99 deletions(-) diff --git a/src/ConsoleHelper.cpp b/src/ConsoleHelper.cpp index 7cc7be5..a5eeee2 100644 --- a/src/ConsoleHelper.cpp +++ b/src/ConsoleHelper.cpp @@ -1,18 +1,23 @@ #include "ConsoleHelper.hpp" -#if YYCC_OS == YYCC_OS_WINDOWS #include "EncodingHelper.hpp" #include "StringHelper.hpp" #include +// Include Windows used headers in Windows. +#if YYCC_OS == YYCC_OS_WINDOWS #include "WinImportPrefix.hpp" #include #include #include #include "WinImportSuffix.hpp" +#endif namespace YYCC::ConsoleHelper { +#pragma region Windows Specific Functions +#if YYCC_OS == YYCC_OS_WINDOWS + static bool RawEnableColorfulConsole(FILE* fs) { if (!_isatty(_fileno(fs))) return false; @@ -26,38 +31,30 @@ namespace YYCC::ConsoleHelper { return true; } - bool EnableColorfulConsole(FILE* fs) { - if (!RawEnableColorfulConsole(stdout)) return false; - if (!RawEnableColorfulConsole(stderr)) return false; - return true; - } + /* + Reference: + * https://stackoverflow.com/questions/45575863/how-to-print-utf-8-strings-to-stdcout-on-windows + * https://stackoverflow.com/questions/69830460/reading-utf-8-input - //template || std::is_same_v<_Ty, wchar_t>, int> = 0> - //static bool FetchEOL(std::basic_string<_Ty>& internal_buffer, std::basic_string<_Ty>& result_buffer) { - // // try finding EOL in internal buffer - // size_t pos; - // if constexpr (std::is_same_v<_Ty, char>) internal_buffer.find_first_of('\n'); - // else internal_buffer.find_first_of(L'\n'); + There is 3 way to make Windows console enable UTF8 mode. - // // check finding result - // if (pos == std::wstring::npos) { - // // the whole string do not include EOL, fully appended to return value - // result_buffer += internal_buffer; - // internal_buffer.clear(); - // // return false mean need more data - // return false; - // } else { - // // split result - // // push into result and remain some in internal buffer. - // result_buffer.append(internal_buffer, 0u, pos); - // internal_buffer.erase(0u, pos + 1u); // +1 because EOL take one place. - // // return true mean success finding - // return true; - // } - //} + First one is calling SetConsoleCP and SetConsoleOutputCP. + The side effect of this is std::cin and std::cout is broken, + however there is a patch for this issue. + + Second one is calling _set_mode with _O_U8TEXT or _O_U16TEXT to enable Unicode mode for Windows console. + This also have side effect which is stronger than first one. + All puts family functions (ASCII-based output functions) will throw assertion exception. + You only can use putws family functions (wide-char-based output functions). + However these functions can not be used without calling _set_mode in Windows design. + + There still is another method, using WriteConsoleW directly visiting console. + This function family can output correct string without calling any extra functions! + This method is what we adopted. + */ template - static std::string PlainRead(HANDLE hStdIn) { + static std::string WinConsoleRead(HANDLE hStdIn) { using _TChar = std::conditional_t<_bIsConsole, wchar_t, char>; // Prepare an internal buffer because the read data may not be fully used. @@ -130,34 +127,7 @@ namespace YYCC::ConsoleHelper { return real_return_buffer; } - std::string ReadLine() { -#if YYCC_OS == YYCC_OS_WINDOWS - - // get stdin mode - HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); - // use different method to get according to whether stdin is redirected - DWORD dwConsoleMode; - if (GetConsoleMode(hStdIn, &dwConsoleMode)) { - return PlainRead(hStdIn); - } else { - return PlainRead(hStdIn); - } - -#elif YYCC_OS == YYCC_OS_LINUX - - // in linux, directly use C++ function to fetch. - std::string cmd; - if (std::getline(std::cin, cmd).fail()) cmd.clear(); - return cmd; - -#endif - } - - void Write(const char* u8_strl) {} - - static void PlainWrite(const std::string& strl) { -#if YYCC_OS == YYCC_OS_WINDOWS - + static void WinConsoleWrite(const std::string& strl) { // Prepare some Win32 variables // fetch stdout handle first HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); @@ -185,8 +155,56 @@ namespace YYCC::ConsoleHelper { WriteFile(hStdOut, strl.c_str(), static_cast(strl_size), &dwWrittenNumberOfChars, NULL); } } + } -#elif YYCC_OS == YYCC_OS_LINUX +#endif +#pragma endregion + + bool EnableColorfulConsole() { +#if YYCC_OS == YYCC_OS_WINDOWS + + if (!RawEnableColorfulConsole(stdout)) return false; + if (!RawEnableColorfulConsole(stderr)) return false; + return true; + +#else + + // just return true and do nothing + return true + +#endif + } + + std::string ReadLine() { +#if YYCC_OS == YYCC_OS_WINDOWS + + // get stdin mode + HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); + // use different method to get according to whether stdin is redirected + DWORD dwConsoleMode; + if (GetConsoleMode(hStdIn, &dwConsoleMode)) { + return WinConsoleRead(hStdIn); + } else { + return WinConsoleRead(hStdIn); + } + +#else + + // in linux, directly use C++ function to fetch. + std::string cmd; + if (std::getline(std::cin, cmd).fail()) cmd.clear(); + return cmd; + +#endif + } + + static void RawWrite(const std::string& strl) { +#if YYCC_OS == YYCC_OS_WINDOWS + + // call Windows specific writer + WinConsoleWrite(strl); + +#else // in linux, directly use C function to write. std::fputs(strl.c_str(), stdout); @@ -197,7 +215,7 @@ namespace YYCC::ConsoleHelper { void Write(const char* u8_fmt, ...) { va_list argptr; va_start(argptr, u8_fmt); - PlainWrite(YYCC::StringHelper::VPrintf(u8_fmt, argptr)); + RawWrite(YYCC::StringHelper::VPrintf(u8_fmt, argptr)); va_end(argptr); } @@ -206,10 +224,9 @@ namespace YYCC::ConsoleHelper { va_start(argptr, u8_fmt); std::string cache(YYCC::StringHelper::VPrintf(u8_fmt, argptr)); cache += "\n"; - PlainWrite(cache); + RawWrite(cache); va_end(argptr); } } -#endif diff --git a/src/ConsoleHelper.hpp b/src/ConsoleHelper.hpp index e8d3b32..a72e5dc 100644 --- a/src/ConsoleHelper.hpp +++ b/src/ConsoleHelper.hpp @@ -1,6 +1,5 @@ #pragma once #include "YYCCInternal.hpp" -#if YYCC_OS == YYCC_OS_WINDOWS #include #include @@ -47,52 +46,64 @@ namespace YYCC::ConsoleHelper { #define YYCC_COLOR_LIGHT_WHITE(T) "\033[97m" T "\033[0m" /** - * @brief Try letting terminal support ASCII color schema. - * @param fs[in] The stream to be set. - * @return true if success, otherwise false. + * @brief Enable Windows console color support. + * @details This actually is enable virtual console feature for stdout and stderr. + * @return True if success, otherwise false. + * @remarks This function only works on Windows and do nothing on other platforms such as Linux, + * because we assume all terminals existing on other platform support color feature as default. */ - bool EnableColorfulConsole(FILE* fs); + bool EnableColorfulConsole(); - /* - Reference: - * https://stackoverflow.com/questions/45575863/how-to-print-utf-8-strings-to-stdcout-on-windows - * https://stackoverflow.com/questions/69830460/reading-utf-8-input - - There is 3 way to make Windows console enable UTF8 mode. - - First one is calling SetConsoleCP and SetConsoleOutputCP. - The side effect of this is std::cin and std::cout is broken, - however there is a patch for this issue. - - Second one is calling _set_mode with _O_U8TEXT or _O_U16TEXT to enable Unicode mode for Windows console. - This also have side effect which is stronger than first one. - All puts family functions (ASCII-based output functions) will throw assertion exception. - You only can use putws family functions (wide-char-based output functions). - However these functions can not be used without calling _set_mode in Windows design. - - There still is another method, using WriteConsoleW directly visiting console. - This function family can output correct string without calling any extra functions! - This method is what we adopted. - */ - /** - * @brief - * @return + * @brief Universal console read function + * @details This function is more like C# Console.ReadLine(). + * It read user input with UTF8 encoding until reaching EOL. + * + * This function provide an universal, platform-independent way to read UTF8 string from console, + * no matter whether it is redirected. + * @return The UTF8 encoded string this function read. EOL is excluded. + * @remarks In Windows, this function will try use native Win32 function for reading, + * because standard C/C++ function can not handle UTF8 input on Windows normally. + * In other platforms, this function will redirect request to std::readline with std::cin, + * which is all programmer commonly used method. + * It also mean that we assume stdin is encoded by UTF8 on these platforms. + * + * Please note that EOL will automatically converted into LF on Windows platform, not CRLF. + * This action is actually remove all CR chars in result string. + * + * This function also can be used as ordering user press Enter key by + * simply calling this function and ignoring its return value. */ std::string ReadLine(); /** - * @brief - * @param u8_fmt The format, or a simple string (format related chars still need escape). - * @param ...[in] The parameter for formatter. + * @brief Universal console write function + * @details This function is more like C# Console.Write(). + * It write user given UTF8 string into console. + * + * This function provide an universal, platform-independent way to write UTF8 string into console, + * no matter whether it is redirected. + * @param u8_fmt[in] The format string. + * If you just want to write a pure string, you should escape formatter chars (%) in this string, + * because this function always take this parameter as a format string. + * @param ...[in] The arguments to be formatted. + * @remarks In Windows, this funcion will use native Win32 function for writing, + * because standard C/C++ function can not handle UTF8 output on Windows normally. + * In other platforms, this function will redirect request to std::fprintf with stdout, + * which is all programmer commonly used method. + * It also mean that we assume stdout is encoded by UTF8 on these platforms. */ void Write(const char* u8_fmt, ...); /** - * @brief - * @param u8_fmt - * @param + * @brief Universal console write function with automatic EOL + * @details This function is same as Write(const char*, ...), + * but it will automatically add EOL in output to break line. + * This is commonly used. + * @param u8_fmt[in] The format string. + * If you just want to write a pure string, you should escape formatter chars (%) in this string, + * because this function always take this parameter as a format string. + * If you want to output plain string, you need escape format + * @param ...[in] The arguments to be formatted. */ void WriteLine(const char* u8_fmt, ...); } - -#endif \ No newline at end of file diff --git a/testbench/main.cpp b/testbench/main.cpp index 048f796..c3add19 100644 --- a/testbench/main.cpp +++ b/testbench/main.cpp @@ -16,7 +16,7 @@ namespace YYCCTestbench { static void ConsoleTestbench() { // Color Test - Console::EnableColorfulConsole(stdout); + Console::EnableColorfulConsole(); Console::WriteLine("Color Test:"); #define TEST_MACRO(col) Console::WriteLine("\t" YYCC_COLOR_ ## col ("\u2588\u2588") YYCC_COLOR_LIGHT_ ## col("\u2588\u2588") " " #col " / LIGHT " #col );