mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
refactor(mobile): extract the terminal fit-scale slice into a module
The slice opens with the already-extracted theme group, so its text is two emissions joined. Four more names stop being externals. Counts: qualified 47, scope declarations 0, rebindings 47, braced bodies 20, unbound catches 0, number properties 9, shorthand properties 0. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { getCellHeight, getCellWidth, getTotalScale } from './document-externals'
|
||||
import { getCellHeight } from './fit-scale'
|
||||
import { getCellWidth, getTotalScale } from './document-externals'
|
||||
import { scope } from './document-scope'
|
||||
|
||||
export function cellToViewportPx(col: number, absRow: number) {
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
/** `smooth-scroll-and-cell-geometry`: one character cell's width in surface pixels. */
|
||||
export declare function getCellWidth(): number
|
||||
|
||||
/** `smooth-scroll-and-cell-geometry`: one character cell's height in surface pixels. */
|
||||
export declare function getCellHeight(): number
|
||||
|
||||
/** `terminal-fit-scale`: the fit scale times the user's pinch scale. */
|
||||
export declare function getTotalScale(): number
|
||||
|
||||
@@ -24,9 +21,6 @@ export declare function notify(message: Record<string, unknown>): void
|
||||
/** `terminal-init-and-write`: whether xterm is showing the alternate screen buffer. */
|
||||
export declare function isAlternateBufferActive(): boolean
|
||||
|
||||
/** `terminal-fit-scale`: recomputes the fit scale and applies it, naming why. */
|
||||
export declare function applyFitScale(reason: string): void
|
||||
|
||||
/** `smooth-scroll-and-cell-geometry`: shows or hides the scroll indicator. */
|
||||
export declare function updateScrollIndicator(visible: boolean): void
|
||||
|
||||
@@ -84,11 +78,8 @@ export declare function handleMsg(msg: unknown): void
|
||||
/** `terminal-init-and-write`: reports an engine failure to the host. */
|
||||
export declare function reportEngineError(summary: string, cause: unknown, fatal: unknown): void
|
||||
|
||||
/** `terminal-init-and-write`: re-fits the row count to the current viewport. */
|
||||
export declare function adjustRowsForViewport(): void
|
||||
|
||||
/** `smooth-scroll-and-cell-geometry`: clamps the pan offsets to the scaled surface. */
|
||||
export declare function clampPan(): void
|
||||
|
||||
/** `smooth-scroll-and-cell-geometry`: writes the pan and scale onto the surface transform. */
|
||||
export declare function updateTransform(): void
|
||||
|
||||
/** `runtime-state-and-text-scaling`: the scale that fits the grid to the viewport. */
|
||||
export declare function computeFitScale(): number
|
||||
|
||||
@@ -29,6 +29,7 @@ export type TerminalOscLinkService = { getLinkData?: (id: number) => { uri?: str
|
||||
|
||||
/** The xterm internals the OSC 8 lookup walks. */
|
||||
export type TerminalDocumentCore = {
|
||||
_renderService?: { dimensions?: { css: { cell: { height?: number } } } }
|
||||
_oscLinkService?: TerminalOscLinkService
|
||||
_inputHandler?: { _oscLinkService?: TerminalOscLinkService }
|
||||
}
|
||||
@@ -84,6 +85,7 @@ export type TerminalDocumentTerminal = {
|
||||
readonly buffer: { readonly active: TerminalDocumentBuffer }
|
||||
readonly options: TerminalDocumentTerminalOptions
|
||||
write: (data: string, callback?: () => void) => void
|
||||
readonly element?: HTMLElement
|
||||
readonly _core?: TerminalDocumentCore
|
||||
readonly modes?: { bracketedPasteMode?: boolean }
|
||||
onLineFeed?: (listener: () => void) => TerminalDocumentDisposable
|
||||
@@ -143,6 +145,10 @@ export type TerminalDocumentScope = {
|
||||
sgrMouseMode: boolean
|
||||
/** `runtime-state`: whether the TUI asked for SGR pixel (1016) mouse reports. */
|
||||
sgrMousePixelsMode: boolean
|
||||
/** `terminal-fit-scale`: the fit scale the document committed. */
|
||||
currentScale: number
|
||||
/** `runtime-state`: the pinch zoom the user applied on top of the fit scale. */
|
||||
userScale: number
|
||||
/** `runtime-state`: Claude's record dot, which iOS WebKit would otherwise promote to emoji. */
|
||||
CLAUDE_STATUS_DOT: string
|
||||
/** `runtime-state`: the variation selector that forces the text glyph. */
|
||||
@@ -298,6 +304,8 @@ export function createTerminalDocumentScope(): TerminalDocumentScope {
|
||||
trackedMouseTrackingMode: 'none',
|
||||
sgrMouseMode: false,
|
||||
sgrMousePixelsMode: false,
|
||||
currentScale: 1,
|
||||
userScale: 1,
|
||||
CLAUDE_STATUS_DOT: statusDot,
|
||||
TEXT_PRESENTATION_SELECTOR: textPresentationSelector,
|
||||
EMOJI_PRESENTATION_SELECTOR: emojiPresentationSelector,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { emitTerminalDocumentModule } from '../../../scripts/build-terminal-document-script.mjs'
|
||||
import { TERMINAL_HTML_FIT_SCALE } from '../terminal-webview-html/terminal-fit-scale'
|
||||
import { compareTerminalDocumentScripts } from './terminal-document-equivalence.test-support'
|
||||
|
||||
/** The slice opens with the already-extracted theme group, so its text is two emissions joined. */
|
||||
const modulePaths = ['./terminal-theme.ts', './fit-scale.ts'].map((relative) =>
|
||||
fileURLToPath(new URL(relative, import.meta.url))
|
||||
)
|
||||
|
||||
describe('the terminal fit-scale slice', () => {
|
||||
it('emits the script the document carries, modulo the six normalisations', async () => {
|
||||
const emitted = (await Promise.all(modulePaths.map(emitTerminalDocumentModule))).join('\n')
|
||||
expect(compareTerminalDocumentScripts(TERMINAL_HTML_FIT_SCALE, emitted, 'scope')).toEqual({
|
||||
equivalent: true,
|
||||
normalisations: {
|
||||
qualifiedReferences: 47,
|
||||
scopeFieldDeclarations: 0,
|
||||
rebindings: 47,
|
||||
bracedBodies: 20,
|
||||
unboundCatches: 0,
|
||||
numberProperties: 9,
|
||||
shorthandProperties: 0
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
computeFitScale,
|
||||
flog,
|
||||
getCellWidth,
|
||||
getTotalScale,
|
||||
repositionOverlay,
|
||||
updateTransform
|
||||
} from './document-externals'
|
||||
import { scope } from './document-scope'
|
||||
|
||||
export function getCellHeight() {
|
||||
if (!scope.term || !scope.term._core) {
|
||||
return 15
|
||||
}
|
||||
const core = scope.term._core
|
||||
if (core._renderService && core._renderService.dimensions) {
|
||||
return core._renderService.dimensions.css.cell.height || 15
|
||||
}
|
||||
return 15
|
||||
}
|
||||
|
||||
// Why: clamp pan so the terminal content always covers the viewport
|
||||
// when zoomed in. When content is smaller than viewport in a
|
||||
// dimension, pin to top-left (no floating in the middle).
|
||||
export function clampPan() {
|
||||
if (!scope.term || !scope.term.element) {
|
||||
return
|
||||
}
|
||||
const ts = getTotalScale()
|
||||
const cw = scope.term.element.scrollWidth * ts
|
||||
const ch = scope.term.element.scrollHeight * ts
|
||||
const vpW = window.innerWidth
|
||||
const vpH = window.innerHeight
|
||||
if (cw > vpW) {
|
||||
scope.panX = Math.min(0, Math.max(vpW - cw, scope.panX))
|
||||
} else {
|
||||
scope.panX = 0
|
||||
}
|
||||
if (ch > vpH) {
|
||||
scope.panY = Math.min(0, Math.max(vpH - ch, scope.panY))
|
||||
} else {
|
||||
scope.panY = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Why: intentional no-op. Mobile replays a live PTY snapshot then applies
|
||||
// live cursor-relative chunks from that same PTY; resizing only the WebView
|
||||
// xterm changes cursor coordinates and makes TUI repaint chunks duplicate or
|
||||
// overlap. Kept as a no-op so its call sites stay legible.
|
||||
export function adjustRowsForViewport() {}
|
||||
|
||||
// Why: cold-start fit. After init() opens xterm, the renderer needs
|
||||
// several frames before cell dimensions are computed. Reading too early
|
||||
// gives cellWidth=0 (renderer service not ready) or scrollWidth=0 (DOM
|
||||
// not laid out), and computeFitScale returns 1 → no zoom.
|
||||
//
|
||||
// Gate: cellWidth × cols is the canonical "logical width" of the grid
|
||||
// and reflects xterm's layout decision, independent of buffer content.
|
||||
// We commit when cellWidth becomes positive (renderer ready). Fallback:
|
||||
// if cellWidth never becomes available, gate on stable positive
|
||||
// scrollWidth (xterm rendered something). Cap at 60 frames (~1s @60Hz)
|
||||
// so a backgrounded WebView never spins forever.
|
||||
const FIT_RETRY_MAX_FRAMES = 60
|
||||
let fitRetryToken = 0
|
||||
export function applyFitScale(reason: string) {
|
||||
if (!scope.term || !scope.term.element) {
|
||||
return
|
||||
}
|
||||
const token = ++fitRetryToken
|
||||
let attempts = 0
|
||||
let lastScrollWidth = -1
|
||||
function attempt() {
|
||||
if (token !== fitRetryToken) {
|
||||
return
|
||||
}
|
||||
if (!scope.term || !scope.term.element) {
|
||||
return
|
||||
}
|
||||
attempts++
|
||||
const cellW = getCellWidth()
|
||||
if (cellW > 0 && scope.term.cols > 0) {
|
||||
commitFitScale(reason, attempts, 'cellW')
|
||||
return
|
||||
}
|
||||
const w = scope.term.element.scrollWidth
|
||||
if (w > 0 && w === lastScrollWidth) {
|
||||
commitFitScale(reason, attempts, 'stableSW')
|
||||
return
|
||||
}
|
||||
lastScrollWidth = w
|
||||
if (attempts >= FIT_RETRY_MAX_FRAMES) {
|
||||
flog('commit-timeout', {
|
||||
reason: reason,
|
||||
attempts: attempts,
|
||||
cellW: cellW,
|
||||
scrollWidth: w,
|
||||
cols: scope.term.cols
|
||||
})
|
||||
commitFitScale(reason, attempts, 'timeout')
|
||||
return
|
||||
}
|
||||
requestAnimationFrame(attempt)
|
||||
}
|
||||
requestAnimationFrame(attempt)
|
||||
}
|
||||
|
||||
export function commitFitScale(reason: string, attempts: number, gate: string) {
|
||||
if (!scope.term || !scope.term.element) {
|
||||
return
|
||||
}
|
||||
const preSnapScale = computeFitScale()
|
||||
scope.currentScale = preSnapScale
|
||||
// Why: when scale is very close to 1 (e.g. 0.97 from xterm scrollbar
|
||||
// sub-pixels) snap to 1 to avoid imperceptible shrinkage that prevents
|
||||
// a second applyFitScale from observing a "no-op needed" state.
|
||||
if (scope.currentScale >= 0.95) {
|
||||
scope.currentScale = 1
|
||||
}
|
||||
scope.userScale = 1
|
||||
scope.panX = 0
|
||||
scope.panY = 0
|
||||
scope.smoothScrollOffsetY = 0
|
||||
updateTransform()
|
||||
adjustRowsForViewport()
|
||||
|
||||
const cellW = getCellWidth()
|
||||
const sw = scope.term.element.scrollWidth
|
||||
const vpW = window.innerWidth
|
||||
const expectedW = cellW * scope.term.cols
|
||||
const suspect = scope.currentScale === 1 && scope.term.cols > 0 && expectedW > vpW + 1 // expected wider than viewport but no zoom
|
||||
if (suspect) {
|
||||
flog('commit-SUSPECT', {
|
||||
reason: reason,
|
||||
attempts: attempts,
|
||||
gate: gate,
|
||||
preSnapScale: preSnapScale,
|
||||
finalScale: scope.currentScale,
|
||||
cellW: cellW,
|
||||
cols: scope.term.cols,
|
||||
expectedW: expectedW,
|
||||
scrollWidth: sw,
|
||||
vpWidth: vpW
|
||||
})
|
||||
}
|
||||
repositionOverlay()
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { adjustRowsForViewport, applyFitScale, clampPan } from './fit-scale'
|
||||
import {
|
||||
adjustRowsForViewport,
|
||||
applyFitScale,
|
||||
clampPan,
|
||||
handleMsg,
|
||||
notify,
|
||||
reportEngineError,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCellHeight, getCellWidth, getTotalScale } from './document-externals'
|
||||
import { getCellHeight } from './fit-scale'
|
||||
import { getCellWidth, getTotalScale } from './document-externals'
|
||||
import { scope } from './document-scope'
|
||||
|
||||
/** Where a viewport point lands in the terminal's cell grid, for an xterm mouse report. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCellHeight, getTotalScale, updateScrollIndicator } from './document-externals'
|
||||
import { getCellHeight } from './fit-scale'
|
||||
import { getTotalScale, updateScrollIndicator } from './document-externals'
|
||||
import { scope } from './document-scope'
|
||||
|
||||
export function clampNormalScrollLines(lines: number) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { applyFitScale, isAlternateBufferActive, updateScrollIndicator } from './document-externals'
|
||||
import { applyFitScale } from './fit-scale'
|
||||
import { isAlternateBufferActive, updateScrollIndicator } from './document-externals'
|
||||
import { emitKeyboardAvoidanceMetrics } from './keyboard-avoidance-metrics'
|
||||
import { scope } from './document-scope'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getCellHeight } from './fit-scale'
|
||||
import {
|
||||
getCellHeight,
|
||||
getTotalScale,
|
||||
routeScrollLines,
|
||||
shouldRouteScrollToTerminalInput
|
||||
|
||||
Reference in New Issue
Block a user