mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(mobile): ship bounded terminal contrast caches
This commit is contained in:
@@ -93,6 +93,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Verify mobile xterm patch matches the pinned upstream build
|
||||
run: node ../config/scripts/regenerate-xterm-patches-mobile.mjs --check
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
|
||||
@@ -461,7 +461,9 @@ jobs:
|
||||
- name: Verify xterm patches match the pinned upstream build
|
||||
env:
|
||||
WORK_DIR: ${{ runner.temp }}/xterm-patch-build
|
||||
run: node config/scripts/regenerate-xterm-patches.mjs --check --work-dir="$WORK_DIR"
|
||||
run: |
|
||||
node config/scripts/regenerate-xterm-patches.mjs --check --work-dir="$WORK_DIR"
|
||||
node config/scripts/regenerate-xterm-patches-mobile.mjs --check --work-dir="$WORK_DIR"
|
||||
|
||||
shell_contracts:
|
||||
name: shell contracts
|
||||
|
||||
@@ -181,6 +181,9 @@ describe('per-job path classification', () => {
|
||||
expectClassification(['config/patches/@xterm__xterm@6.1.0-beta.287.patch'], {
|
||||
xterm_patch_sync: true
|
||||
})
|
||||
expectClassification(['config/scripts/regenerate-xterm-patches-mobile.mjs'], {
|
||||
xterm_patch_sync: true
|
||||
})
|
||||
})
|
||||
|
||||
it('runs native package jobs only for the platform that ships the changed native', () => {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { regenerateXtermPatches } from './regenerate-xterm-patches.mjs'
|
||||
import { splitPatchEntries } from './xterm-patch-text.mjs'
|
||||
|
||||
const REPO_ROOT = path.resolve(import.meta.dirname, '..', '..')
|
||||
const CONTRAST_SOURCE = 'src/browser/ColorContrastCache.ts'
|
||||
|
||||
export function mobileXtermPatchProfile(manifest, mobilePackage, desktopSource) {
|
||||
const coreEntries = manifest.packages.filter((entry) => entry.name === '@xterm/xterm')
|
||||
if (coreEntries.length !== 1) {
|
||||
throw new Error('Expected exactly one pinned @xterm/xterm package in the upstream manifest')
|
||||
}
|
||||
const core = coreEntries[0]
|
||||
if (mobilePackage.dependencies['@xterm/xterm'] !== core.version) {
|
||||
throw new Error(`Mobile @xterm/xterm must pin the upstream manifest version ${core.version}`)
|
||||
}
|
||||
const entries = splitPatchEntries(desktopSource).filter((entry) => entry.path === CONTRAST_SOURCE)
|
||||
if (entries.length !== 1) {
|
||||
throw new Error(`Expected exactly one ${CONTRAST_SOURCE} source stanza`)
|
||||
}
|
||||
const filename = `@xterm__xterm@${core.version}`
|
||||
return {
|
||||
source: entries[0].text,
|
||||
manifest: {
|
||||
...manifest,
|
||||
packages: [
|
||||
{
|
||||
...core,
|
||||
sourcePatch: `mobile/patches/xterm-src/${filename}.src.patch`,
|
||||
patch: `mobile/patches/${filename}.patch`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function regenerateMobileXtermPatch({
|
||||
mode,
|
||||
repoRoot = REPO_ROOT,
|
||||
workDir = path.join(tmpdir(), 'orca-mobile-xterm-patch-build')
|
||||
}) {
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(path.join(repoRoot, 'config', 'patches', 'xterm-upstream.json'), 'utf8')
|
||||
)
|
||||
const core = manifest.packages.find((entry) => entry.name === '@xterm/xterm')
|
||||
if (!core) {
|
||||
throw new Error('Missing pinned @xterm/xterm package in the upstream manifest')
|
||||
}
|
||||
const profile = mobileXtermPatchProfile(
|
||||
manifest,
|
||||
JSON.parse(readFileSync(path.join(repoRoot, 'mobile', 'package.json'), 'utf8')),
|
||||
readFileSync(path.join(repoRoot, core.sourcePatch), 'utf8')
|
||||
)
|
||||
const sourcePath = path.join(repoRoot, profile.manifest.packages[0].sourcePatch)
|
||||
if (mode === 'write') {
|
||||
mkdirSync(path.dirname(sourcePath), { recursive: true })
|
||||
writeFileSync(sourcePath, profile.source)
|
||||
} else if (readFileSync(sourcePath, 'utf8') !== profile.source) {
|
||||
throw new Error(
|
||||
'Mobile contrast source drifted; run regenerate-xterm-patches-mobile.mjs --write'
|
||||
)
|
||||
}
|
||||
regenerateXtermPatches({
|
||||
mode,
|
||||
repoRoot,
|
||||
workDir,
|
||||
manifest: profile.manifest,
|
||||
lockfileRelativePath: path.join('mobile', 'pnpm-lock.yaml')
|
||||
})
|
||||
}
|
||||
|
||||
function main(argv) {
|
||||
const known = (value) => ['--write', '--check'].includes(value) || value.startsWith('--work-dir=')
|
||||
if (argv.some((value) => !known(value))) {
|
||||
throw new Error(
|
||||
'Usage: regenerate-xterm-patches-mobile.mjs [--check | --write] [--work-dir=<path>]'
|
||||
)
|
||||
}
|
||||
if (argv.includes('--write') && argv.includes('--check')) {
|
||||
throw new Error('Pass either --write or --check, not both')
|
||||
}
|
||||
const workDir = argv.find((value) => value.startsWith('--work-dir='))
|
||||
regenerateMobileXtermPatch({
|
||||
mode: argv.includes('--write') ? 'write' : 'check',
|
||||
...(workDir ? { workDir: path.resolve(workDir.slice('--work-dir='.length)) } : {})
|
||||
})
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1] ? pathToFileURL(realpathSync(process.argv[1])).href : null
|
||||
if (invokedPath === import.meta.url) {
|
||||
try {
|
||||
main(process.argv.slice(2))
|
||||
} catch (error) {
|
||||
console.error(`\n${error.message}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { mobileXtermPatchProfile } from './regenerate-xterm-patches-mobile.mjs'
|
||||
import {
|
||||
patchHash,
|
||||
readLockfilePatchHash,
|
||||
readLockfileResolutionHashes,
|
||||
regenerateXtermPatches
|
||||
} from './regenerate-xterm-patches.mjs'
|
||||
import { selectPatchEntries, sourceHunks, splitPatchEntries } from './xterm-patch-text.mjs'
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..', '..')
|
||||
const temporaryDirectories = []
|
||||
const readProject = (file) => readFile(path.join(ROOT, file), 'utf8')
|
||||
const manifest = JSON.parse(await readProject('config/patches/xterm-upstream.json'))
|
||||
const mobilePackage = JSON.parse(await readProject('mobile/package.json'))
|
||||
const desktopCore = manifest.packages.find((entry) => entry.name === '@xterm/xterm')
|
||||
const desktopSource = await readProject(desktopCore.sourcePatch)
|
||||
const profile = mobileXtermPatchProfile(manifest, mobilePackage, desktopSource)
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))
|
||||
)
|
||||
})
|
||||
|
||||
describe('mobile xterm patch generation', () => {
|
||||
it('reuses the pinned core build while selecting only the contrast source', () => {
|
||||
expect(profile.manifest.upstream).toEqual(manifest.upstream)
|
||||
expect(profile.manifest.toolchain).toEqual(manifest.toolchain)
|
||||
expect(profile.manifest.packages).toHaveLength(1)
|
||||
expect(profile.manifest.packages[0]).toMatchObject({
|
||||
name: '@xterm/xterm',
|
||||
version: desktopCore.version,
|
||||
build: desktopCore.build,
|
||||
generatedPaths: desktopCore.generatedPaths
|
||||
})
|
||||
expect(splitPatchEntries(profile.source).map((entry) => entry.path)).toEqual([
|
||||
'src/browser/ColorContrastCache.ts'
|
||||
])
|
||||
expect(profile.source).toBe(
|
||||
selectPatchEntries(desktopSource, (file) => file.endsWith('/ColorContrastCache.ts'))
|
||||
)
|
||||
expect(profile.manifest.packages[0].sourcePatch).toMatch(/^mobile\/patches\/xterm-src\//)
|
||||
expect(profile.manifest.packages[0].patch).toMatch(/^mobile\/patches\//)
|
||||
expect(desktopCore.sourcePatch).toMatch(/^config\/patches\//)
|
||||
})
|
||||
|
||||
it('refuses a divergent or unpinned mobile version before building', () => {
|
||||
for (const version of ['6.1.0-beta.302', `^${desktopCore.version}`, undefined]) {
|
||||
const changed = {
|
||||
...mobilePackage,
|
||||
dependencies: { ...mobilePackage.dependencies, '@xterm/xterm': version }
|
||||
}
|
||||
expect(() => mobileXtermPatchProfile(manifest, changed, desktopSource)).toThrow('must pin')
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses missing or duplicate core entries and contrast stanzas', () => {
|
||||
for (const packages of [[], [desktopCore, desktopCore]]) {
|
||||
expect(() =>
|
||||
mobileXtermPatchProfile({ ...manifest, packages }, mobilePackage, desktopSource)
|
||||
).toThrow('exactly one pinned')
|
||||
}
|
||||
for (const source of ['', profile.source + profile.source]) {
|
||||
expect(() => mobileXtermPatchProfile(manifest, mobilePackage, source)).toThrow(
|
||||
'exactly one src/browser/ColorContrastCache.ts'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('uses the selected manifest and lockfile without requiring desktop files', async () => {
|
||||
const repoRoot = await mkdtemp(path.join(tmpdir(), 'orca-mobile-xterm-profile-'))
|
||||
temporaryDirectories.push(repoRoot)
|
||||
await mkdir(path.join(repoRoot, 'mobile'))
|
||||
const lockfile = 'lockfileVersion: 9.0\n'
|
||||
await writeFile(path.join(repoRoot, 'mobile', 'pnpm-lock.yaml'), lockfile)
|
||||
regenerateXtermPatches({
|
||||
mode: 'check',
|
||||
repoRoot,
|
||||
workDir: path.join(repoRoot, 'scratch'),
|
||||
manifest: { ...profile.manifest, packages: [] },
|
||||
lockfileRelativePath: path.join('mobile', 'pnpm-lock.yaml')
|
||||
})
|
||||
expect(await readFile(path.join(repoRoot, 'mobile', 'pnpm-lock.yaml'), 'utf8')).toBe(lockfile)
|
||||
})
|
||||
|
||||
it('ships only the contrast source and all four rebuilt bundle/map files', async () => {
|
||||
const core = profile.manifest.packages[0]
|
||||
const source = await readProject(core.sourcePatch)
|
||||
const patch = await readProject(core.patch)
|
||||
expect(source).toBe(profile.source)
|
||||
expect(sourceHunks(patch)).toBe(profile.source)
|
||||
expect(
|
||||
splitPatchEntries(patch)
|
||||
.map((entry) => entry.path)
|
||||
.sort()
|
||||
).toEqual([
|
||||
'lib/xterm.js',
|
||||
'lib/xterm.js.map',
|
||||
'lib/xterm.mjs',
|
||||
'lib/xterm.mjs.map',
|
||||
'src/browser/ColorContrastCache.ts'
|
||||
])
|
||||
expect(patch).not.toContain('xterm-composition-transaction-accepted')
|
||||
const lockfile = await readProject('mobile/pnpm-lock.yaml')
|
||||
const key = `${core.name}@${core.version}`
|
||||
const hash = patchHash(patch)
|
||||
expect(readLockfilePatchHash(lockfile, key)).toBe(hash)
|
||||
expect(readLockfileResolutionHashes(lockfile, key).length).toBeGreaterThan(0)
|
||||
expect(new Set(readLockfileResolutionHashes(lockfile, key))).toEqual(new Set([hash]))
|
||||
})
|
||||
})
|
||||
@@ -439,14 +439,15 @@ export function regenerateXtermPatches({
|
||||
mode,
|
||||
repoRoot = DEFAULT_REPO_ROOT,
|
||||
workDir = path.join(tmpdir(), 'orca-xterm-patch-build'),
|
||||
manifest = JSON.parse(readFileSync(path.join(repoRoot, MANIFEST_RELATIVE_PATH), 'utf8')),
|
||||
lockfileRelativePath = 'pnpm-lock.yaml',
|
||||
log = console.info
|
||||
} = {}) {
|
||||
const manifest = JSON.parse(readFileSync(path.join(repoRoot, MANIFEST_RELATIVE_PATH), 'utf8'))
|
||||
assertBuildStepsAllowed(manifest)
|
||||
assertSourcemapPolicy(manifest)
|
||||
mkdirSync(workDir, { recursive: true })
|
||||
|
||||
const lockfilePath = path.join(repoRoot, 'pnpm-lock.yaml')
|
||||
const lockfilePath = path.join(repoRoot, lockfileRelativePath)
|
||||
let lockfile = readFileSync(lockfilePath, 'utf8')
|
||||
let lockfileChanged = false
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ terminal output, and transport behavior do not change.
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 node docs/audits/terminal-contrast-cache-retention/reproduce.mjs
|
||||
ORCA_BACKGROUND_LAUNCH=1 node docs/audits/terminal-contrast-cache-retention/reproduce-dom.mjs
|
||||
ORCA_BACKGROUND_LAUNCH=1 ORCA_AUDIT_MOBILE=1 node docs/audits/terminal-contrast-cache-retention/reproduce-dom.mjs
|
||||
```
|
||||
|
||||
Both scripts exercise the installed CJS and ESM bundles in headless Chromium.
|
||||
@@ -49,6 +50,12 @@ produce the same computed CSS color and rendered row HTML as before.
|
||||
All 12 DOM cases pass across the baseline and both fixed module formats; samples
|
||||
reach the exact 4,096-entry cap. Every page and browser is closed after the run.
|
||||
|
||||
The mobile mode loads the actual generated WebView engine after mobile's
|
||||
postinstall. `ORCA_AUDIT_MOBILE_BASELINE` accepts a pre-fix generated engine to
|
||||
include before cases. Its eight recorded dark/light and normal/dim cases also
|
||||
preserve the corrected CSS colors and row HTML after eviction and recalculation.
|
||||
The fixed engine stays within 4,096 entries; the baseline exceeds 10,000.
|
||||
|
||||
## Validation and limits
|
||||
|
||||
- 122 tests pass across cache, regeneration, contrast, appearance, IME, and
|
||||
@@ -59,8 +66,13 @@ reach the exact 4,096-entry cap. Every page and browser is closed after the run.
|
||||
- Full desktop typecheck, formatting/lint, and changed-code quality pass.
|
||||
- Captured browser: Chromium 147.0.7727.15 on macOS. Heap measurements include
|
||||
GC/allocator variation and other terminal state.
|
||||
- This is a desktop renderer fix. Mobile resolves its own unpatched xterm package
|
||||
when generating its WebView engine; that separate bundle is not fixed here.
|
||||
- Mobile's separate package receives a contrast-only source patch, rebuilt
|
||||
CJS/ESM bundles and source maps. The existing pinned generator verifies both
|
||||
variants, and mobile's postinstall rebuilds its gitignored WebView engine.
|
||||
Desktop IME and SortedList patches are not copied into mobile.
|
||||
- The mobile follow-up passes 90 generator/cache/CI-contract tests and 23 mobile
|
||||
engine/theme tests, mobile typecheck, frozen install, and regeneration checks.
|
||||
These counts overlap with the earlier desktop pass.
|
||||
- `v1.4.198` shipped the same xterm version and automatic 3/4.5 contrast settings.
|
||||
Its appearance path also skipped unchanged theme/ratio assignments, so ordinary
|
||||
reapplication did not periodically clear the cache. User contrast overrides were
|
||||
|
||||
@@ -2,19 +2,26 @@ import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') {
|
||||
throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1')
|
||||
}
|
||||
|
||||
const mobile = process.env.ORCA_AUDIT_MOBILE === '1'
|
||||
const installed = resolve('node_modules/@xterm/xterm/lib/xterm.js')
|
||||
const bundles = [
|
||||
['after', installed],
|
||||
['after', installed.replace(/\.js$/, '.mjs')]
|
||||
]
|
||||
if (process.env.ORCA_AUDIT_XTERM_BASELINE) {
|
||||
bundles.unshift(['before', resolve(process.env.ORCA_AUDIT_XTERM_BASELINE)])
|
||||
const bundles = mobile
|
||||
? [['after', resolve('mobile/src/terminal/terminal-webview-engine.generated.ts')]]
|
||||
: [
|
||||
['after', installed],
|
||||
['after', installed.replace(/\.js$/, '.mjs')]
|
||||
]
|
||||
const baseline = mobile
|
||||
? process.env.ORCA_AUDIT_MOBILE_BASELINE
|
||||
: process.env.ORCA_AUDIT_XTERM_BASELINE
|
||||
if (baseline) {
|
||||
bundles.unshift(['before', resolve(baseline)])
|
||||
}
|
||||
const browser = await chromium.launch({
|
||||
executablePath: process.env.ORCA_AUDIT_CHROMIUM,
|
||||
@@ -27,7 +34,9 @@ let pagesOpened = 0
|
||||
let pagesClosed = 0
|
||||
try {
|
||||
for (const [phase, bundle] of bundles) {
|
||||
const source = await readFile(bundle, 'utf8')
|
||||
const source = mobile
|
||||
? (await import(pathToFileURL(bundle).href)).XTERM_ENGINE_JS
|
||||
: await readFile(bundle, 'utf8')
|
||||
const sha256 = createHash('sha256').update(source).digest('hex')
|
||||
for (const theme of ['dark', 'light']) {
|
||||
for (const mode of ['normal', 'dim']) {
|
||||
@@ -43,7 +52,7 @@ try {
|
||||
},
|
||||
`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`
|
||||
)
|
||||
: page.addScriptTag({ path: bundle }))
|
||||
: page.addScriptTag({ content: source }))
|
||||
const initial = await page.evaluate(
|
||||
async ({ theme, mode }) => {
|
||||
const terminal = new Terminal({
|
||||
@@ -181,7 +190,7 @@ try {
|
||||
}
|
||||
results.push({
|
||||
phase,
|
||||
format: bundle.endsWith('.mjs') ? 'esm' : 'cjs',
|
||||
format: mobile ? 'mobile-webview' : bundle.endsWith('.mjs') ? 'esm' : 'cjs',
|
||||
theme,
|
||||
mode,
|
||||
sha256,
|
||||
@@ -212,6 +221,7 @@ console.log(
|
||||
browser: browserVersion,
|
||||
headless: true,
|
||||
renderer: 'DOM',
|
||||
mobile,
|
||||
pagesOpened,
|
||||
pagesClosed,
|
||||
browserClosed: !browser.isConnected(),
|
||||
|
||||
@@ -0,0 +1,708 @@
|
||||
{
|
||||
"node": "v26.6.0",
|
||||
"browser": "147.0.7727.15",
|
||||
"headless": true,
|
||||
"renderer": "DOM",
|
||||
"mobile": true,
|
||||
"pagesOpened": 8,
|
||||
"pagesClosed": 8,
|
||||
"browserClosed": true,
|
||||
"results": [
|
||||
{
|
||||
"phase": "before",
|
||||
"format": "mobile-webview",
|
||||
"theme": "dark",
|
||||
"mode": "normal",
|
||||
"sha256": "96cb91487352f919c4c374da16305f9074c4a866bf5ad90fbd087a3cc0e83077",
|
||||
"initial": {
|
||||
"minimumContrastRatio": 3,
|
||||
"rawColor": "rgb(5, 50, 25)",
|
||||
"color": "rgb(74, 107, 88)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#4a6b58;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"updates": 0,
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 1000,
|
||||
"contrast": 1002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4094,
|
||||
"contrast": 4096,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4095,
|
||||
"contrast": 4097,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4096,
|
||||
"contrast": 4098,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 5000,
|
||||
"contrast": 5002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 10000,
|
||||
"contrast": 10002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"revisited": {
|
||||
"color": "rgb(74, 107, 88)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#4a6b58;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 10002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
"cleared": {
|
||||
"contrast": 0,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
"recomputed": {
|
||||
"color": "rgb(74, 107, 88)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#4a6b58;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase": "before",
|
||||
"format": "mobile-webview",
|
||||
"theme": "dark",
|
||||
"mode": "dim",
|
||||
"sha256": "96cb91487352f919c4c374da16305f9074c4a866bf5ad90fbd087a3cc0e83077",
|
||||
"initial": {
|
||||
"minimumContrastRatio": 3,
|
||||
"rawColor": "rgb(5, 50, 25)",
|
||||
"color": "rgb(30, 71, 48)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#1e4730;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"updates": 0,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 1000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1001,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4094,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4095,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4095,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4096,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4096,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4097,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 5000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 5001,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 10000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 10001,
|
||||
"probeCached": true
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"revisited": {
|
||||
"color": "rgb(30, 71, 48)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#1e4730;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 10001,
|
||||
"probeCached": true
|
||||
},
|
||||
"cleared": {
|
||||
"contrast": 0,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
"recomputed": {
|
||||
"color": "rgb(30, 71, 48)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#1e4730;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase": "before",
|
||||
"format": "mobile-webview",
|
||||
"theme": "light",
|
||||
"mode": "normal",
|
||||
"sha256": "96cb91487352f919c4c374da16305f9074c4a866bf5ad90fbd087a3cc0e83077",
|
||||
"initial": {
|
||||
"minimumContrastRatio": 4.5,
|
||||
"rawColor": "rgb(245, 250, 240)",
|
||||
"color": "rgb(116, 117, 113)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#747571;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"updates": 0,
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 1000,
|
||||
"contrast": 1002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4094,
|
||||
"contrast": 4096,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4095,
|
||||
"contrast": 4097,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4096,
|
||||
"contrast": 4098,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 5000,
|
||||
"contrast": 5002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 10000,
|
||||
"contrast": 10002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"revisited": {
|
||||
"color": "rgb(116, 117, 113)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#747571;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 10002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
"cleared": {
|
||||
"contrast": 0,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
"recomputed": {
|
||||
"color": "rgb(116, 117, 113)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#747571;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase": "before",
|
||||
"format": "mobile-webview",
|
||||
"theme": "light",
|
||||
"mode": "dim",
|
||||
"sha256": "96cb91487352f919c4c374da16305f9074c4a866bf5ad90fbd087a3cc0e83077",
|
||||
"initial": {
|
||||
"minimumContrastRatio": 4.5,
|
||||
"rawColor": "rgb(245, 250, 240)",
|
||||
"color": "rgb(160, 162, 156)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#a0a29c;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"updates": 0,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 1000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1001,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4094,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4095,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4095,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4096,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4096,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4097,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 5000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 5001,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 10000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 10001,
|
||||
"probeCached": true
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"revisited": {
|
||||
"color": "rgb(160, 162, 156)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#a0a29c;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 10001,
|
||||
"probeCached": true
|
||||
},
|
||||
"cleared": {
|
||||
"contrast": 0,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
"recomputed": {
|
||||
"color": "rgb(160, 162, 156)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#a0a29c;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"format": "mobile-webview",
|
||||
"theme": "dark",
|
||||
"mode": "normal",
|
||||
"sha256": "0da8b0e571e1325f5af62e990318a3dd31c444dab6a260563952498ce1fc9932",
|
||||
"initial": {
|
||||
"minimumContrastRatio": 3,
|
||||
"rawColor": "rgb(5, 50, 25)",
|
||||
"color": "rgb(74, 107, 88)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#4a6b58;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"updates": 0,
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 1000,
|
||||
"contrast": 1002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4094,
|
||||
"contrast": 4096,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4095,
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 4096,
|
||||
"contrast": 3,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 5000,
|
||||
"contrast": 907,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 10000,
|
||||
"contrast": 1812,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"revisited": {
|
||||
"color": "rgb(74, 107, 88)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#4a6b58;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1813,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
"cleared": {
|
||||
"contrast": 0,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
"recomputed": {
|
||||
"color": "rgb(74, 107, 88)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#4a6b58;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"format": "mobile-webview",
|
||||
"theme": "dark",
|
||||
"mode": "dim",
|
||||
"sha256": "0da8b0e571e1325f5af62e990318a3dd31c444dab6a260563952498ce1fc9932",
|
||||
"initial": {
|
||||
"minimumContrastRatio": 3,
|
||||
"rawColor": "rgb(5, 50, 25)",
|
||||
"color": "rgb(30, 71, 48)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#1e4730;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"updates": 0,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 1000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1001,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4094,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4095,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4095,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4096,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4096,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 5000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 905,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 10000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1809,
|
||||
"probeCached": false
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"revisited": {
|
||||
"color": "rgb(30, 71, 48)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#1e4730;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1810,
|
||||
"probeCached": true
|
||||
},
|
||||
"cleared": {
|
||||
"contrast": 0,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
"recomputed": {
|
||||
"color": "rgb(30, 71, 48)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#1e4730;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"format": "mobile-webview",
|
||||
"theme": "light",
|
||||
"mode": "normal",
|
||||
"sha256": "0da8b0e571e1325f5af62e990318a3dd31c444dab6a260563952498ce1fc9932",
|
||||
"initial": {
|
||||
"minimumContrastRatio": 4.5,
|
||||
"rawColor": "rgb(245, 250, 240)",
|
||||
"color": "rgb(116, 117, 113)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#747571;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"updates": 0,
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 1000,
|
||||
"contrast": 1002,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4094,
|
||||
"contrast": 4096,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4095,
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 4096,
|
||||
"contrast": 3,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 5000,
|
||||
"contrast": 907,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 10000,
|
||||
"contrast": 1812,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"revisited": {
|
||||
"color": "rgb(116, 117, 113)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#747571;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1813,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
},
|
||||
"cleared": {
|
||||
"contrast": 0,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
"recomputed": {
|
||||
"color": "rgb(116, 117, 113)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#747571;\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 2,
|
||||
"dimContrast": 0,
|
||||
"probeCached": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"format": "mobile-webview",
|
||||
"theme": "light",
|
||||
"mode": "dim",
|
||||
"sha256": "0da8b0e571e1325f5af62e990318a3dd31c444dab6a260563952498ce1fc9932",
|
||||
"initial": {
|
||||
"minimumContrastRatio": 4.5,
|
||||
"rawColor": "rgb(245, 250, 240)",
|
||||
"color": "rgb(160, 162, 156)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#a0a29c;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"updates": 0,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 1000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1001,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4094,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4095,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4095,
|
||||
"contrast": 1,
|
||||
"dimContrast": 4096,
|
||||
"probeCached": true
|
||||
},
|
||||
{
|
||||
"updates": 4096,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 5000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 905,
|
||||
"probeCached": false
|
||||
},
|
||||
{
|
||||
"updates": 10000,
|
||||
"contrast": 1,
|
||||
"dimContrast": 1809,
|
||||
"probeCached": false
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"revisited": {
|
||||
"color": "rgb(160, 162, 156)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#a0a29c;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1810,
|
||||
"probeCached": true
|
||||
},
|
||||
"cleared": {
|
||||
"contrast": 0,
|
||||
"dimContrast": 0,
|
||||
"probeCached": false
|
||||
},
|
||||
"recomputed": {
|
||||
"color": "rgb(160, 162, 156)",
|
||||
"opacity": "1",
|
||||
"html": "<span style=\"color:#a0a29c;\" class=\"xterm-dim\">M</span><span> </span>",
|
||||
"text": "M",
|
||||
"contrast": 1,
|
||||
"dimContrast": 1,
|
||||
"probeCached": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
## Scope
|
||||
|
||||
Orca ships `@xterm/xterm` with four source changes it needs and upstream has
|
||||
Orca ships `@xterm/xterm` with source changes it needs and upstream has
|
||||
not taken: the IME composition hooks, the `xterm-composition-*` custom events
|
||||
they raise, the `ICompositionHelper` surface those hooks widen, and a `SortedList`
|
||||
fix. pnpm applies them through `config/patches/@xterm__xterm@<version>.patch`.
|
||||
fix, plus a bound on contrast-cache entries. pnpm applies them through
|
||||
`config/patches/@xterm__xterm@<version>.patch`.
|
||||
|
||||
That patch touches eight files. Four are hand-authored source
|
||||
(`src/browser/CoreBrowserTerminal.ts`, `src/browser/Types.ts`,
|
||||
That patch touches nine files. Five are hand-authored source
|
||||
(`src/browser/ColorContrastCache.ts`, `src/browser/CoreBrowserTerminal.ts`, `src/browser/Types.ts`,
|
||||
`src/browser/input/CompositionHelper.ts`, `src/common/SortedList.ts`) and four
|
||||
are the build output those sources produce (`lib/xterm.js`, `lib/xterm.mjs`,
|
||||
and both sourcemaps). The bundle half is 7.3 MB of minified code. It is
|
||||
@@ -122,6 +123,27 @@ Run the checkout outside this repository. A build tree underneath it makes
|
||||
`tsgo` walk up into Orca's own `node_modules` and fail with `TS2300: Duplicate
|
||||
identifier`, which is a symptom of where the tree sits and not of the patch.
|
||||
|
||||
## Mobile Contrast Cache Patch
|
||||
|
||||
Mobile installs xterm in a separate pnpm project. Its patch includes only
|
||||
`ColorContrastCache.ts`; the desktop IME and `SortedList` changes are excluded.
|
||||
`regenerate-xterm-patches-mobile.mjs` derives that source stanza and reuses the
|
||||
desktop manifest's version, upstream commit, toolchain, and build steps. It fails
|
||||
if mobile pins a different version or the contrast stanza is missing or duplicated.
|
||||
|
||||
After editing the desktop contrast source and regenerating the desktop patch:
|
||||
|
||||
```sh
|
||||
node config/scripts/regenerate-xterm-patches-mobile.mjs --write
|
||||
pnpm exec pnpm --dir mobile install
|
||||
node config/scripts/regenerate-xterm-patches-mobile.mjs --check
|
||||
```
|
||||
|
||||
Commit the derived mobile source patch, full patch, and mobile lockfile together.
|
||||
Mobile's postinstall rebuilds its gitignored terminal engine from that patched
|
||||
package. Do not copy desktop's full patch or hand-edit the generated engine.
|
||||
The existing xterm patch-sync CI job verifies both variants.
|
||||
|
||||
## How the Commit Is Known
|
||||
|
||||
Upstream `bin/publish.js` sets `packageJson.commit` before `npm publish`, so
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
|
||||
diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts
|
||||
index fdcd9d133199a6cd6ba9bea9606a02c03ad03b3d..0558a8873f680e8fb2a27cc833c49c5ba658e1dd 100644
|
||||
--- a/src/browser/ColorContrastCache.ts
|
||||
+++ b/src/browser/ColorContrastCache.ts
|
||||
@@ -7,11 +7,17 @@ import { IColorContrastCache } from './Types';
|
||||
import { IColor } from '../common/Types';
|
||||
import { TwoKeyMap } from '../common/MultiKeyMap';
|
||||
|
||||
+const CONTRAST_CACHE_MAX_ENTRIES = 4096;
|
||||
+
|
||||
export class ColorContrastCache implements IColorContrastCache {
|
||||
private _color: TwoKeyMap</* bg */number, /* fg */number, IColor | null> = new TwoKeyMap();
|
||||
private _css: TwoKeyMap</* bg */number, /* fg */number, string | null> = new TwoKeyMap();
|
||||
+ private _entryCount = 0;
|
||||
|
||||
public setCss(bg: number, fg: number, value: string | null): void {
|
||||
+ if (this._css.get(bg, fg) === undefined) {
|
||||
+ this._admitNewEntry();
|
||||
+ }
|
||||
this._css.set(bg, fg, value);
|
||||
}
|
||||
|
||||
@@ -20,6 +26,9 @@ export class ColorContrastCache implements IColorContrastCache {
|
||||
}
|
||||
|
||||
public setColor(bg: number, fg: number, value: IColor | null): void {
|
||||
+ if (this._color.get(bg, fg) === undefined) {
|
||||
+ this._admitNewEntry();
|
||||
+ }
|
||||
this._color.set(bg, fg, value);
|
||||
}
|
||||
|
||||
@@ -30,5 +39,14 @@ export class ColorContrastCache implements IColorContrastCache {
|
||||
public clear(): void {
|
||||
this._color.clear();
|
||||
this._css.clear();
|
||||
+ this._entryCount = 0;
|
||||
+ }
|
||||
+
|
||||
+ private _admitNewEntry(): void {
|
||||
+ // Color pairs outlive atlas pages, including cached misses and DOM-rendered colors.
|
||||
+ if (this._entryCount >= CONTRAST_CACHE_MAX_ENTRIES) {
|
||||
+ this.clear();
|
||||
+ }
|
||||
+ this._entryCount++;
|
||||
}
|
||||
}
|
||||
Generated
+6
-5
@@ -9,6 +9,7 @@ overrides:
|
||||
query-string: 9.5.1
|
||||
|
||||
patchedDependencies:
|
||||
'@xterm/xterm@6.1.0-beta.303': 3a6c0d12de82b2cb60b3bb93dd7f03cf25672986bce77aa3aefac986e4135163
|
||||
expo-notifications@55.0.27:
|
||||
hash: ce20843a3daad4185d7e8571788fa323ba4d11984936188790858650a61749c0
|
||||
path: patches/expo-notifications@55.0.27.patch
|
||||
@@ -34,13 +35,13 @@ importers:
|
||||
version: 2.2.0(react-native@0.83.10(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.8))
|
||||
'@xterm/addon-unicode11':
|
||||
specifier: 0.10.0-beta.300
|
||||
version: 0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303)
|
||||
version: 0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=3a6c0d12de82b2cb60b3bb93dd7f03cf25672986bce77aa3aefac986e4135163))
|
||||
'@xterm/addon-webgl':
|
||||
specifier: 0.20.0-beta.299
|
||||
version: 0.20.0-beta.299(@xterm/xterm@6.1.0-beta.303)
|
||||
version: 0.20.0-beta.299(@xterm/xterm@6.1.0-beta.303(patch_hash=3a6c0d12de82b2cb60b3bb93dd7f03cf25672986bce77aa3aefac986e4135163))
|
||||
'@xterm/xterm':
|
||||
specifier: 6.1.0-beta.303
|
||||
version: 6.1.0-beta.303
|
||||
version: 6.1.0-beta.303(patch_hash=3a6c0d12de82b2cb60b3bb93dd7f03cf25672986bce77aa3aefac986e4135163)
|
||||
buffer:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
@@ -10516,11 +10517,11 @@ snapshots:
|
||||
|
||||
'@xmldom/xmldom@0.9.12': {}
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303)':
|
||||
'@xterm/addon-unicode11@0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=3a6c0d12de82b2cb60b3bb93dd7f03cf25672986bce77aa3aefac986e4135163))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.299(@xterm/xterm@6.1.0-beta.303)':
|
||||
'@xterm/addon-webgl@0.20.0-beta.299(@xterm/xterm@6.1.0-beta.303(patch_hash=3a6c0d12de82b2cb60b3bb93dd7f03cf25672986bce77aa3aefac986e4135163))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ overrides:
|
||||
query-string: 9.5.1
|
||||
|
||||
patchedDependencies:
|
||||
'@xterm/xterm@6.1.0-beta.303': patches/@xterm__xterm@6.1.0-beta.303.patch
|
||||
expo-notifications@55.0.27: patches/expo-notifications@55.0.27.patch
|
||||
react-native-webview@13.16.2: patches/react-native-webview@13.16.2.patch
|
||||
react-native@0.83.10: patches/react-native@0.83.10.patch
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { Script } from 'node:vm'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { XTERM_ENGINE_JS } from './terminal-webview-engine.generated'
|
||||
|
||||
type CachedColor = { css: string; rgba: number }
|
||||
type ContrastCache = {
|
||||
setColor(bg: number, fg: number, value: CachedColor | null): void
|
||||
getColor(bg: number, fg: number): CachedColor | null | undefined
|
||||
setCss(bg: number, fg: number, value: string | null): void
|
||||
getCss(bg: number, fg: number): string | null | undefined
|
||||
_color: { _data: Record<number, Record<number, CachedColor | null>> }
|
||||
_css: { _data: Record<number, Record<number, string | null>> }
|
||||
}
|
||||
type BundledTerminal = {
|
||||
open(element: HTMLElement): void
|
||||
dispose(): void
|
||||
options: { theme: { background: string } }
|
||||
_core: {
|
||||
_themeService: { colors: { contrastCache: ContrastCache; halfContrastCache: ContrastCache } }
|
||||
}
|
||||
}
|
||||
|
||||
const entries = (cache: ContrastCache): number =>
|
||||
[cache._color, cache._css].reduce(
|
||||
(total, map) =>
|
||||
total + Object.values(map._data).reduce((count, row) => count + Object.keys(row).length, 0),
|
||||
0
|
||||
)
|
||||
|
||||
describe('the mobile bundled xterm contrast caches', () => {
|
||||
let terminal: BundledTerminal
|
||||
let cache: ContrastCache
|
||||
let dimCache: ContrastCache
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('OffscreenCanvas', undefined)
|
||||
// Happy DOM has no font rasterizer; cache behavior uses the real bundled implementation.
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm's DOM font probe only reads font and measureText from this canvas context.
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
|
||||
font: '',
|
||||
measureText: () => ({ width: 8 })
|
||||
} as CanvasRenderingContext2D)
|
||||
const Terminal: new (options: { minimumContrastRatio: number }) => BundledTerminal = new Script(
|
||||
`${XTERM_ENGINE_JS}\nwindow.Terminal`
|
||||
).runInThisContext()
|
||||
terminal = new Terminal({ minimumContrastRatio: 3 })
|
||||
document.body.innerHTML = '<div id="terminal"></div>'
|
||||
terminal.open(document.getElementById('terminal')!)
|
||||
cache = terminal._core._themeService.colors.contrastCache
|
||||
dimCache = terminal._core._themeService.colors.halfContrastCache
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
terminal?.dispose()
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('counts cached nulls and leaves replacement writes within capacity', () => {
|
||||
for (let index = 0; index < 4096; index++) {
|
||||
cache.setColor(0, index, null)
|
||||
}
|
||||
const corrected = { css: '#ffffff', rgba: 0xffffffff }
|
||||
for (let index = 0; index < 10000; index++) {
|
||||
cache.setColor(0, 4095, corrected)
|
||||
}
|
||||
expect(cache.getColor(0, 0)).toBeNull()
|
||||
expect(cache.getColor(0, 4095)).toBe(corrected)
|
||||
expect(entries(cache)).toBe(4096)
|
||||
cache.setColor(0, 4096, null)
|
||||
expect(cache.getColor(0, 0)).toBeUndefined()
|
||||
expect(entries(cache)).toBe(1)
|
||||
})
|
||||
|
||||
it('bounds mixed color/CSS entries in both normal and dim caches', () => {
|
||||
for (let index = 0; index < 100000; index++) {
|
||||
cache.setColor(index, 0, null)
|
||||
cache.setCss(index, 1, '#ffffff')
|
||||
dimCache.setColor(index, 0, null)
|
||||
dimCache.setCss(index, 1, '#eeeeee')
|
||||
if (index % 2048 === 2047) {
|
||||
expect(entries(cache)).toBeLessThanOrEqual(4096)
|
||||
expect(entries(dimCache)).toBeLessThanOrEqual(4096)
|
||||
}
|
||||
}
|
||||
expect(cache.getCss(99999, 1)).toBe('#ffffff')
|
||||
expect(dimCache.getCss(99999, 1)).toBe('#eeeeee')
|
||||
expect(entries(cache) + entries(dimCache)).toBeLessThanOrEqual(8192)
|
||||
})
|
||||
|
||||
it('resets both capacities when the actual theme service clears them', () => {
|
||||
for (let index = 0; index < 4096; index++) {
|
||||
cache.setColor(index, 0, null)
|
||||
dimCache.setCss(index, 0, null)
|
||||
}
|
||||
terminal.options.theme = { background: '#123456' }
|
||||
expect(entries(cache)).toBe(0)
|
||||
expect(entries(dimCache)).toBe(0)
|
||||
for (let index = 0; index < 4096; index++) {
|
||||
cache.setColor(index, 0, null)
|
||||
dimCache.setCss(index, 0, null)
|
||||
}
|
||||
expect(cache.getColor(0, 0)).toBeNull()
|
||||
expect(dimCache.getCss(0, 0)).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user