refactor: migrate console helper.

- migrate csharp style console helper.
- i just do a simple migration and mark it as deprecated. it should works like 1.x version.
This commit is contained in:
2025-08-22 23:28:17 +08:00
parent 4bfba6f243
commit ccd0219ead
5 changed files with 360 additions and 335 deletions

View File

@ -29,6 +29,7 @@ PRIVATE
yycc/carton/termcolor.cpp
yycc/carton/wcwidth.cpp
yycc/carton/tabulate.cpp
yycc/carton/csconsole.cpp
)
target_sources(YYCCommonplace
PUBLIC
@ -77,6 +78,7 @@ FILES
yycc/carton/termcolor.hpp
yycc/carton/wcwidth.hpp
yycc/carton/tabulate.hpp
yycc/carton/csconsole.hpp
)
# Setup header infomations
target_include_directories(YYCCommonplace

View File

@ -1,253 +0,0 @@
#include "ConsoleHelper.hpp"
#include "EncodingHelper.hpp"
#include "StringHelper.hpp"
#include <iostream>
// Include Windows used headers in Windows.
#if defined(YYCC_OS_WINDOWS)
#include "WinImportPrefix.hpp"
#include <Windows.h>
#include <io.h>
#include <fcntl.h>
#include "WinImportSuffix.hpp"
#endif
namespace YYCC::ConsoleHelper {
#pragma region Windows Specific Functions
#if defined(YYCC_OS_WINDOWS)
/*
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.
*/
template<bool _bIsConsole>
static yycc_u8string 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.
// For example, we may read x\ny in a single calling but after processing \n, this function will return
// so y will temporarily stored in this internal buffer for next using.
// Thus this function is not thread safe.
static std::basic_string<_TChar> internal_buffer;
// create return value buffer
std::basic_string<_TChar> return_buffer;
// Prepare some variables
DWORD dwReadNumberOfChars;
_TChar szReadChars[64];
size_t eol_pos;
// try fetching EOL
while (true) {
// if internal buffer is empty,
// try fetching it.
if (internal_buffer.empty()) {
// console and non-console use different method to read.
if constexpr (_bIsConsole) {
// console handle, use ReadConsoleW.
// read from console, the read data is wchar based
if (!ReadConsoleW(hStdIn, szReadChars, sizeof(szReadChars) / sizeof(_TChar), &dwReadNumberOfChars, NULL))
break;
} else {
// anything else, use ReadFile instead.
// the read data is utf8 based
if (!ReadFile(hStdIn, szReadChars, sizeof(szReadChars), &dwReadNumberOfChars, NULL))
break;
}
// send to internal buffer
if (dwReadNumberOfChars == 0) break;
internal_buffer.append(szReadChars, dwReadNumberOfChars);
}
// try finding EOL in internal buffer
if constexpr (std::is_same_v<_TChar, char>) eol_pos = internal_buffer.find_first_of('\n');
else eol_pos = internal_buffer.find_first_of(L'\n');
// check finding result
if (eol_pos == std::wstring::npos) {
// the whole string do not include EOL, fully appended to return value
return_buffer += internal_buffer;
internal_buffer.clear();
// need more data, continue while
} else {
// split result
// push into result and remain some in internal buffer.
return_buffer.append(internal_buffer, 0u, eol_pos);
internal_buffer.erase(0u, eol_pos + 1u); // +1 because EOL take one place.
// break while mean success finding
break;
}
}
// post-process for return value
yycc_u8string real_return_buffer;
if constexpr (_bIsConsole) {
// console mode need convert wchar to utf8
YYCC::EncodingHelper::WcharToUTF8(return_buffer, real_return_buffer);
} else {
// non-console just copt the result
real_return_buffer = EncodingHelper::ToUTF8(return_buffer);
}
// every mode need delete \r words
YYCC::StringHelper::Replace(real_return_buffer, YYCC_U8("\r"), YYCC_U8(""));
// return value
return real_return_buffer;
}
static void WinConsoleWrite(const yycc_u8string& strl, bool to_stderr) {
// Prepare some Win32 variables
// fetch stdout handle first
HANDLE hStdOut = GetStdHandle(to_stderr ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
DWORD dwConsoleMode;
DWORD dwWrittenNumberOfChars;
// 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
if (GetConsoleMode(hStdOut, &dwConsoleMode)) {
// console handle, use WriteConsoleW.
// convert utf8 string to wide char first
std::wstring wstrl(YYCC::EncodingHelper::UTF8ToWchar(strl));
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), &dwWrittenNumberOfChars, 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(yycc_u8string::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), &dwWrittenNumberOfChars, NULL);
}
}
}
#endif
#pragma endregion
yycc_u8string ReadLine() {
#if defined(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<true>(hStdIn);
} else {
return WinConsoleRead<false>(hStdIn);
}
#else
// in linux, directly use C++ function to fetch.
std::string cmd;
if (std::getline(std::cin, cmd).fail()) cmd.clear();
return EncodingHelper::ToUTF8(cmd);
#endif
}
template<bool bNeedFmt, bool bIsErr, bool bHasEOL>
static void RawWrite(const yycc_char8_t* u8_fmt, va_list argptr) {
// Buiild string need to be written first
// If no format string or plain string for writing, return.
if (u8_fmt == nullptr) return;
// Build or simply copy string
yycc_u8string strl;
if constexpr (bNeedFmt) {
// treat as format string
va_list argcpy;
va_copy(argcpy, argptr);
strl = YYCC::StringHelper::VPrintf(u8_fmt, argcpy);
va_end(argcpy);
} else {
// treat as plain string
strl = u8_fmt;
}
// Checkout whether add EOL
if constexpr (bHasEOL) {
strl += YYCC_U8("\n");
}
#if defined(YYCC_OS_WINDOWS)
// call Windows specific writer
WinConsoleWrite(strl, bIsErr);
#else
// in linux, directly use C function to write.
std::fputs(EncodingHelper::ToOrdinary(strl.c_str()), bIsErr ? stderr : stdout);
#endif
}
void Format(const yycc_char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
RawWrite<true, false, false>(u8_fmt, argptr);
va_end(argptr);
}
void FormatLine(const yycc_char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
RawWrite<true, false, true>(u8_fmt, argptr);
va_end(argptr);
}
void Write(const yycc_char8_t* u8_strl) {
va_list empty{};
RawWrite<false, false, false>(u8_strl, empty);
}
void WriteLine(const yycc_char8_t* u8_strl) {
va_list empty{};
RawWrite<false, false, true>(u8_strl, empty);
}
void ErrFormat(const yycc_char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
RawWrite<true, true, false>(u8_fmt, argptr);
va_end(argptr);
}
void ErrFormatLine(const yycc_char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
RawWrite<true, true, true>(u8_fmt, argptr);
va_end(argptr);
}
void ErrWrite(const yycc_char8_t* u8_strl) {
va_list empty{};
RawWrite<false, true, false>(u8_strl, empty);
}
void ErrWriteLine(const yycc_char8_t* u8_strl) {
va_list empty{};
RawWrite<false, true, true>(u8_strl, empty);
}
}

