Terminal: multiple issues with the copy/paste operations fixed. Now uses the primary selection when corresponds (middle mouse click, shift+insert). Fixed paste operation, it does not hang the app, pastes instantly contents and behaves exactly the same as other terminals.

Incorporated new features into eterm.
This commit is contained in:
Martín Lucas Golini
2025-12-20 01:58:21 -03:00
parent e5a67727c4
commit 35fbf7c733
12 changed files with 195 additions and 49 deletions

View File

@@ -21,6 +21,12 @@ class EE_API Clipboard {
/** @return The parent window of the clipboard */
EE::Window::Window* getWindow() const;
/** @return The Clipboard Primary Selection Text if available */
virtual std::string getPrimarySelectionText() { return ""; }
/** Set the current clipboard primary selection text */
virtual void setPrimarySelectionText( const std::string& text ) {}
protected:
friend class Window;

View File

@@ -172,9 +172,12 @@ class EE_API Input {
/** Pop the callback id indicated. */
void popCallback( const Uint32& CallbackId );
/** @return The Mouse position vector */
/** @return The Mouse position */
Vector2i getMousePos() const;
/** @return The Mouse position with no clamping */
Vector2i getRelativeMousePos() const;
/** This will change the value of the mouse pos, will not REALLY move the mouse ( for that is
* InjectMousePos ). */
void setMousePos( const Vector2i& Pos );
@@ -287,6 +290,7 @@ class EE_API Input {
Uint32 mLastButtonMiddleClick;
Uint32 mTClick;
Vector2i mMousePos;
Vector2i mRealMousePos;
Uint32 mNumCallBacks;
Float mMouseSpeed;
bool mInputGrabbed;

View File

@@ -49,6 +49,26 @@ std::string ClipboardSDL::getText() {
#endif
}
std::string ClipboardSDL::getPrimarySelectionText() {
#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
return "";
#else
if ( SDL_HasPrimarySelectionText() ) {
char* text = SDL_GetPrimarySelectionText();
std::string str( text );
SDL_free( text );
return str;
}
return "";
#endif
}
void ClipboardSDL::setPrimarySelectionText( const std::string& text ) {
#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
SDL_SetPrimarySelectionText( text.c_str() );
#endif
}
String ClipboardSDL::getWideText() {
return String::fromUtf8( getText() );
}

View File

@@ -21,6 +21,10 @@ class EE_API ClipboardSDL : public Clipboard {
void setText( const std::string& text );
std::string getPrimarySelectionText();
void setPrimarySelectionText( const std::string& text );
protected:
friend class WindowSDL;

View File

@@ -95,6 +95,8 @@ void Input::processEvent( InputEvent* Event ) {
mMousePos.y += static_cast<Int32>( (Float)Event->motion.yrel * mMouseSpeed );
}
mRealMousePos = mMousePos;
if ( mMousePos.x >= (int)mWindow->getWidth() ) {
mMousePos.x = mWindow->getWidth();
} else if ( mMousePos.x < 0 ) {
@@ -308,6 +310,10 @@ Vector2i Input::getMousePos() const {
return mMousePos;
}
Vector2i Input::getRelativeMousePos() const {
return mRealMousePos;
}
void Input::setMousePos( const Vector2i& Pos ) {
mMousePos = Pos;
}

View File

@@ -79,6 +79,8 @@ class PseudoTerminal final : public IPseudoTerminal {
AutoHandle mMaster;
AutoHandle mSlave;
std::string mWriteBuffer;
PseudoTerminal( int columns, int rows, AutoHandle&& master, AutoHandle&& slave );
#endif
};

View File

@@ -14,7 +14,6 @@
#include <eterm/terminal/terminalcolorscheme.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <atomic>
#include <memory>
#include <unordered_map>
#include <vector>
@@ -33,6 +32,7 @@ namespace eterm { namespace Terminal {
enum class TerminalShortcutAction {
PASTE,
PASTE_SELECTION,
COPY,
SCROLLUP_ROW,
SCROLLDOWN_ROW,
@@ -283,7 +283,6 @@ class TerminalDisplay : public ITerminalDisplay {
std::vector<TerminalGlyph> mBuffer;
std::vector<Color> mColors;
std::shared_ptr<TerminalEmulator> mTerminal;
mutable String mClipboard;
mutable std::string mClipboardUtf8;
Uint32 mNumCallBacks;
std::map<Uint32, EventFunc> mCallbacks;
@@ -371,6 +370,8 @@ class TerminalDisplay : public ITerminalDisplay {
void drawBg( bool toFBO = false );
void sanitizeInput( std::string& input );
};
}} // namespace eterm::Terminal

View File

@@ -27,6 +27,7 @@
#include <string.h>
#include <string>
#include <string_view>
#include <unordered_map>
using namespace std::literals;
@@ -82,6 +83,17 @@ struct TerminalCursorHelper {
}
return "steady_underline";
}
static std::unordered_map<std::string, TerminalCursorMode> getTerminalCursorModeMap() {
return {
{ "blinking_block", TerminalCursorMode::BlinkingBlock },
{ "steady_block", TerminalCursorMode::SteadyBlock },
{ "blink_underline", TerminalCursorMode::BlinkUnderline },
{ "steady_underline", TerminalCursorMode::SteadyUnderline },
{ "blink_bar", TerminalCursorMode::BlinkBar },
{ "steady_bar", TerminalCursorMode::SteadyBar },
};
}
};
enum TerminalWinMode {

View File

@@ -42,11 +42,13 @@
#include <unistd.h>
using namespace EE::System;
#if defined( EE_PLATFORM_POSIX )
#include <fcntl.h>
#endif
namespace eterm { namespace Terminal {
#if EE_PLATFORM == EE_PLATFORM_ANDROID
#include <fcntl.h>
int openpty( int* amaster, int* aslave, char* name, struct termios* termp, struct winsize* winp ) {
int master, slave;
char* name_slave;
@@ -129,20 +131,29 @@ int PseudoTerminal::getNumRows() const {
}
int PseudoTerminal::write( const char* s, size_t n ) {
size_t c = n;
while ( n > 0 ) {
ssize_t r = ::write( (int)mMaster, s, n );
if ( r < 0 ) {
return -1;
}
n -= r;
s += r;
if ( !mWriteBuffer.empty() ) {
mWriteBuffer.append( s, n );
return n;
}
return c;
ssize_t r = ::write( (int)mMaster, s, n );
if ( r < 0 ) {
if ( errno == EAGAIN || errno == EWOULDBLOCK ) {
mWriteBuffer.append( s, n );
return n;
}
return -1;
}
if ( (size_t)r < n ) {
mWriteBuffer.append( s + r, n - r );
}
return n;
}
int PseudoTerminal::read( char* s, size_t n, bool block ) {
if ( !block ) {
if ( mWriteBuffer.empty() && !block ) {
struct pollfd pfd;
pfd.fd = mMaster.handle();
pfd.events = POLLIN;
@@ -155,7 +166,36 @@ int PseudoTerminal::read( char* s, size_t n, bool block ) {
perror( "PseudoTerminal::read(poll)" );
return -1;
}
} else if ( !mWriteBuffer.empty() || block ) {
struct pollfd pfd;
pfd.fd = mMaster.handle();
pfd.events = POLLIN;
if ( !mWriteBuffer.empty() )
pfd.events |= POLLOUT;
pfd.revents = 0;
auto i = poll( &pfd, 1, block ? -1 : 0 );
if ( i < 0 && errno != EAGAIN && errno != EINTR ) {
perror( "PseudoTerminal::read(poll)" );
return -1;
}
if ( ( pfd.revents & POLLOUT ) && !mWriteBuffer.empty() ) {
ssize_t r = ::write( (int)mMaster, mWriteBuffer.c_str(), mWriteBuffer.size() );
if ( r > 0 ) {
if ( (size_t)r == mWriteBuffer.size() ) {
mWriteBuffer.clear();
} else {
mWriteBuffer = mWriteBuffer.substr( r );
}
} else if ( r < 0 && errno != EAGAIN && errno != EWOULDBLOCK ) {
perror( "PseudoTerminal::read(write)" ); // Log error but don't fail read
}
}
if ( !( pfd.revents & POLLIN ) )
return 0;
}
ssize_t r = ::read( mMaster.handle(), s, n );
return (int)r;
}
@@ -180,6 +220,9 @@ std::unique_ptr<PseudoTerminal> PseudoTerminal::create( int columns, int rows )
return nullptr;
}
int flags = fcntl( master, F_GETFL, 0 );
fcntl( master, F_SETFL, flags | O_NONBLOCK );
return std::unique_ptr<PseudoTerminal>(
new PseudoTerminal( columns, rows, std::move( master ), std::move( slave ) ) );
#endif

View File

@@ -75,7 +75,7 @@ TerminalKeyMap::TerminalKeyMap( const TerminalKey keys[], size_t keysLen,
}
static TerminalShortcut shortcuts[] = {
{ KEY_INSERT, KEYMOD_SHIFT, TerminalShortcutAction::PASTE, 0, 0 },
{ KEY_INSERT, KEYMOD_SHIFT, TerminalShortcutAction::PASTE_SELECTION, 0, 0 },
{ KEY_V, KEYMOD_SHIFT | KEYMOD_CTRL, TerminalShortcutAction::PASTE, 0, 0 },
{ KEY_C, KEYMOD_SHIFT | KEYMOD_CTRL, TerminalShortcutAction::COPY, 0, 0 },
{ KEY_PAGEUP, KEYMOD_SHIFT, TerminalShortcutAction::SCROLLUP_SCREEN, 0, 0, -1 },
@@ -92,7 +92,9 @@ static TerminalMouseShortcut mouseShortcuts[] = {
{ EE_BUTTON_WUMASK, 0, TerminalShortcutAction::SCROLLUP_ROW, 0, 0, -1 },
{ EE_BUTTON_WDMASK, 0, TerminalShortcutAction::SCROLLDOWN_ROW, 0, 0, -1 },
{ EE_BUTTON_WUMASK, KEYMOD_CTRL, TerminalShortcutAction::FONTSIZE_GROW, 0, 0, 0 },
{ EE_BUTTON_WDMASK, KEYMOD_CTRL, TerminalShortcutAction::FONTSIZE_SHRINK, 0, 0, 0 } };
{ EE_BUTTON_WDMASK, KEYMOD_CTRL, TerminalShortcutAction::FONTSIZE_SHRINK, 0, 0, 0 },
{ EE_BUTTON_MMASK, 0, TerminalShortcutAction::PASTE_SELECTION, 0, 0, -1 },
};
static TerminalKey keys[] = {
/* keysym mask string appkey appcursor */
@@ -678,8 +680,29 @@ void TerminalDisplay::action( TerminalShortcutAction action ) {
switch ( action ) {
case TerminalShortcutAction::PASTE: {
getClipboard();
if ( !mClipboard.empty() )
mTerminal->ttywrite( mClipboardUtf8.c_str(), mClipboardUtf8.size(), 1 );
if ( !mClipboardUtf8.empty() ) {
if ( mMode & MODE_BRCKTPASTE ) {
mTerminal->write( "\033[200~", 6 );
mTerminal->write( mClipboardUtf8.c_str(), mClipboardUtf8.size() );
mTerminal->write( "\033[201~", 6 );
} else {
mTerminal->write( mClipboardUtf8.c_str(), mClipboardUtf8.size() );
}
}
break;
}
case TerminalShortcutAction::PASTE_SELECTION: {
std::string selection = mWindow->getClipboard()->getPrimarySelectionText();
sanitizeInput( selection );
if ( !selection.empty() ) {
if ( mMode & MODE_BRCKTPASTE ) {
mTerminal->write( "\033[200~", 6 );
mTerminal->write( selection.c_str(), selection.size() );
mTerminal->write( "\033[201~", 6 );
} else {
mTerminal->write( selection.c_str(), selection.size() );
}
}
break;
}
case TerminalShortcutAction::COPY: {
@@ -752,24 +775,37 @@ void TerminalDisplay::setIconTitle( const char* title ) {
void TerminalDisplay::setClipboard( const char* text ) {
if ( text == nullptr )
return;
mClipboard = text;
mClipboardUtf8 = mClipboard.toUtf8();
mWindow->getClipboard()->setText( mClipboard );
mWindow->getClipboard()->setText( text );
}
const char* TerminalDisplay::getClipboard() const {
mClipboard = mWindow->getClipboard()->getText();
#ifdef _WIN32
if ( mPasteNewlineFix ) {
size_t pos;
while ( ( pos = mClipboard.find( '\r' ) ) != std::string::npos )
mClipboard.erase( pos, 1 );
}
#endif
mClipboardUtf8 = mClipboard.toUtf8();
mClipboardUtf8 = mWindow->getClipboard()->getText();
const_cast<TerminalDisplay*>( this )->sanitizeInput( mClipboardUtf8 );
return mClipboardUtf8.c_str();
}
void TerminalDisplay::sanitizeInput( std::string& input ) {
if ( !mPasteNewlineFix )
return;
// Replace isolated \n (not part of \r\n) with \r
// This handles Unix-style clipboard text (\n) → simulate Enter as \r
// Preserves \r\n (Windows) as a single line ending → \r
// Keeps lone \r as-is
size_t pos = 0;
while ( ( pos = input.find( '\n', pos ) ) != std::string::npos ) {
if ( pos == 0 || input[pos - 1] != '\r' ) {
input.replace( pos, 1, "\r" );
} else {
// Part of \r\n: remove the \n, keep the preceding \r
input.erase( pos, 1 );
++pos; // Skip over the kept \r if needed
}
++pos;
}
}
bool TerminalDisplay::drawBegin( Uint32 columns, Uint32 rows ) {
if ( columns != mColumns || rows != mRows ) {
TerminalGlyph defaultGlyph{};
@@ -832,12 +868,13 @@ void TerminalDisplay::onMouseDoubleClick( const Vector2i& pos, const Uint32& fla
void TerminalDisplay::onMouseMove( const Vector2i& pos, const Uint32& flags ) {
bool shiftPressed = ( mWindow->getInput()->getModState() & KEYMOD_SHIFT ) != 0;
auto mousePos = mWindow->getInput()->getRelativeMousePos();
bool isCapturingMouse = isAppCapturingMouse() && !shiftPressed;
if ( !isAltScr() && !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
mAlreadyClickedLButton ) {
Vector2f relPos = { pos.x - mPosition.x - mPadding.Left,
pos.y - mPosition.y - mPadding.Top };
Vector2f relPos = { mousePos.x - mPosition.x - mPadding.Left,
mousePos.y - mPosition.y - mPadding.Top };
if ( mLastAutoScroll.getElapsedTime() >= Milliseconds( 16 ) ) {
if ( relPos.y < 0 ) {
@@ -882,22 +919,8 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
invalidateLines();
mWindow->getInput()->captureMouse( true );
}
} else if ( flags & EE_BUTTON_MMASK ) {
if ( !mAlreadyClickedMButton ) {
mAlreadyClickedMButton = true;
auto selection = mTerminal->getSelection();
if ( !selection.empty() && selection != "\n" ) {
for ( auto& chr : selection )
onTextInput( chr );
} else {
getClipboard();
if ( !mClipboard.empty() ) {
for ( auto& chr : mClipboard )
onTextInput( chr );
}
}
}
}
if ( ( flags & EE_BUTTON_LMASK ) ) {
if ( !mAlreadyClickedLButton ) {
mAlreadyClickedLButton = true;
@@ -905,6 +928,15 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
return;
}
}
if ( ( flags & EE_BUTTON_MMASK ) ) {
if ( !mAlreadyClickedMButton ) {
mAlreadyClickedMButton = true;
} else {
return;
}
}
mTerminal->mousereport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
}
@@ -914,6 +946,11 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
mDraggingSel = false;
}
if ( flags & EE_BUTTON_LMASK ) {
auto selection = mTerminal->getSelection();
mWindow->getClipboard()->setPrimarySelectionText( selection );
}
Uint32 smod = sanitizeMod( mWindow->getInput()->getModState() );
if ( flags & EE_BUTTON_LMASK ) {

View File

@@ -103,6 +103,8 @@ UITerminal::UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay
setCommand( "terminal-font-size-shrink",
[this] { mTerm->action( TerminalShortcutAction::FONTSIZE_SHRINK ); } );
setCommand( "terminal-paste", [this] { mTerm->action( TerminalShortcutAction::PASTE ); } );
setCommand( "terminal-paste-selection",
[this] { mTerm->action( TerminalShortcutAction::PASTE_SELECTION ); } );
setCommand( "terminal-copy", [this] { mTerm->action( TerminalShortcutAction::COPY ); } );
setCommand( "terminal-open-link",
[this] { Engine::instance()->openURI( mTerm->getTerminal()->getSelection() ); } );

View File

@@ -196,6 +196,12 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
"Maximum rendering frames per second of the terminal. Default "
"value will be the refresh rate of the screen.",
{ "max-fps" }, 0 );
args::MapFlag<std::string, TerminalCursorMode> cursorStyle(
parser, "cursor-style",
"Sets the cursor-style (accepted values: blinking_block, steady_block, blink_underline, "
"steady_underline, blink_bar, steady_bar)",
{ "cursor-style" }, TerminalCursorHelper::getTerminalCursorModeMap(),
TerminalCursorMode::SteadyUnderline );
args::Flag benchmarkModeFlag(
parser, "benchmark-mode",
"Render as much as possible to measure the rendering performance.", { "benchmark-mode" } );
@@ -322,6 +328,7 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
}
terminal->getTerminal()->setAllowMemoryTrimnming( true );
terminal->setCursorMode( cursorStyle.Get() );
terminal->pushEventCallback( [&closeOnExit]( const TerminalDisplay::Event& event ) {
if ( event.type == TerminalDisplay::EventType::TITLE ) {
windowStringData = event.eventData;
@@ -353,9 +360,11 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
win->runMainLoop( [fontMono] {
bool termNeedsUpdate = false;
win->getInput()->update();
auto mousePos = win->getInput()->getRelativeMousePos();
bool mouseOutsideBounds = mousePos.y < 0 || mousePos.y > win->getSize().getHeight();
if ( terminal )
termNeedsUpdate = !terminal->update();
termNeedsUpdate = !terminal->update( !mouseOutsideBounds );
if ( ( terminal && ( benchmarkMode || terminal->isDirty() ) &&
( !termNeedsUpdate || lastRender.getElapsedTime() >= frameTime ) ) ||