mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-22 13:01:05 +03:00
Added Kitty Graphics Protocol support for eterm module, still a WIP but working.
This commit is contained in:
@@ -1,63 +1,123 @@
|
||||
#include <eepp/system/base64.hpp>
|
||||
#include <array>
|
||||
|
||||
namespace EE { namespace System {
|
||||
|
||||
/* base64.c : base-64 / MIME encode/decode */
|
||||
/* PUBLIC DOMAIN - Jon Mayo - November 13, 2003 */
|
||||
/* $Id: base64.c 156 2007-07-12 23:29:10Z orange $ */
|
||||
namespace {
|
||||
|
||||
/* decode a base64 string in one shot */
|
||||
size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ) {
|
||||
static const Uint8 base64dec_tab[256] = {
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63, 52, 53, 54, 55, 56, 57,
|
||||
58, 59, 60, 61, 255, 255, 255, 0, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6,
|
||||
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
|
||||
25, 255, 255, 255, 255, 255, 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
|
||||
37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255,
|
||||
};
|
||||
constexpr Uint8 BASE64_INVALID = 0xFF;
|
||||
constexpr Uint8 BASE64_PADDING = 0xFE;
|
||||
constexpr Uint8 BASE64_WHITESPACE = 0xFD;
|
||||
|
||||
size_t ii, io;
|
||||
Uint32 v;
|
||||
unsigned rem;
|
||||
constexpr std::array<Uint8, 256> makeBase64DecodeTable() {
|
||||
std::array<Uint8, 256> table{};
|
||||
|
||||
for ( size_t i = 0; i < table.size(); ++i )
|
||||
table[i] = BASE64_INVALID;
|
||||
|
||||
for ( size_t i = 0; i < 26; ++i ) {
|
||||
table[static_cast<unsigned char>( 'A' + i )] = static_cast<Uint8>( i );
|
||||
table[static_cast<unsigned char>( 'a' + i )] = static_cast<Uint8>( i + 26 );
|
||||
}
|
||||
|
||||
for ( size_t i = 0; i < 10; ++i )
|
||||
table[static_cast<unsigned char>( '0' + i )] = static_cast<Uint8>( i + 52 );
|
||||
|
||||
table[static_cast<unsigned char>( '+' )] = 62;
|
||||
table[static_cast<unsigned char>( '/' )] = 63;
|
||||
table[static_cast<unsigned char>( '=' )] = BASE64_PADDING;
|
||||
|
||||
// ASCII whitespace accepted by the historical decoder via isspace():
|
||||
table[static_cast<unsigned char>( ' ' )] = BASE64_WHITESPACE;
|
||||
table[static_cast<unsigned char>( '\t' )] = BASE64_WHITESPACE;
|
||||
table[static_cast<unsigned char>( '\n' )] = BASE64_WHITESPACE;
|
||||
table[static_cast<unsigned char>( '\v' )] = BASE64_WHITESPACE;
|
||||
table[static_cast<unsigned char>( '\f' )] = BASE64_WHITESPACE;
|
||||
table[static_cast<unsigned char>( '\r' )] = BASE64_WHITESPACE;
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
constexpr auto base64dec_tab = makeBase64DecodeTable();
|
||||
|
||||
template <bool AllowWhitespace>
|
||||
size_t decodeBase64( size_t in_len, const char* in, size_t out_len, unsigned char* out ) {
|
||||
size_t ii = 0;
|
||||
size_t io = 0;
|
||||
Uint32 v = 0;
|
||||
unsigned rem = 0;
|
||||
|
||||
while ( ii < in_len ) {
|
||||
/*
|
||||
* Fast path: four ordinary base64 bytes become three output bytes.
|
||||
*
|
||||
* The OR validates all four table entries at once: valid base64 values
|
||||
* are 0..63, while padding/whitespace/invalid entries have high bits set.
|
||||
*
|
||||
* This path is especially useful for protocol payloads that guarantee
|
||||
* contiguous, whitespace-free base64 data.
|
||||
*/
|
||||
if ( rem == 0 && ii + 4 <= in_len && io + 3 <= out_len ) {
|
||||
const Uint8 a = base64dec_tab[static_cast<unsigned char>( in[ii] )];
|
||||
const Uint8 b = base64dec_tab[static_cast<unsigned char>( in[ii + 1] )];
|
||||
const Uint8 c = base64dec_tab[static_cast<unsigned char>( in[ii + 2] )];
|
||||
const Uint8 d = base64dec_tab[static_cast<unsigned char>( in[ii + 3] )];
|
||||
|
||||
if ( ( a | b | c | d ) <= 63 ) {
|
||||
out[io] = static_cast<unsigned char>( ( a << 2 ) | ( b >> 4 ) );
|
||||
out[io + 1] = static_cast<unsigned char>( ( b << 4 ) | ( c >> 2 ) );
|
||||
out[io + 2] = static_cast<unsigned char>( ( c << 6 ) | d );
|
||||
|
||||
ii += 4;
|
||||
io += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const Uint8 ch = base64dec_tab[static_cast<unsigned char>( in[ii++] )];
|
||||
|
||||
if ( ch > 63 ) {
|
||||
if constexpr ( AllowWhitespace ) {
|
||||
if ( ch == BASE64_WHITESPACE )
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preserve the old behavior: stop at '=' or the first parse error.
|
||||
break;
|
||||
}
|
||||
|
||||
for ( io = 0, ii = 0, v = 0, rem = 0; ii < in_len; ii++ ) {
|
||||
unsigned char ch;
|
||||
unsigned char c = (unsigned char)in[ii];
|
||||
if ( isspace( c ) )
|
||||
continue;
|
||||
if ( c == '=' )
|
||||
break; /* stop at = */
|
||||
ch = base64dec_tab[c];
|
||||
if ( ch == 255 )
|
||||
break; /* stop at a parse error */
|
||||
v = ( v << 6 ) | ch;
|
||||
rem += 6;
|
||||
|
||||
if ( rem >= 8 ) {
|
||||
rem -= 8;
|
||||
|
||||
if ( io >= out_len )
|
||||
return -1; /* truncation is failure */
|
||||
out[io++] = ( v >> rem ) & 255;
|
||||
return static_cast<size_t>( -1 );
|
||||
|
||||
out[io++] = static_cast<unsigned char>( ( v >> rem ) & 255 );
|
||||
|
||||
// Every four valid base64 characters rem returns to zero.
|
||||
if ( rem == 0 )
|
||||
v = 0;
|
||||
}
|
||||
}
|
||||
if ( rem >= 8 ) {
|
||||
rem -= 8;
|
||||
if ( io >= out_len )
|
||||
return -1; /* truncation is failure */
|
||||
out[io++] = ( v >> rem ) & 255;
|
||||
}
|
||||
|
||||
return io;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ) {
|
||||
return decodeBase64<true>( in_len, in, out_len, out );
|
||||
}
|
||||
|
||||
size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out,
|
||||
DecodeMode mode ) {
|
||||
return mode == DecodeMode::NoWhitespace ? decodeBase64<false>( in_len, in, out_len, out )
|
||||
: decodeBase64<true>( in_len, in, out_len, out );
|
||||
}
|
||||
|
||||
size_t Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, char* out ) {
|
||||
static const Uint8 base64enc_tab[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
@@ -74,23 +134,27 @@ size_t Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, c
|
||||
while ( rem >= 6 ) {
|
||||
rem -= 6;
|
||||
if ( io >= out_len )
|
||||
return -1; /* truncation is failure */
|
||||
return static_cast<size_t>( -1 ); /* truncation is failure */
|
||||
out[io++] = base64enc_tab[( v >> rem ) & 63];
|
||||
}
|
||||
}
|
||||
|
||||
if ( rem ) {
|
||||
v <<= ( 6 - rem );
|
||||
if ( io >= out_len )
|
||||
return -1; /* truncation is failure */
|
||||
return static_cast<size_t>( -1 ); /* truncation is failure */
|
||||
out[io++] = base64enc_tab[v & 63];
|
||||
}
|
||||
|
||||
while ( io & 3 ) {
|
||||
if ( io >= out_len )
|
||||
return -1; /* truncation is failure */
|
||||
return static_cast<size_t>( -1 ); /* truncation is failure */
|
||||
out[io++] = '=';
|
||||
}
|
||||
|
||||
if ( io >= out_len )
|
||||
return -1; /* no room for null terminator */
|
||||
return static_cast<size_t>( -1 ); /* no room for null terminator */
|
||||
|
||||
out[io] = 0;
|
||||
return io;
|
||||
}
|
||||
@@ -98,31 +162,33 @@ size_t Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, c
|
||||
bool Base64::encode( std::string_view in, std::string& out ) {
|
||||
size_t b64len = encodeSafeOutLen( in.size() );
|
||||
|
||||
if ( out.size() < b64len ) {
|
||||
if ( out.size() < b64len )
|
||||
out.resize( b64len );
|
||||
}
|
||||
|
||||
int len = encode( in.size(), (const unsigned char*)in.data(), out.size(), (char*)&out[0] );
|
||||
const size_t len =
|
||||
encode( in.size(), reinterpret_cast<const unsigned char*>( in.data() ), out.size(), out.data() );
|
||||
|
||||
if ( -1 != len && (size_t)len != out.size() ) {
|
||||
if ( len != static_cast<size_t>( -1 ) && len != out.size() )
|
||||
out.resize( len );
|
||||
}
|
||||
|
||||
return -1 != len;
|
||||
return len != static_cast<size_t>( -1 );
|
||||
}
|
||||
|
||||
size_t Base64::decode( std::string_view in, std::string& out ) {
|
||||
return decode( in, out, DecodeMode::AllowWhitespace );
|
||||
}
|
||||
|
||||
size_t Base64::decode( std::string_view in, std::string& out, DecodeMode mode ) {
|
||||
size_t d64len = decodeSafeOutLen( in.size() );
|
||||
|
||||
if ( out.size() < d64len ) {
|
||||
if ( out.size() < d64len )
|
||||
out.resize( d64len );
|
||||
}
|
||||
|
||||
int len = decode( in.size(), in.data(), out.size(), (unsigned char*)&out[0] );
|
||||
const size_t len = decode( in.size(), in.data(), out.size(),
|
||||
reinterpret_cast<unsigned char*>( out.data() ), mode );
|
||||
|
||||
if ( -1 != len && (size_t)len != out.size() ) {
|
||||
if ( len != static_cast<size_t>( -1 ) && len != out.size() )
|
||||
out.resize( len );
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ class IPseudoTerminal : public eterm::System::IPipe {
|
||||
|
||||
virtual int getNumRows() const = 0;
|
||||
|
||||
virtual bool resize( int columns, int rows ) = 0;
|
||||
virtual bool resize( int columns, int rows, int pixelWidth, int pixelHeight ) = 0;
|
||||
|
||||
bool resize( int columns, int rows ) { return resize( columns, rows, 0, 0 ); }
|
||||
};
|
||||
|
||||
}} // namespace eterm::Terminal
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
#include <eepp/config.hpp>
|
||||
#include <eterm/terminal/terminalgraphics.hpp>
|
||||
#include <eterm/terminal/terminaltypes.hpp>
|
||||
|
||||
using namespace EE;
|
||||
@@ -80,6 +81,9 @@ class ITerminalDisplay {
|
||||
virtual void drawCursor( int cx, int cy, TerminalGlyph g, int ox, int oy,
|
||||
TerminalGlyph og ) = 0;
|
||||
|
||||
virtual void drawGraphics( std::shared_ptr<TerminalGraphicsPresentation> presentation,
|
||||
std::vector<TerminalGraphicsUpdate> updates );
|
||||
|
||||
virtual void drawEnd() = 0;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
#ifndef ETERM_KITTYGRAPHICSPROTOCOL_HPP
|
||||
#define ETERM_KITTYGRAPHICSPROTOCOL_HPP
|
||||
|
||||
#include <eepp/config.hpp>
|
||||
#include <eepp/system/clock.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <eterm/terminal/terminalgraphics.hpp>
|
||||
|
||||
using namespace EE;
|
||||
|
||||
namespace eterm { namespace Terminal {
|
||||
|
||||
enum class KittyGraphicsError : Uint8 {
|
||||
None,
|
||||
InvalidArgument,
|
||||
Unsupported,
|
||||
InvalidData,
|
||||
TooLarge,
|
||||
DecodeFailed,
|
||||
NotFound,
|
||||
NoSpace,
|
||||
NoParent,
|
||||
Cycle,
|
||||
TooDeep
|
||||
};
|
||||
|
||||
struct KittyGraphicsCommandData {
|
||||
std::string_view payload;
|
||||
std::optional<Uint32> format;
|
||||
std::optional<Uint32> dataSize;
|
||||
std::optional<Uint32> more;
|
||||
std::optional<Uint32> imageId;
|
||||
std::optional<Uint32> imageNumber;
|
||||
std::optional<Uint32> usageHint;
|
||||
std::optional<Uint32> placementId;
|
||||
std::optional<Uint32> quiet;
|
||||
std::optional<Uint32> width;
|
||||
std::optional<Uint32> height;
|
||||
std::optional<Uint32> x;
|
||||
std::optional<Uint32> y;
|
||||
std::optional<Uint32> sourceWidth;
|
||||
std::optional<Uint32> sourceHeight;
|
||||
std::optional<Uint32> columns;
|
||||
std::optional<Uint32> rows;
|
||||
std::optional<Uint32> xOffset;
|
||||
std::optional<Uint32> yOffset;
|
||||
std::optional<Int32> zIndex;
|
||||
std::optional<Uint32> cursorMovement;
|
||||
std::optional<Uint32> virtualPlacement;
|
||||
std::optional<Uint32> parentImageId;
|
||||
std::optional<Uint32> parentPlacementId;
|
||||
std::optional<Int32> parentOffsetX;
|
||||
std::optional<Int32> parentOffsetY;
|
||||
char transmission{ 'd' };
|
||||
char compression{ 0 };
|
||||
char deletion{ 'a' };
|
||||
};
|
||||
|
||||
struct KittyTransmitCommand {
|
||||
KittyGraphicsCommandData data;
|
||||
bool display{ false };
|
||||
};
|
||||
|
||||
struct KittyPutCommand {
|
||||
KittyGraphicsCommandData data;
|
||||
};
|
||||
|
||||
struct KittyDeleteCommand {
|
||||
KittyGraphicsCommandData data;
|
||||
};
|
||||
|
||||
struct KittyFrameCommand {
|
||||
KittyGraphicsCommandData data;
|
||||
};
|
||||
|
||||
struct KittyAnimationCommand {
|
||||
KittyGraphicsCommandData data;
|
||||
};
|
||||
|
||||
struct KittyComposeCommand {
|
||||
KittyGraphicsCommandData data;
|
||||
};
|
||||
|
||||
struct KittyQueryCommand {
|
||||
KittyGraphicsCommandData data;
|
||||
};
|
||||
|
||||
using KittyGraphicsCommand =
|
||||
std::variant<KittyTransmitCommand, KittyPutCommand, KittyDeleteCommand, KittyFrameCommand,
|
||||
KittyAnimationCommand, KittyComposeCommand, KittyQueryCommand>;
|
||||
|
||||
struct KittyGraphicsParseResult {
|
||||
std::optional<KittyGraphicsCommand> command;
|
||||
KittyGraphicsError error{ KittyGraphicsError::None };
|
||||
};
|
||||
|
||||
struct KittyGraphicsHandleResult {
|
||||
KittyGraphicsHandleResult() = default;
|
||||
KittyGraphicsHandleResult( std::string response, KittyGraphicsError error, bool changed ) :
|
||||
response( std::move( response ) ), error( error ), changed( changed ) {}
|
||||
|
||||
std::string response;
|
||||
Vector2i cursorMovement;
|
||||
KittyGraphicsError error{ KittyGraphicsError::None };
|
||||
bool changed{ false };
|
||||
};
|
||||
|
||||
struct KittyGraphicsStats {
|
||||
Uint64 decodedBytes{ 0 };
|
||||
Uint64 fullImageUpdates{ 0 };
|
||||
Uint64 rectangleUpdates{ 0 };
|
||||
Uint64 evictions{ 0 };
|
||||
};
|
||||
|
||||
class KittyGraphicsProtocol {
|
||||
public:
|
||||
explicit KittyGraphicsProtocol( size_t maxStorageBytes = 320 * 1024 * 1024,
|
||||
size_t maxImages = 4096, size_t maxPlacements = 65536 );
|
||||
|
||||
static KittyGraphicsParseResult parse( std::string_view command );
|
||||
|
||||
KittyGraphicsHandleResult handle( std::string_view command, Vector2i cursor = {} );
|
||||
|
||||
std::vector<TerminalGraphicsUpdate> takeUpdates();
|
||||
|
||||
std::shared_ptr<TerminalGraphicsPresentation> takePresentation();
|
||||
|
||||
bool hasPendingPresentation() const { return mPresentationDirty; }
|
||||
|
||||
const std::vector<Uint8>* imagePixels( KittyImageId imageId ) const;
|
||||
|
||||
size_t imageCount() const { return mImages.size(); }
|
||||
|
||||
const KittyGraphicsStats& stats() const { return mStats; }
|
||||
|
||||
bool hasVirtualPlacements() const;
|
||||
|
||||
void reset();
|
||||
|
||||
void resync();
|
||||
|
||||
void clearScreen();
|
||||
|
||||
void setAlternateScreen( bool alternate );
|
||||
|
||||
void scrollScreen( int top, int bottom, int rows, bool preserveHistory );
|
||||
|
||||
void setViewport( int scrollPosition, int historyLength, int screenRows );
|
||||
|
||||
bool updateAnimations();
|
||||
|
||||
void setCellPixelSize( Uint32 width, Uint32 height );
|
||||
|
||||
void setPlaceholderCells( std::vector<TerminalGraphicsPlaceholderCell> cells );
|
||||
|
||||
private:
|
||||
struct Image {
|
||||
struct Frame {
|
||||
std::vector<Uint8> rgba;
|
||||
Int32 gapMs{ 40 };
|
||||
Uint32 usageHint{ 0 };
|
||||
};
|
||||
std::vector<Uint8> rgba;
|
||||
std::unordered_map<Uint32, Frame> frames;
|
||||
Sizei size;
|
||||
Uint32 imageNumber{ 0 };
|
||||
Uint32 usageHint{ 0 };
|
||||
bool anonymous{ false };
|
||||
Uint64 creationSerial{ 0 };
|
||||
Uint32 currentFrame{ 1 };
|
||||
Uint32 loopCount{ 1 };
|
||||
Uint32 loopsCompleted{ 0 };
|
||||
Uint8 animationState{ 1 };
|
||||
Int32 rootGapMs{ 0 };
|
||||
EE::System::Clock frameClock;
|
||||
};
|
||||
|
||||
struct PendingTransfer {
|
||||
KittyGraphicsCommandData data;
|
||||
std::vector<Uint8> decodedData;
|
||||
bool display{ false };
|
||||
bool query{ false };
|
||||
bool frame{ false };
|
||||
bool active{ false };
|
||||
};
|
||||
|
||||
struct Placement {
|
||||
TerminalVisiblePlacement visible;
|
||||
Uint64 internalId{ 0 };
|
||||
bool virtualPlacement{ false };
|
||||
KittyImageId parentImageId{ 0 };
|
||||
KittyPlacementId parentPlacementId{ 0 };
|
||||
};
|
||||
|
||||
KittyGraphicsHandleResult handleTransmit( const KittyGraphicsCommandData& data, bool display,
|
||||
bool query, bool frame, Vector2i cursor );
|
||||
KittyGraphicsHandleResult finishTransfer( PendingTransfer transfer, Vector2i cursor );
|
||||
KittyGraphicsHandleResult put( const KittyGraphicsCommandData& data, Vector2i cursor );
|
||||
KittyGraphicsHandleResult remove( const KittyGraphicsCommandData& data, Vector2i cursor );
|
||||
KittyGraphicsHandleResult controlAnimation( const KittyGraphicsCommandData& data );
|
||||
KittyGraphicsHandleResult composeFrames( const KittyGraphicsCommandData& data );
|
||||
KittyImageId allocateImageId();
|
||||
KittyImageId resolveImageId( const KittyGraphicsCommandData& data ) const;
|
||||
bool ensureCapacity( size_t bytes, KittyImageId replacingId, bool addingImage );
|
||||
bool isImagePlaced( KittyImageId imageId ) const;
|
||||
void eraseImage( KittyImageId imageId );
|
||||
std::string response( const KittyGraphicsCommandData& data, KittyGraphicsError error,
|
||||
KittyImageId imageId = 0 ) const;
|
||||
|
||||
std::unordered_map<KittyImageId, Image> mImages;
|
||||
std::vector<Placement> mPlacements;
|
||||
std::vector<Placement> mPrimaryPlacements;
|
||||
std::vector<TerminalGraphicsPlaceholderCell> mPlaceholderCells;
|
||||
std::vector<TerminalGraphicsUpdate> mUpdates;
|
||||
PendingTransfer mPending;
|
||||
size_t mStorageBytes{ 0 };
|
||||
size_t mFrameStorageBytes{ 0 };
|
||||
Uint64 mCreationSerial{ 0 };
|
||||
Uint64 mPresentationGeneration{ 0 };
|
||||
Uint64 mPlacementSerial{ 0 };
|
||||
KittyImageId mNextImageId{ 1 };
|
||||
bool mPresentationDirty{ false };
|
||||
int mScrollPosition{ 0 };
|
||||
int mHistoryLength{ 0 };
|
||||
int mScreenRows{ 0 };
|
||||
Uint32 mCellPixelWidth{ 0 };
|
||||
Uint32 mCellPixelHeight{ 0 };
|
||||
size_t mMaxStorageBytes{ 0 };
|
||||
size_t mMaxImages{ 0 };
|
||||
size_t mMaxPlacements{ 0 };
|
||||
KittyGraphicsStats mStats;
|
||||
};
|
||||
|
||||
}} // namespace eterm::Terminal
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef ETERM_KITTYGRAPHICSRENDERER_HPP
|
||||
#define ETERM_KITTYGRAPHICSRENDERER_HPP
|
||||
|
||||
#include <eepp/graphics/texture.hpp>
|
||||
#include <eterm/terminal/terminalgraphics.hpp>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace EE::Graphics;
|
||||
|
||||
namespace eterm { namespace Terminal {
|
||||
|
||||
class KittyGraphicsRenderer {
|
||||
public:
|
||||
enum class Pass : Uint8 { VeryNegative, Negative, NonNegative };
|
||||
|
||||
bool applyUpdates( std::vector<TerminalGraphicsUpdate>&& updates );
|
||||
|
||||
void setPresentation( std::shared_ptr<const TerminalGraphicsPresentation> presentation );
|
||||
|
||||
void draw( Pass pass, const Vector2f& origin, const Sizef& cellSize, const Sizef& gridSize );
|
||||
|
||||
bool hasPlacements( Pass pass ) const;
|
||||
|
||||
void reset();
|
||||
|
||||
Uint64 lastAppliedSequence() const { return mLastAppliedSequence; }
|
||||
|
||||
private:
|
||||
struct GPUImage {
|
||||
TexturePtr texture;
|
||||
std::unordered_map<Uint32, TexturePtr> frames;
|
||||
Sizei size;
|
||||
};
|
||||
|
||||
std::unordered_map<KittyImageId, GPUImage> mImages;
|
||||
std::shared_ptr<const TerminalGraphicsPresentation> mPresentation;
|
||||
Uint64 mLastAppliedSequence{ 0 };
|
||||
};
|
||||
|
||||
}} // namespace eterm::Terminal
|
||||
|
||||
#endif
|
||||
@@ -55,7 +55,7 @@ class PseudoTerminal final : public IPseudoTerminal {
|
||||
virtual int getNumColumns() const override;
|
||||
virtual int getNumRows() const override;
|
||||
|
||||
virtual bool resize( int columns, int rows ) override;
|
||||
virtual bool resize( int columns, int rows, int pixelWidth, int pixelHeight ) override;
|
||||
virtual int write( const char* s, size_t n ) override;
|
||||
virtual int read( char* buf, size_t n, bool block = false ) override;
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ class VertexBuffer;
|
||||
|
||||
namespace eterm { namespace Terminal {
|
||||
|
||||
class KittyGraphicsRenderer;
|
||||
|
||||
enum class TerminalShortcutAction {
|
||||
PASTE,
|
||||
PASTE_SELECTION,
|
||||
@@ -169,6 +171,10 @@ class TerminalDisplay {
|
||||
|
||||
bool update( bool isMouseOverMe = true );
|
||||
|
||||
Sizei getCellPixelSize() const;
|
||||
|
||||
Sizei getGridPixelSize() const;
|
||||
|
||||
void executeFile( const std::string& cmd );
|
||||
|
||||
void executeBinary( const std::string& binaryPath, const std::string& args = "" );
|
||||
@@ -315,6 +321,7 @@ class TerminalDisplay {
|
||||
std::vector<Color> mColors;
|
||||
std::shared_ptr<TerminalSession> mSession;
|
||||
std::shared_ptr<const TerminalSnapshot> mSnapshot;
|
||||
std::unique_ptr<KittyGraphicsRenderer> mGraphicsRenderer;
|
||||
mutable std::string mClipboardUtf8;
|
||||
Uint32 mNumCallBacks{ 0 };
|
||||
std::map<Uint32, EventFunc> mCallbacks;
|
||||
@@ -349,6 +356,8 @@ class TerminalDisplay {
|
||||
Uint32 mRows{ 0 };
|
||||
Uint32 mClickStep{ 5 };
|
||||
Uint64 mSnapshotGeneration{ 0 };
|
||||
Uint64 mLastAppliedGraphicsSequence{ 0 };
|
||||
bool mGraphicsResyncPending{ false };
|
||||
FontHinting mFontHinting{ FontHinting::Full };
|
||||
FontAntialiasing mFontAntialiasing{ FontAntialiasing::Grayscale };
|
||||
FrameBufferUniquePtr mFrameBuffer;
|
||||
@@ -377,12 +386,16 @@ class TerminalDisplay {
|
||||
|
||||
Vector2i positionToGrid( const Vector2i& pos );
|
||||
|
||||
Vector2i positionToPixel( const Vector2i& pos ) const;
|
||||
|
||||
void onSizeChange();
|
||||
|
||||
void onProcessExit( int exitCode );
|
||||
|
||||
void consumeSnapshot();
|
||||
|
||||
void drainGraphicsUpdates();
|
||||
|
||||
void drainSessionEvents();
|
||||
|
||||
TerminalColorPalette makeColorPalette() const;
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <eterm/system/iprocess.hpp>
|
||||
#include <eterm/terminal/ipseudoterminal.hpp>
|
||||
#include <eterm/terminal/iterminaldisplay.hpp>
|
||||
#include <eterm/terminal/kittygraphicsprotocol.hpp>
|
||||
#include <eterm/terminal/terminaltypes.hpp>
|
||||
#include <memory>
|
||||
#include <stdint.h>
|
||||
@@ -58,6 +59,10 @@ constexpr int ESC_BUF_SIZ = 512;
|
||||
constexpr int ESC_ARG_SIZ = 32;
|
||||
constexpr int STR_BUF_SIZ = ESC_BUF_SIZ;
|
||||
constexpr int STR_ARG_SIZ = ESC_ARG_SIZ;
|
||||
// Kitty recommends small chunks, but unchunked direct transmissions are valid and common in
|
||||
// simple clients. Keep this independently bounded while allowing useful image-sized APCs.
|
||||
constexpr size_t MAX_KITTY_GRAPHICS_APC_SIZE = 16 * 1024 * 1024;
|
||||
constexpr size_t MAX_GENERIC_STRING_SEQUENCE_SIZE = 1024 * 1024;
|
||||
|
||||
/* Internal representation of the screen */
|
||||
struct Term {
|
||||
@@ -119,7 +124,8 @@ struct STREscape {
|
||||
size_t siz; /* allocation size */
|
||||
size_t len; /* raw string length */
|
||||
char* args[STR_ARG_SIZ];
|
||||
int narg; /* nb of args */
|
||||
int narg; /* nb of args */
|
||||
bool discarded; /* oversized sequence: consume input through its terminator without storing */
|
||||
};
|
||||
|
||||
enum class PromptState {
|
||||
@@ -156,6 +162,8 @@ class TerminalEmulator final {
|
||||
|
||||
void resize( int columns, int rows );
|
||||
|
||||
void resize( int columns, int rows, int pixelWidth, int pixelHeight );
|
||||
|
||||
void redraw();
|
||||
|
||||
/** Worker-owned terminal state reset (RIS semantics without replacing the PTY/process). */
|
||||
@@ -220,8 +228,8 @@ class TerminalEmulator final {
|
||||
|
||||
std::string getSelection() const;
|
||||
|
||||
void mousereport( const TerminalMouseEventType& type, const Vector2i& pos, const Uint32& flags,
|
||||
const Uint32& mod );
|
||||
void mousereport( const TerminalMouseEventType& type, const Vector2i& cellPosition,
|
||||
const Vector2i& pixelPosition, const Uint32& flags, const Uint32& mod );
|
||||
|
||||
const bool& isDirty() const { return mDirty; }
|
||||
|
||||
@@ -260,6 +268,8 @@ class TerminalEmulator final {
|
||||
/** Worker-only notification that the display palette changed. */
|
||||
void notifyColorSchemeChanged();
|
||||
|
||||
void requestGraphicsResync();
|
||||
|
||||
Vector2i getSize() const;
|
||||
|
||||
System::IProcess* getProcess() const;
|
||||
@@ -288,6 +298,10 @@ class TerminalEmulator final {
|
||||
bool mPendingPtyResize{ false };
|
||||
int mPendingPtyColumns{ 0 };
|
||||
int mPendingPtyRows{ 0 };
|
||||
int mPendingPtyPixelWidth{ 0 };
|
||||
int mPendingPtyPixelHeight{ 0 };
|
||||
int mPixelWidth{ 0 };
|
||||
int mPixelHeight{ 0 };
|
||||
Clock mPendingPtyResizeClock;
|
||||
|
||||
bool mDirty{ true };
|
||||
@@ -307,6 +321,7 @@ class TerminalEmulator final {
|
||||
TerminalSelection mSel;
|
||||
CSIEscape mCsiescseq;
|
||||
STREscape mStrescseq;
|
||||
KittyGraphicsProtocol mKittyGraphics;
|
||||
|
||||
uint32_t mDefaultFg;
|
||||
uint32_t mDefaultBg;
|
||||
@@ -324,6 +339,16 @@ class TerminalEmulator final {
|
||||
PromptState mPromptState{ PromptState::Unknown };
|
||||
PromptStateChangedCb mPromptStateChangedCb;
|
||||
DataCb mDataCb;
|
||||
Vector2i mKittyPlaceholderCell{ -1, -1 };
|
||||
struct KittyPlaceholderMetadata {
|
||||
Uint32 placementId{ 0 };
|
||||
Uint16 row{ UINT16_MAX };
|
||||
Uint16 column{ UINT16_MAX };
|
||||
Uint8 imageIdMsb{ 0 };
|
||||
Uint8 diacriticCount{ 0 };
|
||||
};
|
||||
std::unordered_map<const TerminalGlyph*, KittyPlaceholderMetadata> mKittyPlaceholderMetadata;
|
||||
Uint32 mKittyUnderlineColor{ 0 };
|
||||
|
||||
void setClipboard( const char* str );
|
||||
|
||||
|
||||
109
src/modules/eterm/include/eterm/terminal/terminalgraphics.hpp
Normal file
109
src/modules/eterm/include/eterm/terminal/terminalgraphics.hpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#ifndef ETERM_TERMINALGRAPHICS_HPP
|
||||
#define ETERM_TERMINALGRAPHICS_HPP
|
||||
|
||||
#include <deque>
|
||||
#include <eepp/math/rect.hpp>
|
||||
#include <eepp/math/size.hpp>
|
||||
#include <eepp/math/vector2.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::Math;
|
||||
|
||||
namespace eterm { namespace Terminal {
|
||||
|
||||
using KittyImageId = Uint32;
|
||||
using KittyPlacementId = Uint32;
|
||||
|
||||
struct TerminalVisiblePlacement {
|
||||
KittyImageId imageId{ 0 };
|
||||
KittyPlacementId placementId{ 0 };
|
||||
Uint32 frameNumber{ 1 };
|
||||
Vector2i visibleAnchorCell;
|
||||
Rect sourcePixels;
|
||||
Uint32 columns{ 0 };
|
||||
Uint32 rows{ 0 };
|
||||
Vector2i firstCellPixelOffset;
|
||||
Int32 zIndex{ 0 };
|
||||
};
|
||||
|
||||
struct TerminalGraphicsPlaceholderCell {
|
||||
KittyImageId imageId{ 0 };
|
||||
KittyPlacementId placementId{ 0 };
|
||||
Vector2i cell;
|
||||
Uint32 imageRow{ 0 };
|
||||
Uint32 imageColumn{ 0 };
|
||||
|
||||
bool operator==( const TerminalGraphicsPlaceholderCell& other ) const {
|
||||
return imageId == other.imageId && placementId == other.placementId && cell == other.cell &&
|
||||
imageRow == other.imageRow && imageColumn == other.imageColumn;
|
||||
}
|
||||
};
|
||||
|
||||
/** Small immutable graphics state associated with a terminal presentation. */
|
||||
struct TerminalGraphicsPresentation {
|
||||
std::vector<TerminalVisiblePlacement> placements;
|
||||
Uint64 generation{ 0 };
|
||||
Uint64 requiredUpdateSequence{ 0 };
|
||||
};
|
||||
|
||||
enum class TerminalGraphicsUpdateType : Uint8 {
|
||||
CreateImage,
|
||||
ReplaceImage,
|
||||
UpdateRegion,
|
||||
CreateFrame,
|
||||
ReplaceFrame,
|
||||
UpdateFrameRegion,
|
||||
DeleteFrame,
|
||||
DeleteImage,
|
||||
ResetScreen,
|
||||
ResetAll,
|
||||
Resync
|
||||
};
|
||||
|
||||
struct TerminalGraphicsUpdate {
|
||||
std::shared_ptr<const std::vector<Uint8>> rgba;
|
||||
Sizei imageSize;
|
||||
Rect region;
|
||||
Uint64 sequence{ 0 };
|
||||
KittyImageId imageId{ 0 };
|
||||
Uint32 frameNumber{ 1 };
|
||||
TerminalGraphicsUpdateType type{ TerminalGraphicsUpdateType::Resync };
|
||||
|
||||
size_t payloadBytes() const { return rgba ? rgba->size() : 0; }
|
||||
};
|
||||
|
||||
/** Bounded ordered worker-to-UI mutation queue with explicit overflow recovery. */
|
||||
class TerminalGraphicsUpdateQueue {
|
||||
public:
|
||||
static constexpr size_t DefaultMaxUpdates = 1024;
|
||||
static constexpr size_t DefaultMaxBytes = 32 * 1024 * 1024;
|
||||
|
||||
explicit TerminalGraphicsUpdateQueue( size_t maxUpdates = DefaultMaxUpdates,
|
||||
size_t maxBytes = DefaultMaxBytes );
|
||||
|
||||
Uint64 enqueue( TerminalGraphicsUpdate update );
|
||||
|
||||
std::vector<TerminalGraphicsUpdate> drain();
|
||||
|
||||
size_t queuedBytes() const;
|
||||
|
||||
bool needsResync() const;
|
||||
|
||||
void resetResync();
|
||||
|
||||
private:
|
||||
mutable std::mutex mMutex;
|
||||
std::deque<TerminalGraphicsUpdate> mUpdates;
|
||||
size_t mMaxUpdates{ 0 };
|
||||
size_t mMaxBytes{ 0 };
|
||||
size_t mQueuedBytes{ 0 };
|
||||
Uint64 mNextSequence{ 0 };
|
||||
bool mNeedsResync{ false };
|
||||
};
|
||||
|
||||
}} // namespace eterm::Terminal
|
||||
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <eterm/system/iprocess.hpp>
|
||||
#include <eterm/terminal/ipseudoterminal.hpp>
|
||||
#include <eterm/terminal/terminalemulator.hpp>
|
||||
#include <eterm/terminal/terminalgraphics.hpp>
|
||||
#include <eterm/terminal/terminaltypes.hpp>
|
||||
|
||||
#include <atomic>
|
||||
@@ -29,6 +30,7 @@ namespace eterm { namespace Terminal {
|
||||
|
||||
/** Immutable worker-to-UI presentation state. Cell selection is already applied as ATTR_REVERSE. */
|
||||
struct TerminalSnapshot {
|
||||
std::shared_ptr<const TerminalGraphicsPresentation> graphics;
|
||||
std::vector<TerminalGlyph> cells;
|
||||
std::vector<Uint8> dirtyRows;
|
||||
std::string title;
|
||||
@@ -118,6 +120,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
void write( std::string data, bool mayEcho = true );
|
||||
void writeRaw( std::string data );
|
||||
void resize( int columns, int rows );
|
||||
void resize( int columns, int rows, int pixelWidth, int pixelHeight );
|
||||
void scrollUp( int amount );
|
||||
void scrollDown( int amount );
|
||||
/** Returns an ordered command id that is copied into snapshots after the scroll is applied. */
|
||||
@@ -125,8 +128,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
void selectionStart( int column, int row, int snap );
|
||||
void selectionExtend( int column, int row, int type, bool done );
|
||||
void selectionClear();
|
||||
void mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags,
|
||||
Uint32 modifiers );
|
||||
void mouseReport( TerminalMouseEventType type, Vector2i cellPosition, Vector2i pixelPosition,
|
||||
Uint32 flags, Uint32 modifiers );
|
||||
void setFocus( bool focus );
|
||||
void setCursorMode( TerminalCursorMode mode );
|
||||
void setColorPalette( TerminalColorPalette palette );
|
||||
@@ -141,6 +144,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
|
||||
std::shared_ptr<const TerminalSnapshot> snapshot() const;
|
||||
std::vector<Event> drainEvents();
|
||||
std::vector<TerminalGraphicsUpdate> drainGraphicsUpdates();
|
||||
void requestGraphicsResync();
|
||||
|
||||
/** Bounded exact-selection request. Returns no value on timeout or during shutdown. */
|
||||
std::optional<std::string>
|
||||
@@ -160,6 +165,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
struct ResizeCommand {
|
||||
int columns{ 0 };
|
||||
int rows{ 0 };
|
||||
int pixelWidth{ 0 };
|
||||
int pixelHeight{ 0 };
|
||||
};
|
||||
struct ScrollCommand {
|
||||
int amount{ 0 };
|
||||
@@ -179,7 +186,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
};
|
||||
struct MouseCommand {
|
||||
TerminalMouseEventType type{ TerminalMouseEventType::MouseMotion };
|
||||
Vector2i position;
|
||||
Vector2i cellPosition;
|
||||
Vector2i pixelPosition;
|
||||
Uint32 flags{ 0 };
|
||||
Uint32 modifiers{ 0 };
|
||||
};
|
||||
@@ -203,6 +211,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
std::shared_ptr<SelectionResponse> response;
|
||||
};
|
||||
struct SelectionClearCommand {};
|
||||
struct GraphicsResyncCommand {};
|
||||
struct ResetCommand {};
|
||||
struct TerminateCommand {};
|
||||
struct AllowTrimCommand : BoolCommand {};
|
||||
@@ -216,7 +225,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
MouseCommand, FocusCommand, CursorModeCommand, PaletteCommand,
|
||||
PresentationRateCommand, AllowTrimCommand, DataEventsCommand,
|
||||
PromptEventsCommand, TerminateCommand, RestartCommand, ResetCommand,
|
||||
SelectionRequestCommand>;
|
||||
SelectionRequestCommand, GraphicsResyncCommand>;
|
||||
|
||||
TerminalSession( PtyPtr&& pty, ProcPtr&& process, size_t historySize,
|
||||
TerminalColorPalette palette );
|
||||
@@ -228,6 +237,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
void processCommand( Command&& command );
|
||||
void enqueueEvent( Event event, bool coalescable );
|
||||
void publishSnapshot( std::shared_ptr<const TerminalSnapshot> snapshot );
|
||||
Uint64 enqueueGraphicsUpdate( TerminalGraphicsUpdate update );
|
||||
|
||||
std::shared_ptr<WorkerDisplay> mWorkerDisplay;
|
||||
std::unique_ptr<TerminalEmulator> mEmulator;
|
||||
@@ -238,6 +248,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
std::deque<Command> mCommands;
|
||||
std::mutex mEventMutex;
|
||||
std::deque<Event> mEvents;
|
||||
TerminalGraphicsUpdateQueue mGraphicsUpdates;
|
||||
mutable std::mutex mPublishedSnapshotMutex;
|
||||
std::shared_ptr<const TerminalSnapshot> mPublishedSnapshot;
|
||||
std::atomic<bool> mShutdownRequested{ false };
|
||||
@@ -246,4 +257,4 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
|
||||
}} // namespace eterm::Terminal
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -116,6 +116,7 @@ enum TerminalWinMode {
|
||||
MODE_BRCKTPASTE = 1 << 16,
|
||||
MODE_NUMLOCK = 1 << 17,
|
||||
MODE_ALTSCRROLL = 1 << 18,
|
||||
MODE_MOUSESGR_PIXELS = 1 << 19,
|
||||
MODE_MOUSE = MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEX10 | MODE_MOUSEMANY,
|
||||
};
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ const char* ITerminalDisplay::getClipboard() const {
|
||||
return "";
|
||||
}
|
||||
|
||||
void ITerminalDisplay::drawGraphics( std::shared_ptr<TerminalGraphicsPresentation>,
|
||||
std::vector<TerminalGraphicsUpdate> ) {}
|
||||
|
||||
void ITerminalDisplay::onProcessExit( int /*exitCode*/ ) {}
|
||||
|
||||
void ITerminalDisplay::onScrollPositionChange() {}
|
||||
|
||||
1559
src/modules/eterm/src/eterm/terminal/kittygraphicsprotocol.cpp
Normal file
1559
src/modules/eterm/src/eterm/terminal/kittygraphicsprotocol.cpp
Normal file
File diff suppressed because it is too large
Load Diff
167
src/modules/eterm/src/eterm/terminal/kittygraphicsrenderer.cpp
Normal file
167
src/modules/eterm/src/eterm/terminal/kittygraphicsrenderer.cpp
Normal file
@@ -0,0 +1,167 @@
|
||||
#include <eterm/terminal/kittygraphicsrenderer.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <eepp/graphics/renderer/renderer.hpp>
|
||||
#include <eepp/graphics/texturefactory.hpp>
|
||||
#include <limits>
|
||||
|
||||
namespace eterm { namespace Terminal {
|
||||
|
||||
static bool isInPass( Int32 zIndex, KittyGraphicsRenderer::Pass pass ) {
|
||||
constexpr Int32 VeryNegativeThreshold = std::numeric_limits<Int32>::min() / 2;
|
||||
return pass == KittyGraphicsRenderer::Pass::VeryNegative ? zIndex < VeryNegativeThreshold
|
||||
: pass == KittyGraphicsRenderer::Pass::Negative
|
||||
? zIndex < 0 && zIndex >= VeryNegativeThreshold
|
||||
: zIndex >= 0;
|
||||
}
|
||||
|
||||
bool KittyGraphicsRenderer::applyUpdates( std::vector<TerminalGraphicsUpdate>&& updates ) {
|
||||
for ( auto& update : updates ) {
|
||||
if ( update.type == TerminalGraphicsUpdateType::Resync ) {
|
||||
reset();
|
||||
return false;
|
||||
}
|
||||
if ( update.type != TerminalGraphicsUpdateType::ResetAll &&
|
||||
update.sequence != mLastAppliedSequence + 1 )
|
||||
return false;
|
||||
|
||||
switch ( update.type ) {
|
||||
case TerminalGraphicsUpdateType::CreateImage:
|
||||
case TerminalGraphicsUpdateType::ReplaceImage: {
|
||||
if ( !update.rgba || update.imageSize.getWidth() <= 0 ||
|
||||
update.imageSize.getHeight() <= 0 )
|
||||
return false;
|
||||
auto texture = TextureFactory::instance()->loadFromPixels(
|
||||
update.rgba->data(), update.imageSize.getWidth(), update.imageSize.getHeight(),
|
||||
4, false, Texture::ClampMode::ClampToEdge, false, false );
|
||||
if ( !texture )
|
||||
return false;
|
||||
texture->setFilter( Texture::Filter::Linear );
|
||||
mImages.insert_or_assign( update.imageId,
|
||||
GPUImage{ std::move( texture ), {}, update.imageSize } );
|
||||
break;
|
||||
}
|
||||
case TerminalGraphicsUpdateType::UpdateRegion: {
|
||||
auto image = mImages.find( update.imageId );
|
||||
if ( image == mImages.end() || !update.rgba )
|
||||
return false;
|
||||
const int width = update.region.Right - update.region.Left;
|
||||
const int height = update.region.Bottom - update.region.Top;
|
||||
if ( width <= 0 || height <= 0 || update.region.Left < 0 || update.region.Top < 0 ||
|
||||
update.region.Right > image->second.size.getWidth() ||
|
||||
update.region.Bottom > image->second.size.getHeight() ||
|
||||
update.rgba->size() != static_cast<size_t>( width ) * height * 4 )
|
||||
return false;
|
||||
image->second.texture->update( update.rgba->data(), width, height,
|
||||
update.region.Left, update.region.Top );
|
||||
break;
|
||||
}
|
||||
case TerminalGraphicsUpdateType::CreateFrame:
|
||||
case TerminalGraphicsUpdateType::ReplaceFrame: {
|
||||
auto image = mImages.find( update.imageId );
|
||||
if ( image == mImages.end() || !update.rgba || update.frameNumber <= 1 ||
|
||||
update.rgba->size() != static_cast<size_t>( image->second.size.getWidth() ) *
|
||||
image->second.size.getHeight() * 4 )
|
||||
return false;
|
||||
auto texture = TextureFactory::instance()->loadFromPixels(
|
||||
update.rgba->data(), image->second.size.getWidth(),
|
||||
image->second.size.getHeight(), 4, false, Texture::ClampMode::ClampToEdge,
|
||||
false, false );
|
||||
if ( !texture )
|
||||
return false;
|
||||
texture->setFilter( Texture::Filter::Linear );
|
||||
image->second.frames.insert_or_assign( update.frameNumber, std::move( texture ) );
|
||||
break;
|
||||
}
|
||||
case TerminalGraphicsUpdateType::UpdateFrameRegion: {
|
||||
auto image = mImages.find( update.imageId );
|
||||
if ( image == mImages.end() || !update.rgba )
|
||||
return false;
|
||||
auto frame = image->second.frames.find( update.frameNumber );
|
||||
if ( frame == image->second.frames.end() )
|
||||
return false;
|
||||
const int width = update.region.Right - update.region.Left;
|
||||
const int height = update.region.Bottom - update.region.Top;
|
||||
if ( width <= 0 || height <= 0 || update.region.Left < 0 || update.region.Top < 0 ||
|
||||
update.region.Right > image->second.size.getWidth() ||
|
||||
update.region.Bottom > image->second.size.getHeight() ||
|
||||
update.rgba->size() != static_cast<size_t>( width ) * height * 4 )
|
||||
return false;
|
||||
frame->second->update( update.rgba->data(), width, height, update.region.Left,
|
||||
update.region.Top );
|
||||
break;
|
||||
}
|
||||
case TerminalGraphicsUpdateType::DeleteFrame: {
|
||||
auto image = mImages.find( update.imageId );
|
||||
if ( image != mImages.end() )
|
||||
image->second.frames.erase( update.frameNumber );
|
||||
break;
|
||||
}
|
||||
case TerminalGraphicsUpdateType::DeleteImage:
|
||||
mImages.erase( update.imageId );
|
||||
break;
|
||||
case TerminalGraphicsUpdateType::ResetScreen:
|
||||
break;
|
||||
case TerminalGraphicsUpdateType::ResetAll:
|
||||
mImages.clear();
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
mLastAppliedSequence = update.sequence;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void KittyGraphicsRenderer::setPresentation(
|
||||
std::shared_ptr<const TerminalGraphicsPresentation> presentation ) {
|
||||
mPresentation = std::move( presentation );
|
||||
}
|
||||
|
||||
void KittyGraphicsRenderer::draw( Pass pass, const Vector2f& origin, const Sizef& cellSize,
|
||||
const Sizef& gridSize ) {
|
||||
if ( !mPresentation )
|
||||
return;
|
||||
auto clippingMask = Renderer::instance()->getClippingMask();
|
||||
clippingMask->clipEnable( origin.x, origin.y, gridSize.getWidth(), gridSize.getHeight() );
|
||||
for ( const auto& placement : mPresentation->placements ) {
|
||||
if ( !isInPass( placement.zIndex, pass ) )
|
||||
continue;
|
||||
auto image = mImages.find( placement.imageId );
|
||||
if ( image == mImages.end() || !image->second.texture )
|
||||
continue;
|
||||
const Vector2f position( origin.x + placement.visibleAnchorCell.x * cellSize.getWidth() +
|
||||
placement.firstCellPixelOffset.x,
|
||||
origin.y + placement.visibleAnchorCell.y * cellSize.getHeight() +
|
||||
placement.firstCellPixelOffset.y );
|
||||
const Float width = placement.columns * cellSize.getWidth();
|
||||
const Float height = placement.rows * cellSize.getHeight();
|
||||
TexturePtr texture = image->second.texture;
|
||||
if ( placement.frameNumber > 1 ) {
|
||||
auto frame = image->second.frames.find( placement.frameNumber );
|
||||
if ( frame != image->second.frames.end() )
|
||||
texture = frame->second;
|
||||
}
|
||||
texture->drawEx( position.x, position.y, width, height, 0, Vector2f::One, Color::White,
|
||||
Color::White, Color::White, Color::White, BlendMode::Alpha(),
|
||||
RENDER_NORMAL, OriginPoint( OriginPoint::OriginTopLeft ),
|
||||
placement.sourcePixels );
|
||||
}
|
||||
clippingMask->clipDisable();
|
||||
}
|
||||
|
||||
bool KittyGraphicsRenderer::hasPlacements( Pass pass ) const {
|
||||
return mPresentation &&
|
||||
std::any_of( mPresentation->placements.begin(), mPresentation->placements.end(),
|
||||
[pass]( const TerminalVisiblePlacement& placement ) {
|
||||
return isInPass( placement.zIndex, pass );
|
||||
} );
|
||||
}
|
||||
|
||||
void KittyGraphicsRenderer::reset() {
|
||||
mImages.clear();
|
||||
mPresentation.reset();
|
||||
mLastAppliedSequence = 0;
|
||||
}
|
||||
|
||||
}} // namespace eterm::Terminal
|
||||
@@ -107,13 +107,13 @@ bool PseudoTerminal::isTTY() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PseudoTerminal::resize( int columns, int rows ) {
|
||||
bool PseudoTerminal::resize( int columns, int rows, int pixelWidth, int pixelHeight ) {
|
||||
struct winsize w;
|
||||
|
||||
w.ws_row = rows;
|
||||
w.ws_col = columns;
|
||||
w.ws_xpixel = 0;
|
||||
w.ws_ypixel = 0;
|
||||
w.ws_xpixel = pixelWidth;
|
||||
w.ws_ypixel = pixelHeight;
|
||||
|
||||
bool masterResized = ioctl( (int)mMaster, TIOCSWINSZ, &w ) >= 0;
|
||||
bool slaveResized =
|
||||
@@ -297,7 +297,7 @@ bool PseudoTerminal::isTTY() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PseudoTerminal::resize( int columns, int rows ) {
|
||||
bool PseudoTerminal::resize( int columns, int rows, int, int ) {
|
||||
if ( !pResizePseudoConsole )
|
||||
return false;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <eepp/window/clipboard.hpp>
|
||||
#include <eterm/system/processfactory.hpp>
|
||||
#include <eterm/terminal/boxdrawdata.hpp>
|
||||
#include <eterm/terminal/kittygraphicsrenderer.hpp>
|
||||
#include <eterm/terminal/terminaldisplay.hpp>
|
||||
#include <limits.h>
|
||||
|
||||
@@ -457,6 +458,9 @@ std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
|
||||
eeSAFE_DELETE( processFactory );
|
||||
return nullptr;
|
||||
}
|
||||
terminal->mSession->resize( termSize.getWidth(), termSize.getHeight(),
|
||||
terminal->getGridPixelSize().getWidth(),
|
||||
terminal->getGridPixelSize().getHeight() );
|
||||
terminal->mSession->setPresentationRate( presentationRateForWindow( window ) );
|
||||
terminal->mProgram = program;
|
||||
terminal->mArgs = args;
|
||||
@@ -479,6 +483,7 @@ TerminalDisplay::~TerminalDisplay() {
|
||||
TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const Float& fontSize,
|
||||
const Sizef& pixelsSize, const bool& useFrameBuffer ) :
|
||||
mWindow( window ),
|
||||
mGraphicsRenderer( std::make_unique<KittyGraphicsRenderer>() ),
|
||||
mFont( font ),
|
||||
mFontSize( fontSize ),
|
||||
mSize( pixelsSize ),
|
||||
@@ -493,6 +498,8 @@ TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const
|
||||
resetColors();
|
||||
|
||||
Sizei gridSize( gridSizeFromTermDimensions( mFont, mFontSize, mSize - mPadding * 2.f ) );
|
||||
mColumns = gridSize.getWidth();
|
||||
mRows = gridSize.getHeight();
|
||||
mDirtyLines.resize( gridSize.getHeight(), 1 );
|
||||
|
||||
mQuadVertex = GLi->quadVertex();
|
||||
@@ -715,7 +722,9 @@ void TerminalDisplay::setKeepAlive( bool keepAlive ) {
|
||||
}
|
||||
|
||||
bool TerminalDisplay::update( bool isMouseOverMe ) {
|
||||
drainGraphicsUpdates();
|
||||
consumeSnapshot();
|
||||
drainGraphicsUpdates();
|
||||
drainSessionEvents();
|
||||
if ( mFocus && isBlinkingCursor() && mClock.getElapsedTime().asSeconds() > 0.7 ) {
|
||||
mMode ^= MODE_BLINK;
|
||||
@@ -734,6 +743,29 @@ bool TerminalDisplay::update( bool isMouseOverMe ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void TerminalDisplay::drainGraphicsUpdates() {
|
||||
if ( !mSession )
|
||||
return;
|
||||
auto updates = mSession->drainGraphicsUpdates();
|
||||
if ( !updates.empty() ) {
|
||||
if ( !mGraphicsRenderer->applyUpdates( std::move( updates ) ) ) {
|
||||
mGraphicsRenderer->reset();
|
||||
mLastAppliedGraphicsSequence = 0;
|
||||
if ( !mGraphicsResyncPending ) {
|
||||
mGraphicsResyncPending = true;
|
||||
mSession->requestGraphicsResync();
|
||||
}
|
||||
return;
|
||||
}
|
||||
mLastAppliedGraphicsSequence = mGraphicsRenderer->lastAppliedSequence();
|
||||
mGraphicsResyncPending = false;
|
||||
if ( mSnapshot && mSnapshot->graphics &&
|
||||
mSnapshot->graphics->requiredUpdateSequence <= mLastAppliedGraphicsSequence )
|
||||
mGraphicsRenderer->setPresentation( mSnapshot->graphics );
|
||||
mDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalDisplay::consumeSnapshot() {
|
||||
if ( !mSession )
|
||||
return;
|
||||
@@ -768,6 +800,13 @@ void TerminalDisplay::consumeSnapshot() {
|
||||
mCursor = mSnapshot->cursor;
|
||||
mCursorGlyph = mSnapshot->cursorGlyph;
|
||||
mCursorMode = mSnapshot->cursorMode;
|
||||
if ( mSnapshot->graphics &&
|
||||
mSnapshot->graphics->requiredUpdateSequence <= mGraphicsRenderer->lastAppliedSequence() ) {
|
||||
mGraphicsRenderer->setPresentation( mSnapshot->graphics );
|
||||
} else if ( mSnapshot->graphics && !mGraphicsResyncPending ) {
|
||||
mGraphicsResyncPending = true;
|
||||
mSession->requestGraphicsResync();
|
||||
}
|
||||
const int presentationBits = mMode & MODE_BLINK;
|
||||
mMode = mSnapshot->windowMode | presentationBits;
|
||||
if ( mFocus )
|
||||
@@ -1093,8 +1132,8 @@ void TerminalDisplay::onMouseMove( const Vector2i& pos, const Uint32& flags ) {
|
||||
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR,
|
||||
false );
|
||||
}
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags,
|
||||
mWindow->getInput()->getModState() );
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
|
||||
}
|
||||
|
||||
void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
|
||||
@@ -1134,8 +1173,8 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
|
||||
}
|
||||
}
|
||||
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
|
||||
mWindow->getInput()->getModState() );
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
|
||||
}
|
||||
|
||||
void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
|
||||
@@ -1181,8 +1220,8 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
|
||||
}
|
||||
}
|
||||
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ), flags,
|
||||
mWindow->getInput()->getModState() );
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
|
||||
}
|
||||
|
||||
static inline Color termColor( unsigned int terminalColor, const std::vector<Color>& colors ) {
|
||||
@@ -1368,6 +1407,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
|
||||
|
||||
auto fontSize = mFont->getFontHeight( mFontSize );
|
||||
auto spaceCharAdvanceX = mFont->getGlyph( 'A', mFontSize, false, false ).advance;
|
||||
const Sizef cellSize( spaceCharAdvanceX, fontSize );
|
||||
|
||||
float x = 0.0f;
|
||||
float y = pos.y;
|
||||
@@ -1446,6 +1486,38 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
|
||||
mVBBackground->draw();
|
||||
mVBBackground->unbind();
|
||||
}
|
||||
const Sizef graphicsGridSize( mColumns * cellSize.getWidth(), mRows * cellSize.getHeight() );
|
||||
mGraphicsRenderer->draw( KittyGraphicsRenderer::Pass::VeryNegative, pos, cellSize,
|
||||
graphicsGridSize );
|
||||
if ( mGraphicsRenderer->hasPlacements( KittyGraphicsRenderer::Pass::VeryNegative ) ) {
|
||||
y = std::floor( pos.y );
|
||||
for ( Uint32 row = 0; row < mRows; ++row ) {
|
||||
x = std::floor( pos.x );
|
||||
for ( Uint32 column = 0; column < mColumns; ++column ) {
|
||||
const auto& glyph = mSnapshot->cells[row * mColumns + column];
|
||||
if ( glyph.mode & ATTR_WDUMMY )
|
||||
continue;
|
||||
auto foreground = termColor( glyph.fg, mColors );
|
||||
auto background = termColor( glyph.bg, mColors );
|
||||
if ( IS_SET( MODE_REVERSE ) ) {
|
||||
foreground = foreground == defaultFg ? defaultBg : foreground.invert();
|
||||
background = background == defaultBg ? defaultFg : background.invert();
|
||||
}
|
||||
if ( glyph.mode & ATTR_REVERSE )
|
||||
background = foreground;
|
||||
const bool wide = glyph.mode & ATTR_WIDE;
|
||||
const Float advance = spaceCharAdvanceX * ( wide ? 2.0f : 1.0f );
|
||||
if ( background != defaultBg ) {
|
||||
mPrimitives.setColor( background );
|
||||
mPrimitives.drawRectangle( Rectf( { x, y }, { advance, lineHeight } ) );
|
||||
}
|
||||
x += advance;
|
||||
}
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
mGraphicsRenderer->draw( KittyGraphicsRenderer::Pass::Negative, pos, cellSize,
|
||||
graphicsGridSize );
|
||||
|
||||
y = std::floor( pos.y );
|
||||
|
||||
@@ -1683,6 +1755,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
|
||||
vbo->unbind();
|
||||
}
|
||||
}
|
||||
mGraphicsRenderer->draw( KittyGraphicsRenderer::Pass::NonNegative, pos, cellSize,
|
||||
graphicsGridSize );
|
||||
|
||||
// Underline is rendered after foreground render because it usually clashes with the underlines
|
||||
// decorations and ends up being not visible, I prefer to do this even it it's not standard.
|
||||
@@ -1777,6 +1851,27 @@ Vector2i TerminalDisplay::positionToGrid( const Vector2i& pos ) {
|
||||
return { mouseX, mouseY };
|
||||
}
|
||||
|
||||
Vector2i TerminalDisplay::positionToPixel( const Vector2i& pos ) const {
|
||||
const Sizei gridPixels = getGridPixelSize();
|
||||
const int x = static_cast<int>( std::floor( pos.x - mPosition.x - mPadding.Left ) );
|
||||
const int y = static_cast<int>( std::floor( pos.y - mPosition.y - mPadding.Top ) );
|
||||
return { eeclamp( x, 0, eemax( 0, gridPixels.getWidth() - 1 ) ),
|
||||
eeclamp( y, 0, eemax( 0, gridPixels.getHeight() - 1 ) ) };
|
||||
}
|
||||
|
||||
Sizei TerminalDisplay::getCellPixelSize() const {
|
||||
return {
|
||||
static_cast<int>( std::round( mFont->getGlyph( 'A', mFontSize, false, false ).advance ) ),
|
||||
static_cast<int>( std::round( mFont->getFontHeight( mFontSize ) ) ) };
|
||||
}
|
||||
|
||||
Sizei TerminalDisplay::getGridPixelSize() const {
|
||||
const Sizei cell = getCellPixelSize();
|
||||
const int columns = mSnapshot ? mSnapshot->columns : static_cast<int>( mColumns );
|
||||
const int rows = mSnapshot ? mSnapshot->rows : static_cast<int>( mRows );
|
||||
return { columns * cell.getWidth(), rows * cell.getHeight() };
|
||||
}
|
||||
|
||||
void TerminalDisplay::onSizeChange() {
|
||||
Sizei gridSize( gridSizeFromTermDimensions(
|
||||
mFont, mFontSize,
|
||||
@@ -1784,7 +1879,10 @@ void TerminalDisplay::onSizeChange() {
|
||||
|
||||
if ( mSession && ( !mSnapshot || gridSize.getWidth() != mSnapshot->columns ||
|
||||
gridSize.getHeight() != mSnapshot->rows ) ) {
|
||||
mSession->resize( gridSize.getWidth(), gridSize.getHeight() );
|
||||
const Sizei cellSize = getCellPixelSize();
|
||||
mSession->resize( gridSize.getWidth(), gridSize.getHeight(),
|
||||
gridSize.getWidth() * cellSize.getWidth(),
|
||||
gridSize.getHeight() * cellSize.getHeight() );
|
||||
mDirtyLines.resize( gridSize.getHeight(), 1 );
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,46 @@ static const unsigned int tabspaces = 4;
|
||||
#define TRUECOLOR( r, g, b ) ( 1 << 24 | ( r ) << 16 | ( g ) << 8 | ( b ) )
|
||||
#define IS_TRUECOL( x ) ( 1 << 24 & ( x ) )
|
||||
|
||||
static int kittyDiacriticIndex( Rune value ) {
|
||||
static constexpr Rune values[] = {
|
||||
0x0305, 0x030D, 0x030E, 0x0310, 0x0312, 0x033D, 0x033E, 0x033F, 0x0346, 0x034A,
|
||||
0x034B, 0x034C, 0x0350, 0x0351, 0x0352, 0x0357, 0x035B, 0x0363, 0x0364, 0x0365,
|
||||
0x0366, 0x0367, 0x0368, 0x0369, 0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F,
|
||||
0x0483, 0x0484, 0x0485, 0x0486, 0x0487, 0x0592, 0x0593, 0x0594, 0x0595, 0x0597,
|
||||
0x0598, 0x0599, 0x059C, 0x059D, 0x059E, 0x059F, 0x05A0, 0x05A1, 0x05A8, 0x05A9,
|
||||
0x05AB, 0x05AC, 0x05AF, 0x05C4, 0x0610, 0x0611, 0x0612, 0x0613, 0x0614, 0x0615,
|
||||
0x0616, 0x0617, 0x0657, 0x0658, 0x0659, 0x065A, 0x065B, 0x065D, 0x065E, 0x06D6,
|
||||
0x06D7, 0x06D8, 0x06D9, 0x06DA, 0x06DB, 0x06DC, 0x06DF, 0x06E0, 0x06E1, 0x06E2,
|
||||
0x06E4, 0x06E7, 0x06E8, 0x06EB, 0x06EC, 0x0730, 0x0732, 0x0733, 0x0735, 0x0736,
|
||||
0x073A, 0x073D, 0x073F, 0x0740, 0x0741, 0x0743, 0x0745, 0x0747, 0x0749, 0x074A,
|
||||
0x07EB, 0x07EC, 0x07ED, 0x07EE, 0x07EF, 0x07F0, 0x07F1, 0x07F3, 0x0816, 0x0817,
|
||||
0x0818, 0x0819, 0x081B, 0x081C, 0x081D, 0x081E, 0x081F, 0x0820, 0x0821, 0x0822,
|
||||
0x0823, 0x0825, 0x0826, 0x0827, 0x0829, 0x082A, 0x082B, 0x082C, 0x082D, 0x0951,
|
||||
0x0953, 0x0954, 0x0F82, 0x0F83, 0x0F86, 0x0F87, 0x135D, 0x135E, 0x135F, 0x17DD,
|
||||
0x193A, 0x1A17, 0x1A75, 0x1A76, 0x1A77, 0x1A78, 0x1A79, 0x1A7A, 0x1A7B, 0x1A7C,
|
||||
0x1B6B, 0x1B6D, 0x1B6E, 0x1B6F, 0x1B70, 0x1B71, 0x1B72, 0x1B73, 0x1CD0, 0x1CD1,
|
||||
0x1CD2, 0x1CDA, 0x1CDB, 0x1CE0, 0x1DC0, 0x1DC1, 0x1DC3, 0x1DC4, 0x1DC5, 0x1DC6,
|
||||
0x1DC7, 0x1DC8, 0x1DC9, 0x1DCB, 0x1DCC, 0x1DD1, 0x1DD2, 0x1DD3, 0x1DD4, 0x1DD5,
|
||||
0x1DD6, 0x1DD7, 0x1DD8, 0x1DD9, 0x1DDA, 0x1DDB, 0x1DDC, 0x1DDD, 0x1DDE, 0x1DDF,
|
||||
0x1DE0, 0x1DE1, 0x1DE2, 0x1DE3, 0x1DE4, 0x1DE5, 0x1DE6, 0x1DFE, 0x20D0, 0x20D1,
|
||||
0x20D4, 0x20D5, 0x20D6, 0x20D7, 0x20DB, 0x20DC, 0x20E1, 0x20E7, 0x20E9, 0x20F0,
|
||||
0x2CEF, 0x2CF0, 0x2CF1, 0x2DE0, 0x2DE1, 0x2DE2, 0x2DE3, 0x2DE4, 0x2DE5, 0x2DE6,
|
||||
0x2DE7, 0x2DE8, 0x2DE9, 0x2DEA, 0x2DEB, 0x2DEC, 0x2DED, 0x2DEE, 0x2DEF, 0x2DF0,
|
||||
0x2DF1, 0x2DF2, 0x2DF3, 0x2DF4, 0x2DF5, 0x2DF6, 0x2DF7, 0x2DF8, 0x2DF9, 0x2DFA,
|
||||
0x2DFB, 0x2DFC, 0x2DFD, 0x2DFE, 0x2DFF, 0xA66F, 0xA67C, 0xA67D, 0xA6F0, 0xA6F1,
|
||||
0xA8E0, 0xA8E1, 0xA8E2, 0xA8E3, 0xA8E4, 0xA8E5, 0xA8E6, 0xA8E7, 0xA8E8, 0xA8E9,
|
||||
0xA8EA, 0xA8EB, 0xA8EC, 0xA8ED, 0xA8EE, 0xA8EF, 0xA8F0, 0xA8F1, 0xAAB0, 0xAAB2,
|
||||
0xAAB3, 0xAAB7, 0xAAB8, 0xAABE, 0xAABF, 0xAAC1, 0xFE20, 0xFE21, 0xFE22, 0xFE23,
|
||||
0xFE24, 0xFE25, 0xFE26, 0x10A0F, 0x10A38, 0x1D185, 0x1D186, 0x1D187, 0x1D188, 0x1D189,
|
||||
0x1D1AA, 0x1D1AB, 0x1D1AC, 0x1D1AD, 0x1D242, 0x1D243, 0x1D244,
|
||||
};
|
||||
for ( size_t i = 0; i < sizeof( values ) / sizeof( values[0] ); ++i ) {
|
||||
if ( values[i] == value )
|
||||
return static_cast<int>( i );
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Arbitrary sizes */
|
||||
#define UTF_INVALID 0xFFFD
|
||||
#define UTF_SIZ 4
|
||||
@@ -867,8 +907,13 @@ void TerminalEmulator::trimMemory() {
|
||||
}
|
||||
|
||||
void TerminalEmulator::clearHistory() {
|
||||
for ( int i = 0; i < mTerm.histcursize; ++i )
|
||||
for ( int i = 0; i < mTerm.histcursize; ++i ) {
|
||||
if ( mTerm.hist[i] ) {
|
||||
for ( int column = 0; column < mTerm.col; ++column )
|
||||
mKittyPlaceholderMetadata.erase( &mTerm.hist[i][column] );
|
||||
}
|
||||
eeSAFE_FREE( mTerm.hist[i] );
|
||||
}
|
||||
eeSAFE_FREE( mTerm.hist );
|
||||
mTerm.histcursize = 0;
|
||||
mTerm.histi = 0;
|
||||
@@ -906,6 +951,12 @@ void TerminalEmulator::notifyColorSchemeChanged() {
|
||||
mColorScheme = scheme;
|
||||
}
|
||||
|
||||
void TerminalEmulator::requestGraphicsResync() {
|
||||
mKittyGraphics.resync();
|
||||
mDirty = true;
|
||||
draw();
|
||||
}
|
||||
|
||||
Vector2i TerminalEmulator::getSize() const {
|
||||
return { mTerm.col, mTerm.row };
|
||||
}
|
||||
@@ -1048,8 +1099,9 @@ void TerminalEmulator::treset( void ) {
|
||||
tswapscreen();
|
||||
}
|
||||
|
||||
xsetmode( 0, MODE_MOUSE | MODE_MOUSESGR | MODE_APPKEYPAD | MODE_APPCURSOR | MODE_FOCUS |
|
||||
MODE_BRCKTPASTE | MODE_MOUSEX10 | MODE_MOUSEMANY );
|
||||
xsetmode( 0, MODE_MOUSE | MODE_MOUSESGR | MODE_MOUSESGR_PIXELS | MODE_APPKEYPAD |
|
||||
MODE_APPCURSOR | MODE_FOCUS | MODE_BRCKTPASTE | MODE_MOUSEX10 |
|
||||
MODE_MOUSEMANY );
|
||||
// Preserve eterm's established behavior and xterm's default alternateScroll resource.
|
||||
xsetmode( 1, MODE_ALTSCRROLL );
|
||||
auto dpy = mDpy.lock();
|
||||
@@ -1098,6 +1150,7 @@ void TerminalEmulator::tscrolldown( int top, int n ) {
|
||||
|
||||
if ( mTerm.scr == 0 )
|
||||
selscroll( top, n );
|
||||
mKittyGraphics.scrollScreen( top, mTerm.bot, n, false );
|
||||
}
|
||||
|
||||
void TerminalEmulator::tscrollup( int top, int n, int copyhist ) {
|
||||
@@ -1151,6 +1204,9 @@ void TerminalEmulator::tscrollup( int top, int n, int copyhist ) {
|
||||
|
||||
if ( mTerm.scr == 0 )
|
||||
selscroll( top, -n );
|
||||
mKittyGraphics.scrollScreen( top, mTerm.bot, -n,
|
||||
copyhist && mTerm.histsize > 0 && !IS_SET( MODE_ALTSCREEN ) &&
|
||||
top == mTerm.top );
|
||||
|
||||
onScrollPositionChange();
|
||||
}
|
||||
@@ -1559,6 +1615,7 @@ void TerminalEmulator::tsetchar( Rune u, TerminalGlyph* attr, int x, int y ) {
|
||||
TLINE( y )[x - 1].mode &= ~ATTR_WIDE;
|
||||
}
|
||||
|
||||
mKittyPlaceholderMetadata.erase( &TLINE( y )[x] );
|
||||
mTerm.dirty[y] = 1;
|
||||
TLINE( y )[x] = *attr;
|
||||
TLINE( y )[x].u = u;
|
||||
@@ -1576,11 +1633,14 @@ void TerminalEmulator::tclearregion( int x1, int y1, int x2, int y2, bool skip_c
|
||||
temp = x1, x1 = x2, x2 = temp;
|
||||
if ( y1 > y2 )
|
||||
temp = y1, y1 = y2, y2 = temp;
|
||||
|
||||
LIMIT( x1, 0, mTerm.col - 1 );
|
||||
LIMIT( x2, 0, mTerm.col - 1 );
|
||||
LIMIT( y1, 0, mTerm.row - 1 );
|
||||
LIMIT( y2, 0, mTerm.row - 1 );
|
||||
for ( int clearY = y1; clearY <= y2; ++clearY ) {
|
||||
for ( int clearX = x1; clearX <= x2; ++clearX )
|
||||
mKittyPlaceholderMetadata.erase( &TLINE( clearY )[clearX] );
|
||||
}
|
||||
|
||||
/*
|
||||
* Fast path for the common full-row clear performed while scrolling: no
|
||||
@@ -1753,6 +1813,7 @@ void TerminalEmulator::tsetattr( int* attr, int l, const char* separators ) {
|
||||
ATTR_BLINK | ATTR_REVERSE | ATTR_INVISIBLE | ATTR_STRUCK );
|
||||
mTerm.c.attr.fg = mDefaultFg;
|
||||
mTerm.c.attr.bg = mDefaultBg;
|
||||
mKittyUnderlineColor = 0;
|
||||
break;
|
||||
case 1:
|
||||
mTerm.c.attr.mode |= ATTR_BOLD;
|
||||
@@ -1816,12 +1877,11 @@ void TerminalEmulator::tsetattr( int* attr, int l, const char* separators ) {
|
||||
mTerm.c.attr.bg = mDefaultBg;
|
||||
break;
|
||||
case 58:
|
||||
/* This starts a sequence to change the color of
|
||||
* "underline" pixels. We don't support that and
|
||||
* instead eat up a following "5;n" or "2;r;g;b". */
|
||||
tdefcolor( attr, separators, &i, l );
|
||||
if ( ( idx = tdefcolor( attr, separators, &i, l ) ) >= 0 )
|
||||
mKittyUnderlineColor = idx;
|
||||
break;
|
||||
case 59: /* reset underline color (unsupported, therefore a no-op) */
|
||||
case 59:
|
||||
mKittyUnderlineColor = 0;
|
||||
break;
|
||||
default:
|
||||
if ( BETWEEN( attr[i], 30, 37 ) ) {
|
||||
@@ -1916,6 +1976,11 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) {
|
||||
case 1006: /* 1006: extended reporting mode */
|
||||
xsetmode( set, MODE_MOUSESGR );
|
||||
break;
|
||||
case 1016: /* 1016: extended reporting in terminal-grid pixels */
|
||||
xsetmode( set, MODE_MOUSESGR_PIXELS );
|
||||
if ( set )
|
||||
xsetmode( 1, MODE_MOUSESGR );
|
||||
break;
|
||||
case 1007: /* wheel sends cursor keys on the alternate screen */
|
||||
xsetmode( set, MODE_ALTSCRROLL );
|
||||
break;
|
||||
@@ -1936,7 +2001,10 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) {
|
||||
tclearregion( 0, 0, mTerm.col - 1, mTerm.row - 1 );
|
||||
}
|
||||
if ( set ^ alt ) /* set is always 1 or 0 */
|
||||
{
|
||||
tswapscreen();
|
||||
mKittyGraphics.setAlternateScreen( set != 0 );
|
||||
}
|
||||
if ( *args != 1049 )
|
||||
break;
|
||||
/* FALLTHROUGH */
|
||||
@@ -2153,6 +2221,7 @@ void TerminalEmulator::csihandle( void ) {
|
||||
// fallthrough
|
||||
case 2: /* all */
|
||||
tclearregion( 0, 0, mTerm.col - 1, mTerm.row - 1 );
|
||||
mKittyGraphics.clearScreen();
|
||||
break;
|
||||
default:
|
||||
goto unknown;
|
||||
@@ -2278,6 +2347,18 @@ void TerminalEmulator::csihandle( void ) {
|
||||
break;
|
||||
case 't': /* Window manipulation */
|
||||
switch ( mCsiescseq.arg[0] ) {
|
||||
case 14: /* Report terminal grid size in pixels. */
|
||||
len =
|
||||
snprintf( buf, sizeof( buf ), "\033[4;%d;%dt", mPixelHeight, mPixelWidth );
|
||||
ttywrite( buf, len, 0 );
|
||||
break;
|
||||
case 16: { /* Report terminal cell size in pixels. */
|
||||
const int cellWidth = mTerm.col > 0 ? mPixelWidth / mTerm.col : 0;
|
||||
const int cellHeight = mTerm.row > 0 ? mPixelHeight / mTerm.row : 0;
|
||||
len = snprintf( buf, sizeof( buf ), "\033[6;%d;%dt", cellHeight, cellWidth );
|
||||
ttywrite( buf, len, 0 );
|
||||
break;
|
||||
}
|
||||
case 22: /* Save window title */
|
||||
// Push current title to title stack
|
||||
mTerm.title_stack.push_back( mTerm.title );
|
||||
@@ -2335,6 +2416,20 @@ void TerminalEmulator::strhandle( void ) {
|
||||
int j, narg, par;
|
||||
|
||||
mTerm.esc &= ~( ESC_STR_END | ESC_STR );
|
||||
if ( mStrescseq.discarded )
|
||||
return;
|
||||
if ( mStrescseq.type == '_' && mStrescseq.len > 0 && mStrescseq.buf[0] == 'G' ) {
|
||||
auto result =
|
||||
mKittyGraphics.handle( std::string_view{ mStrescseq.buf + 1, mStrescseq.len - 1 },
|
||||
Vector2i( mTerm.c.x, mTerm.c.y ) );
|
||||
if ( !result.response.empty() )
|
||||
write( result.response.data(), result.response.size() );
|
||||
if ( result.changed )
|
||||
mDirty = true;
|
||||
if ( result.cursorMovement != Vector2i::Zero )
|
||||
tmoveto( mTerm.c.x + result.cursorMovement.x, mTerm.c.y + result.cursorMovement.y );
|
||||
return;
|
||||
}
|
||||
strparse();
|
||||
par = ( narg = mStrescseq.narg ) ? atoi( mStrescseq.args[0] ) : 0;
|
||||
|
||||
@@ -2851,6 +2946,7 @@ int TerminalEmulator::eschandle( uchar ascii ) {
|
||||
ttywrite( vtiden, strlen( vtiden ), 0 );
|
||||
break;
|
||||
case 'c': /* RIS -- Reset to initial state */
|
||||
mKittyGraphics.reset();
|
||||
treset();
|
||||
resettitle();
|
||||
loadColors();
|
||||
@@ -2979,23 +3075,28 @@ void TerminalEmulator::tputc( Rune u ) {
|
||||
goto check_control_code;
|
||||
}
|
||||
|
||||
if ( mStrescseq.discarded )
|
||||
return;
|
||||
|
||||
const bool kittyGraphics =
|
||||
mStrescseq.type == '_' && mStrescseq.len > 0 && mStrescseq.buf[0] == 'G';
|
||||
const size_t sequenceLimit =
|
||||
kittyGraphics ? MAX_KITTY_GRAPHICS_APC_SIZE : MAX_GENERIC_STRING_SEQUENCE_SIZE;
|
||||
if ( len > sequenceLimit - mStrescseq.len ) {
|
||||
mStrescseq.discarded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( mStrescseq.len + len >= mStrescseq.siz ) {
|
||||
/*
|
||||
* Here is a bug in terminals. If the user never sends
|
||||
* some code to stop the str or esc command, then st
|
||||
* will stop responding. But this is better than
|
||||
* silently failing with unknown characters. At least
|
||||
* then users will report back.
|
||||
*
|
||||
* In the case users ever get fixed, here is the code:
|
||||
*/
|
||||
/*
|
||||
* term.esc = 0;
|
||||
* strhandle();
|
||||
*/
|
||||
if ( mStrescseq.siz > ( SIZE_MAX - UTF_SIZ ) / 2 )
|
||||
const size_t required = mStrescseq.len + len + 1;
|
||||
size_t newSize = mStrescseq.siz;
|
||||
while ( newSize < required && newSize < sequenceLimit )
|
||||
newSize = eemin( newSize * 2, sequenceLimit );
|
||||
if ( newSize < required ) {
|
||||
mStrescseq.discarded = true;
|
||||
return;
|
||||
mStrescseq.siz *= 2;
|
||||
}
|
||||
mStrescseq.siz = newSize;
|
||||
mStrescseq.buf = (char*)xrealloc( mStrescseq.buf, mStrescseq.siz );
|
||||
}
|
||||
|
||||
@@ -3045,6 +3146,35 @@ check_control_code:
|
||||
*/
|
||||
return;
|
||||
}
|
||||
if ( mKittyPlaceholderCell.x >= 0 ) {
|
||||
const int diacritic = kittyDiacriticIndex( u );
|
||||
if ( diacritic >= 0 ) {
|
||||
auto* placeholder = &mTerm.line[mKittyPlaceholderCell.y][mKittyPlaceholderCell.x];
|
||||
auto metadata = mKittyPlaceholderMetadata.find( placeholder );
|
||||
if ( metadata == mKittyPlaceholderMetadata.end() ) {
|
||||
mKittyPlaceholderCell = Vector2i( -1, -1 );
|
||||
return;
|
||||
}
|
||||
switch ( metadata->second.diacriticCount++ ) {
|
||||
case 0:
|
||||
metadata->second.row = static_cast<Uint16>( diacritic );
|
||||
break;
|
||||
case 1:
|
||||
metadata->second.column = static_cast<Uint16>( diacritic );
|
||||
break;
|
||||
case 2:
|
||||
if ( diacritic <= 255 )
|
||||
metadata->second.imageIdMsb = static_cast<Uint8>( diacritic );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
mTerm.dirty[mKittyPlaceholderCell.y] = 1;
|
||||
mDirty = true;
|
||||
return;
|
||||
}
|
||||
mKittyPlaceholderCell = Vector2i( -1, -1 );
|
||||
}
|
||||
if ( selected( mTerm.c.x, mTerm.c.y ) )
|
||||
selclear();
|
||||
|
||||
@@ -3065,6 +3195,15 @@ check_control_code:
|
||||
}
|
||||
|
||||
tsetchar( u, &mTerm.c.attr, mTerm.c.x, mTerm.c.y );
|
||||
if ( u == 0x10EEEE ) {
|
||||
mKittyPlaceholderCell = Vector2i( mTerm.c.x, mTerm.c.y );
|
||||
const Uint32 placementId = IS_TRUECOL( mKittyUnderlineColor )
|
||||
? mKittyUnderlineColor & 0xFFFFFF
|
||||
: mKittyUnderlineColor <= 255 ? mKittyUnderlineColor
|
||||
: 0;
|
||||
mKittyPlaceholderMetadata[&mTerm.line[mTerm.c.y][mTerm.c.x]] =
|
||||
KittyPlaceholderMetadata{ placementId };
|
||||
}
|
||||
mTerm.lastc = u;
|
||||
|
||||
if ( width == 2 ) {
|
||||
@@ -3123,6 +3262,36 @@ void TerminalEmulator::tresize( int col, int row ) {
|
||||
int save_end = 0;
|
||||
int loaded = 0;
|
||||
bool is_alt = IS_SET( MODE_ALTSCREEN );
|
||||
std::vector<KittyPlaceholderMetadata> primaryPlaceholderMetadata;
|
||||
std::vector<KittyPlaceholderMetadata> alternatePlaceholderMetadata;
|
||||
auto collectPlaceholderMetadata = [&]( Line line, int columns,
|
||||
std::vector<KittyPlaceholderMetadata>& output ) {
|
||||
if ( !line )
|
||||
return;
|
||||
for ( int column = 0; column < columns; ++column ) {
|
||||
if ( line[column].u != 0x10EEEE )
|
||||
continue;
|
||||
auto metadata = mKittyPlaceholderMetadata.find( &line[column] );
|
||||
if ( metadata != mKittyPlaceholderMetadata.end() )
|
||||
output.emplace_back( metadata->second );
|
||||
}
|
||||
};
|
||||
if ( mTerm.col > 0 ) {
|
||||
for ( int history = 0; history < mTerm.histlen; ++history ) {
|
||||
const int index =
|
||||
( mTerm.histi - mTerm.histlen + 1 + history + mTerm.histsize ) % mTerm.histsize;
|
||||
collectPlaceholderMetadata( mTerm.hist[index], mTerm.col, primaryPlaceholderMetadata );
|
||||
}
|
||||
Line* primaryLines = is_alt ? mTerm.alt : mTerm.line;
|
||||
Line* alternateLines = is_alt ? mTerm.line : mTerm.alt;
|
||||
for ( int line = 0; line < mTerm.row; ++line ) {
|
||||
collectPlaceholderMetadata( primaryLines[line], mTerm.col, primaryPlaceholderMetadata );
|
||||
collectPlaceholderMetadata( alternateLines[line], mTerm.col,
|
||||
alternatePlaceholderMetadata );
|
||||
}
|
||||
}
|
||||
mKittyPlaceholderMetadata.clear();
|
||||
mKittyPlaceholderCell = Vector2i( -1, -1 );
|
||||
|
||||
if ( col < 1 || row < 1 ) {
|
||||
terminalDiagnostic( "tresize: error resizing to %dx%d\n", col, row );
|
||||
@@ -3304,6 +3473,32 @@ void TerminalEmulator::tresize( int col, int row ) {
|
||||
eemax( mTerm.scr - mTerm.histlen, eemin( mTerm.scr + mTerm.row - 1, mSel.oe.y ) );
|
||||
selnormalize();
|
||||
}
|
||||
auto restorePlaceholderMetadata = [&]( Line line, int columns,
|
||||
const std::vector<KittyPlaceholderMetadata>& metadata,
|
||||
size_t& index ) {
|
||||
if ( !line )
|
||||
return;
|
||||
for ( int column = 0; column < columns && index < metadata.size(); ++column ) {
|
||||
if ( line[column].u == 0x10EEEE )
|
||||
mKittyPlaceholderMetadata[&line[column]] = metadata[index++];
|
||||
}
|
||||
};
|
||||
size_t primaryMetadataIndex = 0;
|
||||
for ( int history = 0; history < mTerm.histlen; ++history ) {
|
||||
const int index =
|
||||
( mTerm.histi - mTerm.histlen + 1 + history + mTerm.histsize ) % mTerm.histsize;
|
||||
restorePlaceholderMetadata( mTerm.hist[index], mTerm.col, primaryPlaceholderMetadata,
|
||||
primaryMetadataIndex );
|
||||
}
|
||||
Line* primaryLines = is_alt ? mTerm.alt : mTerm.line;
|
||||
Line* alternateLines = is_alt ? mTerm.line : mTerm.alt;
|
||||
size_t alternateMetadataIndex = 0;
|
||||
for ( int line = 0; line < mTerm.row; ++line ) {
|
||||
restorePlaceholderMetadata( primaryLines[line], mTerm.col, primaryPlaceholderMetadata,
|
||||
primaryMetadataIndex );
|
||||
restorePlaceholderMetadata( alternateLines[line], mTerm.col, alternatePlaceholderMetadata,
|
||||
alternateMetadataIndex );
|
||||
}
|
||||
|
||||
mDirty = true;
|
||||
onScrollPositionChange();
|
||||
@@ -3357,6 +3552,63 @@ void TerminalEmulator::draw() {
|
||||
cx--;
|
||||
|
||||
drawregion( *dpy, 0, 0, mTerm.col, mTerm.row );
|
||||
std::vector<TerminalGraphicsPlaceholderCell> placeholderCells;
|
||||
const bool scanPlaceholders = mKittyGraphics.hasVirtualPlacements();
|
||||
for ( int y = 0; scanPlaceholders && y < mTerm.row; ++y ) {
|
||||
const TerminalGlyph* previous = nullptr;
|
||||
Uint32 previousPlacementId = 0;
|
||||
Uint32 previousRow = 0;
|
||||
Uint32 previousColumn = 0;
|
||||
Uint8 previousMsb = 0;
|
||||
for ( int x = 0; x < mTerm.col; ++x ) {
|
||||
const auto& glyph = TLINE( y )[x];
|
||||
if ( glyph.u != 0x10EEEE ) {
|
||||
previous = nullptr;
|
||||
continue;
|
||||
}
|
||||
auto metadata = mKittyPlaceholderMetadata.find( &glyph );
|
||||
if ( metadata == mKittyPlaceholderMetadata.end() ) {
|
||||
previous = nullptr;
|
||||
continue;
|
||||
}
|
||||
Uint32 row = metadata->second.row;
|
||||
Uint32 column = metadata->second.column;
|
||||
Uint8 msb = metadata->second.imageIdMsb;
|
||||
const Uint32 placementId = metadata->second.placementId;
|
||||
const bool sameColors =
|
||||
previous && previous->fg == glyph.fg && previousPlacementId == placementId;
|
||||
if ( metadata->second.diacriticCount == 0 && sameColors ) {
|
||||
row = previousRow;
|
||||
column = previousColumn + 1;
|
||||
msb = previousMsb;
|
||||
} else if ( metadata->second.diacriticCount == 1 && sameColors &&
|
||||
row == previousRow ) {
|
||||
column = previousColumn + 1;
|
||||
msb = previousMsb;
|
||||
} else if ( metadata->second.diacriticCount == 2 && sameColors &&
|
||||
row == previousRow && column == previousColumn + 1 ) {
|
||||
msb = previousMsb;
|
||||
}
|
||||
if ( row == UINT16_MAX || column == UINT16_MAX ) {
|
||||
previous = nullptr;
|
||||
continue;
|
||||
}
|
||||
const Uint32 lowImageId = IS_TRUECOL( glyph.fg ) ? glyph.fg & 0xFFFFFF
|
||||
: glyph.fg <= 255 ? glyph.fg
|
||||
: 0;
|
||||
placeholderCells.push_back( { lowImageId | ( static_cast<Uint32>( msb ) << 24 ),
|
||||
placementId, Vector2i( x, y ), row, column } );
|
||||
previous = &glyph;
|
||||
previousRow = row;
|
||||
previousColumn = column;
|
||||
previousMsb = msb;
|
||||
previousPlacementId = placementId;
|
||||
}
|
||||
}
|
||||
mKittyGraphics.setPlaceholderCells( std::move( placeholderCells ) );
|
||||
auto graphicsUpdates = mKittyGraphics.takeUpdates();
|
||||
if ( !graphicsUpdates.empty() || mKittyGraphics.hasPendingPresentation() )
|
||||
dpy->drawGraphics( mKittyGraphics.takePresentation(), std::move( graphicsUpdates ) );
|
||||
|
||||
if ( mTerm.scr == 0 )
|
||||
dpy->drawCursor( cx, mTerm.c.y, mTerm.line[mTerm.c.y][cx], mTerm.ocx, mTerm.ocy,
|
||||
@@ -3379,6 +3631,7 @@ void TerminalEmulator::redraw() {
|
||||
}
|
||||
|
||||
void TerminalEmulator::reset() {
|
||||
mKittyGraphics.reset();
|
||||
treset();
|
||||
redraw();
|
||||
}
|
||||
@@ -3448,8 +3701,10 @@ void TerminalEmulator::reportColorScheme() {
|
||||
ttywrite( buf, len, 0 );
|
||||
}
|
||||
|
||||
void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Vector2i& pos,
|
||||
void TerminalEmulator::mousereport( const TerminalMouseEventType& type,
|
||||
const Vector2i& cellPosition, const Vector2i& pixelPosition,
|
||||
const Uint32& flags, const Uint32& mod ) {
|
||||
const Vector2i& pos = xgetmode( MODE_MOUSESGR_PIXELS ) ? pixelPosition : cellPosition;
|
||||
if ( !xgetmode( (TerminalWinMode)MODE_MOUSE ) && !xgetmode( MODE_MOUSESGR ) &&
|
||||
( TerminalMouseEventType::MouseButtonDown == type ||
|
||||
TerminalMouseEventType::MouseButtonRelease == type ) ) {
|
||||
@@ -3560,6 +3815,10 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
|
||||
}
|
||||
|
||||
void TerminalEmulator::setPtyAndProcess( PtyPtr&& pty, ProcPtr&& process ) {
|
||||
mKittyGraphics.reset();
|
||||
mKittyPlaceholderMetadata.clear();
|
||||
mKittyPlaceholderCell = Vector2i( -1, -1 );
|
||||
mBuflen = 0;
|
||||
mStatus = STARTING;
|
||||
mExitCode = 1;
|
||||
mPty = std::move( pty );
|
||||
@@ -3654,6 +3913,7 @@ void TerminalEmulator::onProcessExit( int exitCode ) {
|
||||
}
|
||||
|
||||
void TerminalEmulator::onScrollPositionChange() {
|
||||
mKittyGraphics.setViewport( mTerm.scr, mTerm.histlen, mTerm.row );
|
||||
auto dpy = mDpy.lock();
|
||||
if ( dpy )
|
||||
dpy->onScrollPositionChange();
|
||||
@@ -3704,11 +3964,19 @@ int TerminalEmulator::write( const char* buf, size_t buflen ) {
|
||||
}
|
||||
|
||||
void TerminalEmulator::resize( int columns, int rows ) {
|
||||
resize( columns, rows, mPixelWidth, mPixelHeight );
|
||||
}
|
||||
|
||||
void TerminalEmulator::resize( int columns, int rows, int pixelWidth, int pixelHeight ) {
|
||||
mPixelWidth = eemax( 0, pixelWidth );
|
||||
mPixelHeight = eemax( 0, pixelHeight );
|
||||
mKittyGraphics.setCellPixelSize( columns > 0 ? mPixelWidth / columns : 0,
|
||||
rows > 0 ? mPixelHeight / rows : 0 );
|
||||
bool is_alt = IS_SET( MODE_ALTSCREEN );
|
||||
|
||||
// Alt doesn't need reflow, we can resize and redraw instantly which looks and feels better
|
||||
if ( is_alt ) {
|
||||
if ( !mPty->resize( columns, rows ) ) {
|
||||
if ( !mPty->resize( columns, rows, mPixelWidth, mPixelHeight ) ) {
|
||||
_die( "Failed to resize pty!" );
|
||||
return;
|
||||
}
|
||||
@@ -3722,6 +3990,8 @@ void TerminalEmulator::resize( int columns, int rows ) {
|
||||
redraw();
|
||||
mPendingPtyColumns = columns;
|
||||
mPendingPtyRows = rows;
|
||||
mPendingPtyPixelWidth = mPixelWidth;
|
||||
mPendingPtyPixelHeight = mPixelHeight;
|
||||
mPendingPtyResize = true;
|
||||
mPendingPtyResizeClock.restart();
|
||||
}
|
||||
@@ -3729,10 +3999,13 @@ void TerminalEmulator::resize( int columns, int rows ) {
|
||||
#define MAX_TTY_READS ( 1024 )
|
||||
|
||||
bool TerminalEmulator::update() {
|
||||
if ( mKittyGraphics.updateAnimations() )
|
||||
mDirty = true;
|
||||
if ( mPendingPtyResize && mPendingPtyResizeClock.getElapsedTime() >= Milliseconds( 100 ) ) {
|
||||
mPendingPtyResize = false;
|
||||
|
||||
if ( !mPty->resize( mPendingPtyColumns, mPendingPtyRows ) ) {
|
||||
if ( !mPty->resize( mPendingPtyColumns, mPendingPtyRows, mPendingPtyPixelWidth,
|
||||
mPendingPtyPixelHeight ) ) {
|
||||
_die( "Failed to resize pty!" );
|
||||
}
|
||||
|
||||
|
||||
77
src/modules/eterm/src/eterm/terminal/terminalgraphics.cpp
Normal file
77
src/modules/eterm/src/eterm/terminal/terminalgraphics.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#include <eterm/terminal/terminalgraphics.hpp>
|
||||
|
||||
namespace eterm { namespace Terminal {
|
||||
|
||||
TerminalGraphicsUpdateQueue::TerminalGraphicsUpdateQueue( size_t maxUpdates, size_t maxBytes ) :
|
||||
mMaxUpdates( maxUpdates ), mMaxBytes( maxBytes ) {}
|
||||
|
||||
Uint64 TerminalGraphicsUpdateQueue::enqueue( TerminalGraphicsUpdate update ) {
|
||||
std::lock_guard<std::mutex> lock( mMutex );
|
||||
const size_t payloadBytes = update.payloadBytes();
|
||||
if ( mNeedsResync )
|
||||
return ++mNextSequence;
|
||||
|
||||
auto isFullImageUpdate = []( TerminalGraphicsUpdateType type ) {
|
||||
return type == TerminalGraphicsUpdateType::CreateImage ||
|
||||
type == TerminalGraphicsUpdateType::ReplaceImage;
|
||||
};
|
||||
if ( !mUpdates.empty() && isFullImageUpdate( update.type ) &&
|
||||
isFullImageUpdate( mUpdates.back().type ) && mUpdates.back().imageId == update.imageId ) {
|
||||
const size_t previousBytes = mUpdates.back().payloadBytes();
|
||||
if ( payloadBytes <= mMaxBytes - ( mQueuedBytes - previousBytes ) ) {
|
||||
update.sequence = mUpdates.back().sequence;
|
||||
if ( mUpdates.back().type == TerminalGraphicsUpdateType::CreateImage )
|
||||
update.type = TerminalGraphicsUpdateType::CreateImage;
|
||||
mQueuedBytes = mQueuedBytes - previousBytes + payloadBytes;
|
||||
mUpdates.back() = std::move( update );
|
||||
return mUpdates.back().sequence;
|
||||
}
|
||||
}
|
||||
|
||||
update.sequence = ++mNextSequence;
|
||||
|
||||
if ( mUpdates.size() >= mMaxUpdates || mQueuedBytes > mMaxBytes ||
|
||||
payloadBytes > mMaxBytes - mQueuedBytes ) {
|
||||
mUpdates.clear();
|
||||
mQueuedBytes = 0;
|
||||
mNeedsResync = true;
|
||||
TerminalGraphicsUpdate resync;
|
||||
resync.sequence = update.sequence;
|
||||
resync.type = TerminalGraphicsUpdateType::Resync;
|
||||
mUpdates.emplace_back( std::move( resync ) );
|
||||
return update.sequence;
|
||||
}
|
||||
|
||||
mQueuedBytes += payloadBytes;
|
||||
mUpdates.emplace_back( std::move( update ) );
|
||||
return mNextSequence;
|
||||
}
|
||||
|
||||
std::vector<TerminalGraphicsUpdate> TerminalGraphicsUpdateQueue::drain() {
|
||||
std::vector<TerminalGraphicsUpdate> updates;
|
||||
std::lock_guard<std::mutex> lock( mMutex );
|
||||
updates.reserve( mUpdates.size() );
|
||||
while ( !mUpdates.empty() ) {
|
||||
updates.emplace_back( std::move( mUpdates.front() ) );
|
||||
mUpdates.pop_front();
|
||||
}
|
||||
mQueuedBytes = 0;
|
||||
return updates;
|
||||
}
|
||||
|
||||
size_t TerminalGraphicsUpdateQueue::queuedBytes() const {
|
||||
std::lock_guard<std::mutex> lock( mMutex );
|
||||
return mQueuedBytes;
|
||||
}
|
||||
|
||||
bool TerminalGraphicsUpdateQueue::needsResync() const {
|
||||
std::lock_guard<std::mutex> lock( mMutex );
|
||||
return mNeedsResync;
|
||||
}
|
||||
|
||||
void TerminalGraphicsUpdateQueue::resetResync() {
|
||||
std::lock_guard<std::mutex> lock( mMutex );
|
||||
mNeedsResync = false;
|
||||
}
|
||||
|
||||
}} // namespace eterm::Terminal
|
||||
@@ -20,7 +20,10 @@ struct TerminalSession::SelectionResponse {
|
||||
class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
|
||||
public:
|
||||
WorkerDisplay( TerminalSession& session, TerminalColorPalette palette ) :
|
||||
mSession( session ), mInitialPalette( std::move( palette ) ), mPalette( mInitialPalette ) {
|
||||
mSession( session ),
|
||||
mInitialPalette( std::move( palette ) ),
|
||||
mPalette( mInitialPalette ),
|
||||
mGraphics( std::make_shared<TerminalGraphicsPresentation>() ) {
|
||||
mMode |= MODE_FOCUSED;
|
||||
}
|
||||
|
||||
@@ -58,8 +61,20 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
|
||||
mCursorVisible = true;
|
||||
}
|
||||
|
||||
void drawGraphics( std::shared_ptr<TerminalGraphicsPresentation> presentation,
|
||||
std::vector<TerminalGraphicsUpdate> updates ) {
|
||||
if ( !presentation )
|
||||
presentation = std::make_shared<TerminalGraphicsPresentation>();
|
||||
presentation->requiredUpdateSequence = mGraphics ? mGraphics->requiredUpdateSequence : 0;
|
||||
for ( auto& update : updates )
|
||||
presentation->requiredUpdateSequence =
|
||||
mSession.enqueueGraphicsUpdate( std::move( update ) );
|
||||
mGraphics = std::move( presentation );
|
||||
}
|
||||
|
||||
void drawEnd() {
|
||||
auto snapshot = std::make_shared<TerminalSnapshot>();
|
||||
snapshot->graphics = mGraphics;
|
||||
snapshot->cells = mCells;
|
||||
snapshot->dirtyRows = mDirtyRows;
|
||||
snapshot->title = mTitle;
|
||||
@@ -232,6 +247,7 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
|
||||
std::vector<TerminalGlyph> mCells;
|
||||
std::vector<Uint8> mDirtyRows;
|
||||
std::string mTitle;
|
||||
std::shared_ptr<const TerminalGraphicsPresentation> mGraphics;
|
||||
Uint64 mGeneration{ 0 };
|
||||
Uint64 mLastAppliedScrollCommand{ 0 };
|
||||
Vector2i mCursor;
|
||||
@@ -302,7 +318,11 @@ void TerminalSession::writeRaw( std::string data ) {
|
||||
}
|
||||
|
||||
void TerminalSession::resize( int columns, int rows ) {
|
||||
enqueue( ResizeCommand{ columns, rows } );
|
||||
resize( columns, rows, 0, 0 );
|
||||
}
|
||||
|
||||
void TerminalSession::resize( int columns, int rows, int pixelWidth, int pixelHeight ) {
|
||||
enqueue( ResizeCommand{ columns, rows, pixelWidth, pixelHeight } );
|
||||
}
|
||||
|
||||
void TerminalSession::scrollUp( int amount ) {
|
||||
@@ -334,9 +354,9 @@ void TerminalSession::selectionClear() {
|
||||
enqueue( SelectionClearCommand{} );
|
||||
}
|
||||
|
||||
void TerminalSession::mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags,
|
||||
Uint32 modifiers ) {
|
||||
enqueue( MouseCommand{ type, position, flags, modifiers } );
|
||||
void TerminalSession::mouseReport( TerminalMouseEventType type, Vector2i cellPosition,
|
||||
Vector2i pixelPosition, Uint32 flags, Uint32 modifiers ) {
|
||||
enqueue( MouseCommand{ type, cellPosition, pixelPosition, flags, modifiers } );
|
||||
}
|
||||
|
||||
void TerminalSession::setFocus( bool focus ) {
|
||||
@@ -383,6 +403,10 @@ void TerminalSession::restart( PtyPtr&& pty, ProcPtr&& process ) {
|
||||
enqueue( RestartCommand{ std::move( pty ), std::move( process ) } );
|
||||
}
|
||||
|
||||
void TerminalSession::requestGraphicsResync() {
|
||||
enqueue( GraphicsResyncCommand{} );
|
||||
}
|
||||
|
||||
std::shared_ptr<const TerminalSnapshot> TerminalSession::snapshot() const {
|
||||
std::lock_guard<std::mutex> lock( mPublishedSnapshotMutex );
|
||||
return mPublishedSnapshot;
|
||||
@@ -410,6 +434,14 @@ std::vector<TerminalSession::Event> TerminalSession::drainEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
std::vector<TerminalGraphicsUpdate> TerminalSession::drainGraphicsUpdates() {
|
||||
return mGraphicsUpdates.drain();
|
||||
}
|
||||
|
||||
Uint64 TerminalSession::enqueueGraphicsUpdate( TerminalGraphicsUpdate update ) {
|
||||
return mGraphicsUpdates.enqueue( std::move( update ) );
|
||||
}
|
||||
|
||||
void TerminalSession::enqueueEvent( Event event, bool coalescable ) {
|
||||
std::lock_guard<std::mutex> lock( mEventMutex );
|
||||
if ( coalescable ) {
|
||||
@@ -480,7 +512,7 @@ void TerminalSession::processCommand( Command&& command ) {
|
||||
} else if constexpr ( std::is_same_v<T, WriteRawCommand> ) {
|
||||
mEmulator->write( value.data.data(), value.data.size() );
|
||||
} else if constexpr ( std::is_same_v<T, ResizeCommand> ) {
|
||||
mEmulator->resize( value.columns, value.rows );
|
||||
mEmulator->resize( value.columns, value.rows, value.pixelWidth, value.pixelHeight );
|
||||
} else if constexpr ( std::is_same_v<T, ScrollCommand> ) {
|
||||
TerminalArg argument( value.amount );
|
||||
if ( value.direction < 0 )
|
||||
@@ -502,7 +534,8 @@ void TerminalSession::processCommand( Command&& command ) {
|
||||
mEmulator->selclear();
|
||||
mEmulator->redraw();
|
||||
} else if constexpr ( std::is_same_v<T, MouseCommand> ) {
|
||||
mEmulator->mousereport( value.type, value.position, value.flags, value.modifiers );
|
||||
mEmulator->mousereport( value.type, value.cellPosition, value.pixelPosition,
|
||||
value.flags, value.modifiers );
|
||||
} else if constexpr ( std::is_same_v<T, FocusCommand> ) {
|
||||
if ( mWorkerDisplay->getMode( MODE_FOCUS ) )
|
||||
mEmulator->ttywrite( value.value ? "\033[I" : "\033[O", 3, false );
|
||||
@@ -556,6 +589,9 @@ void TerminalSession::processCommand( Command&& command ) {
|
||||
value.response->selection = mEmulator->getSelection();
|
||||
value.response->ready = true;
|
||||
value.response->condition.notify_one();
|
||||
} else if constexpr ( std::is_same_v<T, GraphicsResyncCommand> ) {
|
||||
mGraphicsUpdates.resetResync();
|
||||
mEmulator->requestGraphicsResync();
|
||||
}
|
||||
},
|
||||
std::move( command ) );
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
#include "utest.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <eepp/system/base64.hpp>
|
||||
#include <eepp/system/compression.hpp>
|
||||
#include <eepp/system/iostreammemory.hpp>
|
||||
#include <eterm/system/iprocess.hpp>
|
||||
#include <eterm/terminal/ipseudoterminal.hpp>
|
||||
#include <eterm/terminal/iterminaldisplay.hpp>
|
||||
#include <eterm/terminal/terminalemulator.hpp>
|
||||
#include <eterm/terminal/terminalgraphics.hpp>
|
||||
#include <eterm/terminal/terminalsession.hpp>
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
|
||||
using namespace eterm::Terminal;
|
||||
using namespace eterm::System;
|
||||
using namespace EE::System;
|
||||
|
||||
class MockPty : public IPseudoTerminal {
|
||||
public:
|
||||
@@ -22,11 +27,15 @@ class MockPty : public IPseudoTerminal {
|
||||
std::atomic<size_t> mBytesRead{ 0 };
|
||||
int mCols = 80;
|
||||
int mRows = 24;
|
||||
int mPixelWidth = 0;
|
||||
int mPixelHeight = 0;
|
||||
int getNumColumns() const override { return mCols; }
|
||||
int getNumRows() const override { return mRows; }
|
||||
bool resize( int columns, int rows ) override {
|
||||
bool resize( int columns, int rows, int pixelWidth, int pixelHeight ) override {
|
||||
mCols = columns;
|
||||
mRows = rows;
|
||||
mPixelWidth = pixelWidth;
|
||||
mPixelHeight = pixelHeight;
|
||||
return true;
|
||||
}
|
||||
bool isTTY() const override { return true; }
|
||||
@@ -84,6 +93,8 @@ UTEST( eterm_session, command_wakeup_and_snapshot_immutability ) {
|
||||
snapshot.cells[1].u == 'B' && snapshot.cells[2].u == 'C';
|
||||
} );
|
||||
ASSERT_TRUE( first != nullptr );
|
||||
ASSERT_TRUE( first->graphics != nullptr );
|
||||
EXPECT_EQ( static_cast<Uint64>( 0 ), first->graphics->requiredUpdateSequence );
|
||||
const Uint64 firstGeneration = first->generation;
|
||||
|
||||
session->writeRaw( "\rXYZ" );
|
||||
@@ -102,6 +113,68 @@ UTEST( eterm_session, skipped_snapshot_generation_requires_full_redraw ) {
|
||||
EXPECT_FALSE( snapshot.dirtyRowsFollow( 40 ) );
|
||||
}
|
||||
|
||||
UTEST( eterm_session, graphics_update_queue_preserves_order_and_payloads ) {
|
||||
TerminalGraphicsUpdateQueue queue;
|
||||
auto pixels = std::make_shared<const std::vector<Uint8>>( 16, 0x7F );
|
||||
TerminalGraphicsUpdate create;
|
||||
create.type = TerminalGraphicsUpdateType::CreateImage;
|
||||
create.imageId = 7;
|
||||
create.rgba = pixels;
|
||||
TerminalGraphicsUpdate patch;
|
||||
patch.type = TerminalGraphicsUpdateType::UpdateRegion;
|
||||
patch.imageId = 7;
|
||||
patch.rgba = pixels;
|
||||
|
||||
EXPECT_EQ( static_cast<Uint64>( 1 ), queue.enqueue( std::move( create ) ) );
|
||||
EXPECT_EQ( static_cast<Uint64>( 2 ), queue.enqueue( std::move( patch ) ) );
|
||||
EXPECT_EQ( static_cast<size_t>( 32 ), queue.queuedBytes() );
|
||||
auto updates = queue.drain();
|
||||
ASSERT_EQ( static_cast<size_t>( 2 ), updates.size() );
|
||||
EXPECT_EQ( static_cast<Uint64>( 1 ), updates[0].sequence );
|
||||
EXPECT_EQ( static_cast<Uint64>( 2 ), updates[1].sequence );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::CreateImage, updates[0].type );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::UpdateRegion, updates[1].type );
|
||||
EXPECT_TRUE( pixels == updates[0].rgba );
|
||||
}
|
||||
|
||||
UTEST( eterm_session, graphics_update_queue_overflow_requires_resync ) {
|
||||
TerminalGraphicsUpdateQueue queue( 2, 8 );
|
||||
TerminalGraphicsUpdate update;
|
||||
update.type = TerminalGraphicsUpdateType::UpdateRegion;
|
||||
update.rgba = std::make_shared<const std::vector<Uint8>>( 8, 0xFF );
|
||||
queue.enqueue( update );
|
||||
queue.enqueue( std::move( update ) );
|
||||
|
||||
EXPECT_TRUE( queue.needsResync() );
|
||||
auto updates = queue.drain();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::Resync, updates[0].type );
|
||||
EXPECT_EQ( static_cast<Uint64>( 2 ), updates[0].sequence );
|
||||
EXPECT_EQ( static_cast<size_t>( 0 ), queue.queuedBytes() );
|
||||
}
|
||||
|
||||
UTEST( eterm_session, graphics_update_queue_coalesces_superseded_video_frames ) {
|
||||
TerminalGraphicsUpdateQueue queue( 4, 8 );
|
||||
TerminalGraphicsUpdate create;
|
||||
create.type = TerminalGraphicsUpdateType::CreateImage;
|
||||
create.imageId = 7;
|
||||
create.rgba = std::make_shared<const std::vector<Uint8>>( 8, 1 );
|
||||
EXPECT_EQ( static_cast<Uint64>( 1 ), queue.enqueue( std::move( create ) ) );
|
||||
for ( Uint8 frame = 2; frame < 20; ++frame ) {
|
||||
TerminalGraphicsUpdate replacement;
|
||||
replacement.type = TerminalGraphicsUpdateType::ReplaceImage;
|
||||
replacement.imageId = 7;
|
||||
replacement.rgba = std::make_shared<const std::vector<Uint8>>( 8, frame );
|
||||
EXPECT_EQ( static_cast<Uint64>( 1 ), queue.enqueue( std::move( replacement ) ) );
|
||||
}
|
||||
EXPECT_FALSE( queue.needsResync() );
|
||||
EXPECT_EQ( static_cast<size_t>( 8 ), queue.queuedBytes() );
|
||||
auto updates = queue.drain();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::CreateImage, updates[0].type );
|
||||
EXPECT_EQ( static_cast<Uint8>( 19 ), updates[0].rgba->front() );
|
||||
}
|
||||
|
||||
UTEST( eterm_session, ordered_selection_request ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "ordered selection";
|
||||
@@ -347,6 +420,7 @@ class MockDisplay : public ITerminalDisplay {
|
||||
std::vector<Uint32> mResetColorIndices;
|
||||
int mResetColorsCount{ 0 };
|
||||
Uint32 mBackground{ 0x101010FF };
|
||||
std::shared_ptr<TerminalGraphicsPresentation> mGraphics;
|
||||
bool drawBegin( Uint32, Uint32 ) override { return true; }
|
||||
void drawLine( Line line, int, int y, int ) override {
|
||||
++mDrawLines;
|
||||
@@ -360,6 +434,10 @@ class MockDisplay : public ITerminalDisplay {
|
||||
void drawCursor( int, int, TerminalGlyph, int, int, TerminalGlyph ) override {}
|
||||
void drawEnd() override { ++mDrawEnds; }
|
||||
void resetColors() override { ++mResetColorsCount; }
|
||||
void drawGraphics( std::shared_ptr<TerminalGraphicsPresentation> presentation,
|
||||
std::vector<TerminalGraphicsUpdate> ) override {
|
||||
mGraphics = std::move( presentation );
|
||||
}
|
||||
int resetColor( const Uint32& index, const char* ) override {
|
||||
mResetColorIndices.emplace_back( index );
|
||||
return 0;
|
||||
@@ -381,6 +459,424 @@ class MockDisplay : public ITerminalDisplay {
|
||||
}
|
||||
};
|
||||
|
||||
UTEST( eterm, kitty_graphics_parser_preserves_payload_and_types_action ) {
|
||||
auto result = KittyGraphicsProtocol::parse(
|
||||
"a=T,f=32,s=2,v=1,i=7,p=9,q=1,C=1,z=-3,future=value;AAAA;BBBB" );
|
||||
ASSERT_TRUE( result.command.has_value() );
|
||||
auto* transmit = std::get_if<KittyTransmitCommand>( &*result.command );
|
||||
ASSERT_TRUE( transmit != nullptr );
|
||||
EXPECT_TRUE( transmit->display );
|
||||
EXPECT_EQ( static_cast<Uint32>( 32 ), *transmit->data.format );
|
||||
EXPECT_EQ( static_cast<Uint32>( 2 ), *transmit->data.width );
|
||||
EXPECT_EQ( static_cast<Uint32>( 1 ), *transmit->data.height );
|
||||
EXPECT_EQ( static_cast<Int32>( -3 ), *transmit->data.zIndex );
|
||||
EXPECT_STDSTREQ( "AAAA;BBBB", std::string( transmit->data.payload ) );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_parser_rejects_invalid_control_data ) {
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidArgument,
|
||||
KittyGraphicsProtocol::parse( "a=T,m=2;AAAA" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidArgument,
|
||||
KittyGraphicsProtocol::parse( "a=T,i=1,I=2;AAAA" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidArgument,
|
||||
KittyGraphicsProtocol::parse( "a=T,s=4294967296;AAAA" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidArgument,
|
||||
KittyGraphicsProtocol::parse( "a=unknown;AAAA" ).error );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_parser_fuzz_corpus_is_bounded_and_total ) {
|
||||
Uint32 state = 0xC0FFEEu;
|
||||
for ( size_t iteration = 0; iteration < 5000; ++iteration ) {
|
||||
state ^= state << 13;
|
||||
state ^= state >> 17;
|
||||
state ^= state << 5;
|
||||
const size_t length = state % 512;
|
||||
std::string input( length, '\0' );
|
||||
for ( char& character : input ) {
|
||||
state ^= state << 13;
|
||||
state ^= state >> 17;
|
||||
state ^= state << 5;
|
||||
character = static_cast<char>( state & 0x7F );
|
||||
}
|
||||
const auto result = KittyGraphicsProtocol::parse( input );
|
||||
EXPECT_TRUE( result.command.has_value() || result.error != KittyGraphicsError::None );
|
||||
}
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_direct_rgba_chunks_create_worker_image ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
auto first = protocol.handle( "a=t,f=32,s=1,v=1,i=7,m=1;AQID" );
|
||||
EXPECT_EQ( KittyGraphicsError::None, first.error );
|
||||
EXPECT_FALSE( first.changed );
|
||||
auto final = protocol.handle( "m=0;BA==" );
|
||||
EXPECT_EQ( KittyGraphicsError::None, final.error );
|
||||
EXPECT_TRUE( final.changed );
|
||||
EXPECT_STDSTREQ( "\033_Gi=7;OK\033\\", final.response );
|
||||
|
||||
auto pixels = protocol.imagePixels( 7 );
|
||||
ASSERT_TRUE( pixels != nullptr );
|
||||
ASSERT_EQ( static_cast<size_t>( 4 ), pixels->size() );
|
||||
EXPECT_EQ( static_cast<Uint8>( 1 ), ( *pixels )[0] );
|
||||
EXPECT_EQ( static_cast<Uint8>( 4 ), ( *pixels )[3] );
|
||||
auto updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::CreateImage, updates[0].type );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_chunk_continuations_reject_metadata_and_wrong_action ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
EXPECT_EQ( KittyGraphicsError::None, protocol.handle( "a=t,f=32,s=1,v=1,i=8,m=1;AQID" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidArgument, protocol.handle( "m=0,s=1;BA==" ).error );
|
||||
EXPECT_EQ( static_cast<size_t>( 0 ), protocol.imageCount() );
|
||||
EXPECT_EQ( KittyGraphicsError::None, protocol.handle( "a=t,f=32,s=1,v=1,i=8,m=1;AQID" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidArgument, protocol.handle( "a=p,i=8" ).error );
|
||||
EXPECT_EQ( static_cast<size_t>( 0 ), protocol.imageCount() );
|
||||
|
||||
protocol.handle( "a=t,f=32,s=1,v=1,i=8;AQIDBA==" );
|
||||
EXPECT_EQ( KittyGraphicsError::None, protocol.handle( "a=f,i=8,f=32,s=1,v=1,m=1;AQID" ).error );
|
||||
EXPECT_EQ( KittyGraphicsError::InvalidArgument, protocol.handle( "m=0;BA==" ).error );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_image_number_allocates_id_and_echoes_number ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
auto created = protocol.handle( "a=t,f=32,s=1,v=1,I=77;AQIDBA==" );
|
||||
EXPECT_TRUE( created.changed );
|
||||
EXPECT_TRUE( created.response.find( ",I=77;OK" ) != std::string::npos );
|
||||
auto placed = protocol.handle( "a=p,I=77,p=3" );
|
||||
EXPECT_EQ( KittyGraphicsError::None, placed.error );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), protocol.takePresentation()->placements.size() );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_rgb_and_zlib_normalize_to_rgba ) {
|
||||
const std::vector<Uint8> rgb{ 10, 20, 30, 40, 50, 60 };
|
||||
std::vector<Uint8> compressed( Compression::getMaxCompressedBufferSize( rgb.size() ) );
|
||||
IOStreamMemory source( reinterpret_cast<const char*>( rgb.data() ), rgb.size() );
|
||||
IOStreamMemory destination( reinterpret_cast<char*>( compressed.data() ), compressed.size() );
|
||||
ASSERT_EQ( Compression::OK, Compression::compress( destination, source ) );
|
||||
compressed.resize( destination.tell() );
|
||||
std::string encoded;
|
||||
ASSERT_TRUE( Base64::encode(
|
||||
std::string_view( reinterpret_cast<const char*>( compressed.data() ), compressed.size() ),
|
||||
encoded ) );
|
||||
|
||||
KittyGraphicsProtocol protocol;
|
||||
auto result = protocol.handle( "a=t,f=24,s=2,v=1,i=9,o=z;" + encoded );
|
||||
EXPECT_TRUE( result.changed );
|
||||
auto pixels = protocol.imagePixels( 9 );
|
||||
ASSERT_TRUE( pixels != nullptr );
|
||||
const std::vector<Uint8> expected{ 10, 20, 30, 255, 40, 50, 60, 255 };
|
||||
EXPECT_TRUE( expected == *pixels );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_png_decodes_to_rgba ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
const auto result = protocol.handle( "a=t,f=100,i=10;"
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42"
|
||||
"mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" );
|
||||
EXPECT_EQ( KittyGraphicsError::None, result.error );
|
||||
EXPECT_TRUE( result.changed );
|
||||
const auto* pixels = protocol.imagePixels( 10 );
|
||||
ASSERT_TRUE( pixels != nullptr );
|
||||
EXPECT_EQ( static_cast<size_t>( 4 ), pixels->size() );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_placement_uses_final_cursor_and_geometry ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=T,f=32,s=1,v=1,i=21,p=4,c=2,r=3,C=1,x=0,y=0,w=1,h=1;AQIDBA==",
|
||||
Vector2i( 5, 6 ) );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), presentation->placements.size() );
|
||||
const auto& placement = presentation->placements.front();
|
||||
EXPECT_EQ( static_cast<KittyImageId>( 21 ), placement.imageId );
|
||||
EXPECT_EQ( static_cast<KittyPlacementId>( 4 ), placement.placementId );
|
||||
EXPECT_EQ( 5, placement.visibleAnchorCell.x );
|
||||
EXPECT_EQ( 6, placement.visibleAnchorCell.y );
|
||||
EXPECT_EQ( static_cast<Uint32>( 2 ), placement.columns );
|
||||
EXPECT_EQ( static_cast<Uint32>( 3 ), placement.rows );
|
||||
|
||||
auto put = protocol.handle( "a=p,i=21,p=5,c=4,r=2", Vector2i( 1, 2 ) );
|
||||
EXPECT_EQ( 4, put.cursorMovement.x );
|
||||
EXPECT_EQ( 2, put.cursorMovement.y );
|
||||
presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 2 ), presentation->placements.size() );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_placement_derives_missing_cell_geometry ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.setCellPixelSize( 10, 20 );
|
||||
protocol.handle( "a=t,f=32,s=2,v=2,i=22;AAAAAAAAAAAAAAAAAAAAAA==" );
|
||||
auto result = protocol.handle( "a=p,i=22,X=9,Y=19,C=1" );
|
||||
EXPECT_TRUE( result.changed );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), presentation->placements.size() );
|
||||
EXPECT_EQ( static_cast<Uint32>( 2 ), presentation->placements[0].columns );
|
||||
EXPECT_EQ( static_cast<Uint32>( 2 ), presentation->placements[0].rows );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_retransmit_removes_old_placements_and_crop_intersects ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=T,f=32,s=2,v=2,i=23,p=4;AAAAAAAAAAAAAAAAAAAAAA==" );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), protocol.takePresentation()->placements.size() );
|
||||
protocol.handle( "a=t,f=32,s=1,v=1,i=23;AQIDBA==" );
|
||||
EXPECT_TRUE( protocol.takePresentation()->placements.empty() );
|
||||
protocol.handle( "a=t,f=32,s=2,v=2,i=24;AAAAAAAAAAAAAAAAAAAAAA==" );
|
||||
auto placed = protocol.handle( "a=p,i=24,p=5,x=1,y=1,w=99,h=99" );
|
||||
EXPECT_STDSTREQ( "\033_Gi=24,p=5;OK\033\\", placed.response );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), presentation->placements.size() );
|
||||
EXPECT_EQ( 2, presentation->placements[0].sourcePixels.Right );
|
||||
EXPECT_EQ( 2, presentation->placements[0].sourcePixels.Bottom );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_resync_republishes_authoritative_images ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=t,f=32,s=1,v=1,i=27;AQIDBA==" );
|
||||
protocol.takeUpdates();
|
||||
protocol.resync();
|
||||
auto updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 2 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::ResetAll, updates[0].type );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::CreateImage, updates[1].type );
|
||||
EXPECT_EQ( static_cast<KittyImageId>( 27 ), updates[1].imageId );
|
||||
ASSERT_TRUE( updates[1].rgba != nullptr );
|
||||
EXPECT_EQ( static_cast<size_t>( 4 ), updates[1].rgba->size() );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_root_frame_patch_publishes_only_changed_rectangle ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=t,f=32,s=2,v=1,i=29;AQIDBAUGBwg=" );
|
||||
protocol.takeUpdates();
|
||||
auto result = protocol.handle( "a=f,i=29,r=1,f=32,s=1,v=1,x=1,y=0,X=1;CQoLDA==" );
|
||||
EXPECT_TRUE( result.changed );
|
||||
const auto* pixels = protocol.imagePixels( 29 );
|
||||
ASSERT_TRUE( pixels != nullptr );
|
||||
const std::vector<Uint8> expected{ 1, 2, 3, 4, 9, 10, 11, 12 };
|
||||
EXPECT_TRUE( expected == *pixels );
|
||||
auto updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::UpdateRegion, updates[0].type );
|
||||
EXPECT_EQ( 1, updates[0].region.Left );
|
||||
EXPECT_EQ( 0, updates[0].region.Top );
|
||||
EXPECT_EQ( 2, updates[0].region.Right );
|
||||
EXPECT_EQ( 1, updates[0].region.Bottom );
|
||||
ASSERT_TRUE( updates[0].rgba != nullptr );
|
||||
EXPECT_EQ( static_cast<size_t>( 4 ), updates[0].rgba->size() );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_animation_frame_create_control_and_compose ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=T,f=32,s=2,v=1,i=30;AQIDBAUGBwg=" );
|
||||
protocol.takeUpdates();
|
||||
protocol.takePresentation();
|
||||
auto frame = protocol.handle( "a=f,i=30,c=1,f=32,s=1,v=1,x=1,y=0,X=1,z=25;CQoLDA==" );
|
||||
EXPECT_EQ( KittyGraphicsError::None, frame.error );
|
||||
auto updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::CreateFrame, updates[0].type );
|
||||
EXPECT_EQ( static_cast<Uint32>( 2 ), updates[0].frameNumber );
|
||||
ASSERT_TRUE( updates[0].rgba != nullptr );
|
||||
const std::vector<Uint8> expectedFrame{ 1, 2, 3, 4, 9, 10, 11, 12 };
|
||||
EXPECT_TRUE( expectedFrame == *updates[0].rgba );
|
||||
|
||||
auto control = protocol.handle( "a=a,i=30,c=2" );
|
||||
EXPECT_TRUE( control.changed );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), presentation->placements.size() );
|
||||
EXPECT_EQ( static_cast<Uint32>( 2 ), presentation->placements[0].frameNumber );
|
||||
|
||||
auto compose = protocol.handle( "a=c,i=30,r=1,c=2,X=1,Y=0,x=0,y=0,w=1,h=1,C=1" );
|
||||
EXPECT_EQ( KittyGraphicsError::None, compose.error );
|
||||
updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::UpdateFrameRegion, updates[0].type );
|
||||
ASSERT_TRUE( updates[0].rgba != nullptr );
|
||||
const std::vector<Uint8> expectedPatch{ 5, 6, 7, 8 };
|
||||
EXPECT_TRUE( expectedPatch == *updates[0].rgba );
|
||||
EXPECT_TRUE( protocol.handle( "a=a,i=30,c=1,r=1,z=-1,s=3" ).changed );
|
||||
EXPECT_TRUE( protocol.updateAnimations() );
|
||||
presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), presentation->placements.size() );
|
||||
EXPECT_EQ( static_cast<Uint32>( 2 ), presentation->placements[0].frameNumber );
|
||||
EXPECT_TRUE( protocol.handle( "a=d,d=f,i=30" ).changed );
|
||||
updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::DeleteFrame, updates[0].type );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_delete_placements_and_uppercase_frees_data ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=T,f=32,s=1,v=1,i=31,p=1,c=2,r=2;AQIDBA==", Vector2i( 3, 4 ) );
|
||||
protocol.handle( "a=p,i=31,p=2,c=1,r=1", Vector2i( 8, 9 ) );
|
||||
protocol.takeUpdates();
|
||||
|
||||
auto result = protocol.handle( "a=d,d=c", Vector2i( 4, 5 ) );
|
||||
EXPECT_TRUE( result.changed );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), presentation->placements.size() );
|
||||
EXPECT_EQ( static_cast<KittyPlacementId>( 2 ), presentation->placements[0].placementId );
|
||||
EXPECT_TRUE( protocol.imagePixels( 31 ) != nullptr );
|
||||
|
||||
result = protocol.handle( "a=d,d=I,i=31,p=2" );
|
||||
EXPECT_TRUE( result.changed );
|
||||
EXPECT_TRUE( protocol.imagePixels( 31 ) == nullptr );
|
||||
EXPECT_TRUE( protocol.takePresentation()->placements.empty() );
|
||||
auto updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::DeleteImage, updates[0].type );
|
||||
EXPECT_EQ( static_cast<KittyImageId>( 31 ), updates[0].imageId );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_storage_quota_evicts_oldest_unplaced_image ) {
|
||||
KittyGraphicsProtocol protocol( 8, 2, 2 );
|
||||
EXPECT_TRUE( protocol.handle( "a=t,f=32,s=1,v=1,i=41;AQIDBA==" ).changed );
|
||||
EXPECT_TRUE( protocol.handle( "a=t,f=32,s=1,v=1,i=42,N=1;BQYHCA==" ).changed );
|
||||
protocol.takeUpdates();
|
||||
EXPECT_TRUE( protocol.handle( "a=t,f=32,s=1,v=1,i=43;CQoLDA==" ).changed );
|
||||
EXPECT_TRUE( protocol.imagePixels( 41 ) != nullptr );
|
||||
EXPECT_TRUE( protocol.imagePixels( 42 ) == nullptr );
|
||||
EXPECT_TRUE( protocol.imagePixels( 43 ) != nullptr );
|
||||
auto updates = protocol.takeUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 2 ), updates.size() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::DeleteImage, updates[0].type );
|
||||
EXPECT_EQ( static_cast<KittyImageId>( 42 ), updates[0].imageId );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::CreateImage, updates[1].type );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_screen_lifecycle_restores_primary_and_resets_gpu ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=T,f=32,s=1,v=1,i=51;AQIDBA==" );
|
||||
protocol.takeUpdates();
|
||||
protocol.takePresentation();
|
||||
protocol.setAlternateScreen( true );
|
||||
EXPECT_TRUE( protocol.takePresentation()->placements.empty() );
|
||||
protocol.handle( "a=p,i=51,p=2" );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), protocol.takePresentation()->placements.size() );
|
||||
protocol.setAlternateScreen( false );
|
||||
auto primary = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), primary->placements.size() );
|
||||
EXPECT_EQ( static_cast<KittyPlacementId>( 0 ), primary->placements[0].placementId );
|
||||
protocol.clearScreen();
|
||||
EXPECT_TRUE( protocol.takePresentation()->placements.empty() );
|
||||
EXPECT_TRUE( protocol.imagePixels( 51 ) != nullptr );
|
||||
protocol.reset();
|
||||
EXPECT_TRUE( protocol.imagePixels( 51 ) == nullptr );
|
||||
auto updates = protocol.takeUpdates();
|
||||
ASSERT_TRUE( !updates.empty() );
|
||||
EXPECT_EQ( TerminalGraphicsUpdateType::ResetAll, updates.back().type );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_scrolling_tracks_history_and_scrollback ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.setViewport( 0, 0, 4 );
|
||||
protocol.handle( "a=T,f=32,s=1,v=1,i=61,r=1;AQIDBA==", Vector2i( 0, 0 ) );
|
||||
protocol.takePresentation();
|
||||
protocol.scrollScreen( 0, 3, -1, true );
|
||||
protocol.setViewport( 0, 1, 4 );
|
||||
EXPECT_TRUE( protocol.takePresentation()->placements.empty() );
|
||||
protocol.setViewport( 1, 1, 4 );
|
||||
auto history = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), history->placements.size() );
|
||||
EXPECT_EQ( 0, history->placements[0].visibleAnchorCell.y );
|
||||
protocol.setViewport( 0, 0, 4 );
|
||||
EXPECT_TRUE( protocol.takePresentation()->placements.empty() );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_margin_scroll_discards_only_scrolled_out_placements ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.setViewport( 0, 0, 6 );
|
||||
protocol.handle( "a=T,f=32,s=1,v=1,i=62,p=1;AQIDBA==", Vector2i( 0, 0 ) );
|
||||
protocol.handle( "a=p,i=62,p=2", Vector2i( 0, 2 ) );
|
||||
protocol.handle( "a=p,i=62,p=3", Vector2i( 0, 5 ) );
|
||||
protocol.scrollScreen( 1, 4, -2, false );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 2 ), presentation->placements.size() );
|
||||
EXPECT_EQ( static_cast<KittyPlacementId>( 1 ), presentation->placements[0].placementId );
|
||||
EXPECT_EQ( static_cast<KittyPlacementId>( 3 ), presentation->placements[1].placementId );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_margin_scroll_clips_partially_visible_placements ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.setViewport( 0, 0, 6 );
|
||||
protocol.handle( "a=T,f=32,s=1,v=4,i=63,p=1,c=1,r=4;AAAAAAAAAAAAAAAAAAAAAA==",
|
||||
Vector2i( 0, 1 ) );
|
||||
protocol.scrollScreen( 1, 4, -2, false );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), presentation->placements.size() );
|
||||
EXPECT_EQ( 1, presentation->placements[0].visibleAnchorCell.y );
|
||||
EXPECT_EQ( static_cast<Uint32>( 2 ), presentation->placements[0].rows );
|
||||
EXPECT_EQ( 2, presentation->placements[0].sourcePixels.Top );
|
||||
EXPECT_EQ( 4, presentation->placements[0].sourcePixels.Bottom );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_virtual_and_relative_placements_follow_protocol_rules ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=t,f=32,s=1,v=1,i=71;AQIDBA==" );
|
||||
auto virtualPlacement = protocol.handle( "a=p,i=71,p=10,U=1,c=2,r=2", Vector2i( 2, 3 ) );
|
||||
EXPECT_TRUE( virtualPlacement.changed );
|
||||
EXPECT_EQ( 0, virtualPlacement.cursorMovement.x );
|
||||
EXPECT_TRUE( protocol.takePresentation()->placements.empty() );
|
||||
auto relative = protocol.handle( "a=p,i=71,p=11,P=71,Q=10,H=4,V=-1,c=1,r=1" );
|
||||
EXPECT_TRUE( relative.changed );
|
||||
EXPECT_EQ( 0, relative.cursorMovement.x );
|
||||
protocol.setPlaceholderCells( { { 71, 10, Vector2i( 2, 3 ), 0, 0 } } );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 2 ), presentation->placements.size() );
|
||||
auto child = std::find_if(
|
||||
presentation->placements.begin(), presentation->placements.end(),
|
||||
[]( const TerminalVisiblePlacement& placement ) { return placement.placementId == 11; } );
|
||||
ASSERT_TRUE( child != presentation->placements.end() );
|
||||
EXPECT_EQ( 6, child->visibleAnchorCell.x );
|
||||
EXPECT_EQ( 2, child->visibleAnchorCell.y );
|
||||
protocol.handle( "a=d,d=a" );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), protocol.takePresentation()->placements.size() );
|
||||
auto replacement = protocol.handle( "a=p,i=71,p=11,P=71,Q=10,H=1,V=1" );
|
||||
EXPECT_TRUE( replacement.changed );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_relative_placements_report_missing_parents_and_cycles ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
protocol.handle( "a=t,f=32,s=1,v=1,i=72;AQIDBA==" );
|
||||
EXPECT_EQ( KittyGraphicsError::NoParent, protocol.handle( "a=p,i=72,p=2,P=72,Q=99" ).error );
|
||||
EXPECT_TRUE( protocol.handle( "a=p,i=72,p=1", Vector2i( 1, 1 ) ).changed );
|
||||
EXPECT_TRUE( protocol.handle( "a=p,i=72,p=2,P=72,Q=1,H=1,V=0" ).changed );
|
||||
EXPECT_EQ( KittyGraphicsError::Cycle,
|
||||
protocol.handle( "a=p,i=72,p=1,P=72,Q=2,H=1,V=0" ).error );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_independent_client_namespaces_coexist ) {
|
||||
KittyGraphicsProtocol protocol;
|
||||
auto first = protocol.handle( "a=T,f=32,s=1,v=1,I=101,p=1;AQIDBA==" );
|
||||
auto second = protocol.handle( "a=T,f=32,s=1,v=1,I=202,p=1;BQYHCA==", Vector2i( 2, 0 ) );
|
||||
EXPECT_EQ( KittyGraphicsError::None, first.error );
|
||||
EXPECT_EQ( KittyGraphicsError::None, second.error );
|
||||
ASSERT_EQ( static_cast<size_t>( 2 ), protocol.imageCount() );
|
||||
auto presentation = protocol.takePresentation();
|
||||
ASSERT_EQ( static_cast<size_t>( 2 ), presentation->placements.size() );
|
||||
EXPECT_NE( presentation->placements[0].imageId, presentation->placements[1].imageId );
|
||||
EXPECT_TRUE( protocol.handle( "a=d,d=n,I=101" ).changed );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), protocol.takePresentation()->placements.size() );
|
||||
EXPECT_EQ( static_cast<size_t>( 2 ), protocol.imageCount() );
|
||||
}
|
||||
|
||||
UTEST( eterm_session, kitty_graphics_update_and_metadata_cross_worker_boundary ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033_Ga=t,f=32,s=1,v=1,i=13;AQIDBA==\033\\";
|
||||
pty->mLoopWrites = false;
|
||||
MockPty* ptyPtr = pty.get();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
|
||||
auto snapshot = waitForSnapshot( session, []( const TerminalSnapshot& value ) {
|
||||
return value.graphics && value.graphics->requiredUpdateSequence > 0;
|
||||
} );
|
||||
ASSERT_TRUE( snapshot != nullptr );
|
||||
EXPECT_EQ( static_cast<Uint64>( 1 ), snapshot->graphics->requiredUpdateSequence );
|
||||
auto updates = session->drainGraphicsUpdates();
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
|
||||
EXPECT_EQ( static_cast<Uint64>( 1 ), updates[0].sequence );
|
||||
EXPECT_EQ( static_cast<KittyImageId>( 13 ), updates[0].imageId );
|
||||
EXPECT_STDSTREQ( "\033_Gi=13;OK\033\\", ptyPtr->mWrites );
|
||||
}
|
||||
|
||||
UTEST( eterm, modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033[?u\033[>7u\033[<1u\033[<u\033[=3u";
|
||||
@@ -395,6 +891,113 @@ UTEST( eterm, modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol ) {
|
||||
EXPECT_TRUE( ptyPtr->mWrites.empty() );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_unicode_placeholder_uses_color_and_diacritics ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033_Ga=t,f=32,s=2,v=2,i=72,q=2;AAAAAAAAAAAAAAAAAAAAAA==\033\\"
|
||||
"\033_Ga=p,i=72,p=3,U=1,c=2,r=2,q=2\033\\"
|
||||
"\033[38;5;72m\033[58;5;3m\xF4\x8E\xBB\xAE\xCC\x85\xCC\x85";
|
||||
pty->mLoopWrites = false;
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
term->update();
|
||||
ASSERT_TRUE( display->mGraphics != nullptr );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), display->mGraphics->placements.size() );
|
||||
const auto& placement = display->mGraphics->placements[0];
|
||||
EXPECT_EQ( static_cast<KittyImageId>( 72 ), placement.imageId );
|
||||
EXPECT_EQ( static_cast<KittyPlacementId>( 3 ), placement.placementId );
|
||||
EXPECT_EQ( 0, placement.visibleAnchorCell.x );
|
||||
EXPECT_EQ( 0, placement.visibleAnchorCell.y );
|
||||
EXPECT_EQ( 0, placement.sourcePixels.Left );
|
||||
EXPECT_EQ( 0, placement.sourcePixels.Top );
|
||||
EXPECT_EQ( 1, placement.sourcePixels.Right );
|
||||
EXPECT_EQ( 1, placement.sourcePixels.Bottom );
|
||||
term->resize( 100, 30, 1000, 600 );
|
||||
ASSERT_TRUE( display->mGraphics != nullptr );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), display->mGraphics->placements.size() );
|
||||
EXPECT_EQ( static_cast<KittyImageId>( 72 ), display->mGraphics->placements[0].imageId );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_apc_is_fragmentation_safe_and_not_terminal_text ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033_Ga=q,i=31,f=32,s=1,v=1;AAAAAA==\033\\OK";
|
||||
pty->mLoopWrites = false;
|
||||
pty->mMaxRead = 1;
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
|
||||
term->update();
|
||||
while ( !term->update() ) {
|
||||
}
|
||||
|
||||
EXPECT_EQ( static_cast<Rune>( 'O' ), display->mFirstGlyph.u );
|
||||
EXPECT_EQ( static_cast<Rune>( 'K' ), display->mSecondGlyph.u );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_accepts_unchunked_direct_image_larger_than_eight_kibibytes ) {
|
||||
std::vector<Uint8> rgb( 64 * 64 * 3 );
|
||||
for ( size_t offset = 0; offset < rgb.size(); offset += 3 )
|
||||
rgb[offset] = 255;
|
||||
std::string encoded;
|
||||
ASSERT_TRUE( Base64::encode(
|
||||
std::string_view( reinterpret_cast<const char*>( rgb.data() ), rgb.size() ), encoded ) );
|
||||
const std::string command = "a=T,f=24,s=64,v=64,c=10,r=5,C=1;" + encoded;
|
||||
ASSERT_TRUE( KittyGraphicsProtocol::parse( command ).command.has_value() );
|
||||
KittyGraphicsProtocol directProtocol;
|
||||
EXPECT_EQ( KittyGraphicsError::None, directProtocol.handle( command ).error );
|
||||
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033_G" + command + "\033\\";
|
||||
pty->mLoopWrites = false;
|
||||
MockPty* ptyPtr = pty.get();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
|
||||
term->update();
|
||||
while ( !term->update() ) {
|
||||
}
|
||||
|
||||
EXPECT_TRUE( ptyPtr->mWrites.empty() );
|
||||
ASSERT_TRUE( display->mGraphics != nullptr );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), display->mGraphics->placements.size() );
|
||||
EXPECT_EQ( static_cast<Uint32>( 10 ), display->mGraphics->placements[0].columns );
|
||||
EXPECT_EQ( static_cast<Uint32>( 5 ), display->mGraphics->placements[0].rows );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_graphics_anonymous_video_frames_replace_at_same_anchor ) {
|
||||
KittyGraphicsProtocol protocol( 8, 8, 8 );
|
||||
for ( int frame = 0; frame < 20; ++frame ) {
|
||||
const char* pixels = frame % 2 == 0 ? "AQIDBA==" : "BQYHCA==";
|
||||
auto result =
|
||||
protocol.handle( std::string( "a=T,f=32,s=1,v=1,q=2;" ) + pixels, Vector2i( 3, 4 ) );
|
||||
EXPECT_EQ( KittyGraphicsError::None, result.error );
|
||||
EXPECT_TRUE( result.changed );
|
||||
EXPECT_EQ( static_cast<size_t>( 1 ), protocol.imageCount() );
|
||||
ASSERT_EQ( static_cast<size_t>( 1 ), protocol.takePresentation()->placements.size() );
|
||||
}
|
||||
const auto* pixels = protocol.imagePixels( 1 );
|
||||
ASSERT_TRUE( pixels != nullptr );
|
||||
EXPECT_EQ( static_cast<Uint8>( 5 ), ( *pixels )[0] );
|
||||
}
|
||||
|
||||
UTEST( eterm, oversized_kitty_graphics_apc_is_discarded_until_terminator ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033_Ga=t;" + std::string( MAX_KITTY_GRAPHICS_APC_SIZE, 'A' ) + "\033\\OK";
|
||||
pty->mLoopWrites = false;
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
|
||||
term->update();
|
||||
while ( !term->update() ) {
|
||||
}
|
||||
|
||||
EXPECT_EQ( static_cast<Rune>( 'O' ), display->mFirstGlyph.u );
|
||||
EXPECT_EQ( static_cast<Rune>( 'K' ), display->mSecondGlyph.u );
|
||||
}
|
||||
|
||||
UTEST( eterm, cursor_style_and_xterm_version_queries ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033[0 q\033[6 q\033[>0q";
|
||||
@@ -412,6 +1015,37 @@ UTEST( eterm, cursor_style_and_xterm_version_queries ) {
|
||||
EXPECT_STDSTREQ( "\033\\", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 2 ) );
|
||||
}
|
||||
|
||||
UTEST( eterm, pixel_geometry_queries_use_latest_worker_resize ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033[14t\033[16t";
|
||||
pty->mLoopWrites = false;
|
||||
MockPty* ptyPtr = pty.get();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
term->resize( 40, 20, 400, 300 );
|
||||
|
||||
term->update();
|
||||
|
||||
EXPECT_TRUE( ptyPtr->mWrites.find( "\033[4;300;400t" ) != std::string::npos );
|
||||
EXPECT_TRUE( ptyPtr->mWrites.find( "\033[6;15;10t" ) != std::string::npos );
|
||||
}
|
||||
|
||||
UTEST( eterm, sgr_pixel_mouse_mode_uses_grid_relative_pixels ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033[?1000h\033[?1016h";
|
||||
pty->mLoopWrites = false;
|
||||
MockPty* ptyPtr = pty.get();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
term->update();
|
||||
term->mousereport( TerminalMouseEventType::MouseButtonDown, { 2, 3 }, { 20, 30 },
|
||||
EE_BUTTON_LMASK, 0 );
|
||||
|
||||
EXPECT_STDSTREQ( "\033[<0;21;31M", ptyPtr->mWrites );
|
||||
}
|
||||
|
||||
UTEST( eterm, cursor_style_zero_uses_blinking_configured_shape ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033[2 q\033[0 q";
|
||||
@@ -451,12 +1085,14 @@ UTEST( eterm, alternate_scroll_mode_controls_wheel_key_translation ) {
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
|
||||
term->update();
|
||||
term->mousereport( TerminalMouseEventType::MouseButtonDown, { 0, 0 }, EE_BUTTON_WUMASK, 0 );
|
||||
term->mousereport( TerminalMouseEventType::MouseButtonDown, { 0, 0 }, { 0, 0 },
|
||||
EE_BUTTON_WUMASK, 0 );
|
||||
EXPECT_TRUE( ptyPtr->mWrites.empty() );
|
||||
|
||||
ptyPtr->mBuffer += "\033[?1007h";
|
||||
term->update();
|
||||
term->mousereport( TerminalMouseEventType::MouseButtonDown, { 0, 0 }, EE_BUTTON_WUMASK, 0 );
|
||||
term->mousereport( TerminalMouseEventType::MouseButtonDown, { 0, 0 }, { 0, 0 },
|
||||
EE_BUTTON_WUMASK, 0 );
|
||||
EXPECT_STDSTREQ( "\033[A", ptyPtr->mWrites );
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user