From de8aaac34404d7de75b4f84270ffd711e14510b2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 1 Sep 2026 03:30:51 -0700 Subject: [PATCH] refactor(mobile): name terminal WebView modules for their contents fragment-01..10 were arbitrary line-count slices of one template literal. Two seams fell mid-expression -- inside buildMouseClickInput and inside the touchmove listener -- so those pieces had no identity to name. Re-splits at real statement boundaries and names each for what it holds. The composed output is byte-identical: sha256 42cc000f..., 729776 bytes, verified before, after the regroup, and after formatting. Also fixes two ratchet tests that read fragment paths directly, one of which duplicated the composer's file list. --- ...rminal-webview-html-source.test-support.ts | 16 +- mobile/src/terminal/terminal-webview-html.ts | 48 ++--- .../{fragment-01.ts => document-shell.ts} | 2 +- ...ent-03.ts => fit-scale-and-write-queue.ts} | 3 +- .../terminal-webview-html/fragment-10.ts | 132 ------------- ...{fragment-05.ts => host-message-router.ts} | 43 +---- .../message-bridge-and-document-close.ts | 43 +++++ ....ts => mouse-report-and-scroll-routing.ts} | 174 ++++++++---------- ...2.ts => runtime-state-and-text-scaling.ts} | 3 +- .../{fragment-08.ts => selection-overlay.ts} | 3 +- .../selection-state-and-eviction.ts | 71 +++++++ ....ts => smooth-scroll-and-cell-geometry.ts} | 80 +------- ...agment-09.ts => surface-touch-gestures.ts} | 92 ++++++++- .../term-observers-and-mode-mirroring.ts | 67 +++++++ ...gment-04.ts => terminal-init-and-write.ts} | 2 +- .../terminal-webview-scroll-routing.test.ts | 12 +- .../terminal-webview-text-zoom.test.ts | 6 +- 17 files changed, 405 insertions(+), 392 deletions(-) rename mobile/src/terminal/terminal-webview-html/{fragment-01.ts => document-shell.ts} (98%) rename mobile/src/terminal/terminal-webview-html/{fragment-03.ts => fit-scale-and-write-queue.ts} (98%) delete mode 100644 mobile/src/terminal/terminal-webview-html/fragment-10.ts rename mobile/src/terminal/terminal-webview-html/{fragment-05.ts => host-message-router.ts} (77%) create mode 100644 mobile/src/terminal/terminal-webview-html/message-bridge-and-document-close.ts rename mobile/src/terminal/terminal-webview-html/{fragment-06.ts => mouse-report-and-scroll-routing.ts} (56%) rename mobile/src/terminal/terminal-webview-html/{fragment-02.ts => runtime-state-and-text-scaling.ts} (97%) rename mobile/src/terminal/terminal-webview-html/{fragment-08.ts => selection-overlay.ts} (97%) create mode 100644 mobile/src/terminal/terminal-webview-html/selection-state-and-eviction.ts rename mobile/src/terminal/terminal-webview-html/{fragment-07.ts => smooth-scroll-and-cell-geometry.ts} (54%) rename mobile/src/terminal/terminal-webview-html/{fragment-09.ts => surface-touch-gestures.ts} (62%) create mode 100644 mobile/src/terminal/terminal-webview-html/term-observers-and-mode-mirroring.ts rename mobile/src/terminal/terminal-webview-html/{fragment-04.ts => terminal-init-and-write.ts} (98%) diff --git a/mobile/src/terminal/terminal-webview-html-source.test-support.ts b/mobile/src/terminal/terminal-webview-html-source.test-support.ts index 2491591572f..25902305f41 100644 --- a/mobile/src/terminal/terminal-webview-html-source.test-support.ts +++ b/mobile/src/terminal/terminal-webview-html-source.test-support.ts @@ -2,10 +2,18 @@ import { readFileSync } from 'node:fs' const SOURCE_FILES = [ './terminal-webview-html.ts', - ...Array.from( - { length: 10 }, - (_, index) => `./terminal-webview-html/fragment-${String(index + 1).padStart(2, '0')}.ts` - ) + './terminal-webview-html/document-shell.ts', + './terminal-webview-html/runtime-state-and-text-scaling.ts', + './terminal-webview-html/fit-scale-and-write-queue.ts', + './terminal-webview-html/terminal-init-and-write.ts', + './terminal-webview-html/host-message-router.ts', + './terminal-webview-html/selection-state-and-eviction.ts', + './terminal-webview-html/term-observers-and-mode-mirroring.ts', + './terminal-webview-html/mouse-report-and-scroll-routing.ts', + './terminal-webview-html/smooth-scroll-and-cell-geometry.ts', + './terminal-webview-html/selection-overlay.ts', + './terminal-webview-html/surface-touch-gestures.ts', + './terminal-webview-html/message-bridge-and-document-close.ts' ] as const /** Reads the TypeScript source that assembles the in-WebView document. */ diff --git a/mobile/src/terminal/terminal-webview-html.ts b/mobile/src/terminal/terminal-webview-html.ts index b55421bbe4d..40b7a2db22c 100644 --- a/mobile/src/terminal/terminal-webview-html.ts +++ b/mobile/src/terminal/terminal-webview-html.ts @@ -1,28 +1,34 @@ -import { TERMINAL_HTML_FRAGMENT_01 } from './terminal-webview-html/fragment-01' -import { TERMINAL_HTML_FRAGMENT_02 } from './terminal-webview-html/fragment-02' -import { TERMINAL_HTML_FRAGMENT_03 } from './terminal-webview-html/fragment-03' -import { TERMINAL_HTML_FRAGMENT_04 } from './terminal-webview-html/fragment-04' -import { TERMINAL_HTML_FRAGMENT_05 } from './terminal-webview-html/fragment-05' -import { TERMINAL_HTML_FRAGMENT_06 } from './terminal-webview-html/fragment-06' -import { TERMINAL_HTML_FRAGMENT_07 } from './terminal-webview-html/fragment-07' -import { TERMINAL_HTML_FRAGMENT_08 } from './terminal-webview-html/fragment-08' -import { TERMINAL_HTML_FRAGMENT_09 } from './terminal-webview-html/fragment-09' -import { TERMINAL_HTML_FRAGMENT_10 } from './terminal-webview-html/fragment-10' +import { TERMINAL_HTML_DOCUMENT_SHELL } from './terminal-webview-html/document-shell' +import { TERMINAL_HTML_RUNTIME_STATE_AND_TEXT_SCALING } from './terminal-webview-html/runtime-state-and-text-scaling' +import { TERMINAL_HTML_FIT_SCALE_AND_WRITE_QUEUE } from './terminal-webview-html/fit-scale-and-write-queue' +import { TERMINAL_HTML_INIT_AND_WRITE } from './terminal-webview-html/terminal-init-and-write' +import { TERMINAL_HTML_HOST_MESSAGE_ROUTER } from './terminal-webview-html/host-message-router' +import { TERMINAL_HTML_SELECTION_STATE_AND_EVICTION } from './terminal-webview-html/selection-state-and-eviction' +import { TERMINAL_HTML_OBSERVERS_AND_MODE_MIRRORING } from './terminal-webview-html/term-observers-and-mode-mirroring' +import { TERMINAL_HTML_MOUSE_REPORT_AND_SCROLL_ROUTING } from './terminal-webview-html/mouse-report-and-scroll-routing' +import { TERMINAL_HTML_SMOOTH_SCROLL_AND_CELL_GEOMETRY } from './terminal-webview-html/smooth-scroll-and-cell-geometry' +import { TERMINAL_HTML_SELECTION_OVERLAY } from './terminal-webview-html/selection-overlay' +import { TERMINAL_HTML_SURFACE_TOUCH_GESTURES } from './terminal-webview-html/surface-touch-gestures' +import { TERMINAL_HTML_MESSAGE_BRIDGE_AND_DOCUMENT_CLOSE } from './terminal-webview-html/message-bridge-and-document-close' export { MOBILE_TERMINAL_CARET_OPTIONS } from './terminal-webview-html/theme' -// Why: keep the document source stable while each script/style concern remains independently reviewable. +// Why: keep the document source stable while each script/style concern remains independently +// reviewable. Boundaries can only fall where the emitted document allows, so a few modules +// carry a second concern noted at the top of the file. export const XTERM_HTML = [ - TERMINAL_HTML_FRAGMENT_01, - TERMINAL_HTML_FRAGMENT_02, - TERMINAL_HTML_FRAGMENT_03, - TERMINAL_HTML_FRAGMENT_04, - TERMINAL_HTML_FRAGMENT_05, - TERMINAL_HTML_FRAGMENT_06, - TERMINAL_HTML_FRAGMENT_07, - TERMINAL_HTML_FRAGMENT_08, - TERMINAL_HTML_FRAGMENT_09, - TERMINAL_HTML_FRAGMENT_10 + TERMINAL_HTML_DOCUMENT_SHELL, + TERMINAL_HTML_RUNTIME_STATE_AND_TEXT_SCALING, + TERMINAL_HTML_FIT_SCALE_AND_WRITE_QUEUE, + TERMINAL_HTML_INIT_AND_WRITE, + TERMINAL_HTML_HOST_MESSAGE_ROUTER, + TERMINAL_HTML_SELECTION_STATE_AND_EVICTION, + TERMINAL_HTML_OBSERVERS_AND_MODE_MIRRORING, + TERMINAL_HTML_MOUSE_REPORT_AND_SCROLL_ROUTING, + TERMINAL_HTML_SMOOTH_SCROLL_AND_CELL_GEOMETRY, + TERMINAL_HTML_SELECTION_OVERLAY, + TERMINAL_HTML_SURFACE_TOUCH_GESTURES, + TERMINAL_HTML_MESSAGE_BRIDGE_AND_DOCUMENT_CLOSE ].join('') export const XTERM_WEBVIEW_SOURCE = { html: XTERM_HTML } diff --git a/mobile/src/terminal/terminal-webview-html/fragment-01.ts b/mobile/src/terminal/terminal-webview-html/document-shell.ts similarity index 98% rename from mobile/src/terminal/terminal-webview-html/fragment-01.ts rename to mobile/src/terminal/terminal-webview-html/document-shell.ts index 77dba06232a..d6e733cdd77 100644 --- a/mobile/src/terminal/terminal-webview-html/fragment-01.ts +++ b/mobile/src/terminal/terminal-webview-html/document-shell.ts @@ -1,7 +1,7 @@ import { colors } from '../../theme/mobile-theme' import { XTERM_ENGINE_CSS, XTERM_ENGINE_JS } from '../terminal-webview-engine.generated' -export const TERMINAL_HTML_FRAGMENT_01 = ` +export const TERMINAL_HTML_DOCUMENT_SHELL = `
diff --git a/mobile/src/terminal/terminal-webview-html/fragment-03.ts b/mobile/src/terminal/terminal-webview-html/fit-scale-and-write-queue.ts similarity index 98% rename from mobile/src/terminal/terminal-webview-html/fragment-03.ts rename to mobile/src/terminal/terminal-webview-html/fit-scale-and-write-queue.ts index 075ee819ec6..074185d43a5 100644 --- a/mobile/src/terminal/terminal-webview-html/fragment-03.ts +++ b/mobile/src/terminal/terminal-webview-html/fit-scale-and-write-queue.ts @@ -1,6 +1,7 @@ import { TERMINAL_WEBVIEW_THEME_JS } from '../terminal-webview-theme-injected' -export const TERMINAL_HTML_FRAGMENT_03 = `${TERMINAL_WEBVIEW_THEME_JS} +// Also carries the DECSET mouse-mode scanner: emitted-document order pins it between these two concerns. +export const TERMINAL_HTML_FIT_SCALE_AND_WRITE_QUEUE = `${TERMINAL_WEBVIEW_THEME_JS} function getCellHeight() { if (!term || !term._core) return 15; diff --git a/mobile/src/terminal/terminal-webview-html/fragment-10.ts b/mobile/src/terminal/terminal-webview-html/fragment-10.ts deleted file mode 100644 index d461f4a2039..00000000000 --- a/mobile/src/terminal/terminal-webview-html/fragment-10.ts +++ /dev/null @@ -1,132 +0,0 @@ -export const TERMINAL_HTML_FRAGMENT_10 = ` updateTransform(); - } - - var deltaY = ts.lastY - y; - ts.lastTime = now; - if (shouldRouteScrollToTerminalInput()) { - updateTouchVelocity(deltaY, dt); - resetSmoothScrollOffset(); - var effectiveCellH = getCellHeight() * getTotalScale(); - ts.accumDelta += deltaY; - var lines = Math.trunc(ts.accumDelta / effectiveCellH); - if (lines !== 0) { - ts.accumDelta -= lines * effectiveCellH; - routeScrollLines(lines, x, y); - } - } else { - if (enqueueNormalBufferScrollDelta(deltaY)) { - updateTouchVelocity(deltaY, dt); - } else { - ts.velY = 0; - } - } - ts.lastX = x; - ts.lastY = y; - } - }, { capture: true, passive: false }); - - targetSurface.addEventListener('touchend', function(e) { - if (dispatcherShouldBlockSurface()) return; - if (!term) return; - - if (ts.isPinching && e.touches.length < 2) { - ts.isPinching = false; - // Why: a finished pinch snaps to the nearest preset and becomes the new - // font size (reflowing the grid), so pinch-to-zoom IS the in-terminal way - // to set the text size. The CSS pinch zoom (userScale) is reset; the real - // size change reflows columns and RN persists + resizes the PTY to match. - var target = snapToTextScalePreset(currentTextScale * userScale); - var changed = target !== currentTextScale; - userScale = 1; - panX = 0; panY = 0; - applyTextScale(target); - updateTransform(); - notify({ type: 'font-scale-changed', fontScale: target }); - if (changed) notify({ type: 'haptic', kind: 'selection' }); - if (e.touches.length === 1) { - ts.lastX = e.touches[0].clientX; - ts.lastY = e.touches[0].clientY; - ts.lastTime = Date.now(); - ts.velY = 0; - ts.accumDelta = 0; - } - return; - } - - if (e.touches.length === 0) { - var vel = ts.velY; - var FRICTION = 0.972; - var MIN_VEL = 0.012; - function momentumStep() { - vel *= FRICTION; - if (Math.abs(vel) < MIN_VEL) { ts.momentumId = null; return; } - var delta = vel * 16; - if (shouldRouteScrollToTerminalInput()) { - resetSmoothScrollOffset(); - var effectiveCellH = getCellHeight() * getTotalScale(); - ts.accumDelta += delta; - var lines = Math.trunc(ts.accumDelta / effectiveCellH); - if (lines !== 0) { - ts.accumDelta -= lines * effectiveCellH; - routeScrollLines(lines, ts.lastX, ts.lastY); - } - } else { - if (!applyNormalBufferScrollDelta(delta)) { - ts.momentumId = null; - return; - } - } - ts.momentumId = requestAnimationFrame(momentumStep); - } - if (Math.abs(vel) > MIN_VEL) { - ts.momentumId = requestAnimationFrame(momentumStep); - } - } - }, { capture: true, passive: true }); - } - - attachSurfaceEventHandlers(surface); - - function handleIncomingMessage(e) { - var msg; - try { - msg = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; - } catch (ex) { - return; - } - try { - handleMsg(msg); - } catch(ex) { - reportEngineError( - msg && msg.type === 'init' ? 'terminal init failed' : 'terminal message failed', - ex, - msg && msg.type === 'init' && !everReady - ); - } - } - - window.addEventListener('message', handleIncomingMessage); - - document.addEventListener('message', handleIncomingMessage); - - window.addEventListener('resize', function() { - // Why: viewport changed (keyboard open/close, orientation, RN container - // size update). Re-fit so the scale matches the new vpWidth — without - // this, opening the keyboard leaves the terminal at the old scale even - // though there's now less vertical room and the fit ratio may differ. - applyFitScale('window-resize'); - adjustRowsForViewport(); - repositionOverlay(); - clampPan(); - updateTransform(); - }); - - if (window.Terminal) { - notify({ type: 'web-ready' }); - } else { - reportEngineError('terminal engine missing', 'xterm failed to load', true); - } -})(); - - -` diff --git a/mobile/src/terminal/terminal-webview-html/fragment-05.ts b/mobile/src/terminal/terminal-webview-html/host-message-router.ts similarity index 77% rename from mobile/src/terminal/terminal-webview-html/fragment-05.ts rename to mobile/src/terminal/terminal-webview-html/host-message-router.ts index c8852c50d78..29cba25df36 100644 --- a/mobile/src/terminal/terminal-webview-html/fragment-05.ts +++ b/mobile/src/terminal/terminal-webview-html/host-message-router.ts @@ -1,6 +1,6 @@ import { TERMINAL_REFLOW_JS } from '../terminal-webview-reflow-injected' -export const TERMINAL_HTML_FRAGMENT_05 = ` ${TERMINAL_REFLOW_JS} +export const TERMINAL_HTML_HOST_MESSAGE_ROUTER = ` ${TERMINAL_REFLOW_JS} function notify(msg) { if (window.ReactNativeWebView) { @@ -186,45 +186,4 @@ export const TERMINAL_HTML_FRAGMENT_05 = ` ${TERMINAL_REFLOW_JS} } } - // ============================================================ - // SELECTION MODE (long-press → handles → Copy) - // ============================================================ - var WORD_RE = /[\\p{L}\\p{N}_./:@~+=?%-]/u; - var LONG_PRESS_MS = 500; - var LONG_PRESS_SLOP = 10; - // Why: a tap that opens a link/path must survive small finger jitter. The - // long-press slop (10px) only cancels the press-to-select timer; reusing it - // to gate the tap dropped any URL/file tap that wandered >10px — at fit scale - // a few screen px of jitter is a normal tap. Use a wider, time-bounded tap - // window so deliberate scrolls/pans still don't fire a tap. - var TAP_SLOP = 24; - var TAP_MAX_MS = 700; - var EDGE_SCROLL_PX = 40; - var EDGE_SCROLL_INTERVAL = 60; - - var selectionOverlay = document.getElementById('selection-overlay'); - var handleStart = document.getElementById('sel-handle-start'); - var handleEnd = document.getElementById('sel-handle-end'); - var selMenu = document.getElementById('sel-menu'); - var btnCopy = document.getElementById('sel-menu-copy'); - var btnSelAll = document.getElementById('sel-menu-all'); - - // mode: 'navigate' | 'select' - var selMode = 'navigate'; - var sel = null; // { anchor:{col,row}, focus:{col,row}, activeHandle:null|'start'|'end' } - var longPressTimer = null; - var longPressOrigin = null; // {x,y, identifier} - // Why: tap detection is tracked separately from the long-press timer so a - // small jitter that cancels the press-to-select timer does not also cancel - // the tap (which opens links/paths). {x,y,t,identifier} or null once the - // gesture is disqualified as a tap (moved too far or held too long). - var tapCandidate = null; - var edgeScrollTimer = null; - var edgeScrollDir = 0; - var edgeScrollClientX = 0; - var edgeScrollClientY = 0; - - // Eviction watchdog: linesEverWritten counts onLineFeed since last init. - // Once buffer is full, every onLineFeed evicts the top row in xterm and - // we mirror that by decrementing stored absolute rows. ` diff --git a/mobile/src/terminal/terminal-webview-html/message-bridge-and-document-close.ts b/mobile/src/terminal/terminal-webview-html/message-bridge-and-document-close.ts new file mode 100644 index 00000000000..49cd3b4d84f --- /dev/null +++ b/mobile/src/terminal/terminal-webview-html/message-bridge-and-document-close.ts @@ -0,0 +1,43 @@ +export const TERMINAL_HTML_MESSAGE_BRIDGE_AND_DOCUMENT_CLOSE = ` function handleIncomingMessage(e) { + var msg; + try { + msg = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; + } catch (ex) { + return; + } + try { + handleMsg(msg); + } catch(ex) { + reportEngineError( + msg && msg.type === 'init' ? 'terminal init failed' : 'terminal message failed', + ex, + msg && msg.type === 'init' && !everReady + ); + } + } + + window.addEventListener('message', handleIncomingMessage); + + document.addEventListener('message', handleIncomingMessage); + + window.addEventListener('resize', function() { + // Why: viewport changed (keyboard open/close, orientation, RN container + // size update). Re-fit so the scale matches the new vpWidth — without + // this, opening the keyboard leaves the terminal at the old scale even + // though there's now less vertical room and the fit ratio may differ. + applyFitScale('window-resize'); + adjustRowsForViewport(); + repositionOverlay(); + clampPan(); + updateTransform(); + }); + + if (window.Terminal) { + notify({ type: 'web-ready' }); + } else { + reportEngineError('terminal engine missing', 'xterm failed to load', true); + } +})(); + +