1
0
forked from Alepha/Alepha
Files
Alepha/Console.cc

647 lines
17 KiB
C++

static_assert( __cplusplus > 2020'99 );
#include "Console.h"
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stack>
#include <vector>
#include <utility>
#include <string>
#include <sstream>
#include <iostream>
#include <Alepha/Utility/evaluation_helpers.h>
#include <Alepha/Utility/StaticValue.h>
#include <Alepha/IOStreams/OutUnixFileBuf.h>
#include "Enum.h"
#include "ProgramOptions.h"
#include "string_algorithms.h"
#include "delimited_list.h"
#include "AutoRAII.h"
/*
* All of the terminal control code in this library uses ANSI escape sequences (https://en.wikipedia.org/wiki/ANSI_escape_code).
* Instead of using termcap and curses, code can use this simpler library instead. The simple fact is that in 2022, there's
* probably no terminal software that you're using that does not understand these escape sequences. Truth be told, the
* termcap databases are suffering from a tremendous amount of bitrot, as no actual hardware or software in common use
* actually uses anything but the ANSI codes.
*
* Some ANSI engines have a maximum limit to how many "clauses" a CSI sequence can have. To this end, no significant effort
* should be put into trying to collapse the sequences for foreground and background into one SGR command. Despite it taking
* a few more bytes, it's probably more portable to issue multiple commands.
*/
// It's fairly safe to assume, in 2022, that common ANSI terminal sequences are
// universally supported for effectively all cases modern users will care about.
namespace Alepha::Hydrogen ::detail:: Console_m
{
namespace
{
using namespace std::literals::string_literals;
using namespace Utility::exports::evaluation_helpers_m;
using Utility::StaticValue;
namespace C
{
const bool debug= false;
const int defaultScreenWidthLimit= 100;
// The Device Status Report should never be longer than this.
const int maxLengthOfDSR= 64;
}
// TODO, this should be in its own lib.
namespace storage
{
StaticValue< std::string > applicationName;
auto init= enroll <=[]
{
if( applicationName().empty() ) applicationName()= "ALEPHA";
};
}
}
void
exports::setApplicationName( std::string name )
{
storage::applicationName()= std::move( name );
}
const std::string &
exports::applicationName()
{
return storage::applicationName();
}
namespace
{
auto screenWidthEnv() { return applicationName() + "_SCREEN_WIDTH"; }
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
getEnvOrDefault( const std::string env, const int d )
{
if( getenv( env.c_str() ) )
try
{
return boost::lexical_cast< int >( getenv( env.c_str() ) );
}
catch( const boost::bad_lexical_cast & ) {}
return d;
}
int cachedScreenWidth= evaluate <=[]
{
const int underlying= getEnvOrDefault( screenWidthEnv(), Console::main().getScreenSize().columns );
return std::min( underlying, getEnvOrDefault( screenWidthEnvLimit(), C::defaultScreenWidthLimit ) );
};
using ColorState= Enum< "always"_value, "never"_value, "auto"_value >;
std::optional< ColorState > colorState;
bool
colorEnabled()
{
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;
assert( colorState.value() == "auto"_value );
return ::isatty( 1 ); // Auto means only do this for TTYs.
}
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 ) );
if( r < 0 or r > 5 ) throw std::runtime_error( "ext:rgb requested red value `" + rgb.substr( 0, 1 ) + "` out of range [0,5]" );
if( g < 0 or g > 5 ) throw std::runtime_error( "ext:rgb requested green value `" + rgb.substr( 1, 1 ) + "` out of range [0,5]" );
if( b < 0 or b > 5 ) throw std::runtime_error( "ext:rgb requested blue value `" + rgb.substr( 2, 1 ) + "` out of range [0,5]" );
std::ostringstream oss;
oss << "38;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!";
--"color"_option << affectsHelp << colorState << "Select the application color preference. If not passed, the environment variable `"
<< disableColorsEnv() << "` will be respected. Otherwise, `auto` will detect if a TTY is on stdout. `never` will entirely "
<< "disable color output. And `always` will force color output.";
--"list-color-variables"_option << []
{
for( const auto [ name, sgr ]: colorVariables() )
{
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.";
--"dump-color-env-var"_option << []
{
std::cout << "export " << colorsEnv() << "-\"";
auto scopedList= adaptStream( StartDelimitedList{ ":" }, std::cout );
for( const auto &[ name, sgr ]: colorVariables() )
{
std::cout << NextItem << name.name << "=" << sgr.code;
}
std::cout << "\"" << std::endl;
::exit( EXIT_SUCCESS );
}
<< "Emit a BASH command which will set the appropriate environment variable to capture the current color settings for this "
<< "application.";
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() );
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 ]= SGR_String{ value };
}
}
};
std::ostream &
csi( std::ostream &os )
{
return os << "\e[";
}
}
Style
exports::createStyle( const std::string &name, const SGR_String &sgr )
{
if( name == "reset" ) throw std::runtime_error( "The `reset` style name is reserved." );
Style style{ name };
colorVariables().insert( { style, sgr } );
return style;
}
void
exports::sendSGR( std::ostream &os, const SGR_String style )
{
csi( os ) << style.code << 'm';
}
std::ostream &
exports::operator << ( std::ostream &os, const Style &s )
{
if( colorEnabled() and colorVariables().contains( s ) )
{
sendSGR( os, colorVariables().at( s ) );
}
return os;
}
std::ostream &
exports::operator << ( std::ostream &os, decltype( resetStyle ) )
{
if( colorEnabled() )
{
sendSGR( os, resetTextEffects() );
}
return os;
}
enum ConsoleMode
{
cooked, raw, noblock,
};
struct Console::Impl
{
int fd;
IOStreams::OutUnixFileBuf filebuf;
std::ostream stream;
std::stack< std::pair< struct termios, ConsoleMode > > modeStack;
ConsoleMode mode= cooked;
std::optional< int > cachedScreenWidth;
explicit
Impl( const int fd )
: fd( fd ), filebuf( fd ), stream( &filebuf )
{}
};
auto
Console::getMode() const
{
return pimpl().mode;
}
namespace
{
struct BadScreenStateError : std::runtime_error
{
BadScreenStateError() : std::runtime_error( "Error in getting terminal dimensions." ) {}
};
struct UnknownScreenError : std::runtime_error
{
UnknownScreenError() : std::runtime_error( "Terminal is unrecognized. Using defaults." ) {}
};
auto
rawModeGuard( Console console )
{
const bool skip= console.getMode() == ConsoleMode::raw;
return AutoRAII
{
[skip, &console]
{
if( skip ) return;
console.setRaw();
},
[skip, &console]
{
if( skip ) return;
console.popTermMode();
},
};
}
}
Console::Console( const int fd )
: impl( std::make_unique< Impl >( fd ) )
{}
std::ostream &
Console::csi()
{
return pimpl().stream;
}
void
Console::popTermMode()
{
tcsetattr( pimpl().fd, TCSAFLUSH, &pimpl().modeStack.top().first );
pimpl().mode= pimpl().modeStack.top().second;
pimpl().modeStack.pop();
}
namespace
{
struct termios
setRawModeWithMin( const int fd, const int min )
{
struct termios next;
struct termios now;
if( tcgetattr( fd, &now ) == -1 ) throw UnknownScreenError{};
next.c_iflag&= ~( BRKINT | ICRNL | INPCK | ISTRIP | IXON );
next.c_oflag&= ~( OPOST );
next.c_cflag|= CS8;
next.c_lflag&= !( ECHO | ICANON | IEXTEN | ISIG );
next.c_cc[ VMIN ]= min;
next.c_cc[ VTIME ]= 0;
if( tcsetattr( fd, TCSAFLUSH, &next ) ) throw UnknownScreenError{};
return now;
}
}
void
Console::setRaw()
{
const auto old= setRawModeWithMin( pimpl().fd, 1 );
pimpl().modeStack.emplace( old, pimpl().mode );
pimpl().mode= raw;
}
void
Console::setNoblock()
{
const auto old= setRawModeWithMin( pimpl().fd, 0 );
pimpl().modeStack.emplace( old, pimpl().mode );
pimpl().mode= raw;
}
void Console::killLineTail() { csi() << 'K'; }
void Console::killLineHead() { csi() << "1K"; }
void Console::killLine() { csi() << "2K"; }
void Console::hideCursor() { csi() << "?25l"; }
void Console::showCursor() { csi() << "?25h"; }
void Console::saveHardwareCursor() { csi() << 's'; }
void Console::restoreHardwareCursor() { csi() << 'u'; }
void Console::gotoX( const int x ) { csi() << x << 'G'; }
void
Console::gotoY( const int y )
{
cursorUp( 1'000'000 );
cursorDown( y );
}
void Console::gotoXY( const int x, const int y ) { csi() << y << ';' << x << 'H'; }
void Console::cursorUp( const unsigned amt ) { csi() << amt << 'A'; }
void Console::cursorDown( const unsigned amt ) { csi() << amt << 'B'; }
void Console::cursorRight( const unsigned amt ) { csi() << amt << 'C'; }
void Console::cursorLeft( const unsigned amt ) { csi() << amt << 'D'; }
void Console::clearScreen() { csi() << "2J"; }
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 )
{
std::ostringstream oss;
oss << '3' << int( c );
return { std::move( oss ).str() };
}
SGR_String
exports::setBgColor( const BasicTextColor c )
{
std::ostringstream oss;
oss << '4' << int( c );
return { std::move( oss ).str() };
}
SGR_String
exports::setColor( const BasicTextColor fg, const BasicTextColor bg )
{
std::ostringstream oss;
oss << '3' << int( fg ) << ";4" << int( bg );
return { std::move( oss ).str() };
}
SGR_String
exports::setExtFgColor( const TextColor c )
{
std::ostringstream oss;
oss << "38;5;" << int( c );
return { std::move( oss ).str() };
}
SGR_String
exports::setExtBgColor( const TextColor c )
{
std::ostringstream oss;
oss << "48;5;" << int( c );
return { std::move( oss ).str() };
}
SGR_String
exports::setExtColor( const TextColor fg, const TextColor bg )
{
std::ostringstream oss;
oss << "38;2" << int( fg ) << "48;2" << int( bg );
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()
{
return cachedScreenWidth;
}
int
Console::getScreenWidth()
{
if( not pimpl().cachedScreenWidth.has_value() )
{
pimpl().cachedScreenWidth= getScreenSize().columns;
}
return pimpl().cachedScreenWidth.value();
}
ScreenSize
Console::getScreenSize()
try
{
if( not isatty( pimpl().fd ) ) throw UnknownScreenError{};
// Use the `ioctl( TIOCGWINSZ )`, but we'll just defer to 24x80 if we fail that...
struct winsize ws;
const int ec= ioctl( pimpl().fd, TIOCGWINSZ, &ws );
if( ec == -1 or ws.ws_col == 0 ) throw UnknownScreenError{};
return { ws.ws_row, ws.ws_col };
}
catch( const UnknownScreenError & ) { return { 24, 80 }; } // Fallback position....
namespace
{
namespace storage
{
std::unique_ptr< Console > console;
}
}
Console &
Console::main()
{
if( not storage::console ) storage::console= std::make_unique< Console >( 1 ); // stdout
return *storage::console;
}
}