1
0
forked from Alepha/Alepha

Update and consolidate the console and colors code.

This introduces a subset of SGR-Name syntax.
This commit is contained in:
2023-10-20 23:48:00 -04:00
parent 69aa923450
commit 1bb0c56224
7 changed files with 290 additions and 112 deletions

View File

@ -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;

View File

@ -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()
{

View File

@ -2,6 +2,8 @@ static_assert( __cplusplus > 2020'00 );
#pragma once
#include <Alepha/Alepha.h>
#include <string>
#include <memory>
@ -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;
}

View File

@ -32,7 +32,9 @@ static_assert( __cplusplus > 2020'00 );
#include <Alepha/Utility/evaluation_helpers.h>
#include <Alepha/TotalOrder.h>
#include <Alepha/console.h>
#include <Alepha/Console.h>
#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;

17
Testing/colors.h Normal file
View File

@ -0,0 +1,17 @@
static_assert( __cplusplus > 2020'00 );
#pragma once
#include <Alepha/Console.h>
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 ) );
}
}
}

View File

@ -13,12 +13,14 @@ static_assert( __cplusplus > 2020'00 );
#include <functional>
#include <memory>
#include <Alepha/console.h>
#include <Alepha/Console.h>
#include <Alepha/types.h>
#include <Alepha/Utility/evaluation_helpers.h>
#include <Alepha/Utility/StaticValue.h>
#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."; }

View File

@ -1,91 +0,0 @@
static_assert( __cplusplus > 2020'00 );
#pragma once
#include <Alepha/Alepha.h>
#include <string>
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;
}
}