1
0
forked from Alepha/Alepha

A console driver from my ISO scratch work.

This commit is contained in:
2023-10-09 20:36:44 -04:00
parent 3610746a63
commit ed3d64e1df
2 changed files with 568 additions and 0 deletions

399
Console.cpp Normal file
View File

@ -0,0 +1,399 @@
static_assert( __cplusplus > 2020'00 );
#include "console.h"
#include <vector>
#include "ProgramOptions.h"
#include "file_help.h"
#include "Enum.h"
#include "StaticValue.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::Cavorite ::detail:: console
{
namespace
{
using namespace std::literals::string_literals;
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();
}
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"; }
// 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.v_str() ) );
}
catch( const boost::bad_lexical_cast & ) {}
return d;
}
int cachedScreenWidth= evaluate <=[]
{
const int underlying getEnvOrDefault( screenWidthEnv(), 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 getenv( disableColorsEnv() );
if( colorState == "never"_value ) return false;
if( colorState == "always"_value ) return true;
assert( colorState == "auto"_value );
return ::isatty( 1 ); // Auto means only do this for TTYs.
}
StaticValue< std::map< Style, SGR_String > > colorVariables;
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;
}
}
<< "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() << "-\"";
for( const auto &[ name, sgr ]: colorVariables() )
{
if( not first ) std::cout << ":";
first= false;
std::cout << 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_variable_for_color:
if( getenv( colorsEnv() ) )
{
const std::string contents= getenv( colorsEnv() );
for( const auto var: split( varString, ':' ) )
{
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 exports::Console::Mode
{
cooked, raw, noblock,
};
namespace
{
struct BadScreenStateError : std::runtime_error
{
BadScreenStateError() : std::runtime_error( "Error in getting terminal dimensions." ) {}
};
struct UnknowScreenError : std::runtime_error
{
UnknowScreenError() : std::runtime_error( "Terminal is unrecognized. Using defaults." ) {}
};
auto
rawModeGuard( Console console )
{
const bool skip= console.getMode() == Console::raw;
return AutoRAII
{
[skip, &console]
{
if( skip ) return;
console.setRaw();
},
[skip, &console]
{
if( skip ) return;
console.popTermMode();
},
};
}
}
struct Console::Impl
{
int fd;
// TODO: Do we want to make this not gnu libstdc++ specific?
__gnu_cxx::stdio_filebuf< char > filebuf;
std::ostream stream;
std::stack< std::pair< struct termios, decltype( mode ) > > modeStack;
ConsoleMode mode= cooked;
explicit
Impl( const int fd )
: fd( fd ), filebuf( fd, std::ios::out ), stream( &filebuf )
{}
};
Console::Console( const int fd )
: impl( std::make_unique< Impl >( fd ) )
{}
std::ostream &
Console::csi()
{
return pimpl().stream;
}
Console::Mode
Console::getMode() const
{
return pimpl().mode;
}
void
Console::popTermMode()
{
tcsetattr( pimpl().fd, TCSAFLUSH, &pimpl().modeStack.top().first );
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_flag|= CS8;
next.c_lflag&= !( ECHO | ICANNON | IEXTEN | ISIG );
next.c_cc[ VMIN ]= min;
next.c_cc[ VTIME ]= 0;
if( tcsetattr( pimpl().fd, TCSAFLUSH, &next ) ) throw UnknownScreenException{};
return now;
}
}
void
Console::setRaw()
{
setRawModeWithMin( pimpl().fd, 1 );
orig.emplace_back( now, mode );
mode= raw;
}
void
Console::setNoblock()
{
setRawModeWithMin( pimpl().fd, 0 );
orig.emplace_back( now, mode );
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 x )
{
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 {}; }
SGR_String exports::setBlink() { return { "5" }; }
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' << 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" << fg << "48;2" << int( bg );
return { std::move( oss ).str() };
}
}

169
Console.h Normal file
View File