View File

@ -1,82 +0,0 @@
#pragma once
#include "YYCCInternal.hpp"
#include <cstdio>
#include <string>
/**
* @brief The helper providing universal C\# style console function and other console related stuff
* @details
* For how to utilize this functions provided by this namespace, please view \ref console_helper.
*/
namespace YYCC::ConsoleHelper {
/**
* @brief Reads the next line of UTF8 characters from the standard input stream.
* @return
* The next line of UTF8 characters from the input stream.
* Empty string if user just press Enter key or function failed.
*/
yycc_u8string ReadLine();
/**
* @brief
* Writes the text representation of the specified object
* to the standard output stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void Format(const yycc_char8_t* u8_fmt, ...);
/**
* @brief
* Writes the text representation of the specified object,
* followed by the current line terminator,
* to the standard output stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void FormatLine(const yycc_char8_t* u8_fmt, ...);
/**
* @brief Writes the specified string value to the standard output stream.
* @param[in] u8_strl The value to write.
*/
void Write(const yycc_char8_t* u8_strl);
/**
* @brief
* Writes the specified string value, followed by the current line terminator,
* to the standard output stream.
* @param[in] u8_strl The value to write.
*/
void WriteLine(const yycc_char8_t* u8_strl);
/**
* @brief
* Writes the text representation of the specified object
* to the standard error stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void ErrFormat(const yycc_char8_t* u8_fmt, ...);
/**
* @brief
* Writes the text representation of the specified object,
* followed by the current line terminator,
* to the standard error stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void ErrFormatLine(const yycc_char8_t* u8_fmt, ...);
/**
* @brief Writes the specified string value to the standard error stream.
* @param[in] u8_strl The value to write.
*/
void ErrWrite(const yycc_char8_t* u8_strl);
/**
* @brief
* Writes the specified string value, followed by the current line terminator,
* to the standard error stream.
* @param[in] u8_strl The value to write.
*/
void ErrWriteLine(const yycc_char8_t* u8_strl);
}

View File

