export const OBS_BACKEND_LABEL_MAX_CHARS = 96; export const OBS_COMMAND_OUTPUT_MAX_CHARS = 8192; export const OBS_COMMAND_OUTPUT_MAX_ROWS = 64; export const OBS_COMMAND_RENDER_ROW_LIMIT = 20; const truncationMarker = "…"; const outputTruncationSuffix = "\n… output truncated"; const unsafeDisplayControlPattern = /[\p{Cc}\p{Zl}\p{Zp}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/gu; const displayWhitespacePattern = /\s+/gu; export interface BoundedObsCommandRows { readonly rows: readonly T[]; readonly omittedCount: number; } export function normalizeObsBackendLabel(value: string | undefined): string | undefined { const normalized = value?.replace(unsafeDisplayControlPattern, " ").replace(displayWhitespacePattern, " ").trim(); if (!normalized) return undefined; return truncateUnicodeText(normalized, OBS_BACKEND_LABEL_MAX_CHARS, truncationMarker); } export function normalizeObsBackendLabelRecord(labels: Readonly>): Record { const normalizedLabels: Record = {}; for (const [key, value] of Object.entries(labels)) { const normalizedKey = normalizeObsBackendLabel(key); const normalizedValue = normalizeObsBackendLabel(value); if (normalizedKey && normalizedValue !== undefined) normalizedLabels[normalizedKey] = normalizedValue; } return normalizedLabels; } export function selectObsCommandRows(rows: readonly T[]): BoundedObsCommandRows { if (rows.length <= OBS_COMMAND_RENDER_ROW_LIMIT) return { rows, omittedCount: 0 }; return { rows: rows.slice(0, OBS_COMMAND_RENDER_ROW_LIMIT), omittedCount: rows.length - OBS_COMMAND_RENDER_ROW_LIMIT, }; } export function boundObsCommandOutput(output: string): string { const normalized = normalizeObsCommandOutput(output); const rows = normalized.split("\n"); const rowsTruncated = rows.length > OBS_COMMAND_OUTPUT_MAX_ROWS; const content = rowsTruncated ? rows.slice(0, OBS_COMMAND_OUTPUT_MAX_ROWS - 1).join("\n") : normalized; if (!rowsTruncated && content.length <= OBS_COMMAND_OUTPUT_MAX_CHARS) return content; const prefixLimit = OBS_COMMAND_OUTPUT_MAX_CHARS - outputTruncationSuffix.length; const characterBounded = truncateUnicodeText(content, prefixLimit, ""); const rowBounded = selectOutputRows(characterBounded, OBS_COMMAND_OUTPUT_MAX_ROWS - 1); return `${rowBounded}${outputTruncationSuffix}`; } function normalizeObsCommandOutput(output: string): string { return output.replace(unsafeDisplayControlPattern, normalizeObsCommandControlSequence); } function normalizeObsCommandControlSequence(sequence: string): string { let normalized = ""; for (const character of sequence) normalized += character === "\n" ? "\n" : " "; return normalized; } function selectOutputRows(output: string, maximumRows: number): string { const rows = output.split("\n"); if (rows.length <= maximumRows) return output; return rows.slice(0, maximumRows).join("\n"); } function truncateUnicodeText(value: string, maximumChars: number, suffix: string): string { if (value.length <= maximumChars) return value; const prefixLimit = Math.max(0, maximumChars - suffix.length); let prefix = ""; for (const codePoint of value) { if (prefix.length + codePoint.length > prefixLimit) break; prefix += codePoint; } return `${prefix}${suffix}`; }