@ -0,0 +1,169 @@
static_assert( __cplusplus > 2020'00 );
#pragma once
// These are some terminal/console control primitives.
// There are several "modern" terminal assumptions built
// into this library.
//
// As long as this works on most (all?) modern terminal emulators, this should be
// fine.
namespace Alepha::inline Cavorite ::detail:: console
{
inline namespace exports {}
namespace exports
{
struct ScreenSize;
struct CursorPosition;
class Console;
Console &console() noexcept;
struct SGR_String
{
std::string code;
};
inline auto
operator ""_sgr( const char *const p, const std::size_t sz )
{
return SGR_String{ { p, p + sz } };
}
enum class BasicTextColor : int;
enum class TextColor : int;
struct Style
{
std::string name;
TotalOrder operator <=> ( const Style & ) const= default;
};
std::ostream &operator << ( std::ostream &, const Style & );
Style createStyle( const std::string &name, const SGR_String &style );
bool styleVarSet( const std::string &name );
enum ResetStyle { resetStyle };
std::ostream &operator << ( std::ostream &, ResetStyle );
// TODO: Move this to its own library.
const std::string &applicationName();
void setApplication( std::string name );
}
struct exports::ScreenSize
{
int rows;
int columns;
};
struct exports::CursorPosition
{
int x;
int y;
};
class exports::Console
{
private:
class Impl;
std::unique_ptr< Impl > impl;
Impl &pimpl() noexcept { return *impl; }
const Impl &pimpl() const noexcept { return *impl; }
std::ostream &csi();
public:
// A console object can only be constructed on a raw UNIX file descriptor.
explicit Console( int fd );
enum Mode;
Mode getMode() const;
int getScreenWidth();
int getScreenHeight();
// Althought this could be implemented by combining the above observers,
// there are more efficient ways to implement this, thus we actually
// keep them separate.
ScreenSize getScreenSize();
void hideCursor();
void showCursor();
// Pushes the current mode (raw or normal) onto the stack.
void setRaw();
// A nonblock mode is used to prevent terminal IO from blocking. This helps with keypress driven
// programs. This is its own raw mode.
void setNoblock();
// The Console object maintains an internal stack of the last terminal modes it set and how to
// revert tothe previous. There is no arbitrary limit on this stack size. Calling this with
// no previous modes is a no-op.
void popTermMode();
// Line kill functions keep the cursor where it is, but erase the specified text on the line.
void killLine();
void killLineHead();
void killLineTail();
// One should avoid calling these, as some internals may also use these hardware functions.
// It is better to use `gotoXY` and `getXY` to save/restore cursor positions. One should
// maintain a software stack (as a caller) of cursor positions, if necessary.
void saveHardwareCursor();
void restoreHardwareCursor();
void gotoX( int x );
void gotoY( int y );
void gotoXY( int x, int y );
int getX();
int getY();
CursorPosition getXY();
void cursorUp( unsigned amt= 0 );
void cursorDown( unsigned amt= 0 );
void cursorLeft( unsigned amt= 0 );
void cursorRight( unsigned amt= 0 );
void clearScreen(); // `console` library does direct cursor control, so this won't return the cursor to 1,1.
};
namespace exports
{
[[nodiscard]] SGR_String resetTextEffects();
[[nodiscard]] SGR_String setBlink();
[[nodiscard]] SGR_String setFGColor( BasicTextColor fg );
[[nodiscard]] SGR_String setBGColor( BasicTextColor bg );
[[nodiscard]] SGR_String setColor( BasicTextColor fg, BasicTextColor bg );
[[nodiscard]] SGR_String setExtFGColor( TextColor fg );
[[nodiscard]] SGR_String setExtBGColor( TextColor fg );
[[nodiscard]] SGR_String setExtColor( TextColor fg, TextColor bg );
// Basic color wrapping aliases:
[[nodiscard]] inline SGR_String setExtFgColor( const BasicTextColor fg ) { return setExtFgColor( static_cast< TextColor >( fg ) ); }
[[nodiscard]] inline SGR_String setExtBgColor( const BasicTextColor bg ) { return setExtBgColor( static_cast< TextColor >( bg ) ); }
[[nodiscard]] inline SGR_String setExtColor( const TextColor fg, const BasicTextColor bg ) { return setExtColor( ( fg ), static_cast< TextColor >( bg ) ); }
[[nodiscard]] inline SGR_String setExtColor( const BasicTextColor fg, const TextColor bg ) { return setExtColor( static_cast< TextColor >( fg ), ( bg ) ); }
[[nodiscard]] inline SGR_String setExtColor( const BasicTextColor fg, const BasicTextColor bg ) { return setExtColor( static_cast< TextColor >( fg ), static_cast< TextColor >( bg ) ); }
[[nodiscard]] SGR_String setFgTrueColor( int rgb );
[[nodiscard]] SGR_String setFgTrueColor( int r, int g, int b )
[[nodiscard]] SGR_String setBgTrueColor( int rgb );
[[nodiscard]] SGR_String setBgTrueColor( int r, int g, int b )
void sendSGR( std::ostream &os, SGR_String );
}
}