From d501ca9e82a7860e474acf739a6f29bc2b2a2795 Mon Sep 17 00:00:00 2001 From: Spencer Churchill <25377399+splch@users.noreply.github.com> Date: Sat, 21 Mar 2026 11:58:09 -0700 Subject: [PATCH 1/3] Forward touch scroll events as terminal input Dispatches synthetic WheelEvents into hterm's existing onMouse_ pipeline so touch scroll gestures reach programs that listen for them. hterm handles mouse reporting (VT) and alternate screen arrow keys (DECSET 1007) automatically. On the primary screen with mouse reporting disabled, the existing visual scrollback behavior is preserved unchanged. Fixes #2375 Fixes #2537 --- app/TerminalView.m | 10 ++++++++-- app/terminal/term.js | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/TerminalView.m b/app/TerminalView.m index a1eeba926e..624e6f47c7 100644 --- a/app/TerminalView.m +++ b/app/TerminalView.m @@ -31,7 +31,9 @@ - (void)userContentController:(WKUserContentController *)userContentController d } @end -@interface TerminalView () +@interface TerminalView () { + CGFloat _prevScrollY; +} @property (nonatomic) NSMutableArray *keyCommands; @property ScrollbarView *scrollbarView; @@ -104,6 +106,7 @@ - (void)setTerminal:(Terminal *)terminal { } _terminal = terminal; + _prevScrollY = 0; [_terminal addObserver:self forKeyPath:@"loaded" options:NSKeyValueObservingOptionInitial context:nil]; if (_terminal.loaded) [self installTerminalView]; @@ -268,7 +271,10 @@ - (void)userContentController:(WKUserContentController *)userContentController d } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { - [self.terminal.webView evaluateJavaScript:[NSString stringWithFormat:@"exports.newScrollTop(%f)", scrollView.contentOffset.y] completionHandler:nil]; + CGFloat newY = scrollView.contentOffset.y; + CGFloat delta = newY - _prevScrollY; + _prevScrollY = newY; + [self.terminal.webView evaluateJavaScript:[NSString stringWithFormat:@"exports.handleScroll(%f, %f)", newY, delta] completionHandler:nil]; } - (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance { diff --git a/app/terminal/term.js b/app/terminal/term.js index 300fe6e128..731258c8e0 100644 --- a/app/terminal/term.js +++ b/app/terminal/term.js @@ -110,12 +110,38 @@ term.scrollPort_.onTouch = (e) => { }; // Scroll to bottom wrapper exports.scrollToBottom = () => term.scrollEnd(); -// Set scroll position -exports.newScrollTop = (y) => { - // two lines instead of one because the value you read out of scrollTop can be different from the value you write into it +// Set scroll position and optionally forward delta as WheelEvents into hterm's +// onMouse_ pipeline for mouse reporting (VT) and alternate screen arrow keys. +let scrollDeltaRemainder = 0; +exports.handleScroll = (y, delta) => { term.scrollPort_.screen_.scrollTop = y; lastScrollTop = term.scrollPort_.screen_.scrollTop; + + if (delta === 0) return; + if (term.vt.mouseReport === term.vt.MOUSE_REPORT_DISABLED && term.isPrimaryScreen()) + return; + + const charH = term.scrollPort_.characterSize.height; + if (!charH) return; + + scrollDeltaRemainder += delta; + const lines = Math.trunc(scrollDeltaRemainder / charH); + if (lines === 0) return; + scrollDeltaRemainder -= lines * charH; + + const abs = Math.abs(lines); + const down = lines > 0; + for (let i = 0; i < abs; i++) { + term.scrollPort_.screen_.dispatchEvent(new WheelEvent('wheel', { + deltaY: down ? 1 : -1, + deltaMode: WheelEvent.DOM_DELTA_LINE, + bubbles: true, + cancelable: true, + })); + } }; +// Keep old name for compatibility +exports.newScrollTop = (y) => exports.handleScroll(y, 0); // Send scroll height and position to native code let lastScrollHeight, lastScrollTop; @@ -154,6 +180,7 @@ exports.getCharacterSize = () => { }; exports.clearScrollback = () => term.clearScrollback(); + exports.setUserGesture = () => term.accessibilityReader_.hasUserGesture = true; hterm.openUrl = (url) => native.openLink(url); From 73a8b15caef442c23d72a341863b24c07154565f Mon Sep 17 00:00:00 2001 From: Spencer Churchill <25377399+splch@users.noreply.github.com> Date: Sat, 21 Mar 2026 12:00:18 -0700 Subject: [PATCH 2/3] Simplify: keep newScrollTop untouched, add handleScrollDelta separately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Purely additive change to term.js — no existing code modified. --- app/TerminalView.m | 4 +++- app/terminal/term.js | 55 +++++++++++++++++++++----------------------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/app/TerminalView.m b/app/TerminalView.m index 624e6f47c7..b27d02568e 100644 --- a/app/TerminalView.m +++ b/app/TerminalView.m @@ -272,9 +272,11 @@ - (void)userContentController:(WKUserContentController *)userContentController d - (void)scrollViewDidScroll:(UIScrollView *)scrollView { CGFloat newY = scrollView.contentOffset.y; + [self.terminal.webView evaluateJavaScript:[NSString stringWithFormat:@"exports.newScrollTop(%f)", newY] completionHandler:nil]; CGFloat delta = newY - _prevScrollY; _prevScrollY = newY; - [self.terminal.webView evaluateJavaScript:[NSString stringWithFormat:@"exports.handleScroll(%f, %f)", newY, delta] completionHandler:nil]; + if (delta != 0) + [self.terminal.webView evaluateJavaScript:[NSString stringWithFormat:@"exports.handleScrollDelta(%f)", delta] completionHandler:nil]; } - (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance { diff --git a/app/terminal/term.js b/app/terminal/term.js index 731258c8e0..8b16d79c1f 100644 --- a/app/terminal/term.js +++ b/app/terminal/term.js @@ -110,38 +110,12 @@ term.scrollPort_.onTouch = (e) => { }; // Scroll to bottom wrapper exports.scrollToBottom = () => term.scrollEnd(); -// Set scroll position and optionally forward delta as WheelEvents into hterm's -// onMouse_ pipeline for mouse reporting (VT) and alternate screen arrow keys. -let scrollDeltaRemainder = 0; -exports.handleScroll = (y, delta) => { +// Set scroll position +exports.newScrollTop = (y) => { + // two lines instead of one because the value you read out of scrollTop can be different from the value you write into it term.scrollPort_.screen_.scrollTop = y; lastScrollTop = term.scrollPort_.screen_.scrollTop; - - if (delta === 0) return; - if (term.vt.mouseReport === term.vt.MOUSE_REPORT_DISABLED && term.isPrimaryScreen()) - return; - - const charH = term.scrollPort_.characterSize.height; - if (!charH) return; - - scrollDeltaRemainder += delta; - const lines = Math.trunc(scrollDeltaRemainder / charH); - if (lines === 0) return; - scrollDeltaRemainder -= lines * charH; - - const abs = Math.abs(lines); - const down = lines > 0; - for (let i = 0; i < abs; i++) { - term.scrollPort_.screen_.dispatchEvent(new WheelEvent('wheel', { - deltaY: down ? 1 : -1, - deltaMode: WheelEvent.DOM_DELTA_LINE, - bubbles: true, - cancelable: true, - })); - } }; -// Keep old name for compatibility -exports.newScrollTop = (y) => exports.handleScroll(y, 0); // Send scroll height and position to native code let lastScrollHeight, lastScrollTop; @@ -181,6 +155,29 @@ exports.getCharacterSize = () => { exports.clearScrollback = () => term.clearScrollback(); +// Forward scroll delta as synthetic WheelEvents into hterm's onMouse_ pipeline. +let scrollDeltaRemainder = 0; +exports.handleScrollDelta = (delta) => { + if (term.vt.mouseReport === term.vt.MOUSE_REPORT_DISABLED && term.isPrimaryScreen()) + return; + const charH = term.scrollPort_.characterSize.height; + if (!charH) return; + scrollDeltaRemainder += delta; + const lines = Math.trunc(scrollDeltaRemainder / charH); + if (lines === 0) return; + scrollDeltaRemainder -= lines * charH; + const abs = Math.abs(lines); + const down = lines > 0; + for (let i = 0; i < abs; i++) { + term.scrollPort_.screen_.dispatchEvent(new WheelEvent('wheel', { + deltaY: down ? 1 : -1, + deltaMode: WheelEvent.DOM_DELTA_LINE, + bubbles: true, + cancelable: true, + })); + } +}; + exports.setUserGesture = () => term.accessibilityReader_.hasUserGesture = true; hterm.openUrl = (url) => native.openLink(url); From fe0325a1439d9153152960010f248cbe8f4ee66e Mon Sep 17 00:00:00 2001 From: Spencer Churchill <25377399+splch@users.noreply.github.com> Date: Sat, 21 Mar 2026 12:04:42 -0700 Subject: [PATCH 3/3] Inline variables and remove unnecessary bubbles property --- app/terminal/term.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/terminal/term.js b/app/terminal/term.js index 8b16d79c1f..ae4916f990 100644 --- a/app/terminal/term.js +++ b/app/terminal/term.js @@ -166,13 +166,10 @@ exports.handleScrollDelta = (delta) => { const lines = Math.trunc(scrollDeltaRemainder / charH); if (lines === 0) return; scrollDeltaRemainder -= lines * charH; - const abs = Math.abs(lines); - const down = lines > 0; - for (let i = 0; i < abs; i++) { + for (let i = 0; i < Math.abs(lines); i++) { term.scrollPort_.screen_.dispatchEvent(new WheelEvent('wheel', { - deltaY: down ? 1 : -1, + deltaY: lines > 0 ? 1 : -1, deltaMode: WheelEvent.DOM_DELTA_LINE, - bubbles: true, cancelable: true, })); }