mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-22 13:01:05 +03:00
Fix terminal scroll anFix terminal scroll and selection synchronization
eterm:
- derive scroll and history notifications from adopted snapshots instead
of delayed worker events
- prevent PTY parsing from publishing its temporary live-buffer viewport
- restore scrollback correctly when new output wraps the history ring
- keep selections attached to their text during output and absolute scrolling
- preserve terminal focus and selection while interacting with the scrollbar
- keep the scrollbar available while a selection is active
- suppress application mouse reports when Shift overrides mouse capture
- preserve scrollback when Kitty reports standalone modifier keys
- retain scroll-to-bottom behavior for regular and modified input keys
- remove obsolete worker scroll and history event tracking
- add regression coverage for viewport, selection, focus, keyboard, and
history behaviord selection synchronization
This commit is contained in:
@@ -371,6 +371,7 @@ class TerminalDisplay {
|
||||
bool mAlreadyClickedMButton{ false };
|
||||
bool mKeepAlive{ true };
|
||||
bool mDraggingSel{ false };
|
||||
bool mSelectionOverridesMouseCapture{ false };
|
||||
int mMode{ MODE_VISIBLE | MODE_FOCUSED };
|
||||
TerminalCursorMode mCursorMode{ SteadyUnderline };
|
||||
Clock mClock;
|
||||
|
||||
@@ -326,6 +326,8 @@ class TerminalEmulator final {
|
||||
|
||||
bool mDirty{ true };
|
||||
bool mAllDirty{ true };
|
||||
bool mProcessingPtyInput{ false };
|
||||
int mPtyHistoryLinesPushed{ 0 };
|
||||
Clock mPresentationClock;
|
||||
Time mPresentationInterval{ Microseconds( 1000000.0 / 60.0 ) };
|
||||
Clock mSynchronizedUpdateClock;
|
||||
@@ -394,6 +396,7 @@ class TerminalEmulator final {
|
||||
void csihandle();
|
||||
bool handleKittyKeyboardProtocol();
|
||||
KittyKeyboardState& activeKeyboardState();
|
||||
void ttywriteInternal( const char* s, size_t n, int may_echo, bool scrollToBottom );
|
||||
void resetKittyKeyboardProtocol();
|
||||
void csiparse();
|
||||
void csireset();
|
||||
|
||||
@@ -93,19 +93,18 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
|
||||
using ProcPtr = std::unique_ptr<IProcess>;
|
||||
|
||||
enum class EventType : Uint8 {
|
||||
Title,
|
||||
IconTitle,
|
||||
HistoryLength,
|
||||
ScrollPosition,
|
||||
Bell,
|
||||
Clipboard,
|
||||
ProcessExit,
|
||||
RestartFailure,
|
||||
SnapshotReady,
|
||||
Data,
|
||||
PromptState,
|
||||
Color,
|
||||
Error
|
||||
Title = 0,
|
||||
IconTitle = 1,
|
||||
// Values 2 and 3 were retired; keep later values stable for ABI compatibility.
|
||||
Bell = 4,
|
||||
Clipboard = 5,
|
||||
ProcessExit = 6,
|
||||
RestartFailure = 7,
|
||||
SnapshotReady = 8,
|
||||
Data = 9,
|
||||
PromptState = 10,
|
||||
Color = 11,
|
||||
Error = 12
|
||||
};
|
||||
|
||||
struct Event {
|
||||
|
||||
@@ -170,8 +170,10 @@ class UITerminal : public UITouchDraggableWidget {
|
||||
virtual Uint32 onFocus( NodeFocusReason reason );
|
||||
|
||||
virtual Uint32 onFocusLoss();
|
||||
virtual Uint32 onMessage( const NodeMessage* msg );
|
||||
|
||||
virtual void updateScroll();
|
||||
void syncScrollOffset();
|
||||
|
||||
virtual void onContentSizeChange();
|
||||
|
||||
|
||||
@@ -770,6 +770,7 @@ bool TerminalDisplay::update( bool isMouseOverMe ) {
|
||||
if ( !( mWindow->getInput()->getPressTrigger() & EE_BUTTON_LMASK ) ) {
|
||||
mWindow->getInput()->captureMouse( false );
|
||||
mDraggingSel = false;
|
||||
mSelectionOverridesMouseCapture = false;
|
||||
} else if ( !isMouseOverMe ) {
|
||||
onMouseMove( mWindow->getInput()->getMousePos(),
|
||||
mWindow->getInput()->getPressTrigger() );
|
||||
@@ -809,6 +810,8 @@ void TerminalDisplay::consumeSnapshot() {
|
||||
return;
|
||||
|
||||
const Vector2i previousCursor = mCursor;
|
||||
const int previousHistoryLength = mSnapshot ? mSnapshot->historyLength : 0;
|
||||
const int previousScrollPosition = mSnapshot ? mSnapshot->scrollPosition : 0;
|
||||
const bool dimensionsChanged = snapshot->columns != static_cast<int>( mColumns ) ||
|
||||
snapshot->rows != static_cast<int>( mRows );
|
||||
if ( dimensionsChanged ) {
|
||||
@@ -857,6 +860,13 @@ void TerminalDisplay::consumeSnapshot() {
|
||||
mMode |= MODE_BLINK;
|
||||
mClock.restart();
|
||||
}
|
||||
// Scrollbar state must be announced only after the immutable state it describes has been
|
||||
// adopted. Worker events can otherwise race publication and make the UI feed an older absolute
|
||||
// position back into the session.
|
||||
if ( previousScrollPosition != mSnapshot->scrollPosition )
|
||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
||||
if ( previousHistoryLength != mSnapshot->historyLength )
|
||||
sendEvent( { EventType::HISTORY_LENGTH_CHANGE } );
|
||||
mDirty = true;
|
||||
}
|
||||
|
||||
@@ -871,9 +881,6 @@ void TerminalDisplay::drainSessionEvents() {
|
||||
case TerminalSession::EventType::IconTitle:
|
||||
sendEvent( { EventType::ICON_TITLE, std::move( event.data ) } );
|
||||
break;
|
||||
case TerminalSession::EventType::ScrollPosition:
|
||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
||||
break;
|
||||
case TerminalSession::EventType::Bell:
|
||||
sendEvent( { EventType::BELL } );
|
||||
break;
|
||||
@@ -906,9 +913,6 @@ void TerminalDisplay::drainSessionEvents() {
|
||||
Log::error( "Terminal worker error: %s", event.data.c_str() );
|
||||
sendEvent( { EventType::WORKER_ERROR, std::move( event.data ) } );
|
||||
break;
|
||||
case TerminalSession::EventType::HistoryLength:
|
||||
sendEvent( { EventType::HISTORY_LENGTH_CHANGE } );
|
||||
break;
|
||||
case TerminalSession::EventType::SnapshotReady:
|
||||
break;
|
||||
}
|
||||
@@ -1139,9 +1143,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;
|
||||
const Uint32 modifiers = mWindow->getInput()->getModState();
|
||||
const bool shiftPressed = ( modifiers & KEYMOD_SHIFT ) != 0;
|
||||
auto mousePos = mWindow->getInput()->getRelativeMousePos();
|
||||
bool isCapturingMouse = isAppCapturingMouse() && !shiftPressed;
|
||||
const bool appCapturingMouse = isAppCapturingMouse();
|
||||
const bool selectionOverride =
|
||||
mSelectionOverridesMouseCapture || ( appCapturingMouse && shiftPressed );
|
||||
const bool isCapturingMouse = appCapturingMouse && !selectionOverride;
|
||||
|
||||
if ( !isAltScr() && !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
|
||||
mAlreadyClickedLButton ) {
|
||||
@@ -1163,17 +1171,22 @@ void TerminalDisplay::onMouseMove( const Vector2i& pos, const Uint32& flags ) {
|
||||
( mDraggingSel || getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_READY ) ) {
|
||||
auto gridPos{ positionToGrid( pos ) };
|
||||
mSession->selectionExtend(
|
||||
gridPos.x, gridPos.y,
|
||||
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR,
|
||||
false );
|
||||
gridPos.x, gridPos.y, modifiers & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR, false );
|
||||
}
|
||||
// Shift overrides application mouse capture so the user can select terminal text. Sending the
|
||||
// same event to the application would make the override ineffective.
|
||||
if ( !selectionOverride ) {
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, modifiers );
|
||||
}
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
|
||||
}
|
||||
|
||||
void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
|
||||
bool shiftPressed = ( mWindow->getInput()->getModState() & KEYMOD_SHIFT ) != 0;
|
||||
bool isCapturingMouse = isAppCapturingMouse() && !shiftPressed;
|
||||
const Uint32 modifiers = mWindow->getInput()->getModState();
|
||||
const bool shiftPressed = ( modifiers & KEYMOD_SHIFT ) != 0;
|
||||
const bool appCapturingMouse = isAppCapturingMouse();
|
||||
const bool selectionOverride = appCapturingMouse && shiftPressed;
|
||||
const bool isCapturingMouse = appCapturingMouse && !selectionOverride;
|
||||
|
||||
if ( ( flags & EE_BUTTON_LMASK ) && mDraggingSel )
|
||||
return;
|
||||
@@ -1187,6 +1200,7 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
|
||||
if ( !mDraggingSel ) {
|
||||
mSession->selectionStart( gridPos.x, gridPos.y, 0 );
|
||||
mDraggingSel = true;
|
||||
mSelectionOverridesMouseCapture = selectionOverride;
|
||||
invalidateLines();
|
||||
mWindow->getInput()->captureMouse( true );
|
||||
}
|
||||
@@ -1208,11 +1222,18 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
|
||||
}
|
||||
}
|
||||
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
|
||||
if ( !selectionOverride ) {
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, modifiers );
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
|
||||
const Uint32 modifiers = mWindow->getInput()->getModState();
|
||||
const bool shiftPressed = ( modifiers & KEYMOD_SHIFT ) != 0;
|
||||
const bool appCapturingMouse = isAppCapturingMouse();
|
||||
const bool selectionOverride =
|
||||
mSelectionOverridesMouseCapture || ( appCapturingMouse && shiftPressed );
|
||||
if ( ( flags & EE_BUTTON_LMASK ) && mDraggingSel ) {
|
||||
mDraggingSel = false;
|
||||
}
|
||||
@@ -1221,11 +1242,12 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
|
||||
mWindow->getClipboard()->setPrimarySelectionText( getSelection() );
|
||||
}
|
||||
|
||||
Uint32 smod = sanitizeMod( mWindow->getInput()->getModState() );
|
||||
Uint32 smod = sanitizeMod( modifiers );
|
||||
|
||||
if ( flags & EE_BUTTON_LMASK ) {
|
||||
mAlreadyClickedLButton = false;
|
||||
mWindow->getInput()->captureMouse( false );
|
||||
mSelectionOverridesMouseCapture = false;
|
||||
}
|
||||
|
||||
if ( flags & EE_BUTTON_MMASK )
|
||||
@@ -1255,8 +1277,10 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
|
||||
}
|
||||
}
|
||||
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, mWindow->getInput()->getModState() );
|
||||
if ( !selectionOverride ) {
|
||||
mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ),
|
||||
positionToPixel( pos ), flags, modifiers );
|
||||
}
|
||||
}
|
||||
|
||||
static inline Color termColor( unsigned int terminalColor, const std::vector<Color>& colors ) {
|
||||
|
||||
@@ -947,28 +947,33 @@ size_t TerminalEmulator::ttyread( void ) {
|
||||
mDataCb( mBuf + mBuflen, ret );
|
||||
|
||||
int old_scr = mTerm.scr;
|
||||
int old_histi = mTerm.histi;
|
||||
// Selection coordinates share the viewport's coordinate space. Translate them before
|
||||
// the temporary live-screen switch so parser writes cannot mistake historical text for
|
||||
// a live cell and clear the selection.
|
||||
if ( old_scr > 0 )
|
||||
selmove( -old_scr );
|
||||
mTerm.scr = 0;
|
||||
|
||||
mBuflen += ret;
|
||||
// Parsing must update the live screen at scr == 0, but that temporary viewport is not
|
||||
// presentation state. In particular, DECRST 2026 can call draw() from inside twrite().
|
||||
// Publishing there lets the asynchronous UI mistake the live-screen override for a user
|
||||
// scroll and feed it back through the scrollbar before the viewport is restored below.
|
||||
mPtyHistoryLinesPushed = 0;
|
||||
mProcessingPtyInput = true;
|
||||
written = twrite( mBuf, mBuflen, 0 );
|
||||
mProcessingPtyInput = false;
|
||||
mBuflen -= written;
|
||||
/* keep any incomplete UTF-8 byte sequence for the next call */
|
||||
if ( mBuflen > 0 )
|
||||
memmove( mBuf, mBuf + written, mBuflen );
|
||||
|
||||
if ( old_scr > 0 ) {
|
||||
int lines_pushed = 0;
|
||||
if ( mTerm.histsize > 0 ) {
|
||||
lines_pushed = ( mTerm.histi - old_histi + mTerm.histsize ) % mTerm.histsize;
|
||||
}
|
||||
const int lines_pushed = mPtyHistoryLinesPushed;
|
||||
mTerm.scr = eemin( mTerm.histlen, old_scr + lines_pushed );
|
||||
if ( lines_pushed > 0 ) {
|
||||
mSel.ob.y += lines_pushed;
|
||||
mSel.oe.y += lines_pushed;
|
||||
|
||||
selmove( mTerm.scr );
|
||||
if ( mTerm.scr != old_scr )
|
||||
onScrollPositionChange();
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
@@ -1025,7 +1030,9 @@ void TerminalEmulator::kscrollto( const TerminalArg* a ) {
|
||||
int n = a->i;
|
||||
|
||||
if ( 0 <= n && n <= mTerm.histlen ) {
|
||||
int delta = n - mTerm.scr;
|
||||
mTerm.scr = n;
|
||||
selmove( delta );
|
||||
tfulldirt();
|
||||
onScrollPositionChange();
|
||||
}
|
||||
@@ -1117,10 +1124,17 @@ bool TerminalEmulator::isScrolling() const {
|
||||
}
|
||||
|
||||
void TerminalEmulator::ttywrite( const char* s, size_t n, int may_echo ) {
|
||||
ttywriteInternal( s, n, may_echo, true );
|
||||
}
|
||||
|
||||
void TerminalEmulator::ttywriteInternal( const char* s, size_t n, int may_echo,
|
||||
bool scrollToBottom ) {
|
||||
const char* next;
|
||||
|
||||
TerminalArg arg = { (int)mTerm.scr };
|
||||
kscrolldown( &arg );
|
||||
if ( scrollToBottom ) {
|
||||
TerminalArg arg = { (int)mTerm.scr };
|
||||
kscrolldown( &arg );
|
||||
}
|
||||
|
||||
if ( may_echo && IS_SET( MODE_ECHO ) )
|
||||
twrite( s, (int)n, 1 );
|
||||
@@ -1149,6 +1163,22 @@ static Uint32 keyboardSanitizeMod( Uint32 mod ) {
|
||||
return mod & KEYMOD_CTRL_SHIFT_ALT_META;
|
||||
}
|
||||
|
||||
static bool isModifierKey( Keycode keycode ) {
|
||||
switch ( keycode ) {
|
||||
case KEY_LCTRL:
|
||||
case KEY_LSHIFT:
|
||||
case KEY_LALT:
|
||||
case KEY_LGUI:
|
||||
case KEY_RCTRL:
|
||||
case KEY_RSHIFT:
|
||||
case KEY_RALT:
|
||||
case KEY_RGUI:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static char legacyControlCharacter( Scancode scancode ) {
|
||||
if ( scancode >= SCANCODE_A && scancode <= SCANCODE_Z )
|
||||
return static_cast<char>( scancode - SCANCODE_A + 1 );
|
||||
@@ -1181,7 +1211,8 @@ void TerminalEmulator::keyEvent( const KittyKeyEvent& event ) {
|
||||
}
|
||||
const auto enhanced = KittyKeyboardEncoder::encode( event, flags );
|
||||
if ( enhanced.handled ) {
|
||||
ttywrite( enhanced.bytes.data(), enhanced.bytes.size(), 1 );
|
||||
ttywriteInternal( enhanced.bytes.data(), enhanced.bytes.size(), 1,
|
||||
!isModifierKey( event.keycode ) );
|
||||
mExpectedTextInput = enhanced.expectedText;
|
||||
return;
|
||||
}
|
||||
@@ -1470,6 +1501,8 @@ void TerminalEmulator::tscrollup( int top, int n, int copyhist ) {
|
||||
|
||||
for ( i = 0; i < n; i++ )
|
||||
historyStealPush( &mTerm.line[top + i], mTerm.col );
|
||||
if ( mProcessingPtyInput )
|
||||
mPtyHistoryLinesPushed += n;
|
||||
|
||||
if ( attop )
|
||||
mTerm.scr = mTerm.histlen;
|
||||
@@ -3874,8 +3907,9 @@ void TerminalEmulator::drawregion( ITerminalDisplay& dpy, int x1, int y1, int x2
|
||||
|
||||
void TerminalEmulator::draw() {
|
||||
// DEC private mode 2026 makes the bytes between DECSET and DECRST one presentation unit.
|
||||
// Parsing continues normally, but no partially cleared/rebuilt frame may reach the UI.
|
||||
if ( mTerm.is_syncing )
|
||||
// PTY parsing can also temporarily force scr to zero while updating the live screen. Neither
|
||||
// state is a stable presentation boundary, so no partial frame or viewport may reach the UI.
|
||||
if ( mTerm.is_syncing || mProcessingPtyInput )
|
||||
return;
|
||||
|
||||
int cx = mTerm.c.x /*, ocx = term.ocx, ocy = term.ocy*/;
|
||||
|
||||
@@ -135,12 +135,6 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
|
||||
if ( auto* process = mEmulator->getProcess() )
|
||||
snapshot->processId = process->pid();
|
||||
}
|
||||
if ( snapshot->historyLength != mLastHistoryLength ) {
|
||||
mLastHistoryLength = snapshot->historyLength;
|
||||
Event event{ EventType::HistoryLength };
|
||||
event.value = mLastHistoryLength;
|
||||
mSession.enqueueEvent( std::move( event ), true );
|
||||
}
|
||||
mSession.publishSnapshot( std::move( snapshot ) );
|
||||
}
|
||||
|
||||
@@ -239,8 +233,6 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
|
||||
mSession.enqueueEvent( std::move( event ), false );
|
||||
}
|
||||
|
||||
void onScrollPositionChange() { mSession.enqueueEvent( { EventType::ScrollPosition }, true ); }
|
||||
|
||||
void setPalette( TerminalColorPalette palette ) {
|
||||
mInitialPalette = palette;
|
||||
mPalette = std::move( palette );
|
||||
@@ -284,7 +276,6 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
|
||||
TerminalGlyph mCursorGlyph;
|
||||
int mColumns{ 0 };
|
||||
int mRows{ 0 };
|
||||
int mLastHistoryLength{ -1 };
|
||||
Uint32 mPresentationRate{ 60 };
|
||||
bool mCursorVisible{ false };
|
||||
};
|
||||
@@ -498,10 +489,9 @@ void TerminalSession::enqueueEvent( Event event, bool coalescable ) {
|
||||
std::lock_guard<std::mutex> lock( mEventMutex );
|
||||
if ( coalescable ) {
|
||||
for ( auto it = mEvents.rbegin(); it != mEvents.rend(); ++it ) {
|
||||
const bool replaceable =
|
||||
it->type == EventType::Title || it->type == EventType::IconTitle ||
|
||||
it->type == EventType::HistoryLength || it->type == EventType::ScrollPosition ||
|
||||
it->type == EventType::SnapshotReady;
|
||||
const bool replaceable = it->type == EventType::Title ||
|
||||
it->type == EventType::IconTitle ||
|
||||
it->type == EventType::SnapshotReady;
|
||||
if ( !replaceable )
|
||||
break;
|
||||
if ( it->type == event.type ) {
|
||||
|
||||
@@ -93,7 +93,11 @@ UITerminal::UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay
|
||||
syncFontRenderingConfig();
|
||||
registerNewTerminal();
|
||||
mVScroll->setParent( this );
|
||||
mVScroll->on( Event::OnValueChange, [this]( const Event* ) { updateScroll(); } );
|
||||
mVScroll->on( Event::OnValueChange, [this]( const Event* ) {
|
||||
updateScroll();
|
||||
if ( !mApplyingScrollController )
|
||||
stopScrollController();
|
||||
} );
|
||||
|
||||
setCommand( "terminal-scroll-up-screen",
|
||||
[this] { mTerm->action( TerminalShortcutAction::SCROLLUP_SCREEN ); } );
|
||||
@@ -162,7 +166,9 @@ void UITerminal::onContentSizeChange() {
|
||||
mPendingContentSizeChange = false;
|
||||
updateScrollPosition();
|
||||
mVScroll->setPageStep( contentSize > 0 ? ( visibleArea / (Float)contentSize ) : 1.f );
|
||||
updateScroll();
|
||||
// This is a worker-to-UI state synchronization, not a user scroll. Feeding it back through
|
||||
// onScrollChange() queues a stale absolute position if the worker advances in the meantime.
|
||||
syncScrollOffset();
|
||||
}
|
||||
|
||||
const ScrollBarMode& UITerminal::getVerticalScrollMode() const {
|
||||
@@ -225,15 +231,19 @@ int UITerminal::getScrollableArea() const {
|
||||
}
|
||||
|
||||
void UITerminal::updateScroll() {
|
||||
int totalScroll = getScrollableArea();
|
||||
int initScroll( mScrollOffset );
|
||||
syncScrollOffset();
|
||||
|
||||
if ( initScroll != mScrollOffset )
|
||||
onScrollChange();
|
||||
}
|
||||
|
||||
void UITerminal::syncScrollOffset() {
|
||||
int totalScroll = getScrollableArea();
|
||||
mScrollOffset = 0;
|
||||
|
||||
if ( mVScroll->isVisible() && totalScroll > 0 )
|
||||
mScrollOffset = totalScroll * mVScroll->getValue();
|
||||
|
||||
if ( initScroll != mScrollOffset )
|
||||
onScrollChange();
|
||||
}
|
||||
|
||||
void UITerminal::onScrollChange() {
|
||||
@@ -531,8 +541,7 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& event ) {
|
||||
Uint32 UITerminal::onMouseMove( const Vector2i& position, const Uint32& flags ) {
|
||||
if ( mViewType == ScrollViewType::Overlay && ScrollBarMode::Auto == mVScrollMode ) {
|
||||
mMouseClock.restart();
|
||||
bool visible =
|
||||
!mTerm->isAltScr() && getContentSize() > getVisibleArea() && !mTerm->hasSelection();
|
||||
bool visible = !mTerm->isAltScr() && getContentSize() > getVisibleArea();
|
||||
mVScroll->setVisible( visible )->setEnabled( visible );
|
||||
}
|
||||
|
||||
@@ -647,12 +656,26 @@ Uint32 UITerminal::onFocus( NodeFocusReason reason ) {
|
||||
|
||||
Uint32 UITerminal::onFocusLoss() {
|
||||
getUISceneNode()->getWindow()->stopTextInput();
|
||||
mTerm->clearSuppressedKeys();
|
||||
mTerm->setFocus( false );
|
||||
Node* focusNode = getEventDispatcher()->getFocusNode();
|
||||
const bool scrollBarFocus = focusNode == mVScroll || mVScroll->isParentOf( focusNode );
|
||||
if ( !scrollBarFocus ) {
|
||||
mTerm->clearSuppressedKeys();
|
||||
mTerm->setFocus( false );
|
||||
}
|
||||
invalidateDraw();
|
||||
return UIWidget::onFocusLoss();
|
||||
}
|
||||
|
||||
Uint32 UITerminal::onMessage( const NodeMessage* msg ) {
|
||||
if ( msg->getMsg() == NodeMessage::Focus &&
|
||||
( msg->getSender() == mVScroll || mVScroll->isParentOf( msg->getSender() ) ) ) {
|
||||
// The scrollbar is part of the terminal. Keep keyboard/PTY focus on the terminal instead
|
||||
// of reporting a transient focus-out/focus-in pair to the application.
|
||||
setFocus();
|
||||
}
|
||||
return UITouchDraggableWidget::onMessage( msg );
|
||||
}
|
||||
|
||||
void UITerminal::createDefaultContextMenuOptions( UIPopUpMenu* menu ) {
|
||||
if ( !mCreateDefaultContextMenuOptions )
|
||||
return;
|
||||
|
||||
@@ -293,6 +293,28 @@ UTEST( eterm_session, focus_reporting_is_ordered_on_worker ) {
|
||||
EXPECT_STDSTREQ( "\033[I", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 3 ) );
|
||||
}
|
||||
|
||||
UTEST( eterm_session, focus_commands_do_not_clear_selection ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "persistent selection";
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
|
||||
ASSERT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
|
||||
return !snapshot.cells.empty() && snapshot.cells[0].u == 'p';
|
||||
} ) != nullptr );
|
||||
|
||||
session->selectionStart( 0, 0, 0 );
|
||||
session->selectionExtend( 9, 0, SEL_REGULAR, false );
|
||||
auto selected = session->requestSelection();
|
||||
ASSERT_TRUE( selected.has_value() );
|
||||
ASSERT_STDSTREQ( "persistent", *selected );
|
||||
|
||||
session->setFocus( false );
|
||||
session->setFocus( true );
|
||||
auto afterFocusChange = session->requestSelection();
|
||||
ASSERT_TRUE( afterFocusChange.has_value() );
|
||||
EXPECT_STDSTREQ( "persistent", *afterFocusChange );
|
||||
}
|
||||
|
||||
UTEST( eterm_session, replaceable_events_coalesce_without_losing_ordered_events ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
@@ -421,6 +443,7 @@ class MockDisplay : public ITerminalDisplay {
|
||||
public:
|
||||
int mDrawLines{ 0 };
|
||||
int mDrawEnds{ 0 };
|
||||
std::vector<int> mPublishedScrollPositions;
|
||||
uint32_t mFirstMode{ 0 };
|
||||
uint32_t mSecondMode{ 0 };
|
||||
TerminalGlyph mFirstGlyph;
|
||||
@@ -440,7 +463,11 @@ class MockDisplay : public ITerminalDisplay {
|
||||
}
|
||||
}
|
||||
void drawCursor( int, int, TerminalGlyph, int, int, TerminalGlyph ) override {}
|
||||
void drawEnd() override { ++mDrawEnds; }
|
||||
void drawEnd() override {
|
||||
++mDrawEnds;
|
||||
if ( mEmulator )
|
||||
mPublishedScrollPositions.emplace_back( mEmulator->scrollPos() );
|
||||
}
|
||||
void resetColors() override { ++mResetColorsCount; }
|
||||
void drawGraphics( std::shared_ptr<TerminalGraphicsPresentation> presentation,
|
||||
std::vector<TerminalGraphicsUpdate> ) override {
|
||||
@@ -1048,6 +1075,34 @@ UTEST( eterm, kitty_keyboard_protocol_encodes_worker_key_without_duplicate_text
|
||||
EXPECT_STDSTREQ( "\033[97;1u\033[13;5u", ptyPtr->mWrites );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_modifier_key_does_not_scroll_to_bottom ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033[>8u";
|
||||
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();
|
||||
|
||||
for ( int i = 0; i < 40; ++i ) {
|
||||
std::string line = "Line " + std::to_string( i ) + "\r\n";
|
||||
term->write( line.c_str(), line.size() );
|
||||
term->update();
|
||||
}
|
||||
ptyPtr->mLoopWrites = false;
|
||||
|
||||
TerminalArg scroll( 5 );
|
||||
term->kscrollup( &scroll );
|
||||
ptyPtr->mWrites.clear();
|
||||
|
||||
term->keyEvent( { KEY_LCTRL, SCANCODE_LCTRL, 0, KEYMOD_LCTRL, KittyKeyEventType::Press } );
|
||||
EXPECT_FALSE( ptyPtr->mWrites.empty() );
|
||||
EXPECT_EQ( 5, term->scrollPos() );
|
||||
|
||||
term->keyEvent( { KEY_A, SCANCODE_A, 0, KEYMOD_LCTRL, KittyKeyEventType::Press } );
|
||||
EXPECT_EQ( 0, term->scrollPos() );
|
||||
}
|
||||
|
||||
UTEST( eterm, kitty_keyboard_protocol_preserves_altgr_text ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
pty->mBuffer = "\033[>15u";
|
||||
@@ -1464,6 +1519,33 @@ UTEST( eterm, synchronized_updates_publish_only_complete_frames ) {
|
||||
EXPECT_STDSTREQ( "complete", term->getSelection() );
|
||||
}
|
||||
|
||||
UTEST( eterm, pty_parsing_does_not_publish_temporary_bottom_viewport ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
|
||||
for ( int line = 0; line < 40; ++line ) {
|
||||
const std::string output = "history " + std::to_string( line ) + "\r\n";
|
||||
term->write( output.data(), output.size() );
|
||||
term->update();
|
||||
}
|
||||
|
||||
TerminalArg scroll( 5 );
|
||||
term->kscrollup( &scroll );
|
||||
ASSERT_EQ( 5, term->scrollPos() );
|
||||
display->mPublishedScrollPositions.clear();
|
||||
|
||||
const char synchronizedOutput[] = "\033[?2026hnew output\r\n\033[?2026l";
|
||||
term->write( synchronizedOutput, sizeof( synchronizedOutput ) - 1 );
|
||||
term->update();
|
||||
|
||||
ASSERT_FALSE( display->mPublishedScrollPositions.empty() );
|
||||
for ( int scrollPosition : display->mPublishedScrollPositions )
|
||||
EXPECT_TRUE( scrollPosition > 0 );
|
||||
EXPECT_EQ( 6, display->mPublishedScrollPositions.back() );
|
||||
}
|
||||
|
||||
UTEST( eterm, sgr_colon_subparameters_preserve_groups_and_optional_color_space ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
@@ -2272,6 +2354,88 @@ UTEST( eterm, scroll_position_after_ttyread ) {
|
||||
EXPECT_STDSTREQ( "New output", term->getSelection() );
|
||||
}
|
||||
|
||||
UTEST( eterm, ttyread_keeps_scrolled_selection_attached_to_text ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
|
||||
for ( int i = 0; i < 40; ++i ) {
|
||||
std::string line = "Line " + std::to_string( i ) + "\r\n";
|
||||
term->write( line.c_str(), line.size() );
|
||||
term->update();
|
||||
}
|
||||
|
||||
TerminalArg scroll( 5 );
|
||||
term->kscrollup( &scroll );
|
||||
term->selstart( 0, 23, 0 );
|
||||
term->selextend( 6, 23, SEL_REGULAR, false );
|
||||
ASSERT_STDSTREQ( "Line 35", term->getSelection() );
|
||||
|
||||
term->write( "New output\r\n", 12 );
|
||||
term->update();
|
||||
|
||||
EXPECT_EQ( 6, term->scrollPos() );
|
||||
EXPECT_STDSTREQ( "Line 35", term->getSelection() );
|
||||
}
|
||||
|
||||
UTEST( eterm, ttyread_restores_viewport_after_history_ring_wrap ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 4 );
|
||||
|
||||
for ( int i = 0; i < 28; ++i ) {
|
||||
std::string line = "Line " + std::to_string( i ) + "\r\n";
|
||||
term->write( line.c_str(), line.size() );
|
||||
term->update();
|
||||
}
|
||||
|
||||
TerminalArg scroll( 2 );
|
||||
term->kscrollup( &scroll );
|
||||
ASSERT_EQ( 2, term->scrollPos() );
|
||||
|
||||
// One PTY read pushes exactly histsize rows, wrapping histi back to its original index.
|
||||
// The viewport must still account for all four pushed rows and clamp to the oldest history.
|
||||
const char burst[] = "Burst 0\r\nBurst 1\r\nBurst 2\r\nBurst 3\r\n";
|
||||
term->write( burst, sizeof( burst ) - 1 );
|
||||
term->update();
|
||||
EXPECT_EQ( 4, term->scrollPos() );
|
||||
|
||||
TerminalArg bottom( INT_MAX );
|
||||
term->kscrolldown( &bottom );
|
||||
term->selstart( 0, 22, 0 );
|
||||
term->selextend( 6, 22, SEL_REGULAR, false );
|
||||
EXPECT_STDSTREQ( "Burst 3", term->getSelection() );
|
||||
}
|
||||
|
||||
UTEST( eterm, absolute_scrolling_keeps_selection_attached_to_text ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
||||
|
||||
for ( int i = 0; i < 40; ++i ) {
|
||||
std::string line = "Line " + std::to_string( i ) + "\r\n";
|
||||
term->write( line.c_str(), line.size() );
|
||||
term->update();
|
||||
}
|
||||
|
||||
TerminalArg scroll( 5 );
|
||||
term->kscrollto( &scroll );
|
||||
term->selstart( 0, 23, 0 );
|
||||
term->selextend( 6, 23, SEL_REGULAR, false );
|
||||
ASSERT_STDSTREQ( "Line 35", term->getSelection() );
|
||||
|
||||
scroll.i = 10;
|
||||
term->kscrollto( &scroll );
|
||||
EXPECT_STDSTREQ( "Line 35", term->getSelection() );
|
||||
|
||||
scroll.i = 2;
|
||||
term->kscrollto( &scroll );
|
||||
EXPECT_STDSTREQ( "Line 35", term->getSelection() );
|
||||
}
|
||||
|
||||
UTEST( eterm, history_corruption_on_resize ) {
|
||||
auto pty = std::make_unique<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
|
||||
Reference in New Issue
Block a user