forked from Alepha/Alepha
A console driver from my ISO scratch work.
This commit is contained in:
399
Console.cpp
Normal file
399
Console.cpp
Normal 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
169
Console.h
Normal 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 );
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user