@ -0,0 +1,247 @@
#include "csconsole.hpp"
#include "../macro/os_detector.hpp"
#include "../string/op.hpp"
#include "../string/reinterpret.hpp"
#include "../encoding/windows.hpp"
#include <iostream>
#include <cstdarg>
#include <cstdio>
#if defined(YYCC_OS_WINDOWS)
#include "../windows/import_guard_head.hpp"
#include <Windows.h>
#include "../windows/import_guard_tail.hpp"
#endif
#define OP ::yycc::string::op
#define REINTERPRET ::yycc::string::reinterpret
#define ENC ::yycc::encoding::windows
namespace yycc::carton::csconsole {
#pragma region Windows Console Specific Functions
#if defined(YYCC_OS_WINDOWS)
template<bool BIsConsole>
static std::u8string win_console_read(HANDLE hStdIn) {
using TChar = std::conditional_t<BIsConsole, wchar_t, char>;
// Prepare an internal buffer because the read data may not be fully used.
// For example, we may read x\ny in a single calling but after processing \n, this function will return
// so y will temporarily stored in this internal buffer for next using.
// Thus this function is not thread safe.
static std::basic_string<TChar> internal_buffer;
// create return value buffer
std::basic_string<TChar> return_buffer;
// Prepare some variables
DWORD dwReadNumberOfChars;
TChar szReadChars[64];
size_t eol_pos;
// try fetching EOL
while (true) {
// if internal buffer is empty,
// try fetching it.
if (internal_buffer.empty()) {
// console and non-console use different method to read.
if constexpr (BIsConsole) {
// console handle, use ReadConsoleW.
// read from console, the read data is wchar based
if (!ReadConsoleW(hStdIn, szReadChars, sizeof(szReadChars) / sizeof(TChar), &dwReadNumberOfChars, NULL)) break;
} else {
// anything else, use ReadFile instead.
// the read data is utf8 based
if (!ReadFile(hStdIn, szReadChars, sizeof(szReadChars), &dwReadNumberOfChars, NULL)) break;
}
// send to internal buffer
if (dwReadNumberOfChars == 0) break;
internal_buffer.append(szReadChars, dwReadNumberOfChars);
}
// try finding EOL in internal buffer
if constexpr (std::is_same_v<TChar, char>) eol_pos = internal_buffer.find_first_of('\n');
else eol_pos = internal_buffer.find_first_of(L'\n');
// check finding result
if (eol_pos == std::wstring::npos) {
// the whole string do not include EOL, fully appended to return value
return_buffer += internal_buffer;
internal_buffer.clear();
// need more data, continue while
} else {
// split result
// push into result and remain some in internal buffer.
return_buffer.append(internal_buffer, 0u, eol_pos);
internal_buffer.erase(0u, eol_pos + 1u); // +1 because EOL take one place.
// break while mean success finding
break;
}
}
// post-process for return value
std::u8string real_return_buffer;
if constexpr (BIsConsole) {
// console mode need convert wchar to utf8
auto rv = ENC::to_utf8(return_buffer);
if (rv.has_value()) real_return_buffer = std::move(rv.value());
} else {
// non-console just copt the result
real_return_buffer = REINTERPRET::as_utf8(return_buffer);
}
// every mode need delete \r words
OP::replace(real_return_buffer, u8"\r", u8"");
// return value
return real_return_buffer;
}
static void win_console_write(const std::u8string_view& strl, bool to_stderr) {
// Prepare some Win32 variables
// fetch stdout handle first
HANDLE hStdOut = GetStdHandle(to_stderr ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
DWORD dwConsoleMode;
DWORD dwWrittenNumberOfChars;
// 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
if (GetConsoleMode(hStdOut, &dwConsoleMode)) {
// console handle, use WriteConsoleW.
// convert utf8 string to wide char first
auto rv = ENC::to_wchar(strl);
if (!rv.has_value()) return;
std::wstring wstrl(std::move(rv.value()));
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), &dwWrittenNumberOfChars, 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::u8string_view::value_type);
// write string with size check
if (strl_size <= std::numeric_limits<DWORD>::max()) {
WriteFile(hStdOut, strl.data(), static_cast<DWORD>(strl_size), &dwWrittenNumberOfChars, NULL);
}
}
}
#endif
#pragma endregion
#pragma region Read Functions
std::u8string read_line() {
#if defined(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 win_console_read<true>(hStdIn);
} else {
return win_console_read<false>(hStdIn);
}
#else
// in linux, directly use C++ function to fetch.
std::string cmd;
if (std::getline(std::cin, cmd).fail()) cmd.clear();
return REINTERPRET::as_utf8(cmd);
#endif
}
#pragma endregion
#pragma region Write Functions
template<bool BNeedFmt, bool BIsErr, bool BHasEOL>
static void raw_write(const char8_t* u8_fmt, va_list argptr) {
// Buiild string need to be written first
// If no format string or plain string for writing, return.
if (u8_fmt == nullptr) return;
// Build or simply copy string
std::u8string strl;
if constexpr (BNeedFmt) {
// treat as format string
va_list argcpy;
va_copy(argcpy, argptr);
auto rv = OP::vprintf(u8_fmt, argcpy);
va_end(argcpy);
// check format result
if (!rv.has_value()) return;
else strl = std::move(rv.value());
} else {
// treat as plain string
strl = u8_fmt;
}
// Checkout whether add EOL
if constexpr (BHasEOL) {
strl += u8'\n';
}
#if defined(YYCC_OS_WINDOWS)
// call Windows specific writer
win_console_write(strl, BIsErr);
#else
// in linux, directly use C function to write.
std::fputs(REINTERPRET::as_ordinary(strl.c_str()), BIsErr ? stderr : stdout);
#endif
}
void format(const char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
raw_write<true, false, false>(u8_fmt, argptr);
va_end(argptr);
}
void format_line(const char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
raw_write<true, false, true>(u8_fmt, argptr);
va_end(argptr);
}
void write(const char8_t* u8_strl) {
va_list empty{};
raw_write<false, false, false>(u8_strl, empty);
}
void write_line(const char8_t* u8_strl) {
va_list empty{};
raw_write<false, false, true>(u8_strl, empty);
}
void eformat(const char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
raw_write<true, true, false>(u8_fmt, argptr);
va_end(argptr);
}
void eformat_line(const char8_t* u8_fmt, ...) {
va_list argptr;
va_start(argptr, u8_fmt);
raw_write<true, true, true>(u8_fmt, argptr);
va_end(argptr);
}
void ewrite(const char8_t* u8_strl) {
va_list empty{};
raw_write<false, true, false>(u8_strl, empty);
}
void ewrite_line(const char8_t* u8_strl) {
va_list empty{};
raw_write<false, true, true>(u8_strl, empty);
}
#pragma endregion
}

