@@ -17,6 +17,7 @@ import appSettings from "lib/settings";
1717import LigaturesAddon from "./ligatures" ;
1818import { getTerminalSettings } from "./terminalDefaults" ;
1919import TerminalThemeManager from "./terminalThemeManager" ;
20+ import TerminalTouchSelection from "./terminalTouchSelection" ;
2021
2122export default class TerminalComponent {
2223 constructor ( options = { } ) {
@@ -55,6 +56,7 @@ export default class TerminalComponent {
5556 this . pid = null ;
5657 this . isConnected = false ;
5758 this . serverMode = options . serverMode !== false ; // Default true
59+ this . touchSelection = null ;
5860
5961 this . init ( ) ;
6062 }
@@ -99,12 +101,8 @@ export default class TerminalComponent {
99101 }
100102
101103 setupEventHandlers ( ) {
102- // Handle terminal resize
103- this . terminal . onResize ( ( size ) => {
104- if ( this . serverMode ) {
105- this . resizeTerminal ( size . cols , size . rows ) ;
106- }
107- } ) ;
104+ // terminal resize handling
105+ this . setupResizeHandling ( ) ;
108106
109107 // Handle terminal title changes
110108 this . terminal . onTitleChange ( ( title ) => {
@@ -120,6 +118,157 @@ export default class TerminalComponent {
120118 this . setupCopyPasteHandlers ( ) ;
121119 }
122120
121+ /**
122+ * Setup resize handling for keyboard events and content preservation
123+ */
124+ setupResizeHandling ( ) {
125+ let resizeTimeout = null ;
126+ let lastKnownScrollPosition = 0 ;
127+ let isResizing = false ;
128+ let resizeCount = 0 ;
129+ const RESIZE_DEBOUNCE = 150 ;
130+ const MAX_RAPID_RESIZES = 3 ;
131+
132+ // Store original dimensions for comparison
133+ let originalRows = this . terminal . rows ;
134+ let originalCols = this . terminal . cols ;
135+
136+ this . terminal . onResize ( ( size ) => {
137+ // Track resize events
138+ resizeCount ++ ;
139+ isResizing = true ;
140+
141+ // Store current scroll position before resize
142+ if ( this . terminal . buffer && this . terminal . buffer . active ) {
143+ lastKnownScrollPosition = this . terminal . buffer . active . viewportY ;
144+ }
145+
146+ // Clear any existing timeout
147+ if ( resizeTimeout ) {
148+ clearTimeout ( resizeTimeout ) ;
149+ }
150+
151+ // Debounced resize handling
152+ resizeTimeout = setTimeout ( async ( ) => {
153+ try {
154+ // Only proceed with server resize if dimensions actually changed significantly
155+ const rowDiff = Math . abs ( size . rows - originalRows ) ;
156+ const colDiff = Math . abs ( size . cols - originalCols ) ;
157+
158+ // If this is a minor resize (likely intermediate state), skip server update
159+ if ( rowDiff < 2 && colDiff < 2 && resizeCount > 1 ) {
160+ console . log ( "Skipping minor resize to prevent instability" ) ;
161+ isResizing = false ;
162+ resizeCount = 0 ;
163+ return ;
164+ }
165+
166+ // Handle server resize
167+ if ( this . serverMode ) {
168+ await this . resizeTerminal ( size . cols , size . rows ) ;
169+ }
170+
171+ // Preserve scroll position for content-heavy terminals
172+ this . preserveViewportPosition ( lastKnownScrollPosition ) ;
173+
174+ // Update stored dimensions
175+ originalRows = size . rows ;
176+ originalCols = size . cols ;
177+
178+ // Mark resize as complete
179+ isResizing = false ;
180+ resizeCount = 0 ;
181+
182+ // Notify touch selection if it exists
183+ if ( this . touchSelection ) {
184+ this . touchSelection . onTerminalResize ( size ) ;
185+ }
186+ } catch ( error ) {
187+ console . error ( "Resize handling failed:" , error ) ;
188+ isResizing = false ;
189+ resizeCount = 0 ;
190+ }
191+ } , RESIZE_DEBOUNCE ) ;
192+ } ) ;
193+
194+ // Also handle viewport changes for scroll position preservation
195+ this . terminal . onData ( ( ) => {
196+ // If we're not resizing and user types, everything is stable
197+ if ( ! isResizing && this . terminal . buffer && this . terminal . buffer . active ) {
198+ lastKnownScrollPosition = this . terminal . buffer . active . viewportY ;
199+ }
200+ } ) ;
201+ }
202+
203+ /**
204+ * Preserve viewport position during resize to prevent jumping
205+ */
206+ preserveViewportPosition ( targetScrollPosition ) {
207+ if ( ! this . terminal . buffer || ! this . terminal . buffer . active ) return ;
208+
209+ const buffer = this . terminal . buffer . active ;
210+ const maxScroll = Math . max ( 0 , buffer . length - this . terminal . rows ) ;
211+
212+ // Ensure scroll position is within valid bounds
213+ const safeScrollPosition = Math . min ( targetScrollPosition , maxScroll ) ;
214+
215+ // Only adjust if we have significant content and the position is different
216+ if (
217+ buffer . length > this . terminal . rows &&
218+ Math . abs ( buffer . viewportY - safeScrollPosition ) > 2
219+ ) {
220+ // Gradually adjust to prevent jarring movements
221+ const steps = 3 ;
222+ const diff = safeScrollPosition - buffer . viewportY ;
223+ const stepSize = Math . ceil ( Math . abs ( diff ) / steps ) ;
224+
225+ let currentStep = 0 ;
226+ const adjustStep = ( ) => {
227+ if ( currentStep >= steps ) return ;
228+
229+ const currentPos = buffer . viewportY ;
230+ const remaining = safeScrollPosition - currentPos ;
231+ const adjustment =
232+ Math . sign ( remaining ) * Math . min ( stepSize , Math . abs ( remaining ) ) ;
233+
234+ if ( Math . abs ( adjustment ) >= 1 ) {
235+ this . terminal . scrollLines ( adjustment ) ;
236+ }
237+
238+ currentStep ++ ;
239+ if ( currentStep < steps && Math . abs ( remaining ) > 1 ) {
240+ setTimeout ( adjustStep , 50 ) ;
241+ }
242+ } ;
243+
244+ setTimeout ( adjustStep , 100 ) ;
245+ }
246+ }
247+
248+ /**
249+ * Setup touch selection for mobile devices
250+ */
251+ setupTouchSelection ( ) {
252+ // Only initialize touch selection on mobile devices
253+ if ( window . cordova && this . container ) {
254+ const terminalSettings = getTerminalSettings ( ) ;
255+ this . touchSelection = new TerminalTouchSelection (
256+ this . terminal ,
257+ this . container ,
258+ {
259+ tapHoldDuration :
260+ terminalSettings . touchSelectionTapHoldDuration || 600 ,
261+ moveThreshold : terminalSettings . touchSelectionMoveThreshold || 8 ,
262+ handleSize : terminalSettings . touchSelectionHandleSize || 24 ,
263+ hapticFeedback :
264+ terminalSettings . touchSelectionHapticFeedback !== false ,
265+ showContextMenu :
266+ terminalSettings . touchSelectionShowContextMenu !== false ,
267+ } ,
268+ ) ;
269+ }
270+ }
271+
123272 /**
124273 * Setup copy/paste keyboard handlers
125274 */
@@ -250,6 +399,9 @@ export default class TerminalComponent {
250399 setTimeout ( ( ) => {
251400 this . fitAddon . fit ( ) ;
252401 this . terminal . focus ( ) ;
402+
403+ // Initialize touch selection after terminal is mounted
404+ this . setupTouchSelection ( ) ;
253405 } , 10 ) ;
254406 } catch ( error ) {
255407 console . error ( "Failed to mount terminal:" , error ) ;
@@ -613,6 +765,12 @@ export default class TerminalComponent {
613765 dispose ( ) {
614766 this . terminate ( ) ;
615767
768+ // Dispose touch selection
769+ if ( this . touchSelection ) {
770+ this . touchSelection . destroy ( ) ;
771+ this . touchSelection = null ;
772+ }
773+
616774 // Dispose addons
617775 this . disposeImageAddon ( ) ;
618776 this . disposeLigaturesAddon ( ) ;
0 commit comments