Merge branch 'feature/kitty-graphics-protocol' into develop

This commit is contained in:
Martín Lucas Golini
2026-09-04 19:48:05 -03:00
36 changed files with 4039 additions and 142 deletions
+12 -1
View File
@@ -1,6 +1,7 @@
# Project Architecture: eepp & ecode
This repository contains two primary components: a core framework (`eepp`) and an application built on top of it (`ecode`).
This repository contains the core framework (`eepp`) and several applications and tools built on
top of it, including `ecode`, `eterm`, and `eeiv`.
## 1. eepp (Core Framework)
@@ -12,6 +13,16 @@ This repository contains two primary components: a core framework (`eepp`) and a
* **Relationship:** `ecode` is built *using* the `eepp` GUI framework. It acts as the primary real-world consumer of `eepp`.
* **Goal:** Development on `ecode` is often used to test, improve, and drive new features in the underlying `eepp` library.
## 3. eterm (Terminal Emulator)
`eterm` is the repository's terminal emulator. Its name is always written in lowercase: `eterm`,
not `eTerm`. Its implementation lives under `src/modules/eterm/`.
## 4. eeiv (Image Viewer)
`eeiv` is the repository's image viewer. Its application entry point is
`src/tools/eeiv/eeiv.cpp`.
## Documentation & Code References
When working on this project, rely on the following resources to understand existing implementations:
+44
View File
@@ -0,0 +1,44 @@
# eterm Kitty graphics protocol
eterm implements the terminal-receiver side of the Kitty graphics protocol. Protocol parsing,
decoding, image storage, placement state, animation, and terminal lifecycle integration run on the
terminal worker. The UI thread consumes immutable placement metadata and an ordered pixel-update
stream, and exclusively owns the OpenGL texture cache.
## Supported features
- direct RGB and RGBA transmission, including chunking and zlib compression;
- POSIX shared-memory transmission, including byte offsets and explicit sizes;
- PNG transmission;
- image IDs, image numbers, placement IDs, source cropping, cell geometry, and pixel offsets;
- root-image rectangle updates, animation frames, frame composition, and animation control;
- deletion selectors, quiet modes, capability queries, storage quotas, usage hints, and eviction;
- primary/alternate-screen state, reset, clear, scrolling, margins, scrollback, and resize;
- Unicode placeholder placements (U+10EEEE and Kitty's combining-diacritic encoding);
- relative placements, including parent validation, cycle detection, and descendant movement;
- terminal pixel-size queries and SGR pixel mouse coordinates.
Local file and temporary-file transfer media are intentionally not enabled. POSIX shared-memory
objects are opened read-only and immediately unlinked as required by the protocol. Direct
transmission remains the portable path and works through SSH.
## Resource and threading behavior
Decoded root images are stored as premultiplied-independent RGBA8 CPU buffers on the worker. GPU
textures exist only on the UI/GL thread. Incremental updates carry monotonically increasing sequence
numbers; a bounded queue gap triggers a complete graphics resynchronization. Image count, placement
count, APC size, decoded transfer size, and total image storage are bounded. Unreferenced images are
evicted oldest-first when storage is needed.
Graphics follow the same presentation cadence and DEC synchronized-update boundary as text. A
placement anchored in normal-screen history moves with terminal scrolling and is projected into the
current scrollback viewport. Margin scrolling clips or removes only placements wholly participating
in the scrolled region. Resize preserves screen-coordinate placements and reflows Unicode placeholder
metadata with its text cells.
## Compatibility testing
The eterm unit suite covers fragmented APC input, malformed/fuzzed commands, raw/zlib/PNG transfer,
independent image namespaces, replacement, partial updates, placement geometry, scrolling and
scrollback, screen lifecycle, animation, Unicode placeholders, relative placement graphs, ordered
worker/UI delivery, queue overflow, and resynchronization.
+20 -3
View File
@@ -11,22 +11,39 @@ namespace EE { namespace System {
class EE_API Base64 {
public:
enum class DecodeMode {
AllowWhitespace,
NoWhitespace,
NoWhitespaceStrict,
};
/** Encode binary data into base64 digits with MIME style === pads
** @return The final length of the output */
static size_t encode( size_t in_len, const unsigned char* in, size_t out_len, char* out );
/** Decode base64 digits with MIME style === pads into binary data
/** Decode base64 digits with MIME style === pads into binary data.
** Preserves the historical behavior of ignoring ASCII whitespace.
** @return The final length of the output */
static size_t decode( size_t in_len, const char* in, size_t out_len, unsigned char* out );
/** Decode base64 digits with MIME style === pads into binary data.
** DecodeMode::NoWhitespace avoids all whitespace handling and enables the fastest path.
** @return The final length of the output */
static size_t decode( size_t in_len, const char* in, size_t out_len, unsigned char* out,
DecodeMode mode );
/** Encodes a string into a base64 string
** @return True if encoding was successful */
static bool encode( std::string_view in, std::string& out );
/** Decodes a base64 string to a string
** @return True if encoding was successful */
/** Decodes a base64 string to a string, ignoring ASCII whitespace.
** @return The final length of the output, or size_t(-1) on truncation */
static size_t decode( std::string_view in, std::string& out );
/** Decodes a base64 string to a string.
** @return The final length of the output, or size_t(-1) on truncation */
static size_t decode( std::string_view in, std::string& out, DecodeMode mode );
/** @return A safe encoding output length for an input of the length indicated */
static inline size_t encodeSafeOutLen( size_t in_len ) {
return ( ( in_len + 2 ) / 3 ) * 4 + 1;
+2
View File
@@ -7,6 +7,8 @@ namespace EE { namespace System {
class EE_API CPU {
public:
static bool hasSSSE3();
static bool hasAVX2();
static bool hasNEON();
+8
View File
@@ -530,10 +530,18 @@ void Texture::update( const Uint8* pixels, Uint32 width, Uint32 height, Uint32 x
{
ScopedTexture saver( mTexture );
const bool tightlyPackedThreeChannelRows =
( pf == Image::PixelFormat::PIXEL_FORMAT_RGB ||
pf == Image::PixelFormat::PIXEL_FORMAT_BGR ) &&
( static_cast<size_t>( width ) * 3 ) % 4 != 0;
if ( tightlyPackedThreeChannelRows )
glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
glTexSubImage2D( GL_TEXTURE_2D, 0, x, y, width, height,
(unsigned int)convertPixelFormatToGLFormat( pf ), GL_UNSIGNED_BYTE,
pixels );
if ( tightlyPackedThreeChannelRows )
glPixelStorei( GL_UNPACK_ALIGNMENT, 4 );
if ( hasLocalCopy() ) {
Image image( pixels, width, height, mChannels );
+177 -58
View File
@@ -1,63 +1,176 @@
#include <array>
#include <eepp/system/base64.hpp>
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;
}
size_t decodeBase64Strict( size_t inLen, const char* input, size_t outLen, unsigned char* output ) {
size_t outputOffset = 0;
size_t inputOffset = 0;
while ( inputOffset + 4 <= inLen ) {
const Uint8 a = base64dec_tab[static_cast<unsigned char>( input[inputOffset] )];
const Uint8 b = base64dec_tab[static_cast<unsigned char>( input[inputOffset + 1] )];
const Uint8 c = base64dec_tab[static_cast<unsigned char>( input[inputOffset + 2] )];
const Uint8 d = base64dec_tab[static_cast<unsigned char>( input[inputOffset + 3] )];
if ( ( a | b | c | d ) > 63 )
break;
if ( outputOffset + 3 > outLen )
return static_cast<size_t>( -1 );
output[outputOffset] = static_cast<unsigned char>( ( a << 2 ) | ( b >> 4 ) );
output[outputOffset + 1] = static_cast<unsigned char>( ( b << 4 ) | ( c >> 2 ) );
output[outputOffset + 2] = static_cast<unsigned char>( ( c << 6 ) | d );
inputOffset += 4;
outputOffset += 3;
}
const size_t remaining = inLen - inputOffset;
if ( remaining == 0 )
return outputOffset;
if ( remaining == 1 || remaining > 4 )
return static_cast<size_t>( -1 );
const Uint8 a = base64dec_tab[static_cast<unsigned char>( input[inputOffset] )];
const Uint8 b = base64dec_tab[static_cast<unsigned char>( input[inputOffset + 1] )];
if ( a > 63 || b > 63 || outputOffset >= outLen )
return static_cast<size_t>( -1 );
output[outputOffset++] = static_cast<unsigned char>( ( a << 2 ) | ( b >> 4 ) );
if ( remaining == 2 )
return ( b & 0x0F ) == 0 ? outputOffset : static_cast<size_t>( -1 );
const Uint8 c = base64dec_tab[static_cast<unsigned char>( input[inputOffset + 2] )];
if ( c == BASE64_PADDING ) {
return remaining == 4 && input[inputOffset + 3] == '=' && ( b & 0x0F ) == 0
? outputOffset
: static_cast<size_t>( -1 );
}
if ( c > 63 || outputOffset >= outLen )
return static_cast<size_t>( -1 );
output[outputOffset++] = static_cast<unsigned char>( ( b << 4 ) | ( c >> 2 ) );
if ( remaining == 3 )
return ( c & 0x03 ) == 0 ? outputOffset : static_cast<size_t>( -1 );
if ( input[inputOffset + 3] != '=' || ( c & 0x03 ) != 0 )
return static_cast<size_t>( -1 );
return outputOffset;
}
} // 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 ) {
if ( mode == DecodeMode::NoWhitespaceStrict )
return decodeBase64Strict( in_len, in, out_len, out );
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 +187,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 +215,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;
}
+28
View File
@@ -12,6 +12,28 @@
namespace EE { namespace System {
bool CPU::hasSSSE3() {
#ifdef EE_ARCH_X86_64
static bool isSSSE3 = []() {
#if defined( COMPILER_MSVC )
int cpuInfo[4];
__cpuid( cpuInfo, 0 );
if ( cpuInfo[0] < 1 )
return false;
__cpuid( cpuInfo, 1 );
return ( cpuInfo[2] & ( 1 << 9 ) ) != 0;
#elif defined( COMPILER_GCC_CLANG )
return __builtin_cpu_supports( "ssse3" );
#else
return false;
#endif
}();
return isSSSE3;
#else
return false;
#endif
}
bool CPU::hasAVX2() {
#ifdef EE_ARCH_X86_64
static bool isAVX2 = []() {
@@ -20,6 +42,12 @@ bool CPU::hasAVX2() {
__cpuid( cpuInfo, 0 );
if ( cpuInfo[0] < 7 )
return false;
__cpuid( cpuInfo, 1 );
constexpr int OSXSAVE = 1 << 27;
constexpr int AVX = 1 << 28;
if ( ( cpuInfo[2] & ( OSXSAVE | AVX ) ) != ( OSXSAVE | AVX ) ||
( _xgetbv( 0 ) & 0x6 ) != 0x6 )
return false;
__cpuid( cpuInfo, 7 );
return ( cpuInfo[1] & ( 1 << 5 ) ) != 0;
#elif defined( COMPILER_GCC_CLANG )
@@ -44,7 +44,9 @@ class IPseudoTerminal : public eterm::System::IPipe {
virtual int getNumRows() const = 0;
virtual bool resize( int columns, int rows ) = 0;
virtual bool resize( int columns, int rows, int pixelWidth, int pixelHeight ) = 0;
bool resize( int columns, int rows ) { return resize( columns, rows, 0, 0 ); }
};
}} // namespace eterm::Terminal
@@ -22,6 +22,7 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#include <eepp/config.hpp>
#include <eterm/terminal/terminalgraphics.hpp>
#include <eterm/terminal/terminaltypes.hpp>
using namespace EE;
@@ -80,6 +81,9 @@ class ITerminalDisplay {
virtual void drawCursor( int cx, int cy, TerminalGlyph g, int ox, int oy,
TerminalGlyph og ) = 0;
virtual void drawGraphics( std::shared_ptr<TerminalGraphicsPresentation> presentation,
std::vector<TerminalGraphicsUpdate> updates );
virtual void drawEnd() = 0;
protected:
@@ -0,0 +1,250 @@
#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> dataOffset;
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::shared_ptr<std::vector<Uint8>> pixels;
std::unordered_map<Uint32, Frame> frames;
Sizei size;
Uint64 creationSerial{ 0 };
EE::System::Clock frameClock;
Uint32 imageNumber{ 0 };
Uint32 usageHint{ 0 };
Uint32 currentFrame{ 1 };
Uint32 loopCount{ 1 };
Uint32 loopsCompleted{ 0 };
Int32 rootGapMs{ 0 };
Uint8 animationState{ 1 };
Uint8 channels{ 4 };
bool anonymous{ false };
};
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 ensureRootRGBA( KittyImageId imageId, Image& image );
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;
std::vector<Uint8> mDecodedScratch;
std::vector<Uint8> mPixelScratch;
std::vector<Uint8> mEncodedScratch;
std::vector<Uint8> mComposeSourceScratch;
size_t mStorageBytes{ 0 };
size_t mFrameStorageBytes{ 0 };
Uint64 mCreationSerial{ 0 };
Uint64 mPresentationGeneration{ 0 };
Uint64 mPlacementSerial{ 0 };
KittyImageId mNextImageId{ 1 };
bool mPresentationDirty{ false };
int mScrollPosition{ 0 };
int mHistoryLength{ 0 };
int mScreenRows{ 0 };
Uint32 mCellPixelWidth{ 0 };
Uint32 mCellPixelHeight{ 0 };
size_t mMaxStorageBytes{ 0 };
size_t mMaxImages{ 0 };
size_t mMaxPlacements{ 0 };
KittyGraphicsStats mStats;
};
}} // namespace eterm::Terminal
#endif
@@ -0,0 +1,43 @@
#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;
Uint8 channels{ 4 };
};
std::unordered_map<KittyImageId, GPUImage> mImages;
std::shared_ptr<const TerminalGraphicsPresentation> mPresentation;
Uint64 mLastAppliedSequence{ 0 };
};
}} // namespace eterm::Terminal
#endif
@@ -55,7 +55,7 @@ class PseudoTerminal final : public IPseudoTerminal {
virtual int getNumColumns() const override;
virtual int getNumRows() const override;
virtual bool resize( int columns, int rows ) override;
virtual bool resize( int columns, int rows, int pixelWidth, int pixelHeight ) override;
virtual int write( const char* s, size_t n ) override;
virtual int read( char* buf, size_t n, bool block = false ) override;
@@ -30,6 +30,8 @@ class VertexBuffer;
namespace eterm { namespace Terminal {
class KittyGraphicsRenderer;
enum class TerminalShortcutAction {
PASTE,
PASTE_SELECTION,
@@ -169,6 +171,10 @@ class TerminalDisplay {
bool update( bool isMouseOverMe = true );
Sizei getCellPixelSize() const;
Sizei getGridPixelSize() const;
void executeFile( const std::string& cmd );
void executeBinary( const std::string& binaryPath, const std::string& args = "" );
@@ -315,6 +321,7 @@ class TerminalDisplay {
std::vector<Color> mColors;
std::shared_ptr<TerminalSession> mSession;
std::shared_ptr<const TerminalSnapshot> mSnapshot;
std::unique_ptr<KittyGraphicsRenderer> mGraphicsRenderer;
mutable std::string mClipboardUtf8;
Uint32 mNumCallBacks{ 0 };
std::map<Uint32, EventFunc> mCallbacks;
@@ -349,6 +356,8 @@ class TerminalDisplay {
Uint32 mRows{ 0 };
Uint32 mClickStep{ 5 };
Uint64 mSnapshotGeneration{ 0 };
Uint64 mLastAppliedGraphicsSequence{ 0 };
bool mGraphicsResyncPending{ false };
FontHinting mFontHinting{ FontHinting::Full };
FontAntialiasing mFontAntialiasing{ FontAntialiasing::Grayscale };
FrameBufferUniquePtr mFrameBuffer;
@@ -377,12 +386,16 @@ class TerminalDisplay {
Vector2i positionToGrid( const Vector2i& pos );
Vector2i positionToPixel( const Vector2i& pos ) const;
void onSizeChange();
void onProcessExit( int exitCode );
void consumeSnapshot();
void drainGraphicsUpdates();
void drainSessionEvents();
TerminalColorPalette makeColorPalette() const;
@@ -41,6 +41,7 @@
#include <eterm/system/iprocess.hpp>
#include <eterm/terminal/ipseudoterminal.hpp>
#include <eterm/terminal/iterminaldisplay.hpp>
#include <eterm/terminal/kittygraphicsprotocol.hpp>
#include <eterm/terminal/terminaltypes.hpp>
#include <memory>
#include <stdint.h>
@@ -58,6 +59,10 @@ constexpr int ESC_BUF_SIZ = 512;
constexpr int ESC_ARG_SIZ = 32;
constexpr int STR_BUF_SIZ = ESC_BUF_SIZ;
constexpr int STR_ARG_SIZ = ESC_ARG_SIZ;
// Kitty recommends small chunks, but unchunked direct transmissions are valid and common in
// simple clients. Keep this independently bounded while allowing useful image-sized APCs.
constexpr size_t MAX_KITTY_GRAPHICS_APC_SIZE = 16 * 1024 * 1024;
constexpr size_t MAX_GENERIC_STRING_SEQUENCE_SIZE = 1024 * 1024;
/* Internal representation of the screen */
struct Term {
@@ -119,7 +124,8 @@ struct STREscape {
size_t siz; /* allocation size */
size_t len; /* raw string length */
char* args[STR_ARG_SIZ];
int narg; /* nb of args */
int narg; /* nb of args */
bool discarded; /* oversized sequence: consume input through its terminator without storing */
};
enum class PromptState {
@@ -156,6 +162,8 @@ class TerminalEmulator final {
void resize( int columns, int rows );
void resize( int columns, int rows, int pixelWidth, int pixelHeight );
void redraw();
/** Worker-owned terminal state reset (RIS semantics without replacing the PTY/process). */
@@ -220,8 +228,8 @@ class TerminalEmulator final {
std::string getSelection() const;
void mousereport( const TerminalMouseEventType& type, const Vector2i& pos, const Uint32& flags,
const Uint32& mod );
void mousereport( const TerminalMouseEventType& type, const Vector2i& cellPosition,
const Vector2i& pixelPosition, const Uint32& flags, const Uint32& mod );
const bool& isDirty() const { return mDirty; }
@@ -260,6 +268,8 @@ class TerminalEmulator final {
/** Worker-only notification that the display palette changed. */
void notifyColorSchemeChanged();
void requestGraphicsResync();
Vector2i getSize() const;
System::IProcess* getProcess() const;
@@ -288,6 +298,10 @@ class TerminalEmulator final {
bool mPendingPtyResize{ false };
int mPendingPtyColumns{ 0 };
int mPendingPtyRows{ 0 };
int mPendingPtyPixelWidth{ 0 };
int mPendingPtyPixelHeight{ 0 };
int mPixelWidth{ 0 };
int mPixelHeight{ 0 };
Clock mPendingPtyResizeClock;
bool mDirty{ true };
@@ -307,6 +321,7 @@ class TerminalEmulator final {
TerminalSelection mSel;
CSIEscape mCsiescseq;
STREscape mStrescseq;
KittyGraphicsProtocol mKittyGraphics;
uint32_t mDefaultFg;
uint32_t mDefaultBg;
@@ -324,6 +339,16 @@ class TerminalEmulator final {
PromptState mPromptState{ PromptState::Unknown };
PromptStateChangedCb mPromptStateChangedCb;
DataCb mDataCb;
Vector2i mKittyPlaceholderCell{ -1, -1 };
struct KittyPlaceholderMetadata {
Uint32 placementId{ 0 };
Uint16 row{ UINT16_MAX };
Uint16 column{ UINT16_MAX };
Uint8 imageIdMsb{ 0 };
Uint8 diacriticCount{ 0 };
};
std::unordered_map<const TerminalGlyph*, KittyPlaceholderMetadata> mKittyPlaceholderMetadata;
Uint32 mKittyUnderlineColor{ 0 };
void setClipboard( const char* str );
@@ -0,0 +1,110 @@
#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>> pixels;
Sizei imageSize;
Rect region;
Uint64 sequence{ 0 };
KittyImageId imageId{ 0 };
Uint32 frameNumber{ 1 };
TerminalGraphicsUpdateType type{ TerminalGraphicsUpdateType::Resync };
Uint8 channels{ 4 };
size_t payloadBytes() const { return pixels ? pixels->size() : 0; }
};
/** Bounded ordered worker-to-UI mutation queue with explicit overflow recovery. */
class TerminalGraphicsUpdateQueue {
public:
static constexpr size_t DefaultMaxUpdates = 1024;
static constexpr size_t DefaultMaxBytes = 32 * 1024 * 1024;
explicit TerminalGraphicsUpdateQueue( size_t maxUpdates = DefaultMaxUpdates,
size_t maxBytes = DefaultMaxBytes );
Uint64 enqueue( TerminalGraphicsUpdate update );
std::vector<TerminalGraphicsUpdate> drain();
size_t queuedBytes() const;
bool needsResync() const;
void resetResync();
private:
mutable std::mutex mMutex;
std::deque<TerminalGraphicsUpdate> mUpdates;
size_t mMaxUpdates{ 0 };
size_t mMaxBytes{ 0 };
size_t mQueuedBytes{ 0 };
Uint64 mNextSequence{ 0 };
bool mNeedsResync{ false };
};
}} // namespace eterm::Terminal
#endif
@@ -6,6 +6,7 @@
#include <eterm/system/iprocess.hpp>
#include <eterm/terminal/ipseudoterminal.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/terminal/terminalgraphics.hpp>
#include <eterm/terminal/terminaltypes.hpp>
#include <atomic>
@@ -29,6 +30,7 @@ namespace eterm { namespace Terminal {
/** Immutable worker-to-UI presentation state. Cell selection is already applied as ATTR_REVERSE. */
struct TerminalSnapshot {
std::shared_ptr<const TerminalGraphicsPresentation> graphics;
std::vector<TerminalGlyph> cells;
std::vector<Uint8> dirtyRows;
std::string title;
@@ -118,6 +120,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
void write( std::string data, bool mayEcho = true );
void writeRaw( std::string data );
void resize( int columns, int rows );
void resize( int columns, int rows, int pixelWidth, int pixelHeight );
void scrollUp( int amount );
void scrollDown( int amount );
/** Returns an ordered command id that is copied into snapshots after the scroll is applied. */
@@ -125,8 +128,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
void selectionStart( int column, int row, int snap );
void selectionExtend( int column, int row, int type, bool done );
void selectionClear();
void mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags,
Uint32 modifiers );
void mouseReport( TerminalMouseEventType type, Vector2i cellPosition, Vector2i pixelPosition,
Uint32 flags, Uint32 modifiers );
void setFocus( bool focus );
void setCursorMode( TerminalCursorMode mode );
void setColorPalette( TerminalColorPalette palette );
@@ -141,6 +144,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
std::shared_ptr<const TerminalSnapshot> snapshot() const;
std::vector<Event> drainEvents();
std::vector<TerminalGraphicsUpdate> drainGraphicsUpdates();
void requestGraphicsResync();
/** Bounded exact-selection request. Returns no value on timeout or during shutdown. */
std::optional<std::string>
@@ -160,6 +165,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
struct ResizeCommand {
int columns{ 0 };
int rows{ 0 };
int pixelWidth{ 0 };
int pixelHeight{ 0 };
};
struct ScrollCommand {
int amount{ 0 };
@@ -179,7 +186,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
};
struct MouseCommand {
TerminalMouseEventType type{ TerminalMouseEventType::MouseMotion };
Vector2i position;
Vector2i cellPosition;
Vector2i pixelPosition;
Uint32 flags{ 0 };
Uint32 modifiers{ 0 };
};
@@ -203,6 +211,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
std::shared_ptr<SelectionResponse> response;
};
struct SelectionClearCommand {};
struct GraphicsResyncCommand {};
struct ResetCommand {};
struct TerminateCommand {};
struct AllowTrimCommand : BoolCommand {};
@@ -216,7 +225,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
MouseCommand, FocusCommand, CursorModeCommand, PaletteCommand,
PresentationRateCommand, AllowTrimCommand, DataEventsCommand,
PromptEventsCommand, TerminateCommand, RestartCommand, ResetCommand,
SelectionRequestCommand>;
SelectionRequestCommand, GraphicsResyncCommand>;
TerminalSession( PtyPtr&& pty, ProcPtr&& process, size_t historySize,
TerminalColorPalette palette );
@@ -228,6 +237,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
void processCommand( Command&& command );
void enqueueEvent( Event event, bool coalescable );
void publishSnapshot( std::shared_ptr<const TerminalSnapshot> snapshot );
Uint64 enqueueGraphicsUpdate( TerminalGraphicsUpdate update );
std::shared_ptr<WorkerDisplay> mWorkerDisplay;
std::unique_ptr<TerminalEmulator> mEmulator;
@@ -238,6 +248,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
std::deque<Command> mCommands;
std::mutex mEventMutex;
std::deque<Event> mEvents;
TerminalGraphicsUpdateQueue mGraphicsUpdates;
mutable std::mutex mPublishedSnapshotMutex;
std::shared_ptr<const TerminalSnapshot> mPublishedSnapshot;
std::atomic<bool> mShutdownRequested{ false };
@@ -246,4 +257,4 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
}} // namespace eterm::Terminal
#endif
#endif
@@ -116,6 +116,7 @@ enum TerminalWinMode {
MODE_BRCKTPASTE = 1 << 16,
MODE_NUMLOCK = 1 << 17,
MODE_ALTSCRROLL = 1 << 18,
MODE_MOUSESGR_PIXELS = 1 << 19,
MODE_MOUSE = MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEX10 | MODE_MOUSEMANY,
};
@@ -79,6 +79,9 @@ const char* ITerminalDisplay::getClipboard() const {
return "";
}
void ITerminalDisplay::drawGraphics( std::shared_ptr<TerminalGraphicsPresentation>,
std::vector<TerminalGraphicsUpdate> ) {}
void ITerminalDisplay::onProcessExit( int /*exitCode*/ ) {}
void ITerminalDisplay::onScrollPositionChange() {}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
#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::ReplaceImage: {
if ( !update.pixels || update.imageSize.getWidth() <= 0 ||
update.imageSize.getHeight() <= 0 ||
( update.channels != 3 && update.channels != 4 ) ||
update.pixels->size() != static_cast<size_t>( update.imageSize.getWidth() ) *
update.imageSize.getHeight() * update.channels )
return false;
auto existing = mImages.find( update.imageId );
if ( existing != mImages.end() && existing->second.texture &&
existing->second.size == update.imageSize &&
existing->second.channels == update.channels ) {
existing->second.texture->update(
update.pixels->data(), update.imageSize.getWidth(),
update.imageSize.getHeight(), 0, 0,
Image::channelsToPixelFormat( update.channels ) );
existing->second.frames.clear();
break;
}
[[fallthrough]];
}
case TerminalGraphicsUpdateType::CreateImage: {
if ( !update.pixels || update.imageSize.getWidth() <= 0 ||
update.imageSize.getHeight() <= 0 ||
( update.channels != 3 && update.channels != 4 ) ||
update.pixels->size() != static_cast<size_t>( update.imageSize.getWidth() ) *
update.imageSize.getHeight() * update.channels )
return false;
auto texture = TextureFactory::instance()->loadFromPixels(
update.pixels->data(), update.imageSize.getWidth(),
update.imageSize.getHeight(), update.channels, 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, update.channels } );
break;
}
case TerminalGraphicsUpdateType::UpdateRegion: {
auto image = mImages.find( update.imageId );
if ( image == mImages.end() || !update.pixels )
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.pixels->size() != static_cast<size_t>( width ) * height * 4 )
return false;
image->second.texture->update( update.pixels->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.pixels || update.frameNumber <= 1 ||
update.pixels->size() != static_cast<size_t>( image->second.size.getWidth() ) *
image->second.size.getHeight() * 4 )
return false;
auto texture = TextureFactory::instance()->loadFromPixels(
update.pixels->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.pixels )
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.pixels->size() != static_cast<size_t>( width ) * height * 4 )
return false;
frame->second->update( update.pixels->data(), width, height, update.region.Left,
update.region.Top );
break;
}
case TerminalGraphicsUpdateType::DeleteFrame: {
auto image = mImages.find( update.imageId );
if ( image != mImages.end() )
image->second.frames.erase( update.frameNumber );
break;
}
case TerminalGraphicsUpdateType::DeleteImage:
mImages.erase( update.imageId );
break;
case TerminalGraphicsUpdateType::ResetScreen:
break;
case TerminalGraphicsUpdateType::ResetAll:
mImages.clear();
break;
default:
return false;
}
mLastAppliedSequence = update.sequence;
}
return true;
}
void KittyGraphicsRenderer::setPresentation(
std::shared_ptr<const TerminalGraphicsPresentation> presentation ) {
mPresentation = std::move( presentation );
}
void KittyGraphicsRenderer::draw( Pass pass, const Vector2f& origin, const Sizef& cellSize,
const Sizef& gridSize ) {
if ( !mPresentation )
return;
auto clippingMask = Renderer::instance()->getClippingMask();
clippingMask->clipEnable( origin.x, origin.y, gridSize.getWidth(), gridSize.getHeight() );
for ( const auto& placement : mPresentation->placements ) {
if ( !isInPass( placement.zIndex, pass ) )
continue;
auto image = mImages.find( placement.imageId );
if ( image == mImages.end() || !image->second.texture )
continue;
const Vector2f position( origin.x + placement.visibleAnchorCell.x * cellSize.getWidth() +
placement.firstCellPixelOffset.x,
origin.y + placement.visibleAnchorCell.y * cellSize.getHeight() +
placement.firstCellPixelOffset.y );
const Float width = placement.columns * cellSize.getWidth();
const Float height = placement.rows * cellSize.getHeight();
TexturePtr texture = image->second.texture;
if ( placement.frameNumber > 1 ) {
auto frame = image->second.frames.find( placement.frameNumber );
if ( frame != image->second.frames.end() )
texture = frame->second;
}
texture->drawEx( position.x, position.y, width, height, 0, Vector2f::One, Color::White,
Color::White, Color::White, Color::White, BlendMode::Alpha(),
RENDER_NORMAL, OriginPoint( OriginPoint::OriginTopLeft ),
placement.sourcePixels );
}
clippingMask->clipDisable();
}
bool KittyGraphicsRenderer::hasPlacements( Pass pass ) const {
return mPresentation &&
std::any_of( mPresentation->placements.begin(), mPresentation->placements.end(),
[pass]( const TerminalVisiblePlacement& placement ) {
return isInPass( placement.zIndex, pass );
} );
}
void KittyGraphicsRenderer::reset() {
mImages.clear();
mPresentation.reset();
mLastAppliedSequence = 0;
}
}} // namespace eterm::Terminal
@@ -107,13 +107,13 @@ bool PseudoTerminal::isTTY() const {
return true;
}
bool PseudoTerminal::resize( int columns, int rows ) {
bool PseudoTerminal::resize( int columns, int rows, int pixelWidth, int pixelHeight ) {
struct winsize w;
w.ws_row = rows;
w.ws_col = columns;
w.ws_xpixel = 0;
w.ws_ypixel = 0;
w.ws_xpixel = pixelWidth;
w.ws_ypixel = pixelHeight;
bool masterResized = ioctl( (int)mMaster, TIOCSWINSZ, &w ) >= 0;
bool slaveResized =
@@ -297,7 +297,7 @@ bool PseudoTerminal::isTTY() const {
return true;
}
bool PseudoTerminal::resize( int columns, int rows ) {
bool PseudoTerminal::resize( int columns, int rows, int, int ) {
if ( !pResizePseudoConsole )
return false;
@@ -9,6 +9,7 @@
#include <eepp/window/clipboard.hpp>
#include <eterm/system/processfactory.hpp>
#include <eterm/terminal/boxdrawdata.hpp>
#include <eterm/terminal/kittygraphicsrenderer.hpp>
#include <eterm/terminal/terminaldisplay.hpp>
#include <limits.h>
@@ -457,6 +458,9 @@ std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
eeSAFE_DELETE( processFactory );
return nullptr;
}
terminal->mSession->resize( termSize.getWidth(), termSize.getHeight(),
terminal->getGridPixelSize().getWidth(),
terminal->getGridPixelSize().getHeight() );
terminal->mSession->setPresentationRate( presentationRateForWindow( window ) );
terminal->mProgram = program;
terminal->mArgs = args;
@@ -479,6 +483,7 @@ TerminalDisplay::~TerminalDisplay() {
TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const Float& fontSize,
const Sizef& pixelsSize, const bool& useFrameBuffer ) :
mWindow( window ),
mGraphicsRenderer( std::make_unique<KittyGraphicsRenderer>() ),
mFont( font ),
mFontSize( fontSize ),
mSize( pixelsSize ),
@@ -493,6 +498,8 @@ TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const
resetColors();
Sizei gridSize( gridSizeFromTermDimensions( mFont, mFontSize, mSize - mPadding * 2.f ) );
mColumns = gridSize.getWidth();
mRows = gridSize.getHeight();
mDirtyLines.resize( gridSize.getHeight(), 1 );
mQuadVertex = GLi->quadVertex();
@@ -715,7 +722,9 @@ void TerminalDisplay::setKeepAlive( bool keepAlive ) {
}
bool TerminalDisplay::update( bool isMouseOverMe ) {
drainGraphicsUpdates();
consumeSnapshot();
drainGraphicsUpdates();
drainSessionEvents();
if ( mFocus && isBlinkingCursor() && mClock.getElapsedTime().asSeconds() > 0.7 ) {
mMode ^= MODE_BLINK;
@@ -734,6 +743,29 @@ bool TerminalDisplay::update( bool isMouseOverMe ) {
return true;
}
void TerminalDisplay::drainGraphicsUpdates() {
if ( !mSession )
return;
auto updates = mSession->drainGraphicsUpdates();
if ( !updates.empty() ) {
if ( !mGraphicsRenderer->applyUpdates( std::move( updates ) ) ) {
mGraphicsRenderer->reset();
mLastAppliedGraphicsSequence = 0;
if ( !mGraphicsResyncPending ) {
mGraphicsResyncPending = true;
mSession->requestGraphicsResync();
}
return;
}
mLastAppliedGraphicsSequence = mGraphicsRenderer->lastAppliedSequence();
mGraphicsResyncPending = false;
if ( mSnapshot && mSnapshot->graphics &&
mSnapshot->graphics->requiredUpdateSequence <= mLastAppliedGraphicsSequence )
mGraphicsRenderer->setPresentation( mSnapshot->graphics );
mDirty = true;
}
}
void TerminalDisplay::consumeSnapshot() {
if ( !mSession )
return;
@@ -768,6 +800,13 @@ void TerminalDisplay::consumeSnapshot() {
mCursor = mSnapshot->cursor;
mCursorGlyph = mSnapshot->cursorGlyph;
mCursorMode = mSnapshot->cursorMode;
if ( mSnapshot->graphics &&
mSnapshot->graphics->requiredUpdateSequence <= mGraphicsRenderer->lastAppliedSequence() ) {
mGraphicsRenderer->setPresentation( mSnapshot->graphics );
} else if ( mSnapshot->graphics && !mGraphicsResyncPending ) {
mGraphicsResyncPending = true;
mSession->requestGraphicsResync();
}
const int presentationBits = mMode & MODE_BLINK;
mMode = mSnapshot->windowMode | presentationBits;
if ( mFocus )
@@ -1093,8 +1132,8 @@ void TerminalDisplay::onMouseMove( const Vector2i& pos, const Uint32& flags ) {
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR,
false );
}
mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ),
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
}
void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
@@ -1134,8 +1173,8 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
}
}
mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ),
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
}
void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
@@ -1181,8 +1220,8 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
}
}
mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ),
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
}
static inline Color termColor( unsigned int terminalColor, const std::vector<Color>& colors ) {
@@ -1368,6 +1407,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
auto fontSize = mFont->getFontHeight( mFontSize );
auto spaceCharAdvanceX = mFont->getGlyph( 'A', mFontSize, false, false ).advance;
const Sizef cellSize( spaceCharAdvanceX, fontSize );
float x = 0.0f;
float y = pos.y;
@@ -1446,6 +1486,38 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
mVBBackground->draw();
mVBBackground->unbind();
}
const Sizef graphicsGridSize( mColumns * cellSize.getWidth(), mRows * cellSize.getHeight() );
mGraphicsRenderer->draw( KittyGraphicsRenderer::Pass::VeryNegative, pos, cellSize,
graphicsGridSize );
if ( mGraphicsRenderer->hasPlacements( KittyGraphicsRenderer::Pass::VeryNegative ) ) {
y = std::floor( pos.y );
for ( Uint32 row = 0; row < mRows; ++row ) {
x = std::floor( pos.x );
for ( Uint32 column = 0; column < mColumns; ++column ) {
const auto& glyph = mSnapshot->cells[row * mColumns + column];
if ( glyph.mode & ATTR_WDUMMY )
continue;
auto foreground = termColor( glyph.fg, mColors );
auto background = termColor( glyph.bg, mColors );
if ( IS_SET( MODE_REVERSE ) ) {
foreground = foreground == defaultFg ? defaultBg : foreground.invert();
background = background == defaultBg ? defaultFg : background.invert();
}
if ( glyph.mode & ATTR_REVERSE )
background = foreground;
const bool wide = glyph.mode & ATTR_WIDE;
const Float advance = spaceCharAdvanceX * ( wide ? 2.0f : 1.0f );
if ( background != defaultBg ) {
mPrimitives.setColor( background );
mPrimitives.drawRectangle( Rectf( { x, y }, { advance, lineHeight } ) );
}
x += advance;
}
y += lineHeight;
}
}
mGraphicsRenderer->draw( KittyGraphicsRenderer::Pass::Negative, pos, cellSize,
graphicsGridSize );
y = std::floor( pos.y );
@@ -1683,6 +1755,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
vbo->unbind();
}
}
mGraphicsRenderer->draw( KittyGraphicsRenderer::Pass::NonNegative, pos, cellSize,
graphicsGridSize );
// Underline is rendered after foreground render because it usually clashes with the underlines
// decorations and ends up being not visible, I prefer to do this even it it's not standard.
@@ -1777,6 +1851,27 @@ Vector2i TerminalDisplay::positionToGrid( const Vector2i& pos ) {
return { mouseX, mouseY };
}
Vector2i TerminalDisplay::positionToPixel( const Vector2i& pos ) const {
const Sizei gridPixels = getGridPixelSize();
const int x = static_cast<int>( std::floor( pos.x - mPosition.x - mPadding.Left ) );
const int y = static_cast<int>( std::floor( pos.y - mPosition.y - mPadding.Top ) );
return { eeclamp( x, 0, eemax( 0, gridPixels.getWidth() - 1 ) ),
eeclamp( y, 0, eemax( 0, gridPixels.getHeight() - 1 ) ) };
}
Sizei TerminalDisplay::getCellPixelSize() const {
return {
static_cast<int>( std::round( mFont->getGlyph( 'A', mFontSize, false, false ).advance ) ),
static_cast<int>( std::round( mFont->getFontHeight( mFontSize ) ) ) };
}
Sizei TerminalDisplay::getGridPixelSize() const {
const Sizei cell = getCellPixelSize();
const int columns = mSnapshot ? mSnapshot->columns : static_cast<int>( mColumns );
const int rows = mSnapshot ? mSnapshot->rows : static_cast<int>( mRows );
return { columns * cell.getWidth(), rows * cell.getHeight() };
}
void TerminalDisplay::onSizeChange() {
Sizei gridSize( gridSizeFromTermDimensions(
mFont, mFontSize,
@@ -1784,7 +1879,10 @@ void TerminalDisplay::onSizeChange() {
if ( mSession && ( !mSnapshot || gridSize.getWidth() != mSnapshot->columns ||
gridSize.getHeight() != mSnapshot->rows ) ) {
mSession->resize( gridSize.getWidth(), gridSize.getHeight() );
const Sizei cellSize = getCellPixelSize();
mSession->resize( gridSize.getWidth(), gridSize.getHeight(),
gridSize.getWidth() * cellSize.getWidth(),
gridSize.getHeight() * cellSize.getHeight() );
mDirtyLines.resize( gridSize.getHeight(), 1 );
}
@@ -61,6 +61,7 @@ using namespace EE::System;
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <thread>
#if defined( EE_ARCH_X86_64 )
#include <immintrin.h>
@@ -79,6 +80,8 @@ namespace eterm { namespace Terminal {
namespace {
constexpr Rune KittyGraphicsPlaceholder = 0x10EEEE;
bool terminalDiagnosticsEnabled() {
#ifdef EE_DEBUG
return true;
@@ -124,6 +127,39 @@ TerminalCursorMode blinkingCursorVariant( TerminalCursorMode mode ) {
} // namespace
#if defined( EE_ARCH_X86_64 )
#if defined( __GNUC__ ) || defined( __clang__ )
__attribute__( ( target( "avx2" ) ) )
#endif
static int findKittyAPCControlAVX2( const char* data, int begin, int end ) {
const __m256i highThreeBitMask = _mm256_set1_epi8( static_cast<char>( 0xE0 ) );
const __m256i c1Prefix = _mm256_set1_epi8( static_cast<char>( 0x80 ) );
const __m256i bell = _mm256_set1_epi8( 0x07 );
const __m256i cancel = _mm256_set1_epi8( 0x18 );
const __m256i substitute = _mm256_set1_epi8( 0x1A );
const __m256i escape = _mm256_set1_epi8( 0x1B );
int offset = begin;
for ( ; offset + 32 <= end; offset += 32 ) {
const __m256i bytes =
_mm256_loadu_si256( reinterpret_cast<const __m256i*>( data + offset ) );
const __m256i c1 =
_mm256_cmpeq_epi8( _mm256_and_si256( bytes, highThreeBitMask ), c1Prefix );
const __m256i explicitControls = _mm256_or_si256(
_mm256_or_si256( _mm256_cmpeq_epi8( bytes, bell ), _mm256_cmpeq_epi8( bytes, cancel ) ),
_mm256_or_si256( _mm256_cmpeq_epi8( bytes, substitute ),
_mm256_cmpeq_epi8( bytes, escape ) ) );
if ( _mm256_movemask_epi8( _mm256_or_si256( c1, explicitControls ) ) != 0 )
break;
}
while ( offset < end ) {
const unsigned char byte = static_cast<unsigned char>( data[offset] );
if ( byte == '\a' || byte == 030 || byte == 032 || byte == 033 ||
( byte >= 0x80 && byte <= 0x9F ) )
break;
++offset;
}
return offset;
}
#if defined( __GNUC__ ) || defined( __clang__ )
__attribute__( ( target( "avx2" ) ) )
#endif
@@ -165,6 +201,21 @@ static int trailingNonSpaceWidthNEON( Line line, int width, int minimumWidth ) {
}
#endif
static int findKittyAPCControl( const char* data, int begin, int end ) {
#if defined( EE_ARCH_X86_64 )
if ( CPU::hasAVX2() )
return findKittyAPCControlAVX2( data, begin, end );
#endif
while ( begin < end ) {
const unsigned char byte = static_cast<unsigned char>( data[begin] );
if ( byte == '\a' || byte == 030 || byte == 032 || byte == 033 ||
( byte >= 0x80 && byte <= 0x9F ) )
break;
++begin;
}
return begin;
}
/* identification sequence returned in DA and DECID */
static const char* vtiden = "\033[?6c";
@@ -211,6 +262,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 +958,15 @@ 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] && !mKittyPlaceholderMetadata.empty() ) {
for ( int column = 0; column < mTerm.col; ++column ) {
if ( mTerm.hist[i][column].u == KittyGraphicsPlaceholder )
mKittyPlaceholderMetadata.erase( &mTerm.hist[i][column] );
}
}
eeSAFE_FREE( mTerm.hist[i] );
}
eeSAFE_FREE( mTerm.hist );
mTerm.histcursize = 0;
mTerm.histi = 0;
@@ -906,6 +1004,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 +1152,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 +1203,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 +1257,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 +1668,8 @@ void TerminalEmulator::tsetchar( Rune u, TerminalGlyph* attr, int x, int y ) {
TLINE( y )[x - 1].mode &= ~ATTR_WIDE;
}
if ( TLINE( y )[x].u == KittyGraphicsPlaceholder )
mKittyPlaceholderMetadata.erase( &TLINE( y )[x] );
mTerm.dirty[y] = 1;
TLINE( y )[x] = *attr;
TLINE( y )[x].u = u;
@@ -1576,11 +1687,18 @@ 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 );
if ( !mKittyPlaceholderMetadata.empty() ) {
for ( int clearY = y1; clearY <= y2; ++clearY ) {
for ( int clearX = x1; clearX <= x2; ++clearX ) {
if ( TLINE( clearY )[clearX].u == KittyGraphicsPlaceholder )
mKittyPlaceholderMetadata.erase( &TLINE( clearY )[clearX] );
}
}
}
/*
* Fast path for the common full-row clear performed while scrolling: no
@@ -1753,6 +1871,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 +1935,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 +2034,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 +2059,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 +2279,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 +2405,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 +2474,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;
@@ -2569,10 +2722,16 @@ void TerminalEmulator::strdump( void ) {
}
void TerminalEmulator::strreset( void ) {
auto old = mStrescseq.buf;
char* buffer = mStrescseq.buf;
size_t capacity = mStrescseq.siz;
constexpr size_t MaxRetainedStringCapacity = 128 * 1024;
if ( !buffer || capacity > MaxRetainedStringCapacity ) {
buffer = (char*)xrealloc( buffer, STR_BUF_SIZ );
capacity = STR_BUF_SIZ;
}
mStrescseq = STREscape{};
mStrescseq.buf = (char*)xrealloc( old, STR_BUF_SIZ );
mStrescseq.siz = STR_BUF_SIZ;
mStrescseq.buf = buffer;
mStrescseq.siz = capacity;
}
void TerminalEmulator::sendbreak( const TerminalArg* ) {
@@ -2851,6 +3010,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 +3139,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 +3210,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 +3259,15 @@ check_control_code:
}
tsetchar( u, &mTerm.c.attr, mTerm.c.x, mTerm.c.y );
if ( u == KittyGraphicsPlaceholder ) {
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 ) {
@@ -3091,6 +3294,38 @@ int TerminalEmulator::twrite( const char* buf, int buflen, int show_ctrl ) {
int n;
for ( n = 0; n < buflen; n += charsize ) {
/* Kitty control data and payload are ASCII transport bytes. Once ESC _ G has been
* recognized, append ordinary bytes in bulk instead of routing every Base64 byte through
* UTF-8 decoding and the terminal character state machine. Control bytes remain on the
* normal path so fragmented ESC \\ termination and malformed strings retain their exact
* behavior. */
if ( !show_ctrl && ( mTerm.esc & ESC_STR ) && mStrescseq.type == '_' &&
mStrescseq.len > 0 && mStrescseq.buf[0] == 'G' ) {
const int end = findKittyAPCControl( buf, n, buflen );
const size_t bytes = static_cast<size_t>( end - n );
if ( bytes != 0 ) {
if ( !mStrescseq.discarded ) {
if ( mStrescseq.len > MAX_KITTY_GRAPHICS_APC_SIZE ||
bytes > MAX_KITTY_GRAPHICS_APC_SIZE - mStrescseq.len ) {
mStrescseq.discarded = true;
} else {
const size_t required = mStrescseq.len + bytes + 1;
if ( required > mStrescseq.siz ) {
size_t capacity = mStrescseq.siz;
while ( capacity < required )
capacity = eemin( capacity * 2, MAX_KITTY_GRAPHICS_APC_SIZE + 1 );
mStrescseq.buf = (char*)xrealloc( mStrescseq.buf, capacity );
mStrescseq.siz = capacity;
}
std::memcpy( mStrescseq.buf + mStrescseq.len, buf + n, bytes );
mStrescseq.len += bytes;
}
}
n = end;
if ( n == buflen )
return buflen;
}
}
if ( IS_SET( MODE_UTF8 ) ) {
/* process a complete utf8 char */
charsize = utf8decode( buf + n, &u, buflen - n );
@@ -3123,6 +3358,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 != KittyGraphicsPlaceholder )
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 +3569,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 == KittyGraphicsPlaceholder )
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 +3648,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 != KittyGraphicsPlaceholder ) {
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 +3727,7 @@ void TerminalEmulator::redraw() {
}
void TerminalEmulator::reset() {
mKittyGraphics.reset();
treset();
redraw();
}
@@ -3448,8 +3797,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 +3911,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 +4009,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 +4060,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 +4086,8 @@ void TerminalEmulator::resize( int columns, int rows ) {
redraw();
mPendingPtyColumns = columns;
mPendingPtyRows = rows;
mPendingPtyPixelWidth = mPixelWidth;
mPendingPtyPixelHeight = mPixelHeight;
mPendingPtyResize = true;
mPendingPtyResizeClock.restart();
}
@@ -3729,10 +4095,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!" );
}
@@ -3790,6 +4159,16 @@ bool TerminalEmulator::update() {
onProcessExit( mExitCode );
}
/* A non-blocking read can temporarily catch up with a producer that was blocked writing to the
* PTY. Do not enter the worker's 8 ms idle wait immediately after consuming data: give the
* producer a scheduling opportunity and probe the PTY once more. This matters for high-volume
* protocols such as Kitty graphics, where otherwise every transport chunk can pay one idle
* interval after the receiver becomes faster than the sender's wakeup latency. */
if ( reads > 0 && !readBudgetSaturated ) {
std::this_thread::yield();
return false;
}
return !readBudgetSaturated;
}
@@ -0,0 +1,77 @@
#include <eterm/terminal/terminalgraphics.hpp>
namespace eterm { namespace Terminal {
TerminalGraphicsUpdateQueue::TerminalGraphicsUpdateQueue( size_t maxUpdates, size_t maxBytes ) :
mMaxUpdates( maxUpdates ), mMaxBytes( maxBytes ) {}
Uint64 TerminalGraphicsUpdateQueue::enqueue( TerminalGraphicsUpdate update ) {
std::lock_guard<std::mutex> lock( mMutex );
const size_t payloadBytes = update.payloadBytes();
if ( mNeedsResync )
return ++mNextSequence;
auto isFullImageUpdate = []( TerminalGraphicsUpdateType type ) {
return type == TerminalGraphicsUpdateType::CreateImage ||
type == TerminalGraphicsUpdateType::ReplaceImage;
};
if ( !mUpdates.empty() && isFullImageUpdate( update.type ) &&
isFullImageUpdate( mUpdates.back().type ) && mUpdates.back().imageId == update.imageId ) {
const size_t previousBytes = mUpdates.back().payloadBytes();
if ( payloadBytes <= mMaxBytes - ( mQueuedBytes - previousBytes ) ) {
update.sequence = mUpdates.back().sequence;
if ( mUpdates.back().type == TerminalGraphicsUpdateType::CreateImage )
update.type = TerminalGraphicsUpdateType::CreateImage;
mQueuedBytes = mQueuedBytes - previousBytes + payloadBytes;
mUpdates.back() = std::move( update );
return mUpdates.back().sequence;
}
}
update.sequence = ++mNextSequence;
if ( mUpdates.size() >= mMaxUpdates || mQueuedBytes > mMaxBytes ||
payloadBytes > mMaxBytes - mQueuedBytes ) {
mUpdates.clear();
mQueuedBytes = 0;
mNeedsResync = true;
TerminalGraphicsUpdate resync;
resync.sequence = update.sequence;
resync.type = TerminalGraphicsUpdateType::Resync;
mUpdates.emplace_back( std::move( resync ) );
return update.sequence;
}
mQueuedBytes += payloadBytes;
mUpdates.emplace_back( std::move( update ) );
return mNextSequence;
}
std::vector<TerminalGraphicsUpdate> TerminalGraphicsUpdateQueue::drain() {
std::vector<TerminalGraphicsUpdate> updates;
std::lock_guard<std::mutex> lock( mMutex );
updates.reserve( mUpdates.size() );
while ( !mUpdates.empty() ) {
updates.emplace_back( std::move( mUpdates.front() ) );
mUpdates.pop_front();
}
mQueuedBytes = 0;
return updates;
}
size_t TerminalGraphicsUpdateQueue::queuedBytes() const {
std::lock_guard<std::mutex> lock( mMutex );
return mQueuedBytes;
}
bool TerminalGraphicsUpdateQueue::needsResync() const {
std::lock_guard<std::mutex> lock( mMutex );
return mNeedsResync;
}
void TerminalGraphicsUpdateQueue::resetResync() {
std::lock_guard<std::mutex> lock( mMutex );
mNeedsResync = false;
}
}} // namespace eterm::Terminal
@@ -20,7 +20,10 @@ struct TerminalSession::SelectionResponse {
class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
public:
WorkerDisplay( TerminalSession& session, TerminalColorPalette palette ) :
mSession( session ), mInitialPalette( std::move( palette ) ), mPalette( mInitialPalette ) {
mSession( session ),
mInitialPalette( std::move( palette ) ),
mPalette( mInitialPalette ),
mGraphics( std::make_shared<TerminalGraphicsPresentation>() ) {
mMode |= MODE_FOCUSED;
}
@@ -58,8 +61,20 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
mCursorVisible = true;
}
void drawGraphics( std::shared_ptr<TerminalGraphicsPresentation> presentation,
std::vector<TerminalGraphicsUpdate> updates ) {
if ( !presentation )
presentation = std::make_shared<TerminalGraphicsPresentation>();
presentation->requiredUpdateSequence = mGraphics ? mGraphics->requiredUpdateSequence : 0;
for ( auto& update : updates )
presentation->requiredUpdateSequence =
mSession.enqueueGraphicsUpdate( std::move( update ) );
mGraphics = std::move( presentation );
}
void drawEnd() {
auto snapshot = std::make_shared<TerminalSnapshot>();
snapshot->graphics = mGraphics;
snapshot->cells = mCells;
snapshot->dirtyRows = mDirtyRows;
snapshot->title = mTitle;
@@ -232,6 +247,7 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
std::vector<TerminalGlyph> mCells;
std::vector<Uint8> mDirtyRows;
std::string mTitle;
std::shared_ptr<const TerminalGraphicsPresentation> mGraphics;
Uint64 mGeneration{ 0 };
Uint64 mLastAppliedScrollCommand{ 0 };
Vector2i mCursor;
@@ -302,7 +318,11 @@ void TerminalSession::writeRaw( std::string data ) {
}
void TerminalSession::resize( int columns, int rows ) {
enqueue( ResizeCommand{ columns, rows } );
resize( columns, rows, 0, 0 );
}
void TerminalSession::resize( int columns, int rows, int pixelWidth, int pixelHeight ) {
enqueue( ResizeCommand{ columns, rows, pixelWidth, pixelHeight } );
}
void TerminalSession::scrollUp( int amount ) {
@@ -334,9 +354,9 @@ void TerminalSession::selectionClear() {
enqueue( SelectionClearCommand{} );
}
void TerminalSession::mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags,
Uint32 modifiers ) {
enqueue( MouseCommand{ type, position, flags, modifiers } );
void TerminalSession::mouseReport( TerminalMouseEventType type, Vector2i cellPosition,
Vector2i pixelPosition, Uint32 flags, Uint32 modifiers ) {
enqueue( MouseCommand{ type, cellPosition, pixelPosition, flags, modifiers } );
}
void TerminalSession::setFocus( bool focus ) {
@@ -383,6 +403,10 @@ void TerminalSession::restart( PtyPtr&& pty, ProcPtr&& process ) {
enqueue( RestartCommand{ std::move( pty ), std::move( process ) } );
}
void TerminalSession::requestGraphicsResync() {
enqueue( GraphicsResyncCommand{} );
}
std::shared_ptr<const TerminalSnapshot> TerminalSession::snapshot() const {
std::lock_guard<std::mutex> lock( mPublishedSnapshotMutex );
return mPublishedSnapshot;
@@ -410,6 +434,14 @@ std::vector<TerminalSession::Event> TerminalSession::drainEvents() {
return events;
}
std::vector<TerminalGraphicsUpdate> TerminalSession::drainGraphicsUpdates() {
return mGraphicsUpdates.drain();
}
Uint64 TerminalSession::enqueueGraphicsUpdate( TerminalGraphicsUpdate update ) {
return mGraphicsUpdates.enqueue( std::move( update ) );
}
void TerminalSession::enqueueEvent( Event event, bool coalescable ) {
std::lock_guard<std::mutex> lock( mEventMutex );
if ( coalescable ) {
@@ -480,7 +512,7 @@ void TerminalSession::processCommand( Command&& command ) {
} else if constexpr ( std::is_same_v<T, WriteRawCommand> ) {
mEmulator->write( value.data.data(), value.data.size() );
} else if constexpr ( std::is_same_v<T, ResizeCommand> ) {
mEmulator->resize( value.columns, value.rows );
mEmulator->resize( value.columns, value.rows, value.pixelWidth, value.pixelHeight );
} else if constexpr ( std::is_same_v<T, ScrollCommand> ) {
TerminalArg argument( value.amount );
if ( value.direction < 0 )
@@ -502,7 +534,8 @@ void TerminalSession::processCommand( Command&& command ) {
mEmulator->selclear();
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, MouseCommand> ) {
mEmulator->mousereport( value.type, value.position, value.flags, value.modifiers );
mEmulator->mousereport( value.type, value.cellPosition, value.pixelPosition,
value.flags, value.modifiers );
} else if constexpr ( std::is_same_v<T, FocusCommand> ) {
if ( mWorkerDisplay->getMode( MODE_FOCUS ) )
mEmulator->ttywrite( value.value ? "\033[I" : "\033[O", 3, false );
@@ -556,6 +589,9 @@ void TerminalSession::processCommand( Command&& command ) {
value.response->selection = mEmulator->getSelection();
value.response->ready = true;
value.response->condition.notify_one();
} else if constexpr ( std::is_same_v<T, GraphicsResyncCommand> ) {
mGraphicsUpdates.resetResync();
mEmulator->requestGraphicsResync();
}
},
std::move( command ) );
+795 -4
View File
@@ -1,16 +1,22 @@
#include "utest.hpp"
#include <atomic>
#include <chrono>
#include <deque>
#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:
@@ -18,15 +24,20 @@ class MockPty : public IPseudoTerminal {
std::string mWrites;
bool mLoopWrites{ true };
size_t mMaxRead{ std::numeric_limits<size_t>::max() };
std::deque<size_t> mReadSizes;
size_t mReadOffset{ 0 };
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; }
@@ -39,7 +50,12 @@ class MockPty : public IPseudoTerminal {
int read( char* buf, size_t n, bool ) override {
if ( mReadOffset == mBuffer.size() )
return 0;
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, mMaxRead } );
size_t readLimit = mMaxRead;
if ( !mReadSizes.empty() ) {
readLimit = mReadSizes.front();
mReadSizes.pop_front();
}
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, readLimit } );
memcpy( buf, mBuffer.data() + mReadOffset, toRead );
mReadOffset += toRead;
mBytesRead.fetch_add( toRead, std::memory_order_relaxed );
@@ -84,6 +100,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 +120,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.pixels = pixels;
TerminalGraphicsUpdate patch;
patch.type = TerminalGraphicsUpdateType::UpdateRegion;
patch.imageId = 7;
patch.pixels = 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].pixels );
}
UTEST( eterm_session, graphics_update_queue_overflow_requires_resync ) {
TerminalGraphicsUpdateQueue queue( 2, 8 );
TerminalGraphicsUpdate update;
update.type = TerminalGraphicsUpdateType::UpdateRegion;
update.pixels = 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.pixels = 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.pixels = 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].pixels->front() );
}
UTEST( eterm_session, ordered_selection_request ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "ordered selection";
@@ -347,6 +427,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 +441,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 +466,448 @@ 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_preserve_rgb24 ) {
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, 40, 50, 60 };
EXPECT_TRUE( expected == *pixels );
auto updates = protocol.takeUpdates();
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
EXPECT_EQ( static_cast<Uint8>( 3 ), updates.front().channels );
EXPECT_TRUE( updates.front().pixels && expected == *updates.front().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].pixels != nullptr );
EXPECT_EQ( static_cast<size_t>( 4 ), updates[1].pixels->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].pixels != nullptr );
EXPECT_EQ( static_cast<size_t>( 4 ), updates[0].pixels->size() );
}
UTEST( eterm, kitty_graphics_rgb24_root_converts_only_when_mutated ) {
KittyGraphicsProtocol protocol;
ASSERT_EQ( KittyGraphicsError::None,
protocol.handle( "a=t,f=24,s=2,v=1,i=92;AQIDBAUG" ).error );
auto updates = protocol.takeUpdates();
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
EXPECT_EQ( static_cast<Uint8>( 3 ), updates[0].channels );
ASSERT_EQ( KittyGraphicsError::None,
protocol.handle( "a=f,i=92,r=1,f=32,s=1,v=1,x=1,y=0,X=1;BwgJCg==" ).error );
const std::vector<Uint8> expected{ 1, 2, 3, 255, 7, 8, 9, 10 };
ASSERT_TRUE( protocol.imagePixels( 92 ) != nullptr );
EXPECT_TRUE( expected == *protocol.imagePixels( 92 ) );
updates = protocol.takeUpdates();
ASSERT_EQ( static_cast<size_t>( 2 ), updates.size() );
EXPECT_EQ( TerminalGraphicsUpdateType::ReplaceImage, updates[0].type );
EXPECT_EQ( static_cast<Uint8>( 4 ), updates[0].channels );
EXPECT_EQ( TerminalGraphicsUpdateType::UpdateRegion, updates[1].type );
}
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].pixels != nullptr );
const std::vector<Uint8> expectedFrame{ 1, 2, 3, 4, 9, 10, 11, 12 };
EXPECT_TRUE( expectedFrame == *updates[0].pixels );
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].pixels != nullptr );
const std::vector<Uint8> expectedPatch{ 5, 6, 7, 8 };
EXPECT_TRUE( expectedPatch == *updates[0].pixels );
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 +922,237 @@ 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_bulk_apc_accepts_every_input_split_boundary ) {
const std::string stream = "\033_Ga=T,f=32,s=1,v=1,q=2;AQIDBA==\033\\";
for ( size_t split = 1; split < stream.size(); ++split ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = stream;
pty->mLoopWrites = false;
pty->mReadSizes = { split, stream.size() - split };
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() ) {
}
ASSERT_TRUE( display->mGraphics != nullptr );
ASSERT_EQ( static_cast<size_t>( 1 ), display->mGraphics->placements.size() );
}
}
UTEST( eterm, kitty_graphics_strict_base64_rejects_invalid_payload_bytes ) {
KittyGraphicsProtocol protocol;
EXPECT_EQ( KittyGraphicsError::InvalidData,
protocol.handle( "a=t,f=32,s=1,v=1;AQI BA==" ).error );
EXPECT_EQ( KittyGraphicsError::InvalidData,
protocol.handle( "a=t,f=32,s=1,v=1;AQIDBA=$" ).error );
EXPECT_EQ( KittyGraphicsError::InvalidData,
protocol.handle( "a=t,f=32,s=1,v=1;AQ=DBA==" ).error );
EXPECT_EQ( KittyGraphicsError::InvalidData, protocol.handle( "a=t,f=32,s=1,v=1;AB==" ).error );
EXPECT_EQ( KittyGraphicsError::InvalidData,
protocol.handle( "a=t,f=32,s=1,v=1,m=1;AQ==" ).error );
EXPECT_EQ( KittyGraphicsError::InvalidData,
protocol.handle( "a=t,f=32,s=1,v=1,m=1;AQI!" ).error );
}
UTEST( eterm, kitty_graphics_strict_base64_validates_final_quantum ) {
auto decode = []( std::string_view encoded ) {
std::vector<Uint8> output( Base64::decodeSafeOutLen( encoded.size() ) );
return Base64::decode( encoded.size(), encoded.data(), output.size(), output.data(),
Base64::DecodeMode::NoWhitespaceStrict );
};
EXPECT_EQ( static_cast<size_t>( 1 ), decode( "AA" ) );
EXPECT_EQ( static_cast<size_t>( 2 ), decode( "AAA" ) );
EXPECT_EQ( static_cast<size_t>( 1 ), decode( "AA==" ) );
EXPECT_EQ( static_cast<size_t>( 2 ), decode( "AAA=" ) );
for ( std::string_view invalid :
{ "A", "AB", "AAB", "AB==", "AAB=", "====", "A===", "AA=A", "AAAA=", "AAAA====" } )
EXPECT_EQ( static_cast<size_t>( -1 ), decode( invalid ) );
}
UTEST( eterm, kitty_graphics_strict_base64_decodes_boundaries_and_large_payloads ) {
for ( size_t length = 1; length <= 257; ++length ) {
std::vector<Uint8> boundarySource( length );
for ( size_t i = 0; i < boundarySource.size(); ++i )
boundarySource[i] = static_cast<Uint8>( ( i * 197 + length ) & 0xFF );
std::string boundaryEncoded;
ASSERT_TRUE( Base64::encode(
std::string_view( reinterpret_cast<const char*>( boundarySource.data() ),
boundarySource.size() ),
boundaryEncoded ) );
std::vector<Uint8> boundaryDecoded( Base64::decodeSafeOutLen( boundaryEncoded.size() ) );
const size_t boundaryDecodedSize =
Base64::decode( boundaryEncoded.size(), boundaryEncoded.data(), boundaryDecoded.size(),
boundaryDecoded.data(), Base64::DecodeMode::NoWhitespaceStrict );
ASSERT_EQ( boundarySource.size(), boundaryDecodedSize );
boundaryDecoded.resize( boundaryDecodedSize );
EXPECT_TRUE( boundarySource == boundaryDecoded );
}
std::vector<Uint8> source( 65537 );
for ( size_t i = 0; i < source.size(); ++i )
source[i] = static_cast<Uint8>( ( i * 131 + i / 7 ) & 0xFF );
std::string encoded;
ASSERT_TRUE( Base64::encode(
std::string_view( reinterpret_cast<const char*>( source.data() ), source.size() ),
encoded ) );
std::vector<Uint8> decoded( Base64::decodeSafeOutLen( encoded.size() ) );
const size_t decodedSize =
Base64::decode( encoded.size(), encoded.data(), decoded.size(), decoded.data(),
Base64::DecodeMode::NoWhitespaceStrict );
ASSERT_EQ( source.size(), decodedSize );
decoded.resize( decodedSize );
EXPECT_TRUE( source == decoded );
encoded[encoded.size() / 2] = '!';
EXPECT_EQ( static_cast<size_t>( -1 ),
Base64::decode( encoded.size(), encoded.data(), decoded.size(), decoded.data(),
Base64::DecodeMode::NoWhitespaceStrict ) );
}
UTEST( eterm, kitty_graphics_reuses_unreferenced_replacement_pixel_storage ) {
KittyGraphicsProtocol protocol;
ASSERT_EQ( KittyGraphicsError::None,
protocol.handle( "a=t,f=24,s=2,v=1,i=91,q=2;AQIDBAUG" ).error );
auto updates = protocol.takeUpdates();
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
updates.clear();
const auto* storage = protocol.imagePixels( 91 );
ASSERT_TRUE( storage != nullptr );
const auto* firstAllocation = storage->data();
ASSERT_EQ( KittyGraphicsError::None,
protocol.handle( "a=t,f=24,s=2,v=1,i=91,q=2;BwgJCgsM" ).error );
EXPECT_TRUE( storage == protocol.imagePixels( 91 ) );
const std::vector<Uint8> expected{ 7, 8, 9, 10, 11, 12 };
EXPECT_TRUE( expected == *protocol.imagePixels( 91 ) );
ASSERT_TRUE( firstAllocation != protocol.imagePixels( 91 )->data() );
updates = protocol.takeUpdates();
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
updates.clear();
ASSERT_EQ( KittyGraphicsError::None,
protocol.handle( "a=t,f=24,s=2,v=1,i=91,q=2;DQ4PEBES" ).error );
EXPECT_TRUE( storage == protocol.imagePixels( 91 ) );
const std::vector<Uint8> recycledExpected{ 13, 14, 15, 16, 17, 18 };
EXPECT_TRUE( recycledExpected == *protocol.imagePixels( 91 ) );
EXPECT_TRUE( firstAllocation == protocol.imagePixels( 91 )->data() );
}
UTEST( eterm, kitty_graphics_rejects_shared_memory_transmission ) {
KittyGraphicsProtocol protocol;
EXPECT_EQ( KittyGraphicsError::Unsupported,
protocol.handle( "a=T,t=s,f=24,s=1,v=1,q=2;L2VlcHAtc2ht" ).error );
}
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 +1170,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 +1240,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 );
}
@@ -83,6 +83,7 @@ UTEST( GitConflict, KeepsMergeOperationAfterAllConflictsAreStaged ) {
ASSERT_EQ( EXIT_SUCCESS, run( { "init", "-b", "main" } ) );
ASSERT_EQ( EXIT_SUCCESS, run( { "config", "user.name", "eepp tests" } ) );
ASSERT_EQ( EXIT_SUCCESS, run( { "config", "user.email", "eepp-tests@example.invalid" } ) );
ASSERT_EQ( EXIT_SUCCESS, run( { "config", "rerere.enabled", "false" } ) );
const std::string file = ( temp.path / "conflict.txt" ).string();
ASSERT_TRUE( FileSystem::fileWrite( file, "base\n" ) );
ASSERT_EQ( EXIT_SUCCESS, run( { "add", "conflict.txt" } ) );
@@ -40,9 +40,9 @@ class AIAssistantPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
const LLMProviders& getProviders() { return mProviders; }
@@ -95,9 +95,9 @@ class AutoCompletePlugin : public Plugin {
bool isReady() const override { return true; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
void onRegister( UICodeEditor* ) override;
@@ -68,9 +68,9 @@ class DebuggerPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
void onSaveState( IniFile* state ) override;
@@ -43,9 +43,9 @@ class DiscordRPCplugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
protected:
DiscordIPC mIPC;
@@ -50,9 +50,9 @@ class FormatterPlugin : public Plugin {
std::string getDescription() { return Definition().description; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
virtual String::HashType getConfigFileHash() { return mConfigHash; }
+2 -2
View File
@@ -83,9 +83,9 @@ class GitPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
void onSaveState( IniFile* state ) override;
@@ -76,9 +76,9 @@ class LinterPlugin : public Plugin {
std::string getDescription() { return Definition().description; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
virtual String::HashType getConfigFileHash() { return mConfigHash; }
@@ -39,9 +39,9 @@ class SpellCheckerPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
void drawAfterLineText( UICodeEditor* editor, const Int64& index, Vector2f position,
const Float& fontSize, const Float& lineHeight ) override;
@@ -36,9 +36,9 @@ class XMLToolsPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
bool hasSettingsPage() const override { return true; }
bool hasSettingsPage() const { return true; }
void registerSettings( SettingsPage& page ) override;
void registerSettings( SettingsPage& page );
bool getHighlightMatch() const;