From 1e990b74ae11d2c988be83d398e82c5346a27ced Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Sun, 9 Jun 2024 21:34:28 +0800 Subject: [PATCH] feat: update console output method - remove the macros disable the warning and error of std functions in MSVC because YYCC has disable them in header. - update console input output functions. provide CSharp-like interface for UTF8 console input output. - console output function is done by WriteConsoleW and WriteFile. - console input function still work in progress. - rename console ASCII color macros - add console ASCII color test. - remove EnableUTF8Console because no longer needed. - add a bunch of annotation to describe YYCC UTF8 console strategy. - add UNICODE macro in CMakeLists.txt to order CMake generate Visual Studio solution with UNICODE charset enabled, not MBCS. --- src/CMakeLists.txt | 8 +-- src/ConsoleHelper.cpp | 84 +++++++++++++++++++++------ src/ConsoleHelper.hpp | 119 ++++++++++++++++++++++++--------------- testbench/CMakeLists.txt | 6 ++ testbench/main.cpp | 57 ++++++++++++------- 5 files changed, 187 insertions(+), 87 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b145ccd..6604759 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -41,11 +41,11 @@ PROPERTIES CXX_STANDARD_REQUIRED 17 CXX_EXTENSION OFF ) -# Disable MSVC standard library warnings +# Order Unicode charset for private using target_compile_definitions(YYCCommonplace -PUBLIC - $<$:_CRT_SECURE_NO_WARNINGS> - $<$:_CRT_NONSTDC_NO_DEPRECATE> +PRIVATE + $<$:UNICODE> + $<$:_UNICODE> ) # Order build as UTF-8 in MSVC target_compile_options(YYCCommonplace diff --git a/src/ConsoleHelper.cpp b/src/ConsoleHelper.cpp index 8741514..eb27309 100644 --- a/src/ConsoleHelper.cpp +++ b/src/ConsoleHelper.cpp @@ -13,7 +13,7 @@ namespace YYCC::ConsoleHelper { - bool EnsureTerminalColor(FILE* fs) { + static bool RawEnableColorfulConsole(FILE* fs) { if (!_isatty(_fileno(fs))) return false; HANDLE h_output; @@ -21,38 +21,86 @@ namespace YYCC::ConsoleHelper { h_output = (HANDLE)_get_osfhandle(_fileno(fs)); if (!GetConsoleMode(h_output, &dw_mode)) return false; - if (!SetConsoleMode(h_output, dw_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) return false; + if (!SetConsoleMode(h_output, dw_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT)) return false; return true; } - bool EnsureTerminalUTF8(FILE* fs) { - if (!SetConsoleCP(CP_UTF8)) return false; - if (!SetConsoleOutputCP(CP_UTF8)) return false; - - /*_setmode(_fileno(stdout), _O_U8TEXT);*/ - int _ = _setmode(_fileno(fs), _O_U8TEXT); + bool EnableColorfulConsole(FILE* fs) { + if (!RawEnableColorfulConsole(stdout)) return false; + if (!RawEnableColorfulConsole(stderr)) return false; return true; } - bool FGets(std::string& u8_buf, FILE* stream) { - std::wstring wcmd; - if (std::getline(std::wcin, wcmd).fail()) return false; - YYCC::EncodingHelper::WcharToChar(wcmd.c_str(), u8_buf, CP_UTF8); - return true; + std::string ReadLine() { +#if YYCC_OS == YYCC_OS_WINDOWS + + return std::string(); + +#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 FPuts(const char* u8_buf, FILE* stream) { - std::fputws(YYCC::EncodingHelper::UTF8ToWchar(u8_buf).c_str(), stream); + void Write(const char* u8_strl) {} + + static void PlainWrite(const std::string& strl) { +#if YYCC_OS == YYCC_OS_WINDOWS + + // fetch stdout handle first + HANDLE hstdout = GetStdHandle(STD_OUTPUT_HANDLE); + // if stdout was redirected, this handle may point to a file handle or anything else, + // WriteConsoleW can not write data into such scenario, so we need check whether this handle is console handle + DWORD console_mode; + if (GetConsoleMode(hstdout, &console_mode)) { + // console handle, use WriteConsoleW. + // convert utf8 string to wide char first + std::wstring wstrl(YYCC::EncodingHelper::UTF8ToWchar(strl.c_str())); + size_t wstrl_size = wstrl.size(); + // write string with size check + if (wstrl_size <= std::numeric_limits::max()) { + WriteConsoleW(hstdout, wstrl.c_str(), static_cast(wstrl_size), NULL, NULL); + } + } else { + // anything else, use WriteFile instead. + // WriteFile do not need extra convertion, because it is direct writing. + // check whether string length is overflow + size_t strl_size = strl.size() * sizeof(std::string::value_type); + // write string with size check + if (strl_size <= std::numeric_limits::max()) { + WriteFile(hstdout, strl.c_str(), static_cast(strl_size), NULL, NULL); + } + } + +#elif YYCC_OS == YYCC_OS_LINUX + + // in linux, directly use C function to write. + std::fputs(strl.c_str(), stdout); + +#endif } - void FPrintf(FILE* stream, const char* u8_fmt, ...) { + void Write(const char* u8_fmt, ...) { va_list argptr; va_start(argptr, u8_fmt); - FPuts(YYCC::StringHelper::VPrintf(u8_fmt, argptr).c_str(), stream); + PlainWrite(YYCC::StringHelper::VPrintf(u8_fmt, argptr)); va_end(argptr); } - + + void WriteLine(const char* u8_fmt, ...) { + va_list argptr; + va_start(argptr, u8_fmt); + std::string cache(YYCC::StringHelper::VPrintf(u8_fmt, argptr)); + cache += "\n"; + PlainWrite(cache); + va_end(argptr); + } + } #endif diff --git a/src/ConsoleHelper.hpp b/src/ConsoleHelper.hpp index 5ca8964..e8d3b32 100644 --- a/src/ConsoleHelper.hpp +++ b/src/ConsoleHelper.hpp @@ -7,63 +7,92 @@ namespace YYCC::ConsoleHelper { -#define YYCC_TERMCOLHDR_BLACK "\033[30m" -#define YYCC_TERMCOLHDR_RED "\033[31m" -#define YYCC_TERMCOLHDR_GREEN "\033[32m" -#define YYCC_TERMCOLHDR_YELLOW "\033[33m" -#define YYCC_TERMCOLHDR_BLUE "\033[34m" -#define YYCC_TERMCOLHDR_MAGENTA "\033[35m" -#define YYCC_TERMCOLHDR_CYAN "\033[36m" -#define YYCC_TERMCOLHDR_WHITE "\033[37m" +#define YYCC_COLORHDR_BLACK "\033[30m" +#define YYCC_COLORHDR_RED "\033[31m" +#define YYCC_COLORHDR_GREEN "\033[32m" +#define YYCC_COLORHDR_YELLOW "\033[33m" +#define YYCC_COLORHDR_BLUE "\033[34m" +#define YYCC_COLORHDR_MAGENTA "\033[35m" +#define YYCC_COLORHDR_CYAN "\033[36m" +#define YYCC_COLORHDR_WHITE "\033[37m" -#define YYCC_TERMCOLHDR_LIGHT_BLACK "\033[90m" -#define YYCC_TERMCOLHDR_LIGHT_RED "\033[91m" -#define YYCC_TERMCOLHDR_LIGHT_GREEN "\033[92m" -#define YYCC_TERMCOLHDR_LIGHT_YELLOW "\033[93m" -#define YYCC_TERMCOLHDR_LIGHT_BLUE "\033[94m" -#define YYCC_TERMCOLHDR_LIGHT_MAGENTA "\033[95m" -#define YYCC_TERMCOLHDR_LIGHT_CYAN "\033[96m" -#define YYCC_TERMCOLHDR_LIGHT_WHITE "\033[97m" +#define YYCC_COLORHDR_LIGHT_BLACK "\033[90m" +#define YYCC_COLORHDR_LIGHT_RED "\033[91m" +#define YYCC_COLORHDR_LIGHT_GREEN "\033[92m" +#define YYCC_COLORHDR_LIGHT_YELLOW "\033[93m" +#define YYCC_COLORHDR_LIGHT_BLUE "\033[94m" +#define YYCC_COLORHDR_LIGHT_MAGENTA "\033[95m" +#define YYCC_COLORHDR_LIGHT_CYAN "\033[96m" +#define YYCC_COLORHDR_LIGHT_WHITE "\033[97m" -#define YYCC_TERMCOLTAIL "\033[0m" +#define YYCC_COLORTAIL "\033[0m" -#define YYCC_TERMCOL_BLACK(T) "\033[30m" T "\033[0m" -#define YYCC_TERMCOL_RED(T) "\033[31m" T "\033[0m" -#define YYCC_TERMCOL_GREEN(T) "\033[32m" T "\033[0m" -#define YYCC_TERMCOL_YELLOW(T) "\033[33m" T "\033[0m" -#define YYCC_TERMCOL_BLUE(T) "\033[34m" T "\033[0m" -#define YYCC_TERMCOL_MAGENTA(T) "\033[35m" T "\033[0m" -#define YYCC_TERMCOL_CYAN(T) "\033[36m" T "\033[0m" -#define YYCC_TERMCOL_WHITE(T) "\033[37m" T "\033[0m" +#define YYCC_COLOR_BLACK(T) "\033[30m" T "\033[0m" +#define YYCC_COLOR_RED(T) "\033[31m" T "\033[0m" +#define YYCC_COLOR_GREEN(T) "\033[32m" T "\033[0m" +#define YYCC_COLOR_YELLOW(T) "\033[33m" T "\033[0m" +#define YYCC_COLOR_BLUE(T) "\033[34m" T "\033[0m" +#define YYCC_COLOR_MAGENTA(T) "\033[35m" T "\033[0m" +#define YYCC_COLOR_CYAN(T) "\033[36m" T "\033[0m" +#define YYCC_COLOR_WHITE(T) "\033[37m" T "\033[0m" -#define YYCC_TERMCOL_LIGHT_BLACK(T) "\033[90m" T "\033[0m" -#define YYCC_TERMCOL_LIGHT_RED(T) "\033[91m" T "\033[0m" -#define YYCC_TERMCOL_LIGHT_GREEN(T) "\033[92m" T "\033[0m" -#define YYCC_TERMCOL_LIGHT_YELLOW(T) "\033[93m" T "\033[0m" -#define YYCC_TERMCOL_LIGHT_BLUE(T) "\033[94m" T "\033[0m" -#define YYCC_TERMCOL_LIGHT_MAGENTA(T) "\033[95m" T "\033[0m" -#define YYCC_TERMCOL_LIGHT_CYAN(T) "\033[96m" T "\033[0m" -#define YYCC_TERMCOL_LIGHT_WHITE(T) "\033[97m" T "\033[0m" +#define YYCC_COLOR_LIGHT_BLACK(T) "\033[90m" T "\033[0m" +#define YYCC_COLOR_LIGHT_RED(T) "\033[91m" T "\033[0m" +#define YYCC_COLOR_LIGHT_GREEN(T) "\033[92m" T "\033[0m" +#define YYCC_COLOR_LIGHT_YELLOW(T) "\033[93m" T "\033[0m" +#define YYCC_COLOR_LIGHT_BLUE(T) "\033[94m" T "\033[0m" +#define YYCC_COLOR_LIGHT_MAGENTA(T) "\033[95m" T "\033[0m" +#define YYCC_COLOR_LIGHT_CYAN(T) "\033[96m" T "\033[0m" +#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. */ - bool EnsureTerminalColor(FILE* fs); - /** - * @brief Try setting terminal to UTF8 encoding. - * @param fs[in] The stream to be set. - * @return true if success, otherwise false. - * @remarks If you enable UTF8 for a stream, you must use stream functions provided in this namespace to operate that stream, - * because after UTF8 modification, some old standard functions are not work and it may takes a little bit performance reduction. - */ - bool EnsureTerminalUTF8(FILE* fs); + bool EnableColorfulConsole(FILE* fs); - bool FGets(std::string& u8_buf, FILE* stream); - void FPuts(const char* u8_buf, FILE* stream); - void FPrintf(FILE* stream, const char* u8_fmt, ...); + /* + 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 + */ + 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. + */ + void Write(const char* u8_fmt, ...); + /** + * @brief + * @param u8_fmt + * @param + */ + void WriteLine(const char* u8_fmt, ...); + } #endif \ No newline at end of file diff --git a/testbench/CMakeLists.txt b/testbench/CMakeLists.txt index cbbc08d..07986f2 100644 --- a/testbench/CMakeLists.txt +++ b/testbench/CMakeLists.txt @@ -21,6 +21,12 @@ PROPERTIES CXX_STANDARD_REQUIRED 17 CXX_EXTENSION OFF ) +# Order Unicode charset for private using +target_compile_definitions(YYCCTestbench +PRIVATE + $<$:UNICODE> + $<$:_UNICODE> +) # Order build as UTF-8 in MSVC target_compile_options(YYCCTestbench PRIVATE diff --git a/testbench/main.cpp b/testbench/main.cpp index 0fcb0a2..e767746 100644 --- a/testbench/main.cpp +++ b/testbench/main.cpp @@ -1,22 +1,42 @@ #include #include -namespace Testbench { +namespace Console = YYCC::ConsoleHelper; + +namespace YYCCTestbench { static void Assert(bool condition, const char* description) { if (condition) { - YYCC::ConsoleHelper::FPrintf(stdout, YYCC_TERMCOL_LIGHT_GREEN("OK: %s\n"), description); + Console::WriteLine(YYCC_COLOR_LIGHT_GREEN("OK: %s"), description); } else { - YYCC::ConsoleHelper::FPrintf(stdout, YYCC_TERMCOL_LIGHT_RED("Failed: %s\n"), description); + Console::WriteLine(YYCC_COLOR_LIGHT_RED("Failed: %s\n"), description); std::abort(); } } - static void TerminalTestbench() { - // UTF8 Test + static void ConsoleTestbench() { + // Color Test + Console::EnableColorfulConsole(stdout); + Console::WriteLine("Color Test:"); + + // U+2588 is full block +#define TEST_MACRO(col) Console::WriteLine("\t" YYCC_COLOR_ ## col ("\u2588\u2588") YYCC_COLOR_LIGHT_ ## col("\u2588\u2588") " " #col " / LIGHT " #col ); + + TEST_MACRO(BLACK); + TEST_MACRO(RED); + TEST_MACRO(GREEN); + TEST_MACRO(YELLOW); + TEST_MACRO(BLUE); + TEST_MACRO(MAGENTA); + TEST_MACRO(CYAN); + TEST_MACRO(WHITE); + +#undef TEST_MACRO + + // UTF8 Output Test // Ref: https://stackoverflow.com/questions/478201/how-to-test-an-application-for-correct-encoding-e-g-utf-8 - YYCC::ConsoleHelper::EnsureTerminalUTF8(stdout); - YYCC::ConsoleHelper::FPuts("UTF8 Test:\n", stdout); + //Console::EnableUTF8Console(stdout); + Console::WriteLine("UTF8 Output Test:"); static std::vector c_TestStrings { "\u30E6\u30FC\u30B6\u30FC\u5225\u30B5\u30A4\u30C8", // JAPAN "\u7B80\u4F53\u4E2D\u6587", // CHINA @@ -32,12 +52,9 @@ namespace Testbench { "\xF0\x9F\x8D\xA3 \xE2\x9C\x96 \xF0\x9F\x8D\xBA", // EMOJI }; for (const auto* ptr : c_TestStrings) { - YYCC::ConsoleHelper::FPrintf(stdout, "\t%s\n", ptr); + Console::WriteLine("\t%s", ptr); } - // Color Test - YYCC::ConsoleHelper::EnsureTerminalColor(stdout); - YYCC::ConsoleHelper::FPuts(YYCC_TERMCOL_LIGHT_CYAN("Colorful Terminal\n"), stdout); } static void StringTestbench() { @@ -151,28 +168,28 @@ namespace Testbench { filters.Add("All Files (*.*)", {"*.*"}); params.SetDefaultFileTypeIndex(0u); if (YYCC::DialogHelper::OpenFileDialog(params, ret)) { - YYCC::ConsoleHelper::FPrintf(stdout, "Open File: %s\n", ret.c_str()); + Console::WriteLine("Open File: %s", ret.c_str()); } if (YYCC::DialogHelper::OpenMultipleFileDialog(params, rets)) { - YYCC::ConsoleHelper::FPuts("Open Multiple Files:\n", stdout); + Console::WriteLine("Open Multiple Files:"); for (const auto& item : rets) { - YYCC::ConsoleHelper::FPrintf(stdout, "\t%s\n", item.c_str()); + Console::WriteLine("\t%s", item.c_str()); } } if (YYCC::DialogHelper::SaveFileDialog(params, ret)) { - YYCC::ConsoleHelper::FPrintf(stdout, "Save File: %s\n", ret.c_str()); + Console::WriteLine("Save File: %s", ret.c_str()); } params.Clear(); if (YYCC::DialogHelper::OpenFolderDialog(params, ret)) { - YYCC::ConsoleHelper::FPrintf(stdout, "Open Folder: %s\n", ret.c_str()); + Console::WriteLine("Open Folder: %s", ret.c_str()); } } } int main(int argc, char** args) { - Testbench::TerminalTestbench(); - Testbench::StringTestbench(); - Testbench::ParserTestbench(); - //Testbench::DialogTestbench(); + YYCCTestbench::ConsoleTestbench(); + //YYCCTestbench::StringTestbench(); + //YYCCTestbench::ParserTestbench(); + //YYCCTestbench::DialogTestbench(); }