diff --git a/muxplex/frontend/terminal.js b/muxplex/frontend/terminal.js index 33809f3..21c9605 100644 --- a/muxplex/frontend/terminal.js +++ b/muxplex/frontend/terminal.js @@ -248,10 +248,13 @@ window._openTerminal = openTerminal; window._closeTerminal = closeTerminal; // --------------------------------------------------------------------------- -// Touch scroll — translates vertical swipe into xterm.js scrollback +// Touch scroll — forwards vertical swipe to xterm.js as a WheelEvent. +// xterm.js processes this identically to a real mouse wheel: if mouse +// reporting is enabled (tmux set -g mouse on), escape sequences are sent +// to the PTY so tmux/applications handle the scroll (inside). +// Mouse wheel scroll (the `wheel` event path) is unaffected — this adds +// touch on top. // --------------------------------------------------------------------------- -// Runs once after DOM is ready. Mouse wheel scroll is unaffected (separate -// `wheel` event path handled natively by xterm.js). ;(function initTerminalTouchScroll() { var container = document.getElementById('terminal-container'); if (!container || typeof container.addEventListener !== 'function') return; @@ -264,21 +267,28 @@ window._closeTerminal = closeTerminal; container.addEventListener('touchmove', function (e) { if (!_term) return; - e.preventDefault(); // prevent page scroll while swiping inside terminal + e.preventDefault(); var y = e.touches[0].clientY; - var deltaY = _touchLastY - y; // positive = swipe up = scroll toward newer content + var deltaY = _touchLastY - y; // positive = finger moved up = scroll toward newer content _touchLastY = y; - // Convert pixels to lines. xterm.js fontSize is set per device (12 mobile, 14 desktop). - // lineHeight ratio is 1.2 — gives ~14-17px per line. - var pxPerLine = (_term.options.fontSize || 14) * 1.2; - var lines = deltaY / pxPerLine; + if (Math.abs(deltaY) < 2) return; // ignore micro-jitter - // Only fire if movement is meaningful (avoids micro-jitter) - if (Math.abs(lines) >= 0.3) { - _term.scrollLines(Math.round(lines)); - } + // Dispatch a synthetic WheelEvent on the xterm.js viewport so xterm.js + // handles it exactly as it handles a real mouse wheel: if mouse reporting + // is enabled (tmux mouse mode), it sends escape sequences to the PTY + // (scrolls inside the session). This is the correct "inside" path — + // unlike local-buffer scroll which never reaches the PTY. + var viewport = container.querySelector('.xterm-viewport'); + if (!viewport) return; + + viewport.dispatchEvent(new WheelEvent('wheel', { + deltaY: deltaY * 3, // scale: ~40px swipe ≈ 1 wheel click + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + bubbles: true, + cancelable: true, + })); }, { passive: false }); // passive: false required to allow e.preventDefault() container.addEventListener('touchend', function () { diff --git a/muxplex/frontend/tests/test_terminal.mjs b/muxplex/frontend/tests/test_terminal.mjs index 3dfb846..2102c57 100644 --- a/muxplex/frontend/tests/test_terminal.mjs +++ b/muxplex/frontend/tests/test_terminal.mjs @@ -725,17 +725,13 @@ const terminalSrc = fs.readFileSync( new URL('../terminal.js', import.meta.url), 'utf8' ); -test('terminal.js registers touchmove handler on terminal-container', () => { - // Source inspection: verify the touch scroll IIFE is present and uses - // e.preventDefault() (passive: false is required for prevent default) - assert.ok(terminalSrc.includes('touchmove'), - 'touchmove listener must be present'); - assert.ok(terminalSrc.includes('e.preventDefault'), - 'must call preventDefault to prevent page scroll while swiping inside terminal'); - assert.ok(terminalSrc.includes('scrollLines'), - 'must call _term.scrollLines to scroll terminal'); - assert.ok(terminalSrc.includes('passive: false'), - 'touchmove must be non-passive to allow preventDefault'); +test('terminal.js touchmove dispatches WheelEvent to xterm viewport', () => { + assert.ok(terminalSrc.includes('touchmove'), 'touchmove listener must be present'); + assert.ok(terminalSrc.includes('e.preventDefault'), 'must call preventDefault to prevent page scroll'); + assert.ok(terminalSrc.includes('WheelEvent'), 'must dispatch WheelEvent (not call scrollLines)'); + assert.ok(terminalSrc.includes('.xterm-viewport'), 'must target xterm viewport element'); + assert.ok(!terminalSrc.includes('scrollLines'), 'must NOT call scrollLines — that only moves local scrollback, not the PTY'); + assert.ok(terminalSrc.includes('passive: false'), 'touchmove must be non-passive to allow preventDefault'); }); test('terminal.js touchstart and touchend are passive', () => {