251 lines
9.8 KiB
TypeScript
251 lines
9.8 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
export const EMOJI_TABLE_FIXTURE = readFileSync(
|
|
path.join(__dirname, 'fixtures', 'terminal-emoji-table.md'),
|
|
'utf8'
|
|
)
|
|
export const NARROW_TERMINAL_MAX_COLS = 120
|
|
|
|
export function longMarkdownTableScript(runId: string): string {
|
|
const names = [
|
|
['Sam Syntax', 'Compiler', 'Online', '😀', '9200', 'Semicolons are optional (rage ensues)'],
|
|
['Tori Token', 'Auth', 'Idle', '🚀', '4800', 'JWT expires during their standup'],
|
|
['Uma Unpin', 'Frontend', 'Online', '🔥', '3500', 'Absolute positioning enjoyer'],
|
|
['Vic Variable', 'Types', 'AFK', '💡', '6700', 'any is not a type, it is a cry for help'],
|
|
['Wally Watchdog', 'Security', 'Online', '📦', '8200', 'Found a vuln in your vuln scanner'],
|
|
['Xena XPath', 'DB', 'Idle', '🔐', '7300', 'Indexes everything, including the fridge'],
|
|
['Yuki Yank', 'CLI', 'Online', '🎯', '5900', 'rm -rf / is not a party trick'],
|
|
['Zane Zealot', 'OSS', 'Offline', '🤖', '10000', 'Contributor to 47 repos, sleeps never'],
|
|
['Artie ASCII', 'Docs', 'Online', '🧠', '2900', 'Wrote a novel in README comments'],
|
|
['Bianca Batch', 'ML', 'AFK', '💾', '9400', 'Training a model to write PR descriptions'],
|
|
['Carlos Cache', 'CDN', 'Idle', '⚙', '4900', 'Stale data is still data'],
|
|
['Diana Draft', 'Planning', 'Online', '📚', '1800', 'Needs 3 more sprints to estimate'],
|
|
['Edgar Exit', 'Ops', 'Online', '🔧', '7600', 'Graceful shutdown specialist'],
|
|
['Fiona Fallback', 'Resilience', 'Idle', '🧲', '5500', 'Circuit breaker connoisseur'],
|
|
['Gabe Garbage', 'GC', 'Offline', '🧹', '4100', 'Stop-the-world is my catchphrase'],
|
|
['Holly Hotfix', 'Release', 'Online', '🧪', '6300', 'Friday deploy champion'],
|
|
['Ira Idempotent', 'API', 'AFK', '🔁', '6900', 'PUT me in coach'],
|
|
['Jules Jitter', 'Mobile', 'Idle', '📱', '3200', 'Offline-first, coffee-second'],
|
|
['Ken Kafka', 'Streams', 'Online', '📡', '7100', 'Rebalancing is a lifestyle'],
|
|
['Luna Latency', 'Edge', 'Offline', '🧭', '4400', 'Response time measured in business days'],
|
|
['Max Marshal', 'Memory', 'Online', '🧩', '8700', "Leak-free since '24"],
|
|
['Nora Null', 'Safety', 'AFK', '❓', '3800', 'null is a person, not a value'],
|
|
['Otto Offset', 'Cursors', 'Idle', '👆', '2600', 'Infinite scroll for the infinite soul'],
|
|
['Pam Payload', 'Serialization', 'Online', '📦', '5800', 'JSON.stringify is my yoga'],
|
|
['Reed Regex', 'Matching', 'Offline', '🔍', '6800', 'Now I have two problems']
|
|
]
|
|
return `
|
|
const rows = ${JSON.stringify(names)}
|
|
const widths = [16, 14, 12, 6, 7, 42]
|
|
function isCombiningMark(codePoint) {
|
|
return (codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
(codePoint >= 0xfe00 && codePoint <= 0xfe0f)
|
|
}
|
|
function isWideCodePoint(codePoint) {
|
|
return codePoint > 0xffff ||
|
|
(codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
|
(codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
|
|
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
(codePoint >= 0xfe10 && codePoint <= 0xfe6f) ||
|
|
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
(codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
|
}
|
|
function cellWidth(text) {
|
|
let width = 0
|
|
for (const char of String(text)) {
|
|
const codePoint = char.codePointAt(0)
|
|
if (codePoint === undefined || isCombiningMark(codePoint)) continue
|
|
width += isWideCodePoint(codePoint) ? 2 : 1
|
|
}
|
|
return width
|
|
}
|
|
function cell(value, width) {
|
|
const text = String(value)
|
|
return text + ' '.repeat(Math.max(1, width - cellWidth(text)))
|
|
}
|
|
function line(parts) {
|
|
return '| ' + parts.map((part, index) => cell(part, widths[index])).join(' | ') + ' |'
|
|
}
|
|
const outputRows = []
|
|
outputRows.push(line(['Name', 'Team', 'Status', 'Icon', 'Score', 'Notes']))
|
|
outputRows.push('|-' + widths.map((width) => '-'.repeat(width)).join('-|-') + '-|')
|
|
for (let repeat = 0; repeat < 4; repeat += 1) {
|
|
for (const row of rows) outputRows.push(line(row))
|
|
}
|
|
async function writeStdout(chunk) {
|
|
await new Promise((resolve) => process.stdout.write(chunk, resolve))
|
|
if (process.platform === 'win32') await new Promise((resolve) => setTimeout(resolve, 8))
|
|
}
|
|
await writeStdout('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
|
for (const row of outputRows) {
|
|
await writeStdout(row + '\\n')
|
|
}
|
|
await writeStdout('\\x1b[?2026l')
|
|
await writeStdout('LONG_TABLE_SCROLL_RESTORE_${runId}\\n')
|
|
`
|
|
}
|
|
|
|
export function emojiFixtureMarkdownTableScript(table: string, runId: string): string {
|
|
const marker = `EMOJI_FIXTURE_TABLE_RESTORE_${runId}`
|
|
const widthMarker = `${marker} WIDTH`
|
|
return `
|
|
const table = ${JSON.stringify(table)}
|
|
const minimumWidths = [2, 5, 4, 7, 7, 4, 3, 4]
|
|
const preferredWidths = [5, 17, 10, 18, 30, 12, 10, 10]
|
|
const tableOverhead = preferredWidths.length * 3 + 1
|
|
const widthBudget = Math.max(
|
|
minimumWidths.reduce((sum, width) => sum + width, 0),
|
|
Math.min(
|
|
preferredWidths.reduce((sum, width) => sum + width, 0),
|
|
(process.stdout.columns || 100) - tableOverhead - 1
|
|
)
|
|
)
|
|
let remaining = widthBudget
|
|
let remainingPreferred = preferredWidths.reduce((sum, width) => sum + width, 0)
|
|
const widths = preferredWidths.map((preferred, index) => {
|
|
const minimum = minimumWidths[index]
|
|
const width = Math.max(minimum, Math.floor((remaining * preferred) / remainingPreferred))
|
|
remaining -= width
|
|
remainingPreferred -= preferred
|
|
return width
|
|
})
|
|
const generatedTableWidth = widths.reduce((sum, width) => sum + width, 0) + tableOverhead
|
|
const border = {
|
|
top: ['┌', '┬', '┐'],
|
|
middle: ['├', '┼', '┤'],
|
|
bottom: ['└', '┴', '┘'],
|
|
vertical: '│',
|
|
horizontal: '─'
|
|
}
|
|
function splitMarkdownRow(row) {
|
|
return row.trim().slice(1, -1).split('|').map((cell) => cell.trim())
|
|
}
|
|
function isSeparatorRow(row) {
|
|
return /^\\|(?:\\s*:?-+:?\\s*\\|)+\\s*$/.test(row)
|
|
}
|
|
function cellWidth(text) {
|
|
let width = 0
|
|
for (const char of String(text)) {
|
|
const codePoint = char.codePointAt(0)
|
|
if (codePoint === undefined || (codePoint >= 0x0300 && codePoint <= 0x036f)) continue
|
|
if (codePoint === 0xfe0f || codePoint === 0x200d) continue
|
|
width += codePoint > 0xffff || (codePoint >= 0x1100 && codePoint <= 0x115f) ? 2 : 1
|
|
}
|
|
return width
|
|
}
|
|
function padCell(value, width) {
|
|
const text = String(value)
|
|
return text + ' '.repeat(Math.max(0, width - cellWidth(text)))
|
|
}
|
|
function splitToWidth(text, width) {
|
|
const parts = []
|
|
let line = ''
|
|
for (const char of String(text)) {
|
|
const next = line + char
|
|
if (line && cellWidth(next) > width) {
|
|
parts.push(line)
|
|
line = char
|
|
} else {
|
|
line = next
|
|
}
|
|
}
|
|
if (line) parts.push(line)
|
|
return parts
|
|
}
|
|
function wrapCell(value, width) {
|
|
const words = String(value).split(/\\s+/)
|
|
const lines = []
|
|
let line = ''
|
|
for (const word of words) {
|
|
if (cellWidth(word) > width) {
|
|
if (line) {
|
|
lines.push(line)
|
|
line = ''
|
|
}
|
|
lines.push(...splitToWidth(word, width))
|
|
continue
|
|
}
|
|
const next = line ? line + ' ' + word : word
|
|
if (line && cellWidth(next) > width) {
|
|
lines.push(line)
|
|
line = word
|
|
} else {
|
|
line = next
|
|
}
|
|
}
|
|
if (line) lines.push(line)
|
|
return lines.length ? lines : ['']
|
|
}
|
|
function rule(parts) {
|
|
return parts[0] + widths.map((width) => border.horizontal.repeat(width + 2)).join(parts[1]) + parts[2]
|
|
}
|
|
function renderRow(cells) {
|
|
const wrappedCells = widths.map((width, index) => wrapCell(cells[index] ?? '', width))
|
|
const height = Math.max(...wrappedCells.map((cell) => cell.length))
|
|
const rows = []
|
|
for (let line = 0; line < height; line += 1) {
|
|
rows.push(
|
|
border.vertical +
|
|
widths
|
|
.map((width, index) => ' ' + padCell(wrappedCells[index][line] ?? '', width) + ' ')
|
|
.join(border.vertical) +
|
|
border.vertical
|
|
)
|
|
}
|
|
return rows
|
|
}
|
|
async function writeStdout(chunk) {
|
|
await new Promise((resolve) => process.stdout.write(chunk, resolve))
|
|
if (process.platform === 'win32') await new Promise((resolve) => setTimeout(resolve, 8))
|
|
}
|
|
const parsedRows = table
|
|
.split(/\\r?\\n/)
|
|
.filter((row) => row.trim().startsWith('|') && !isSeparatorRow(row))
|
|
.map(splitMarkdownRow)
|
|
const rendered = [rule(border.top)]
|
|
for (const [index, row] of parsedRows.entries()) {
|
|
rendered.push(...renderRow(row))
|
|
rendered.push(rule(index === parsedRows.length - 1 ? border.bottom : border.middle))
|
|
}
|
|
await writeStdout('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
|
for (const line of rendered) {
|
|
await writeStdout(line + '\\r\\n')
|
|
}
|
|
await writeStdout('\\x1b[?2026l')
|
|
await writeStdout('${widthMarker}:' + generatedTableWidth + '\\r\\n')
|
|
`
|
|
}
|
|
|
|
export function emojiFixtureTableWidthMarker(runId: string): string {
|
|
return `EMOJI_FIXTURE_TABLE_RESTORE_${runId} WIDTH:`
|
|
}
|
|
|
|
export function narrowSignerMarkdownTableScript(runId: string): string {
|
|
const marker = `NARROW_SIGNER_TABLE_RESTORE_${runId}`
|
|
const rows = [
|
|
'| # | Status | Signer | Action |',
|
|
'| ---: | --- | --- | --- |',
|
|
'| 1 | signed | did:key:z6Mkuw5kQqz1QvZ9f3d2aB7f19f0cAC7B4F3c9E725aD19cD12e6A8B3F4c5D6e7F8a9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c9D0e1F2 | approve deployment |',
|
|
'| 2 | waiting | did:web:example.signing.service:teams:release:prod:primary-key-2026-06-08-with-extra-qualifiers-and-long-human-readable-suffix | counter-sign |',
|
|
'| 3 | signed | 0x742d35Cc6634C0532925a3b844Bc454e4438f44e9E8F12A7C4D9B6530F9D2C8E7A6B5C4D3E2F1A0B998877665544332211 | archive receipt |'
|
|
]
|
|
const repeatedRows = Array.from({ length: 8 }, (_, index) =>
|
|
rows.concat(`| ${index + 4} | signed | signer-row-${index}-${'a'.repeat(96)} | verify |`)
|
|
).flat()
|
|
return `
|
|
const rows = ${JSON.stringify(repeatedRows)}
|
|
async function writeStdout(chunk) {
|
|
await new Promise((resolve) => process.stdout.write(chunk, resolve))
|
|
if (process.platform === 'win32') await new Promise((resolve) => setTimeout(resolve, 8))
|
|
}
|
|
await writeStdout('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
|
for (const row of rows) {
|
|
await writeStdout(row + '\\r\\n')
|
|
}
|
|
await writeStdout('\\x1b[?2026l')
|
|
await writeStdout('${marker}\\r\\n')
|
|
`
|
|
}
|