From 1bb0c5622458d841a02f03641abdd4ac56f22dee Mon Sep 17 00:00:00 2001 From: ADAM David Alan Martin Date: Fri, 20 Oct 2023 23:48:00 -0400 Subject: [PATCH] Update and consolidate the console and colors code. This introduces a subset of SGR-Name syntax. --- AutoRAII.test/0.cc | 2 +- Console.cpp | 182 ++++++++++++++++++++++++++++++++++++++++++-- Console.h | 81 ++++++++++++++++++-- Testing/TableTest.h | 17 ++++- Testing/colors.h | 17 +++++ Testing/test.h | 12 +-- console.h | 91 ---------------------- 7 files changed, 290 insertions(+), 112 deletions(-) create mode 100644 Testing/colors.h delete mode 100644 console.h diff --git a/AutoRAII.test/0.cc b/AutoRAII.test/0.cc index 2397ac0..133ec4e 100644 --- a/AutoRAII.test/0.cc +++ b/AutoRAII.test/0.cc @@ -13,7 +13,7 @@ main( const int argcnt, const char *const argvec[] ) static auto tests= Alepha::Utility::enroll <=[] { - using namespace Alepha::exports::auto_raii; + using namespace Alepha::Hydrogen::exports::auto_raii; using namespace Alepha::Testing::exports::literals; using Alepha::Testing::exports::TestState; diff --git a/Console.cpp b/Console.cpp index 7b08a48..3940ab9 100644 --- a/Console.cpp +++ b/Console.cpp @@ -39,7 +39,7 @@ static_assert( __cplusplus > 2020'00 ); // universally supported for effectively all cases modern users will care about. -namespace Alepha::Cavorite ::detail:: console +namespace Alepha::Hydrogen ::detail:: console { namespace { @@ -87,6 +87,7 @@ namespace Alepha::Cavorite ::detail:: console auto screenWidthEnvLimit() { return applicationName() + "_SCREEN_WIDTH_LIMIT"; } auto disableColorsEnv() { return applicationName() + "_DISABLE_COLOR_TEXT"; } auto colorsEnv() { return applicationName() + "_COLORS"; } + auto sgr_nameEnv() { return applicationName() + "_SGR_NAMES"; } // TODO: Put this in a library int @@ -113,7 +114,7 @@ namespace Alepha::Cavorite ::detail:: console bool colorEnabled() { - if( not colorState.has_value() ) return getenv( disableColorsEnv().c_str() ); + if( not colorState.has_value() ) return not ::getenv( disableColorsEnv().c_str() ); if( colorState.value() == "never"_value ) return false; if( colorState.value() == "always"_value ) return true; @@ -124,6 +125,98 @@ namespace Alepha::Cavorite ::detail:: console StaticValue< std::map< Style, SGR_String > > colorVariables; + + SGR_String + parse( const std::string &token ) + { + const std::map< std::string, std::function< SGR_String () > > simple= + { + { "reset", resetTextEffects }, + { "bold", setBold }, + { "dim", setFaint }, + { "faint", setFaint }, + { "italic", setItalic }, + { "underline", setUnderline }, + { "under", setUnderline }, + { "blink", setBlink }, + { "strike", setStrike }, + { "strikethrough", setStrike }, + { "strikethru", setStrike }, + { "doubleunderline", setDoubleUnderline }, + { "doubleunder", setDoubleUnderline }, + { "framed", setFramed }, + { "encircled", setEncircled }, + { "overline", setOverline }, + }; + if( simple.contains( token ) ) return simple.at( token )(); + + // The `fg:` is optional in "SGR Name"... + if( token.starts_with( "fg:" ) ) return parse( token.substr( 2 ) ); + + if( token.starts_with( "ansi:" ) ) return { '3' + token.substr( 5 ) }; + if( token.starts_with( "ext:grey" ) ) + { + std::ostringstream oss; + oss << "3;5;"; + oss << int( TextColor::greyscale_base ) + boost::lexical_cast< int >( token.substr( 8 ) ); + return { std::move( oss ).str() }; + } + if( token.starts_with( "ext:rgb" ) ) + { + const std::string rgb= token.substr( 7 ); + if( rgb.size() != 3 ) throw std::runtime_error{ "RGB request with more than 3 digits..." }; + const int r= boost::lexical_cast< int >( rgb.substr( 0, 1 ) ); + const int g= boost::lexical_cast< int >( rgb.substr( 1, 1 ) ); + const int b= boost::lexical_cast< int >( rgb.substr( 2, 1 ) ); + std::ostringstream oss; + oss << "3;5;"; + oss << int( TextColor::rgb_base ) + r * int( TextColor::red_radix ) + g * int( TextColor::green_radix ) + b * int( TextColor::blue_radix ); + return { std::move( oss ).str() }; + } + if( token.starts_with( "ext:" ) ) return { "38;5;" + token.substr( 4 ) }; + if( token.starts_with( "#" ) ) + { + const auto code= token.substr( 1 ); + const auto [ r_s, g_s, b_s ]= evaluate <=[&] + { + if( code.size() == 3 ) return std::tuple{ code.substr( 0, 1 ), code.substr( 1, 1 ), code.substr( 2, 1 ) }; + if( code.size() == 6 ) return std::tuple{ code.substr( 0, 2 ), code.substr( 2, 2 ), code.substr( 4, 2 ) }; + throw std::runtime_error( "True color code parse error." ); + }; + int r, g, b; + { + std::istringstream iss{ r_s }; + iss >> std::hex >> r; + } + + { + std::istringstream iss{ g_s }; + iss >> std::hex >> g; + } + + { + std::istringstream iss{ b_s }; + iss >> std::hex >> b; + } + + std::ostringstream oss; + oss << "38;2;" << std::dec << r << ';' << g << ';' << b; + return { std::move( oss ).str() }; + } + + throw std::runtime_error{ "Unrecognized SGR Name keyword: `" + token + "`" }; + } + + SGR_String + parseTokens( std::vector< std::string > tokens ) + { + + SGR_String rv; + for( const auto token: tokens ) rv+= parse( token ); + + return rv; + } + auto init= enroll <=[] { --"screen-width"_option << affectsHelp << cachedScreenWidth << "Sets the screen width for use in automatic word-wrapping. !default!"; @@ -136,6 +229,7 @@ namespace Alepha::Cavorite ::detail:: console { std::cout << name.name << ": ^[[" << sgr.code << "m" << std::endl; } + ::exit( EXIT_SUCCESS ); } << "Emit a list with the color variables supported by this application. For use with the `" << colorsEnv() << "` environment variable."; @@ -156,7 +250,27 @@ namespace Alepha::Cavorite ::detail:: console << "Emit a BASH command which will set the appropriate environment variable to capture the current color settings for this " << "application."; - parse_environment_variable_for_color: + parse_environment_variables_for_color: + // First the SGR Name language + if( getenv( sgr_nameEnv().c_str() ) ) + { + const std::string contents= getenv( sgr_nameEnv().c_str() ); + + for( const auto var: split( contents, ';' ) ) + { + const auto parsed= split( var, '=' ); + if( parsed.size() != 2 ) + { + throw std::runtime_error{ "Color environment variable parse error in: `" + var + "`." }; + } + + const Style name{ parsed.at( 0 ) }; + const auto value= parsed.at( 1 ); + + colorVariables()[ name ]= parseTokens( split( value, ' ' ) ); + } + } + // Then the regular terminal codes if( getenv( colorsEnv().c_str() ) ) { const std::string contents= getenv( colorsEnv().c_str() ); @@ -180,7 +294,7 @@ namespace Alepha::Cavorite ::detail:: console std::ostream & csi( std::ostream &os ) { - return os << "\e"; + return os << "\e["; } } @@ -213,7 +327,7 @@ namespace Alepha::Cavorite ::detail:: console std::ostream & exports::operator << ( std::ostream &os, decltype( resetStyle ) ) { - if( colorEnabled ) + if( colorEnabled() ) { sendSGR( os, resetTextEffects() ); } @@ -370,9 +484,18 @@ namespace Alepha::Cavorite ::detail:: console void Console::clearScreen() { csi() << "2J"; } - SGR_String exports::resetTextEffects() { return {}; } + SGR_String exports::resetTextEffects() { return { "0" }; } + SGR_String exports::setBold() { return { "1" }; } + SGR_String exports::setFaint() { return { "2" }; } + SGR_String exports::setItalic() { return { "3" }; } + SGR_String exports::setUnderline() { return { "4" }; } SGR_String exports::setBlink() { return { "5" }; } + SGR_String exports::setStrike() { return { "9" }; } + SGR_String exports::setDoubleUnderline() { return { "21" }; } + SGR_String exports::setFramed() { return { "51" }; } + SGR_String exports::setEncircled() { return { "52" }; } + SGR_String exports::setOverline() { return { "52" }; } SGR_String exports::setFgColor( const BasicTextColor c ) @@ -423,6 +546,53 @@ namespace Alepha::Cavorite ::detail:: console return { std::move( oss ).str() }; } + SGR_String + exports::setExtUlColor( const TextColor ul ) + { + std::ostringstream oss; + oss << "58;5;" << int( ul ); + return { std::move( oss ).str() }; + } + + SGR_String exports::setFgTrueColor( const int rgb ) { return setFgTrueColor( rgb & 0xFF, ( rgb >> 8 ) & 0xFF, ( rgb >> 16 ) & 0xFF ); } + + SGR_String + exports::setFgTrueColor( const int r, const int g, const int b ) + { + std::ostringstream oss; + oss << "38;2;" << r << ';' << g << ';' << b; + return { std::move( oss ).str() }; + } + + SGR_String exports::setBgTrueColor( const int rgb ) { return setBgTrueColor( rgb & 0xFF, ( rgb >> 8 ) & 0xFF, ( rgb >> 16 ) & 0xFF ); } + + SGR_String + exports::setBgTrueColor( const int r, const int g, const int b ) + { + std::ostringstream oss; + oss << "48;2;" << r << ';' << g << ';' << b; + return { std::move( oss ).str() }; + } + + SGR_String exports::setUlTrueColor( const int rgb ) { return setUlTrueColor( rgb & 0xFF, ( rgb >> 8 ) & 0xFF, ( rgb >> 16 ) & 0xFF ); } + + SGR_String + exports::setUlTrueColor( const int r, const int g, const int b ) + { + std::ostringstream oss; + oss << "58;2;" << r << ';' << g << ';' << b; + return { std::move( oss ).str() }; + } + + SGR_String + exports::operator ""_sgr( const char *const p, const std::size_t sz ) + { + const std::string s{ p, p + sz }; + + const auto tokens= split( s, ' ' ); + return parseTokens( tokens ); + } + int exports::getConsoleWidth() { diff --git a/Console.h b/Console.h index 7e9ff15..32f2755 100644 --- a/Console.h +++ b/Console.h @@ -2,6 +2,8 @@ static_assert( __cplusplus > 2020'00 ); #pragma once +#include + #include #include @@ -14,7 +16,7 @@ static_assert( __cplusplus > 2020'00 ); // As long as this works on most (all?) modern terminal emulators, this should be // fine. -namespace Alepha::inline Cavorite ::detail:: console +namespace Alepha::Hydrogen ::detail:: console { inline namespace exports {} @@ -30,12 +32,23 @@ namespace Alepha::inline Cavorite ::detail:: console std::string code; }; - inline auto - operator ""_sgr( const char *const p, const std::size_t sz ) + [[nodiscard]] inline SGR_String + operator + ( const SGR_String lhs, const SGR_String rhs ) { - return SGR_String{ { p, p + sz } }; + if( lhs.code.empty() ) return rhs; + if( rhs.code.empty() ) return lhs; + return SGR_String{ lhs.code + ';' + rhs.code }; } + inline SGR_String & + operator += ( SGR_String &lhs, const SGR_String rhs ) + { + return lhs= lhs + rhs; + } + + // Parses sgr token names, like "bold ext:red" + [[nodiscard]] SGR_String operator ""_sgr( const char *p, std::size_t sz ); + enum class BasicTextColor : int; enum class TextColor : int; @@ -143,7 +156,17 @@ namespace Alepha::inline Cavorite ::detail:: console { [[nodiscard]] SGR_String resetTextEffects(); + // Non Colour effects (Mostly sorted by ANSI/ECMA SGR code numeric order) + [[nodiscard]] SGR_String setBold(); + [[nodiscard]] SGR_String setFaint(); + [[nodiscard]] SGR_String setItalic(); + [[nodiscard]] SGR_String setUnderline(); [[nodiscard]] SGR_String setBlink(); + [[nodiscard]] SGR_String setStrike(); + [[nodiscard]] SGR_String setDoubleUnderline(); + [[nodiscard]] SGR_String setFramed(); + [[nodiscard]] SGR_String setEncircled(); + [[nodiscard]] SGR_String setOverline(); [[nodiscard]] SGR_String setFgColor( BasicTextColor fg ); [[nodiscard]] SGR_String setBgColor( BasicTextColor bg ); @@ -152,6 +175,7 @@ namespace Alepha::inline Cavorite ::detail:: console [[nodiscard]] SGR_String setExtFgColor( TextColor fg ); [[nodiscard]] SGR_String setExtBgColor( TextColor fg ); [[nodiscard]] SGR_String setExtColor( TextColor fg, TextColor bg ); + [[nodiscard]] SGR_String setExtUlColor( TextColor ul ); // Basic color wrapping aliases: [[nodiscard]] inline SGR_String setExtFgColor( const BasicTextColor fg ) { return setExtFgColor( static_cast< TextColor >( fg ) ); } @@ -167,13 +191,60 @@ namespace Alepha::inline Cavorite ::detail:: console [[nodiscard]] SGR_String setBgTrueColor( int rgb ); [[nodiscard]] SGR_String setBgTrueColor( int r, int g, int b ); + [[nodiscard]] SGR_String setUlTrueColor( int rgb ); + [[nodiscard]] SGR_String setUlTrueColor( int r, int g, int b ); + void sendSGR( std::ostream &os, SGR_String ); int getConsoleWidth(); } + + enum class exports::BasicTextColor : int + { + black= 0, + red= 1, + green= 2, + brown= 3, + blue= 4, + magenta= 5, + cyan= 6, + grey= 7, + }; + + enum class exports::TextColor : int + { + black= 0, + dim_red= 1, + dim_green= 2, + dim_brown= 3, + dim_blue= 4, + dim_magenta= 5, + dim_cyan= 6, + bright_grey= 7, + + // Note that bright and dim grey are reverse, since bright grey is dim white and dim grey si bright black. + // The names are more understandable this way, I think + + dim_grey= 8, + bright_red= 9, + bright_green= 10, + bright_brown= 11, + bright_blue= 12, + bright_magenta= 13, + bright_cyan= 14, + white= 15, + + rgb_base= 16, + red_radix= 36, + green_radix= 6, + blue_radix= 0, + + greyscale_base= 232, // Add N to this to get the greyscale offset. + }; + } -namespace Alepha::Cavorite::inline exports::inline console +namespace Alepha::Hydrogen::inline exports::inline console { using namespace detail::console::exports; } diff --git a/Testing/TableTest.h b/Testing/TableTest.h index 93dd3b0..7b7d9ec 100644 --- a/Testing/TableTest.h +++ b/Testing/TableTest.h @@ -32,7 +32,9 @@ static_assert( __cplusplus > 2020'00 ); #include #include -#include +#include + +#include "colors.h" namespace Alepha::Hydrogen::Testing ::detail:: table_test { @@ -41,6 +43,14 @@ namespace Alepha::Hydrogen::Testing ::detail:: table_test enum class OutputMode { All, Relaxed }; } + namespace C + { + inline namespace Colors + { + using namespace testing_colors::C::Colors; + } + } + template< typename F > concept FunctionVariable= requires( const F &f ) @@ -60,7 +70,6 @@ namespace Alepha::Hydrogen::Testing ::detail:: table_test { const bool debug= false; const bool debugCaseTypes= false or C::debug; - using namespace Alepha::console::C; } using std::begin, std::end; @@ -260,11 +269,11 @@ namespace Alepha::Hydrogen::Testing ::detail:: table_test const auto result= witness == expected; if( not result ) { - std::cout << C::red << " FAILURE" << C::normal << ": " << comment << std::endl; + std::cout << C::testFail << " FAILURE" << resetStyle << ": " << comment << std::endl; ++failureCount; printDebugging< outputMode >( witness, expected ); } - else std::cout << C::green << " SUCCESS" << C::normal << ": " << comment << std::endl; + else std::cout << C::testPass << " SUCCESS" << resetStyle << ": " << comment << std::endl; } return failureCount; diff --git a/Testing/colors.h b/Testing/colors.h new file mode 100644 index 0000000..d448e01 --- /dev/null +++ b/Testing/colors.h @@ -0,0 +1,17 @@ +static_assert( __cplusplus > 2020'00 ); + +#pragma once + +#include + +namespace Alepha::Hydrogen::Testing ::detail:: testing_colors +{ + namespace C + { + inline namespace Colors + { + inline const auto testFail= createStyle( "test-failure", setFgColor( BasicTextColor::red ) ); + inline const auto testPass= createStyle( "test-success", setFgColor( BasicTextColor::green ) ); + } + } +} diff --git a/Testing/test.h b/Testing/test.h index 36c706f..19f26dc 100644 --- a/Testing/test.h +++ b/Testing/test.h @@ -13,12 +13,14 @@ static_assert( __cplusplus > 2020'00 ); #include #include -#include +#include #include #include #include +#include "colors.h" + namespace Alepha::Hydrogen::Testing { inline namespace exports { inline namespace testing {} } @@ -33,7 +35,7 @@ namespace Alepha::Hydrogen::Testing const bool debugTestRegistration= false or C::debug; const bool debugTestRun= false or C::debug; - using namespace Alepha::Hydrogen::exports::C; + using namespace testing_colors::C::Colors; } using namespace std::literals::string_literals; @@ -196,9 +198,9 @@ namespace Alepha::Hydrogen::Testing if( explicitlyNamed( name ) or not disabled and selected( name ) ) { - std::cout << C::green << "BEGIN " << C::normal << ": " << name << std::endl; + std::cout << C::testPass << "BEGIN " << resetStyle << ": " << name << std::endl; test(); - std::cout << C::green << "SUCCESS" << C::normal << ": " << name << std::endl; + std::cout << C::testPass << "SUCCESS" << resetStyle << ": " << name << std::endl; } } catch( ... ) @@ -206,7 +208,7 @@ namespace Alepha::Hydrogen::Testing try { failed= true; - std::cout << C::red << "FAILURE" << C::normal << ": " << name; + std::cout << C::testFail << "FAILURE" << resetStyle << ": " << name; throw; } catch( const TestFailureException &fail ) { std::cout << " -- " << fail.failureCount << " failures."; } diff --git a/console.h b/console.h deleted file mode 100644 index 2082ee1..0000000 --- a/console.h +++ /dev/null @@ -1,91 +0,0 @@ -static_assert( __cplusplus > 2020'00 ); - -#pragma once - -#include - -#include - -namespace Alepha::Hydrogen -{ - inline namespace exports { inline namespace console {} } - - namespace detail::console - { - inline namespace exports {} - - namespace C - { - const std::string csi= "\e["; - const std::string color_code= "m"; - - enum Layer - { - fg_code= '3', - bg_code= '4', - }; - - enum class Color : char - { - black= '0', - red= '1', - green= '2', - brown= '3', - blue= '4', - magenta= '5', - cyan= '6', - white= '7', - }; - } - using C::Layer; - using C::Color; - - inline std::string - make_color( const Layer layer, const Color color ) - { - return C::csi + char(layer) + char(color) + C::color_code; - } - - namespace C - { - inline namespace exports - { - const std::string normal= C::csi + '0' + color_code; - - inline namespace fg - { - const std::string black= make_color( C::fg_code, C::Color::black ); - const std::string red= make_color( C::fg_code, C::Color::red ); - const std::string green= make_color( C::fg_code, C::Color::green ); - const std::string brown= make_color( C::fg_code, C::Color::brown ); - const std::string blue= make_color( C::fg_code, C::Color::blue ); - const std::string magenta= make_color( C::fg_code, C::Color::magenta ); - const std::string cyan= make_color( C::fg_code, C::Color::cyan ); - const std::string white= make_color( C::fg_code, C::Color::white ); - } - - namespace bg - { - const std::string black= make_color( C::bg_code, C::Color::black ); - const std::string red= make_color( C::bg_code, C::Color::red ); - const std::string green= make_color( C::bg_code, C::Color::green ); - const std::string brown= make_color( C::bg_code, C::Color::brown ); - const std::string blue= make_color( C::bg_code, C::Color::blue ); - const std::string magenta= make_color( C::bg_code, C::Color::magenta ); - const std::string cyan= make_color( C::bg_code, C::Color::cyan ); - const std::string white= make_color( C::bg_code, C::Color::white ); - } - } - } - - namespace exports - { - namespace C= detail::console::C; - } - } - - namespace exports::console - { - using namespace detail::console::exports; - } -}