Added Kitty Graphics Protocol support for eterm module, still a WIP but working.

This commit is contained in:
Martín Lucas Golini
2026-09-03 17:19:27 -03:00
parent 53553b7861
commit 57754feb31
22 changed files with 3543 additions and 119 deletions

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 );

View 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

View File

@@ -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

View File

@@ -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,
};

View File

@@ -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() {}

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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;

View File

@@ -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 );
}

View File

@@ -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!" );
}

View 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

View File

@@ -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 ) );

View File

@@ -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 );
}