View File

@ -0,0 +1,111 @@
#pragma once
#include <string>
/**
* @brief The helper providing universal C\# style console function and other console related stuff
* @details
* The origin of this namespace is coming from the requirement of UTF8 console in Windows.
* There are 3 ways to make Windows console enable UTF8 mode.
*
* First one is calling \c SetConsoleCP and \c SetConsoleOutputCP.
* The side effect of this is \c std::cin and \c std::cout is broken,
* however there is a patch for this issue.
*
* Second one is calling \c _set_mode with \c _O_U8TEXT or \c _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 \c _set_mode in Windows design.
*
* There still is another method, using \c WriteConsoleW directly visiting console.
* This function family can output correct string without calling any extra functions!
* This method is what we adopted and finally become this namespace.
*
* Reference:
* \li https://stackoverflow.com/questions/45575863/how-to-print-utf-8-strings-to-stdcout-on-windows
* \li https://stackoverflow.com/questions/69830460/reading-utf-8-input
*
* For how to utilize this functions provided by this namespace, please view \ref console_helper.
*
* @warning
* All functions provided by this namespace are too aggressive.
* Once you use it, you should not use any other input output functions.
*
* @deprecated
* This namespace provided functions are too aggressive and can not cover all use scenario.
* So I start to give this up when migrating this namespace during developing YYCC 2.x version.
* I just do a simple type fix and rename when migrating this namespace to make it "just works".
* There is no suggestion for using this namespace. And there is no update for this namespace.
* Programmer should treat Windows UTF8 issue on their own.
*/
namespace yycc::carton::csconsole {
/**
* @brief Reads the next line of UTF8 characters from the standard input stream.
* @return
* The next line of UTF8 characters from the input stream.
* Empty string if user just press Enter key or function failed.
*/
std::u8string read_line();
/**
* @brief
* Writes the text representation of the specified object
* to the standard output stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void format(const char8_t* u8_fmt, ...);
/**
* @brief
* Writes the text representation of the specified object,
* followed by the current line terminator,
* to the standard output stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void format_line(const char8_t* u8_fmt, ...);
/**
* @brief Writes the specified string value to the standard output stream.
* @param[in] u8_strl The value to write.
*/
void write(const char8_t* u8_strl);
/**
* @brief
* Writes the specified string value, followed by the current line terminator,
* to the standard output stream.
* @param[in] u8_strl The value to write.
*/
void write_line(const char8_t* u8_strl);
/**
* @brief
* Writes the text representation of the specified object
* to the standard error stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void eformat(const char8_t* u8_fmt, ...);
/**
* @brief
* Writes the text representation of the specified object,
* followed by the current line terminator,
* to the standard error stream using the specified format information.
* @param[in] u8_fmt The format string.
* @param[in] ... The arguments of format string.
*/
void eformat_line(const char8_t* u8_fmt, ...);
/**
* @brief Writes the specified string value to the standard error stream.
* @param[in] u8_strl The value to write.
*/
void ewrite(const char8_t* u8_strl);
/**
* @brief
* Writes the specified string value, followed by the current line terminator,
* to the standard error stream.
* @param[in] u8_strl The value to write.
*/
void ewrite_line(const char8_t* u8_strl);
}