From 35fbf7c733af780ded38d1f31663592137ce957e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 20 Dec 2025 01:58:21 -0300 Subject: [PATCH] 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. --- include/eepp/window/clipboard.hpp | 6 + include/eepp/window/input.hpp | 6 +- .../window/backend/SDL2/clipboardsdl2.cpp | 20 ++++ .../window/backend/SDL2/clipboardsdl2.hpp | 4 + src/eepp/window/input.cpp | 6 + .../include/eterm/terminal/pseudoterminal.hpp | 2 + .../eterm/terminal/terminaldisplay.hpp | 5 +- .../include/eterm/terminal/terminaltypes.hpp | 12 ++ .../src/eterm/terminal/pseudoterminal.cpp | 67 ++++++++++-- .../src/eterm/terminal/terminaldisplay.cpp | 103 ++++++++++++------ src/modules/eterm/src/eterm/ui/uiterminal.cpp | 2 + src/tools/eterm/eterm.cpp | 11 +- 12 files changed, 195 insertions(+), 49 deletions(-) diff --git a/include/eepp/window/clipboard.hpp b/include/eepp/window/clipboard.hpp index acd7bad56..594e31b8c 100644 --- a/include/eepp/window/clipboard.hpp +++ b/include/eepp/window/clipboard.hpp @@ -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; diff --git a/include/eepp/window/input.hpp b/include/eepp/window/input.hpp index 219febc64..d37d665cf 100644 --- a/include/eepp/window/input.hpp +++ b/include/eepp/window/input.hpp @@ -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; diff --git a/src/eepp/window/backend/SDL2/clipboardsdl2.cpp b/src/eepp/window/backend/SDL2/clipboardsdl2.cpp index 92e9d0d04..6bae880a4 100644 --- a/src/eepp/window/backend/SDL2/clipboardsdl2.cpp +++ b/src/eepp/window/backend/SDL2/clipboardsdl2.cpp @@ -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() ); } diff --git a/src/eepp/window/backend/SDL2/clipboardsdl2.hpp b/src/eepp/window/backend/SDL2/clipboardsdl2.hpp index 43c264f08..6832454ed 100644 --- a/src/eepp/window/backend/SDL2/clipboardsdl2.hpp +++ b/src/eepp/window/backend/SDL2/clipboardsdl2.hpp @@ -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; diff --git a/src/eepp/window/input.cpp b/src/eepp/window/input.cpp index 6900a32aa..f8de3679d 100644 --- a/src/eepp/window/input.cpp +++ b/src/eepp/window/input.cpp @@ -95,6 +95,8 @@ void Input::processEvent( InputEvent* Event ) { mMousePos.y += static_cast( (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; } diff --git a/src/modules/eterm/include/eterm/terminal/pseudoterminal.hpp b/src/modules/eterm/include/eterm/terminal/pseudoterminal.hpp index aebab07b0..a1be5478c 100644 --- a/src/modules/eterm/include/eterm/terminal/pseudoterminal.hpp +++ b/src/modules/eterm/include/eterm/terminal/pseudoterminal.hpp @@ -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 }; diff --git a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp index c16f2a60c..ba0ce901a 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -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 mBuffer; std::vector mColors; std::shared_ptr mTerminal; - mutable String mClipboard; mutable std::string mClipboardUtf8; Uint32 mNumCallBacks; std::map mCallbacks; @@ -371,6 +370,8 @@ class TerminalDisplay : public ITerminalDisplay { void drawBg( bool toFBO = false ); + void sanitizeInput( std::string& input ); + }; }} // namespace eterm::Terminal diff --git a/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp b/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp index a4ba1e00c..8d0865096 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp @@ -27,6 +27,7 @@ #include #include #include +#include using namespace std::literals; @@ -82,6 +83,17 @@ struct TerminalCursorHelper { } return "steady_underline"; } + + static std::unordered_map 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 { diff --git a/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp b/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp index 7e56062b6..3589be0a6 100644 --- a/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp +++ b/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp @@ -42,11 +42,13 @@ #include using namespace EE::System; +#if defined( EE_PLATFORM_POSIX ) +#include +#endif + namespace eterm { namespace Terminal { #if EE_PLATFORM == EE_PLATFORM_ANDROID -#include - 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::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( new PseudoTerminal( columns, rows, std::move( master ), std::move( slave ) ) ); #endif diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp index 99580e415..bcd902f93 100644 --- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp @@ -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( 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 ) { diff --git a/src/modules/eterm/src/eterm/ui/uiterminal.cpp b/src/modules/eterm/src/eterm/ui/uiterminal.cpp index ad25355f2..7ba9d1769 100644 --- a/src/modules/eterm/src/eterm/ui/uiterminal.cpp +++ b/src/modules/eterm/src/eterm/ui/uiterminal.cpp @@ -103,6 +103,8 @@ UITerminal::UITerminal( const std::shared_ptr& 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() ); } ); diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index aae2977a0..223f28409 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -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 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 ) ) ||