export function formatBytes(bytes?: number): string { if (bytes === undefined || !Number.isFinite(bytes) || bytes <= 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let n = bytes; let i = 0; while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } return `${n.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;} const SIZE_UNITS: Record<string, number> = { B: 1, KIB: 1024, MIB: 1024 ** 2, GIB: 1024 ** 3, TIB: 1024 ** 4, KB: 1000, MB: 1e6, GB: 1e9, TB: 1e12,}; export function parseSize(s: string): number { const m = s.match(/([\d.]+)\s*([KMGT]?I?B)/i); if (!m) return 0; return Math.round(parseFloat(m[1]!) * (SIZE_UNITS[m[2]!.toUpperCase()] ?? 1));} export function formatBytesPerSec(bytes?: number): string { if (bytes === undefined || !Number.isFinite(bytes) || bytes <= 0) return ""; const units = ["B/s", "KB/s", "MB/s", "GB/s"]; let n = bytes; let i = 0; while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } return `${n.toFixed(n < 10 && i > 0 ? 1 : 0)} ${units[i]}`;} export function formatCount(n: number): string { if (!Number.isFinite(n) || n <= 0) return "0"; if (n < 10_000) return String(Math.round(n)); const k = Math.round(n / 1_000); if (k < 1_000) return `${k}k`; const m = n / 1_000_000; return m < 10 ? `${m.toFixed(1).replace(/\.0$/, "")}m` : `${Math.round(m)}m`;} export function formatRelative(unixSeconds?: number): string { if (!unixSeconds || !Number.isFinite(unixSeconds) || unixSeconds <= 0) return ""; const diff = Date.now() / 1000 - unixSeconds; if (diff < 60) return "now"; const m = Math.floor(diff / 60); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) { const rm = m % 60; return rm > 0 ? `${h}hr ${rm}m ago` : `${h}hr ago`; } const d = Math.floor(h / 24); if (d < 30) { const rh = h % 24; return rh > 0 ? `${d}d ${rh}hr ago` : `${d}d ago`; } const mo = Math.floor(d / 30); if (mo < 12) return `${mo}mo ago`; return `${Math.floor(mo / 12)}y ago`;} export function formatEtaShort(sec?: number): string { if (sec === undefined || !Number.isFinite(sec) || sec < 0) return ""; const total = Math.round(sec); const d = Math.floor(total / 86400); const h = Math.floor((total % 86400) / 3600); const m = Math.floor((total % 3600) / 60); const s = total % 60; if (d > 0) return [`${d}d`, h > 0 ? `${h}hr` : "", m > 0 ? `${m}m` : ""] .filter(Boolean) .join(" "); if (h > 0) return m > 0 ? `${h}hr ${m}m` : `${h}hr`; if (m > 0) return s > 0 ? `${m}m ${s}s` : `${m}m`; return `${s}s`;} function isJunkCodePoint(cp: number): boolean { if (cp < 0x20 || cp === 0x7f) return true; if (cp === 0xfffd) return true; if (cp >= 0x200b && cp <= 0x200f) return true; if (cp >= 0x2028 && cp <= 0x202e) return true; if (cp === 0x2060 || cp === 0xfeff) return true; if (cp === 0x200d || cp === 0xfe0f || cp === 0x20e3) return true; if (cp >= 0x2600 && cp <= 0x27bf) return true; if (cp >= 0x2b00 && cp <= 0x2bff) return true; if (cp >= 0x1f000 && cp <= 0x1ffff) return true; return false;} export function cleanText(s: string): string { let out = ""; for (const ch of s.normalize("NFC")) { if (!isJunkCodePoint(ch.codePointAt(0)!)) out += ch; } return out.replace(/\s+/g, " ").trim() || "Untitled";} // True for a code point a terminal interprets as part of a control or escape// sequence: C0 controls (including ESC 0x1b and BEL 0x07), DEL 0x7f, and the C1// controls 0x80-0x9f (which encode the 8-bit forms of CSI/OSC/ST).function isControlCodePoint(cp: number): boolean { return cp <= 0x1f || cp === 0x7f || (cp >= 0x80 && cp <= 0x9f);} // Strip control/escape-capable characters from a string that will be printed// verbatim. Use this on attacker-influenced fields that bypass cleanText() —// info hashes and magnet links — so a malicious or hijacked source can't smuggle// e.g. an OSC-52 clipboard-write sequence to the terminal through them. Unlike// cleanText(), it preserves the exact remaining characters (no whitespace// collapsing, NFC folding, or "Untitled" fallback), which matters for// identifiers and URLs.export function stripControl(s: string): string { let out = ""; for (const ch of s) { if (!isControlCodePoint(ch.codePointAt(0)!)) out += ch; } return out;} export function truncate(s: string, max: number): string { if (max <= 1) return s.slice(0, Math.max(0, max)); return s.length <= max ? s : s.slice(0, max - 1) + "…";}
export function formatBytes(bytes?: number): string {
if (bytes === undefined || !Number.isFinite(bytes) || bytes <= 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
let n = bytes;
let i = 0;
while (n >= 1024 && i < units.length - 1) {
n /= 1024;
i++;
}
return `${n.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
}
const SIZE_UNITS: Record<string, number> = {
B: 1,
KIB: 1024,
MIB: 1024 ** 2,
GIB: 1024 ** 3,
TIB: 1024 ** 4,
KB: 1000,
MB: 1e6,
GB: 1e9,
TB: 1e12,
};
export function parseSize(s: string): number {
const m = s.match(/([\d.]+)\s*([KMGT]?I?B)/i);
if (!m) return 0;
return Math.round(parseFloat(m[1]!) * (SIZE_UNITS[m[2]!.toUpperCase()] ?? 1));
}
export function formatBytesPerSec(bytes?: number): string {
if (bytes === undefined || !Number.isFinite(bytes) || bytes <= 0) return "";
const units = ["B/s", "KB/s", "MB/s", "GB/s"];
let n = bytes;
let i = 0;
while (n >= 1024 && i < units.length - 1) {
n /= 1024;
i++;
}
return `${n.toFixed(n < 10 && i > 0 ? 1 : 0)} ${units[i]}`;
}
export function formatCount(n: number): string {
if (!Number.isFinite(n) || n <= 0) return "0";
if (n < 10_000) return String(Math.round(n));
const k = Math.round(n / 1_000);
if (k < 1_000) return `${k}k`;
const m = n / 1_000_000;
return m < 10 ? `${m.toFixed(1).replace(/\.0$/, "")}m` : `${Math.round(m)}m`;
}
export function formatRelative(unixSeconds?: number): string {
if (!unixSeconds || !Number.isFinite(unixSeconds) || unixSeconds <= 0) return "";
const diff = Date.now() / 1000 - unixSeconds;
if (diff < 60) return "now";
const m = Math.floor(diff / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) {
const rm = m % 60;
return rm > 0 ? `${h}hr ${rm}m ago` : `${h}hr ago`;
}
const d = Math.floor(h / 24);
if (d < 30) {
const rh = h % 24;
return rh > 0 ? `${d}d ${rh}hr ago` : `${d}d ago`;
}
const mo = Math.floor(d / 30);
if (mo < 12) return `${mo}mo ago`;
return `${Math.floor(mo / 12)}y ago`;
}
export function formatEtaShort(sec?: number): string {
if (sec === undefined || !Number.isFinite(sec) || sec < 0) return "";
const total = Math.round(sec);
const d = Math.floor(total / 86400);
const h = Math.floor((total % 86400) / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
if (d > 0)
return [`${d}d`, h > 0 ? `${h}hr` : "", m > 0 ? `${m}m` : ""]
.filter(Boolean)
.join(" ");
if (h > 0) return m > 0 ? `${h}hr ${m}m` : `${h}hr`;
if (m > 0) return s > 0 ? `${m}m ${s}s` : `${m}m`;
return `${s}s`;
}
function isJunkCodePoint(cp: number): boolean {
if (cp < 0x20 || cp === 0x7f) return true;
if (cp === 0xfffd) return true;
if (cp >= 0x200b && cp <= 0x200f) return true;
if (cp >= 0x2028 && cp <= 0x202e) return true;
if (cp === 0x2060 || cp === 0xfeff) return true;
if (cp === 0x200d || cp === 0xfe0f || cp === 0x20e3) return true;
if (cp >= 0x2600 && cp <= 0x27bf) return true;
if (cp >= 0x2b00 && cp <= 0x2bff) return true;
if (cp >= 0x1f000 && cp <= 0x1ffff) return true;
return false;
}
export function cleanText(s: string): string {
let out = "";
for (const ch of s.normalize("NFC")) {
if (!isJunkCodePoint(ch.codePointAt(0)!)) out += ch;
}
return out.replace(/\s+/g, " ").trim() || "Untitled";
}
// True for a code point a terminal interprets as part of a control or escape
// sequence: C0 controls (including ESC 0x1b and BEL 0x07), DEL 0x7f, and the C1
// controls 0x80-0x9f (which encode the 8-bit forms of CSI/OSC/ST).
function isControlCodePoint(cp: number): boolean {
return cp <= 0x1f || cp === 0x7f || (cp >= 0x80 && cp <= 0x9f);
}
// Strip control/escape-capable characters from a string that will be printed
// verbatim. Use this on attacker-influenced fields that bypass cleanText() —
// info hashes and magnet links — so a malicious or hijacked source can't smuggle
// e.g. an OSC-52 clipboard-write sequence to the terminal through them. Unlike
// cleanText(), it preserves the exact remaining characters (no whitespace
// collapsing, NFC folding, or "Untitled" fallback), which matters for
// identifiers and URLs.
export function stripControl(s: string): string {
let out = "";
for (const ch of s) {
if (!isControlCodePoint(ch.codePointAt(0)!)) out += ch;
}
return out;
}
export function truncate(s: string, max: number): string {
if (max <= 1) return s.slice(0, Math.max(0, max));
return s.length <= max ? s : s.slice(0, max - 1) + "…";
}