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.
This commit is contained in:
yyc12345 2024-06-09 21:34:28 +08:00
parent 019034a9c2
commit 1e990b74ae
5 changed files with 187 additions and 87 deletions

View File

@ -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
$<$<CXX_COMPILER_ID:MSVC>:_CRT_SECURE_NO_WARNINGS>
$<$<CXX_COMPILER_ID:MSVC>:_CRT_NONSTDC_NO_DEPRECATE>
PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:UNICODE>
$<$<CXX_COMPILER_ID:MSVC>:_UNICODE>
)
# Order build as UTF-8 in MSVC
target_compile_options(YYCCommonplace

View File

@ -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,35 +21,83 @@ 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<DWORD>::max()) {
WriteConsoleW(hstdout, wstrl.c_str(), static_cast<DWORD>(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<DWORD>::max()) {
WriteFile(hstdout, strl.c_str(), static_cast<DWORD>(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);
}

View File

@ -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);
/*
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, ...);
bool FGets(std::string& u8_buf, FILE* stream);
void FPuts(const char* u8_buf, FILE* stream);
void FPrintf(FILE* stream, const char* u8_fmt, ...);
}
#endif

View File

@ -21,6 +21,12 @@ PROPERTIES
CXX_STANDARD_REQUIRED 17
CXX_EXTENSION OFF
)
# Order Unicode charset for private using
target_compile_definitions(YYCCTestbench
PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:UNICODE>
$<$<CXX_COMPILER_ID:MSVC>:_UNICODE>
)
# Order build as UTF-8 in MSVC
target_compile_options(YYCCTestbench
PRIVATE

View File

@ -1,22 +1,42 @@
#include <YYCCommonplace.hpp>
#include <cstdio>
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<const char*> 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();
}