diff --git a/src/logdata/include/onelinelog.h b/src/logdata/include/onelinelog.h index 470f4891..74285a67 100644 --- a/src/logdata/include/onelinelog.h +++ b/src/logdata/include/onelinelog.h @@ -29,13 +29,13 @@ class OneLineLog { OneLineLog& operator=( OneLineLog&& ) = default; OneLineLog( OneLineLog&& ) = default; - QString string(); + QString string() const; - QString expandedString(); + QString expandedString() const; - QString process( std::function fn ); + QString process( std::function fn ) const; - inline bool empty() + inline bool empty() const { return buffer_.isEmpty() || !decoder_ || !reg_; } diff --git a/src/logdata/src/onelinelog.cpp b/src/logdata/src/onelinelog.cpp index 749cdf9e..2bb28ba0 100644 --- a/src/logdata/src/onelinelog.cpp +++ b/src/logdata/src/onelinelog.cpp @@ -16,7 +16,7 @@ OneLineLog::OneLineLog( const char* data, OneLineLog::Length len, std::shared_pt { } -QString OneLineLog::string() +QString OneLineLog::string() const { if ( empty() ) { LOG_WARNING << buffer_.isEmpty() << ",count:" << decoder_.use_count() << "," @@ -35,7 +35,7 @@ QString OneLineLog::string() return log; } -QString OneLineLog::expandedString() +QString OneLineLog::expandedString() const { if ( empty() ) { LOG_INFO << buffer_.isEmpty() << ",count:" << decoder_.use_count() << "," @@ -51,7 +51,7 @@ QString OneLineLog::expandedString() return untabify( std::move( log ) ); } -QString OneLineLog::process( std::function fn ) +QString OneLineLog::process( std::function fn ) const { if ( empty() ) { LOG_INFO << buffer_.isEmpty() << ",count:" << decoder_.use_count() << "," diff --git a/src/settings/include/configuration.h b/src/settings/include/configuration.h index 4c3ecb1d..76fc0949 100644 --- a/src/settings/include/configuration.h +++ b/src/settings/include/configuration.h @@ -491,6 +491,15 @@ class Configuration final : public Persistable { hideAnsiColorSequences_ = hide; } + bool displayAnsiColorSequences() const + { + return displayAnsiColorSequences_; + } + void setDisplayAnsiColorSequences( bool display ) + { + displayAnsiColorSequences_ = display; + } + int defaultEncodingMib() const { return defaultEncodingMib_; @@ -500,7 +509,8 @@ class Configuration final : public Persistable { defaultEncodingMib_ = mib; } - std::map darkPalette() const { + std::map darkPalette() const + { return darkPalette_; } @@ -581,6 +591,7 @@ class Configuration final : public Persistable { bool optimizeForNotLatinEncodings_ = false; bool hideAnsiColorSequences_ = false; + bool displayAnsiColorSequences_ = false; int defaultEncodingMib_ = -1; diff --git a/src/settings/src/configuration.cpp b/src/settings/src/configuration.cpp index f18eb04b..702f5aeb 100644 --- a/src/settings/src/configuration.cpp +++ b/src/settings/src/configuration.cpp @@ -261,6 +261,10 @@ void Configuration::retrieveFromStorage( QSettings& settings ) = settings .value( "view.hideAnsiColorSequences", DefaultConfiguration.hideAnsiColorSequences_ ) .toBool(); + displayAnsiColorSequences_ = settings + .value( "view.displayAnsiColorSequences", + DefaultConfiguration.displayAnsiColorSequences_ ) + .toBool(); useTextWrap_ = settings.value( "view.textWrap", DefaultConfiguration.useTextWrap() ).toBool(); @@ -300,7 +304,7 @@ void Configuration::retrieveFromStorage( QSettings& settings ) const auto mapping = settings.value( "shortcuts.mapping" ).toMap(); for ( auto keys = mapping.begin(); keys != mapping.end(); ++keys ) { auto action = keys.key().toStdString(); - if (action == ShortcutAction::LogViewJumpToButtom) { + if ( action == ShortcutAction::LogViewJumpToButtom ) { action = ShortcutAction::LogViewJumpToBottom; } shortcuts_.emplace( action, keys.value().toStringList() ); @@ -314,7 +318,7 @@ void Configuration::retrieveFromStorage( QSettings& settings ) settings.setArrayIndex( static_cast( shortcutIndex ) ); auto action = settings.value( "action", "" ).toString(); if ( !action.isEmpty() ) { - if (action == ShortcutAction::LogViewJumpToButtom) { + if ( action == ShortcutAction::LogViewJumpToButtom ) { action = ShortcutAction::LogViewJumpToBottom; } const auto keys = settings.value( "keys", QStringList() ).toStringList(); @@ -395,6 +399,7 @@ void Configuration::saveToStorage( QSettings& settings ) const settings.setValue( "view.scaleFactorRounding", scaleFactorRounding_ ); settings.setValue( "view.hideAnsiColorSequences", hideAnsiColorSequences_ ); + settings.setValue( "view.displayAnsiColorSequences", displayAnsiColorSequences_ ); settings.setValue( "defaultView.searchAutoRefresh", searchAutoRefresh_ ); settings.setValue( "defaultView.searchIgnoreCase", searchIgnoreCase_ ); diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 56e97109..4f2e7bee 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -46,6 +46,9 @@ add_library( ${CMAKE_CURRENT_SOURCE_DIR}/include/decompressor.h ${CMAKE_CURRENT_SOURCE_DIR}/include/fontutils.h ${CMAKE_CURRENT_SOURCE_DIR}/include/colorlabelsmanager.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/ansi.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/sgrparser.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/colorfultextparser.h ${CMAKE_CURRENT_SOURCE_DIR}/include/highlighteredit.ui ${CMAKE_CURRENT_SOURCE_DIR}/include/highlightersetedit.ui ${CMAKE_CURRENT_SOURCE_DIR}/include/highlightersdialog.ui @@ -91,6 +94,8 @@ add_library( ${CMAKE_CURRENT_SOURCE_DIR}/src/downloader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/decompressor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/colorlabelsmanager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/sgrparser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/colorfultextparser.cpp ) set_target_properties(klogg_ui PROPERTIES AUTOUIC ON) diff --git a/src/ui/include/abstractlogview.h b/src/ui/include/abstractlogview.h index 1452b20b..03f9ffd4 100644 --- a/src/ui/include/abstractlogview.h +++ b/src/ui/include/abstractlogview.h @@ -64,6 +64,7 @@ #include "quickfindmux.h" #include "regularexpressionpattern.h" #include "selection.h" +#include "sgrparser.h" #include "viewtools.h" class QMenu; diff --git a/src/ui/include/ansi.h b/src/ui/include/ansi.h new file mode 100644 index 00000000..9dce7371 --- /dev/null +++ b/src/ui/include/ansi.h @@ -0,0 +1,94 @@ +// +// Created by marvin on 23-7-18. +// +#pragma once + +#include + +namespace ANSI { + +enum SequenceStartCnt { + HEAD_CNT = 2, +}; + +// reference: https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ +enum SequenceFirst { + EXC = 0x1B, // Escape(转义) +}; + +// reference: https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ +enum SequenceSecond { + PAD = 0x40, // 0x80 Padding Character(填充字符) + HOP = 0x41, // 0x81 High Octet Preset(高字节前置) + BPH = 0x42, // 0x82 Break Permitted Here(此处允许中断) + NBH = 0x43, // 0x83 No Break Here(此处禁止中断) + IND = 0x44, // 0x84 Index(索引) + NEL = 0x45, // 0x85 Next Line(下一行) + SSA = 0x46, // 0x86 Start of Selected Area(选择区域开始) + ESA = 0x47, // 0x87 End of Selected Area(选择区域结束) + HTS = 0x48, // 0x88 Horizontal Tab Set(水平制表设置) + HTJ = 0x49, // 0x89 Horizontal Tab Justified(水平制表调整) + VTS = 0x4A, // 0x8A Vertical Tab Set(垂直制表设置) + PLD = 0x4B, // 0x8B Partial Line Forward(部分行前移) + PLU = 0x4C, // 0x8C Partial Line Backward(部分行后移) + RI = 0x4D, // 0x8D Reverse Line Feed(逆向馈行) + SS2 = 0x4E, // 0x8E Single-Shift 2(单个移动2) + SS3 = 0x4F, // 0x8F Single-Shift 3(单个移动3) + DCS = 0x50, // 0x90 Device Control String(设备控制串) + PU1 = 0x51, // 0x91 Private Use 1(私用1) + PU2 = 0x52, // 0x92 Private Use 2(私用2) + STS = 0x53, // 0x93 Set Transmit State(发送规则设置) + CCH = 0x54, // 0x94 Cancel Character(取消字符) + MW = 0x55, // 0x95 Message Waiting(消息等待) + SPA = 0x56, // 0x96 Start of Protected Area(保护区域开始) + EPA = 0x57, // 0x97 End of Protected Area(保护区域结束) + SOS = 0x58, // 0x98 Start of String(串开始) + SGC = 0x59, // 0x99 Single Graphic Char Intro(单个图形字符描述) + SCI = 0x5A, // 0x9A Single Char Intro(单个字符描述) + CSI = 0x5B, // 0x9B Control Sequence Intro(控制顺序描述) + ST = 0x5C, // 0x9C String Terminator(串终止) + OSC = 0x5D, // 0x9D OS Command(操作系统指令) + PM = 0x5E, // 0x9E Private Message(私讯) + APC = 0x5F, // 0x9F App Program Command(应用程序命令) + + SECOND_BYTE_BEGIN = PAD, + SECOND_BYTE_END = APC, +}; + +// reference: https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ +enum CSIParameterBytes { + // ASCII: 0–9:;<=>? + NUM_BEGIN = 0x30, + NUM_END = 0x39, + SUB_PARA_SEPARATOR = 0x3A, + PARA_SEPARATOR = 0x3B, + STANDARDIZATION_KEEP_BEGIN = 0x3C, + STANDARDIZATION_KEEP_END = 0x3F, + + CSI_PARAMETER_BEGIN = 0x30, + CSI_PARAMETER_END = 0x3F, +}; + +enum CSIIntermediateBytes { + // ASCII: Space、!"#$%&'()*+,-./ + CSIIntermediateBegin = 0x20, + CSIIntermediateEnd = 0x2F, +}; + +enum CSIFinalBytes { + // ASCII: @A–Z[\]^_`a–z{|}~ + SGR = 0x6D, // m + + CSIFinalBegin = 0x40, + CSIFinalEnd = 0x7E, + + CSIFinalExperimentalBegin = 0x70, + CSIFinalExperimentalEnd = CSIFinalEnd, +}; + +enum class Return { + PARSE_ERROR = -1, + PARSE_SUCC = 0, +}; + +} // namespace ANSI diff --git a/src/ui/include/colorfultextparser.h b/src/ui/include/colorfultextparser.h new file mode 100644 index 00000000..1b06756a --- /dev/null +++ b/src/ui/include/colorfultextparser.h @@ -0,0 +1,68 @@ +// +// Created by marvin on 23-7-24. +// + +#pragma once + +#include + +#include + +#include "containers.h" +#include "highlightedmatch.h" +#include "sgrparser.h" + +namespace ansi { +using position_t = QString::size_type; +} + +struct TextColorAttr { + ANSI::Color color; + ansi::position_t start; + ansi::position_t len; + operator HighlightedMatch(); +}; + +struct ColorfulText { + QString text; + klogg::vector color; +}; + +// ANSI: start, data +using SGRSequence = std::pair; + +class CSIFilter { + public: + CSIFilter() = default; + ~CSIFilter() = default; + + static klogg::vector filter( QString& text ); +}; + +class ColorfulTextParser { + public: + enum class Mode { + MARKED_TEXT, + ALL_TEXT, + }; + + public: + explicit ColorfulTextParser( const ANSI::TextAttribute& defaultAttr, + const ANSI::TextAttribute& currentAttr ); + + // QString + ColorfulText parse( QString strings, Mode mode = Mode::ALL_TEXT ); + + klogg::vector parse( const klogg::vector& strings, + Mode mode = Mode::ALL_TEXT ); + + private: + void markedStringToText( klogg::vector& textList, + const klogg::vector& sgrSeqs, QString&& string ); + void allStringToText( klogg::vector& textList, + const klogg::vector& sgrSeqs, QString&& string ); + + private: + ANSI::TextAttribute currentTextAttr_; + ANSI::SGRParser sgrParser_; +}; diff --git a/src/ui/include/optionsdialog.ui b/src/ui/include/optionsdialog.ui index ac145c45..4b58673a 100644 --- a/src/ui/include/optionsdialog.ui +++ b/src/ui/include/optionsdialog.ui @@ -26,7 +26,7 @@ true - 0 + 1 @@ -398,6 +398,13 @@ Miscellaneous + + + + Display ANSI Colors (search performance will be reduced) + + + diff --git a/src/ui/include/sgrparser.h b/src/ui/include/sgrparser.h new file mode 100644 index 00000000..70989a6f --- /dev/null +++ b/src/ui/include/sgrparser.h @@ -0,0 +1,213 @@ +// +// Created by marvin on 23-7-19. +// +#pragma once + +#include +#include +#include + +#include "ansi.h" + +namespace ANSI { + +struct RGB { + uint8_t r, g, b; +}; + +template +RGB toRgb( T color ) +{ + return { (uint8_t)color.red(), (uint8_t)color.green(), (uint8_t)color.blue() }; +} + +struct Color { + RGB front; + RGB back; +}; + +struct TextAttribute { + enum class State { + DEFAULT, + CUSTOM, + }; + + State state; + Color color; +}; + +class SGRParser { + public: + using SGRParseReturn = std::pair; + + public: + explicit SGRParser( const TextAttribute& defaultTextAttr ); + ~SGRParser() = default; + + SGRParser( const SGRParser& ) = delete; + SGRParser( SGRParser&& ) = delete; + SGRParser& operator=( const SGRParser& ) = delete; + SGRParser& operator=( SGRParser&& ) = delete; + + /* + * @param currentTextAttr Properties of the current text + * @param sequence SGR sequence, example: "\033[31m" + * @return {return value, parsed text attribute} + * + * If the return value is ERROR, the parsed value is still guaranteed to be valid. + */ + SGRParseReturn parseSGRSequence( const TextAttribute& currentTextAttr, + const std::string& sequence ); + + private: + TextAttribute defaultTextAttr_; +}; + +class ColorTable; + +class SGRParseCore { + friend class ColorTable; + + public: + enum class ReturnVal { + RETURN_SUCCESS_BREAK, + RETURN_SUCCESS_CONTINUE, + + RETURN_ERROR_BREAK, + RETURN_ERROR_CONTINUE, + }; + + enum class ParseResult { + RESULT_UNSUPPORTED_ATTR, + RESULT_FRONT_COLOR, + RESULT_BACK_COLOR, + RESULT_DEFAULT_FRONT_COLOR, + RESULT_DEFAULT_BACK_COLOR, + RESULT_DEFAULT_TEXT_ATTR, + RESULT_CURRENT_TEXT_ATTR, + }; + + private: + enum class ColorVersion : uint8_t { + BIT_8 = 5, + BIT_24 = 2, + }; + + enum class ParseState { + STATE_WAIT_FIRST_PARAMETER, + STATE_WAIT_VERSION, + STATE_WAIT_BIT_8_ARGS, + STATE_WAIT_BIT_24_ARGS_R, + STATE_WAIT_BIT_24_ARGS_G, + STATE_WAIT_BIT_24_ARGS_B, + }; + + public: + SGRParseCore(); + ~SGRParseCore() = default; + + SGRParseCore( const SGRParseCore& ) = default; + SGRParseCore( SGRParseCore&& ) = default; + SGRParseCore& operator=( const SGRParseCore& ) = default; + SGRParseCore& operator=( SGRParseCore&& ) = default; + + ReturnVal parse( std::string_view& seqs ); + + inline void reset() + { + new ( this ) SGRParseCore(); + } + + inline ParseResult result() + { + return result_; + } + + inline RGB color() + { + return color_; + } + + private: + SGRParseCore( ParseResult result, RGB rgb, + ParseState s = ParseState::STATE_WAIT_FIRST_PARAMETER ); + + ReturnVal stringToParameter( const std::string_view& in, uint8_t& out ); + + ReturnVal setFirstParameter( const std::string_view& num ); + ReturnVal setColorVersion( const std::string_view& num ); + ReturnVal setBit8Color( const std::string_view& num ); + ReturnVal setBit24Color( const std::string_view& num ); + void setBit24ColorValue( uint8_t num ); + + private: + ParseResult result_; + ParseState state_; + RGB color_; + bool bit24Valid_; +}; + +class ColorTable { + public: + enum ColorIndex : uint8_t { + RESET_DEFAULT = 0, + + // 3/4-bit front color + F_BLACK = 30, + F_RED = 31, + F_GREEN = 32, + F_YELLOW = 33, + F_BLUE = 34, + F_MAGENTA = 35, + F_CYAN = 36, + F_WHITE = 37, + + // custom front color + F_CUSTOM_COLOR = 38, + // default front color + F_DEFAULT_COLOR = 39, + + // 3/4-bit back color + B_BLACK = 40, + B_RED = 41, + B_GREEN = 42, + B_YELLOW = 43, + B_BLUE = 44, + B_MAGENTA = 45, + B_CYAN = 46, + B_WHITE = 47, + + // custom back color + B_CUSTOM_COLOR = 48, + // default back color + B_DEFAULT_COLOR = 49, + + // 3/4-bit front bright color + F_BRIGHT_BLACK = 90, + F_BRIGHT_RED = 91, + F_BRIGHT_GREEN = 92, + F_BRIGHT_YELLOW = 93, + F_BRIGHT_BLUE = 94, + F_BRIGHT_MAGENTA = 95, + F_BRIGHT_CYAN = 96, + F_BRIGHT_WHITE = 97, + + // 3/4-bit back bright color + B_BRIGHT_BLACK = 100, + B_BRIGHT_RED = 101, + B_BRIGHT_GREEN = 102, + B_BRIGHT_YELLOW = 103, + B_BRIGHT_BLUE = 104, + B_BRIGHT_MAGENTA = 105, + B_BRIGHT_CYAN = 106, + B_BRIGHT_WHITE = 107, + }; + + public: + static SGRParseCore index( ColorIndex num ); + + private: + static std::map colorTable; +}; + +} // namespace ANSI diff --git a/src/ui/src/abstractlogview.cpp b/src/ui/src/abstractlogview.cpp index 5ac77c3e..9abdfa40 100644 --- a/src/ui/src/abstractlogview.cpp +++ b/src/ui/src/abstractlogview.cpp @@ -85,6 +85,7 @@ #include "active_screen.h" #include "clipboard.h" +#include "colorfultextparser.h" #include "configuration.h" #include "highlighterset.h" #include "highlightersmenu.h" @@ -2424,7 +2425,7 @@ void AbstractLogView::drawTextArea( QPaintDevice* paintDevice ) wrappedLinesNumbers_.clear(); for ( auto currentLine = 0_lcount; currentLine < nbLines; ++currentLine ) { const auto lineNumber = firstLine_ + currentLine; - const QString logLine = logLines[ lineNumber.get() ].string(); + const QString logLine = logLines[ currentLine.get() ].string(); const int xPos = contentStartPosX + ContentMarginWidth; @@ -2502,14 +2503,32 @@ void AbstractLogView::drawTextArea( QPaintDevice* paintDevice ) match.foreColor(), match.backColor() }; }; + // string to print, cut to fit the length and position of the view klogg::vector allHighlights; - allHighlights.reserve( highlighterMatches.size() ); + + const QString& expandedLine = [ & ]() { + if ( !Configuration::get().displayAnsiColorSequences() ) { + return logLines[ currentLine.get() ].expandedString(); + } + + using namespace ANSI; + TextAttribute defaultColor{ TextAttribute::State::DEFAULT, + { toRgb( foreColor ), toRgb( backColor ) } }; + ColorfulTextParser ansiParser{ defaultColor, defaultColor }; + return logLines[ currentLine.get() ].process( + [ &allHighlights, &ansiParser ]( auto& log ) { + log = untabify( std::move( log ) ); + using Mode = ColorfulTextParser::Mode; + auto&& [ text, colors ] = ansiParser.parse( log, Mode::MARKED_TEXT ); + log = std::move( text ); + allHighlights.reserve( colors.size() ); + allHighlights.insert( allHighlights.end(), colors.begin(), colors.end() ); + } ); + }(); + allHighlights.reserve( allHighlights.size() + highlighterMatches.size() ); std::transform( highlighterMatches.cbegin(), highlighterMatches.cend(), std::back_inserter( allHighlights ), untabifyHighlight ); - // string to print, cut to fit the length and position of the view - const QString& expandedLine = logLines[ currentLine.get() ].expandedString(); - // Has the line got elements to be highlighted klogg::vector quickFindMatches; quickFindPattern_->matchLine( expandedLine, quickFindMatches ); diff --git a/src/ui/src/colorfultextparser.cpp b/src/ui/src/colorfultextparser.cpp new file mode 100644 index 00000000..088c6af5 --- /dev/null +++ b/src/ui/src/colorfultextparser.cpp @@ -0,0 +1,193 @@ +// +// Created by marvin on 23-7-24. +// + +#include "colorfultextparser.h" + +#include +#include + +#ifndef Q_OS_WIN +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnonnull" +using RemoveCount = decltype( ( (QRegularExpressionMatch*)nullptr )->capturedLength() ); +#pragma GCC diagnostic pop +#else +using RemoveCount = decltype( ( (QRegularExpressionMatch*)nullptr )->capturedLength() ); +#endif + +static QString pattern{ "\\x1B\\[([0-9]{0,4}([;:][0-9]{1,3})*)?[mK]" }; + +using namespace ANSI; + +TextColorAttr::operator HighlightedMatch() +{ + return HighlightedMatch( LineColumn{ (LineColumn::UnderlyingType)start }, + LineLength{ (LineLength::UnderlyingType)len }, + QColor{ color.front.r, color.front.g, color.front.b }, + QColor{ color.back.r, color.back.g, color.back.b } ); +} + +klogg::vector CSIFilter::filter( QString& string ) +{ + klogg::vector ansiSeqs; + RemoveCount removeAnsiCharCnt = 0; + QRegularExpression reg( pattern ); + auto allResult = reg.globalMatch( string ); + + while ( allResult.hasNext() ) { + auto result = allResult.next(); + +#if QT_VERSION >= QT_VERSION_CHECK( 5, 10, 0 ) + auto ref = result.capturedView(); +#else + auto ref = result.capturedRef(); +#endif + + auto startPos = result.capturedStart() - removeAnsiCharCnt; + ansiSeqs.emplace_back( startPos, ref.toString() ); + removeAnsiCharCnt += result.capturedLength(); + } + string.remove( reg ); + return ansiSeqs; +} + +ColorfulTextParser::ColorfulTextParser( const ANSI::TextAttribute& defaultAttr, + const ANSI::TextAttribute& currentAttr ) + : currentTextAttr_( currentAttr ) + , sgrParser_( defaultAttr ) +{ +} + +ColorfulText ColorfulTextParser::parse( QString string, Mode mode ) +{ + klogg::vector textList; + auto sgrSeqs = CSIFilter::filter( string ); + if ( mode == Mode::ALL_TEXT ) { + allStringToText( textList, sgrSeqs, std::move( string ) ); + } + else if ( mode == Mode::MARKED_TEXT ) { + markedStringToText( textList, sgrSeqs, std::move( string ) ); + } + + return textList.empty() ? ColorfulText{} : textList.front(); +} + +klogg::vector ColorfulTextParser::parse( const klogg::vector& strings, + ColorfulTextParser::Mode mode ) +{ + klogg::vector textList; + for ( auto string : strings ) { + auto sgrSeqs = CSIFilter::filter( string ); + if ( mode == Mode::ALL_TEXT ) { + allStringToText( textList, sgrSeqs, std::move( string ) ); + } + else if ( mode == Mode::MARKED_TEXT ) { + markedStringToText( textList, sgrSeqs, std::move( string ) ); + } + } + return textList; +} + +void ColorfulTextParser::markedStringToText( klogg::vector& textList, + const klogg::vector& sgrSeqs, + QString&& string ) +{ + ColorfulText colorfulText{ string, {} }; + textList.emplace_back( std::move( colorfulText ) ); + auto& colors = textList.back().color; + + // empty SGR sequences process + if ( sgrSeqs.empty() ) { + if ( currentTextAttr_.state == TextAttribute::State::CUSTOM ) { + TextColorAttr desc{ currentTextAttr_.color, 0, string.size() }; + colors.emplace_back( desc ); + } + return; + } + + // get first colorful text pos and attribute + auto firstResult + = sgrParser_.parseSGRSequence( currentTextAttr_, sgrSeqs[ 0 ].second.toStdString() ); + auto curPos = sgrSeqs[ 0 ].first; + auto curTextAttr = firstResult.second; + + for ( size_t i = 0; i < sgrSeqs.size(); ++i ) { + // if exist next colorful text , parse sequence + // else set next pos to string end + ansi::position_t nextPos; + decltype( curTextAttr ) nextTextAttr; + if ( i + 1 < sgrSeqs.size() ) { + auto result + = sgrParser_.parseSGRSequence( curTextAttr, sgrSeqs[ i + 1 ].second.toStdString() ); + nextPos = sgrSeqs[ i + 1 ].first; + nextTextAttr = result.second; + } + else { + nextPos = string.size(); + nextTextAttr = curTextAttr; + } + + if ( curTextAttr.state == TextAttribute::State::CUSTOM ) { + TextColorAttr desc{ curTextAttr.color, curPos, nextPos - curPos }; + colors.emplace_back( desc ); + } + + curPos = nextPos; + curTextAttr = nextTextAttr; + } + currentTextAttr_ = curTextAttr; +} + +void ColorfulTextParser::allStringToText( klogg::vector& textList, + const klogg::vector& sgrSeqs, + QString&& string ) +{ + ColorfulText colorfulText{ string, {} }; + textList.emplace_back( std::move( colorfulText ) ); + auto& colors = textList.back().color; + + // empty SGR sequences process + if ( sgrSeqs.empty() ) { + TextColorAttr desc{ currentTextAttr_.color, 0, string.size() }; + colors.emplace_back( desc ); + return; + } + + ansi::position_t curPos = 0; + auto curTextAttr = currentTextAttr_; + + // first text push back + auto firstResult + = sgrParser_.parseSGRSequence( curTextAttr, sgrSeqs[ 0 ].second.toStdString() ); + auto nextPos = sgrSeqs[ 0 ].first; + auto nextTextAttr = firstResult.second; + if ( curPos < nextPos ) { + TextColorAttr desc{ curTextAttr.color, curPos, nextPos - curPos }; + colors.emplace_back( desc ); + } + + // update context + curPos = nextPos; + curTextAttr = nextTextAttr; + + for ( size_t i = 0; i < sgrSeqs.size(); ++i ) { + if ( i + 1 < sgrSeqs.size() ) { + auto result + = sgrParser_.parseSGRSequence( curTextAttr, sgrSeqs[ i + 1 ].second.toStdString() ); + nextPos = sgrSeqs[ i + 1 ].first; + nextTextAttr = result.second; + } + else { + nextPos = string.size(); + nextTextAttr = curTextAttr; + } + + TextColorAttr desc{ curTextAttr.color, curPos, nextPos - curPos }; + colors.emplace_back( desc ); + + curPos = nextPos; + curTextAttr = nextTextAttr; + } + currentTextAttr_ = curTextAttr; +} diff --git a/src/ui/src/crawlerwidget.cpp b/src/ui/src/crawlerwidget.cpp index f7a58286..ce9db796 100644 --- a/src/ui/src/crawlerwidget.cpp +++ b/src/ui/src/crawlerwidget.cpp @@ -631,7 +631,7 @@ void CrawlerWidget::applyConfiguration() font.setStyleStrategy( QFont::PreferAntialias ); } - if ( config.hideAnsiColorSequences() ) { + if ( config.hideAnsiColorSequences() || config.displayAnsiColorSequences() ) { logData_->setPrefilter( AnsiColorSequenceRegex ); } else { diff --git a/src/ui/src/optionsdialog.cpp b/src/ui/src/optionsdialog.cpp index 4e6b6905..ffb2c23b 100644 --- a/src/ui/src/optionsdialog.cpp +++ b/src/ui/src/optionsdialog.cpp @@ -95,6 +95,11 @@ OptionsDialog::OptionsDialog( QWidget* parent ) buildShortcutsTable( true ); } ); + connect( displayAnsiColorsCheckBox, &QCheckBox::clicked, [ this ]( bool clicked ) { + hideAnsiColorsCheckBox->setChecked( clicked ); + hideAnsiColorsCheckBox->setDisabled( clicked ); + } ); + updateDialogFromConfig(); setupPolling(); @@ -319,7 +324,10 @@ void OptionsDialog::updateDialogFromConfig() styleComboBox->setCurrentText( style ); } - hideAnsiColorsCheckBox->setChecked( config.hideAnsiColorSequences() ); + displayAnsiColorsCheckBox->setChecked( config.displayAnsiColorSequences() ); + hideAnsiColorsCheckBox->setDisabled( config.displayAnsiColorSequences() ); + hideAnsiColorsCheckBox->setChecked( config.hideAnsiColorSequences() + || config.displayAnsiColorSequences() ); // Regexp types mainSearchBox->setCurrentIndex( getRegexpTypeIndex( config.mainRegexpType() ) ); @@ -538,7 +546,9 @@ void OptionsDialog::updateConfigFromDialog() restartAppMessage = config.style() != styleComboBox->currentText(); config.setStyle( styleComboBox->currentText() ); - config.setHideAnsiColorSequences( hideAnsiColorsCheckBox->isChecked() ); + config.setDisplayAnsiColorSequences( displayAnsiColorsCheckBox->isChecked() ); + config.setHideAnsiColorSequences( hideAnsiColorsCheckBox->isChecked() + || displayAnsiColorsCheckBox->isChecked() ); config.setDefaultEncodingMib( encodingComboBox->currentData().toInt() ); diff --git a/src/ui/src/sgrparser.cpp b/src/ui/src/sgrparser.cpp new file mode 100644 index 00000000..0308aab0 --- /dev/null +++ b/src/ui/src/sgrparser.cpp @@ -0,0 +1,417 @@ +// +// Created by marvin on 23-7-19. +// + +#include "sgrparser.h" + +#include +#include + +#include "ansi.h" + +namespace ANSI { + +using ParseResult = SGRParseCore::ParseResult; +using ColorIndex = ColorTable::ColorIndex; + +SGRParser::SGRParser( const TextAttribute& defaultTextAttr ) + : defaultTextAttr_{ TextAttribute::State::DEFAULT, defaultTextAttr.color } +{ +} + +SGRParser::SGRParseReturn SGRParser::parseSGRSequence( const TextAttribute& currentTextAttr, + const std::string& sequence ) +{ + // sequence start byte + CSI final byte size error, return current color + if ( sequence.size() < SequenceStartCnt::HEAD_CNT + 1 ) { + return { Return::PARSE_ERROR, currentTextAttr }; + } + // check sequence format + if ( sequence[ 0 ] != SequenceFirst::EXC || sequence[ 1 ] != SequenceSecond::CSI + || sequence.back() != CSIFinalBytes::SGR ) { + return { Return::PARSE_ERROR, currentTextAttr }; + } + // remove sequence start byte + std::string_view seqView( sequence ); + seqView.remove_prefix( HEAD_CNT ); + + SGRParseCore core{}; + SGRParseCore::ReturnVal ctxRet; + SGRParseReturn ret{ Return::PARSE_SUCC, currentTextAttr }; + + while ( !seqView.empty() ) { + // continuous parsing and logging of results at each step + ctxRet = core.parse( seqView ); + switch ( core.result() ) { + case ParseResult::RESULT_FRONT_COLOR: { + ret.second.state = TextAttribute::State::CUSTOM; + ret.second.color.front = core.color(); + } break; + case ParseResult::RESULT_BACK_COLOR: { + ret.second.state = TextAttribute::State::CUSTOM; + ret.second.color.back = core.color(); + } break; + case ParseResult::RESULT_DEFAULT_FRONT_COLOR: { + ret.second.color.front = defaultTextAttr_.color.front; + } break; + case ParseResult::RESULT_DEFAULT_BACK_COLOR: { + ret.second.color.back = defaultTextAttr_.color.back; + } break; + case ParseResult::RESULT_DEFAULT_TEXT_ATTR: { + ret.second = defaultTextAttr_; + } break; + case ParseResult::RESULT_CURRENT_TEXT_ATTR: + case ParseResult::RESULT_UNSUPPORTED_ATTR: { + // keep parsed attribute, do nothing + // ret.second = currentTextAttr; + } break; + } + + core.reset(); + + // RETURN_ERROR_BREAK aborts parsing and invalidates parsed results + if ( ctxRet == SGRParseCore::ReturnVal::RETURN_ERROR_BREAK ) { + ret = { Return::PARSE_ERROR, currentTextAttr }; + break; + } + } + + return ret; +} + +enum class ConvertRet { + NOT_U8 = -2, + NOT_NUM = -1, + SUCCESS = 0, +}; + +std::pair base10ToU8( const std::string_view& num ) +{ + bool has = false; + int value = 0; + + for ( auto ch : num ) { + if ( ch > '9' || ch < '0' ) { + has = false; + break; + } + + value *= 10; + value += ( ch - '0' ); + has = true; + } + + if ( has ) { + if ( value > std::numeric_limits::max() ) { + return { ConvertRet::NOT_U8, {} }; + } + return { ConvertRet::SUCCESS, static_cast( value ) }; + } + return { ConvertRet::NOT_NUM, {} }; +} + +SGRParseCore::SGRParseCore() + : result_( ParseResult::RESULT_CURRENT_TEXT_ATTR ) + , state_( ParseState::STATE_WAIT_FIRST_PARAMETER ) + , color_() + , bit24Valid_( true ) +{ +} + +SGRParseCore::SGRParseCore( ParseResult result, RGB rgb, ParseState s ) + : result_( result ) + , state_( s ) + , color_( rgb ) + , bit24Valid_( true ) +{ +} + +SGRParseCore::ReturnVal SGRParseCore::stringToParameter( const std::string_view& in, uint8_t& out ) +{ + auto [ ret, value ] = base10ToU8( in ); + // not number parse break, keep current text attribute + if ( ret == ConvertRet::NOT_NUM ) { + result_ = ParseResult ::RESULT_CURRENT_TEXT_ATTR; + return ReturnVal::RETURN_ERROR_BREAK; + } + // not u8 parse continue, use last parse result + else if ( ret == ConvertRet::NOT_U8 ) { + state_ = ParseState::STATE_WAIT_FIRST_PARAMETER; + return ReturnVal::RETURN_ERROR_CONTINUE; + } + out = value; + return ReturnVal::RETURN_SUCCESS_CONTINUE; +} + +SGRParseCore::ReturnVal SGRParseCore::setFirstParameter( const std::string_view& num ) +{ + // in the first parameter, the default value is 0, which will reset all text attributes. + if ( num.empty() ) { + result_ = ParseResult::RESULT_DEFAULT_TEXT_ATTR; + return ReturnVal::RETURN_SUCCESS_BREAK; + } + + uint8_t value; + auto ret = stringToParameter( num, value ); + if ( ret != ReturnVal::RETURN_SUCCESS_CONTINUE ) { + return ret; + } + + *this = ColorTable::index( ColorTable::ColorIndex( value ) ); + // UNKNOWN is not support, so continue + if ( result_ == ParseResult::RESULT_UNSUPPORTED_ATTR ) { + return ReturnVal::RETURN_ERROR_CONTINUE; + } + + // when index valid result, state is STATE_WAIT_FIRST_PARAMETER, + return ( state_ == ParseState::STATE_WAIT_FIRST_PARAMETER + ? ReturnVal::RETURN_SUCCESS_BREAK + : ReturnVal::RETURN_SUCCESS_CONTINUE ); +} + +SGRParseCore::ReturnVal SGRParseCore::setColorVersion( const std::string_view& num ) +{ + // before version parameter is 38, empty parameter will reset parse state, and use last color + if ( num.empty() ) { + state_ = ParseState::STATE_WAIT_FIRST_PARAMETER; + return ReturnVal::RETURN_ERROR_CONTINUE; + } + + uint8_t value; + auto ret = stringToParameter( num, value ); + if ( ret != ReturnVal::RETURN_SUCCESS_CONTINUE ) { + return ret; + } + + // update state + if ( ColorVersion::BIT_8 == ColorVersion( value ) ) { + state_ = ParseState::STATE_WAIT_BIT_8_ARGS; + } + else if ( ColorVersion::BIT_24 == ColorVersion( value ) ) { + state_ = ParseState::STATE_WAIT_BIT_24_ARGS_R; + } + else { + // invalid value will reset parse state, and use last parse result + state_ = ParseState::STATE_WAIT_FIRST_PARAMETER; + return ReturnVal::RETURN_ERROR_CONTINUE; + } + + return ReturnVal::RETURN_SUCCESS_CONTINUE; +} + +SGRParseCore::ReturnVal SGRParseCore::setBit8Color( const std::string_view& num ) +{ + // 8-bit color parameter empty, will use last color, then parse continue + if ( num.empty() ) { + state_ = ParseState::STATE_WAIT_FIRST_PARAMETER; + return ReturnVal::RETURN_ERROR_CONTINUE; + } + + uint8_t value; + auto ret = stringToParameter( num, value ); + if ( ret != ReturnVal::RETURN_SUCCESS_CONTINUE ) { + return ret; + } + + // reference: https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + // Standard colors + if ( value <= 7 ) { + // storage result color position, after index color, recover it + auto result = result_; + *this = ColorTable::index( ColorTable::ColorIndex( ColorTable::F_BLACK + value ) ); + result_ = result; + } + // High-intensity colors + else if ( value >= 8 && value <= 15 ) { + // storage result color position, after index color, recover it + auto result = result_; + *this = ColorTable::index( ColorTable::ColorIndex( ColorTable::F_BRIGHT_BLACK + value ) ); + result_ = result; + } + // Grayscale colors + else if ( value >= 232 ) { + auto colorValue = uint8_t( ( value - 232 ) * 10 + 8 ); + color_.r = colorValue; + color_.g = colorValue; + color_.b = colorValue; + state_ = ParseState::STATE_WAIT_FIRST_PARAMETER; + } + // 216 colors + else { + constexpr uint8_t colorValue[]{ 0, 95, 135, 175, 215, 255 }; + auto val = value - 16; + auto remainder = val % 36; + color_.r = colorValue[ val / 36 ]; + color_.g = colorValue[ remainder / 6 ]; + color_.b = colorValue[ remainder % 6 ]; + state_ = ParseState::STATE_WAIT_FIRST_PARAMETER; + } + + return ReturnVal::RETURN_SUCCESS_BREAK; +} + +SGRParseCore::ReturnVal SGRParseCore::setBit24Color( const std::string_view& num ) +{ + if ( num.empty() ) { + // if bit24 color parameter empty, all the 24bit color parameters are invalid. + bit24Valid_ = false; + } + + // bit24Valid is false, no need to convert num and set color + if ( bit24Valid_ ) { + uint8_t value; + auto ret = stringToParameter( num, value ); + if ( ret != ReturnVal::RETURN_SUCCESS_CONTINUE ) { + return ret; + } + setBit24ColorValue( value ); + + // if state is STATE_WAIT_FIRST_PARAMETER, exist color, so break + return ( state_ == ParseState::STATE_WAIT_FIRST_PARAMETER + ? ReturnVal::RETURN_SUCCESS_BREAK + : ReturnVal::RETURN_SUCCESS_CONTINUE ); + } + else { + setBit24ColorValue( 0 ); + // restore state after ignoring invalid parameters + if ( state_ == ParseState::STATE_WAIT_FIRST_PARAMETER ) { + bit24Valid_ = true; + } + return ReturnVal::RETURN_ERROR_CONTINUE; + } +} + +// save color value and change state +void SGRParseCore::setBit24ColorValue( uint8_t num ) +{ + switch ( state_ ) { + case ParseState::STATE_WAIT_BIT_24_ARGS_R: { + color_.r = num; + state_ = ParseState::STATE_WAIT_BIT_24_ARGS_G; + } break; + case ParseState::STATE_WAIT_BIT_24_ARGS_G: { + color_.g = num; + state_ = ParseState::STATE_WAIT_BIT_24_ARGS_B; + } break; + case ParseState::STATE_WAIT_BIT_24_ARGS_B: { + color_.b = num; + state_ = ParseState::STATE_WAIT_FIRST_PARAMETER; + } break; + default: + // If the code is not written correctly, it will go here + assert( false ); + } +} + +SGRParseCore::ReturnVal SGRParseCore::parse( std::string_view& seqs ) +{ + auto pos = seqs.find_first_of( ";:m" ); + ReturnVal parseRet = ReturnVal::RETURN_SUCCESS_CONTINUE; + + while ( pos != std::string_view::npos ) { + std::string_view num{ seqs.data(), pos }; + + switch ( state_ ) { + case ParseState::STATE_WAIT_FIRST_PARAMETER: { + parseRet = setFirstParameter( num ); + } break; + case ParseState::STATE_WAIT_VERSION: { + parseRet = setColorVersion( num ); + } break; + case ParseState::STATE_WAIT_BIT_8_ARGS: { + parseRet = setBit8Color( num ); + } break; + case ParseState::STATE_WAIT_BIT_24_ARGS_R: { + case ParseState::STATE_WAIT_BIT_24_ARGS_G: + case ParseState::STATE_WAIT_BIT_24_ARGS_B: + parseRet = setBit24Color( num ); + } break; + } + + seqs.remove_prefix( pos + 1 ); + pos = seqs.find_first_of( ";:m" ); + + // if return BREAK, return current parse result + if ( ReturnVal::RETURN_SUCCESS_BREAK == parseRet + || ReturnVal::RETURN_ERROR_BREAK == parseRet ) { + break; + } + } + + return parseRet; +} + +// reference: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit +// {index, {result, color, state}} +// If it is a valid color, state must be STATE_WAIT_FIRST_PARAMETER +std::map ColorTable::colorTable{ + // reset to default + { ColorIndex::RESET_DEFAULT, { ParseResult::RESULT_DEFAULT_TEXT_ATTR, {} } }, + + // 3/4-bit front color + { ColorIndex::F_BLACK, { ParseResult::RESULT_FRONT_COLOR, { 1, 1, 1 } } }, + { ColorIndex::F_RED, { ParseResult::RESULT_FRONT_COLOR, { 222, 56, 43 } } }, + { ColorIndex::F_GREEN, { ParseResult::RESULT_FRONT_COLOR, { 57, 181, 74 } } }, + { ColorIndex::F_YELLOW, { ParseResult::RESULT_FRONT_COLOR, { 255, 199, 6 } } }, + { ColorIndex::F_BLUE, { ParseResult::RESULT_FRONT_COLOR, { 0, 111, 184 } } }, + { ColorIndex::F_MAGENTA, { ParseResult::RESULT_FRONT_COLOR, { 118, 38, 113 } } }, + { ColorIndex::F_CYAN, { ParseResult::RESULT_FRONT_COLOR, { 44, 181, 233 } } }, + { ColorIndex::F_WHITE, { ParseResult::RESULT_FRONT_COLOR, { 204, 204, 204 } } }, + + // custom front color + { ColorIndex::F_CUSTOM_COLOR, + { ParseResult::RESULT_FRONT_COLOR, {}, SGRParseCore::ParseState::STATE_WAIT_VERSION } }, + + // default front color + { ColorIndex::F_DEFAULT_COLOR, { ParseResult::RESULT_DEFAULT_FRONT_COLOR, {} } }, + + // 3/4-bit back color + { ColorIndex::B_BLACK, { ParseResult::RESULT_BACK_COLOR, { 1, 1, 1 } } }, + { ColorIndex::B_RED, { ParseResult::RESULT_BACK_COLOR, { 222, 56, 43 } } }, + { ColorIndex::B_GREEN, { ParseResult::RESULT_BACK_COLOR, { 57, 181, 74 } } }, + { ColorIndex::B_YELLOW, { ParseResult::RESULT_BACK_COLOR, { 255, 199, 6 } } }, + { ColorIndex::B_BLUE, { ParseResult::RESULT_BACK_COLOR, { 0, 111, 184 } } }, + { ColorIndex::B_MAGENTA, { ParseResult::RESULT_BACK_COLOR, { 118, 38, 113 } } }, + { ColorIndex::B_CYAN, { ParseResult::RESULT_BACK_COLOR, { 44, 181, 233 } } }, + { ColorIndex::B_WHITE, { ParseResult::RESULT_BACK_COLOR, { 204, 204, 204 } } }, + + // custom back color + { ColorIndex::B_CUSTOM_COLOR, + { ParseResult::RESULT_BACK_COLOR, {}, SGRParseCore::ParseState::STATE_WAIT_VERSION } }, + + // default front color + { ColorIndex::B_DEFAULT_COLOR, { ParseResult::RESULT_DEFAULT_BACK_COLOR, {} } }, + + // 3/4-bit front bright color + { ColorIndex::F_BRIGHT_BLACK, { ParseResult::RESULT_FRONT_COLOR, { 128, 128, 128 } } }, + { ColorIndex::F_BRIGHT_RED, { ParseResult::RESULT_FRONT_COLOR, { 255, 0, 0 } } }, + { ColorIndex::F_BRIGHT_GREEN, { ParseResult::RESULT_FRONT_COLOR, { 0, 255, 0 } } }, + { ColorIndex::F_BRIGHT_YELLOW, { ParseResult::RESULT_FRONT_COLOR, { 255, 255, 0 } } }, + { ColorIndex::F_BRIGHT_BLUE, { ParseResult::RESULT_FRONT_COLOR, { 0, 0, 255 } } }, + { ColorIndex::F_BRIGHT_MAGENTA, { ParseResult::RESULT_FRONT_COLOR, { 255, 0, 255 } } }, + { ColorIndex::F_BRIGHT_CYAN, { ParseResult::RESULT_FRONT_COLOR, { 0, 255, 255 } } }, + { ColorIndex::F_BRIGHT_WHITE, { ParseResult::RESULT_FRONT_COLOR, { 255, 255, 255 } } }, + + // 3/4-bit back bright color + { ColorIndex::B_BRIGHT_BLACK, { ParseResult::RESULT_BACK_COLOR, { 128, 128, 128 } } }, + { ColorIndex::B_BRIGHT_RED, { ParseResult::RESULT_BACK_COLOR, { 255, 0, 0 } } }, + { ColorIndex::B_BRIGHT_GREEN, { ParseResult::RESULT_BACK_COLOR, { 0, 255, 0 } } }, + { ColorIndex::B_BRIGHT_YELLOW, { ParseResult::RESULT_BACK_COLOR, { 255, 255, 0 } } }, + { ColorIndex::B_BRIGHT_BLUE, { ParseResult::RESULT_BACK_COLOR, { 0, 0, 255 } } }, + { ColorIndex::B_BRIGHT_MAGENTA, { ParseResult::RESULT_BACK_COLOR, { 255, 0, 255 } } }, + { ColorIndex::B_BRIGHT_CYAN, { ParseResult::RESULT_BACK_COLOR, { 0, 255, 255 } } }, + { ColorIndex::B_BRIGHT_WHITE, { ParseResult::RESULT_BACK_COLOR, { 255, 255, 255 } } }, +}; + +SGRParseCore ColorTable::index( ColorIndex num ) +{ + auto ret = colorTable.find( num ); + if ( ret == colorTable.end() ) { + return { SGRParseCore::ParseResult::RESULT_UNSUPPORTED_ATTR, + {}, + SGRParseCore ::ParseState::STATE_WAIT_FIRST_PARAMETER }; + } + return ret->second; +} + +} // namespace ANSI \ No newline at end of file