// ============================================================ // Foreground: Currently-focused desktop process card (rendered at // the top of the Display tab) // ============================================================ import { t } from './core.js'; let latestForeground = null; let agoTickTimer = null; function escapeHtml(s) { if (s === null || s === undefined) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatAgo(epoch) { if (!epoch) return ''; const now = Date.now() / 1000; const diff = Math.max(0, now - epoch); if (diff < 60) { return t('foreground.ago.seconds', { n: Math.floor(diff) }); } if (diff < 3600) { return t('foreground.ago.minutes', { n: Math.floor(diff / 60) }); } if (diff < 86400) { const h = Math.floor(diff / 3600); const m = Math.floor((diff % 3600) / 60); return t('foreground.ago.hours', { n: h, m: m }); } return t('foreground.ago.days', { n: Math.floor(diff / 86400) }); } function formatGeometry(g) { if (!g) return '—'; const w = g.width ?? (g.right - g.left); const h = g.height ?? (g.bottom - g.top); return `${w}×${h} @ (${g.left}, ${g.top})`; } function truncatePath(p, max = 64) { if (!p) return ''; if (p.length <= max) return p; // Keep the tail (filename) visible — that's the part the user cares about. return '…' + p.slice(-(max - 1)); } function renderEmpty(message, errorMsg) { const stage = document.getElementById('foregroundStage'); if (!stage) return; stage.innerHTML = `

${escapeHtml(message)}

${errorMsg ? `

${escapeHtml(errorMsg)}

` : ''}
`; } function renderTile(data) { const stage = document.getElementById('foregroundStage'); if (!stage) return; const procName = data.process_name || '—'; const winTitle = data.window_title || ''; const execPath = data.executable_path || ''; const pid = data.pid ?? '—'; const startedEpoch = data.started_at; const startedAgo = startedEpoch ? formatAgo(startedEpoch) : '—'; const startedAbs = startedEpoch ? new Date(startedEpoch * 1000).toLocaleString() : ''; const geom = formatGeometry(data.window_geometry); const platform = data.platform || '—'; const monitorId = data.monitor_id; // Chips: only render ones that apply const chips = []; if (data.is_fullscreen) { chips.push(`${escapeHtml(t('foreground.fullscreen'))}`); } else if (!data.is_minimized) { chips.push(`${escapeHtml(t('foreground.windowed'))}`); } if (data.is_minimized) { chips.push(`${escapeHtml(t('foreground.minimized'))}`); } if (monitorId !== null && monitorId !== undefined) { chips.push(`${escapeHtml(t('foreground.monitor', { n: monitorId + 1 }))}`); } if (data.is_browser) { chips.push(`${escapeHtml(t('foreground.browser'))}`); } // Optional browser-only detail rows (page title + URL when available) const browserRows = []; if (data.is_browser) { if (data.browser_page_title) { browserRows.push(`
${escapeHtml(t('foreground.page_title'))}
${escapeHtml(data.browser_page_title)}
`); } if (data.browser_url) { browserRows.push(`
${escapeHtml(t('foreground.url'))}
${escapeHtml(truncatePath(data.browser_url, 80))}
`); } } stage.innerHTML = `
Foreground

${escapeHtml(procName)}

${escapeHtml(winTitle)}
${chips.join('')}
${browserRows.join('')}
${escapeHtml(t('foreground.executable'))}
${escapeHtml(truncatePath(execPath))}
${escapeHtml(t('foreground.pid'))}
${escapeHtml(String(pid))}
${escapeHtml(t('foreground.started'))}
${escapeHtml(startedAgo)}
${escapeHtml(t('foreground.geometry'))}
${escapeHtml(geom)}
${escapeHtml(t('foreground.platform'))}
${escapeHtml(platform)}
`; } function startAgoTicker() { if (agoTickTimer) return; agoTickTimer = setInterval(() => { const el = document.querySelector('.fg-ago[data-started]'); if (!el) return; const epoch = parseFloat(el.getAttribute('data-started')); if (!epoch) return; el.textContent = formatAgo(epoch); }, 15000); } export function updateForegroundUI(data) { latestForeground = data; if (!data || data.available === false) { const errMsg = data && data.error ? data.error : ''; renderEmpty(t('foreground.unavailable'), errMsg); } else if (!data.process_name && !data.pid) { renderEmpty(t('foreground.no_process'), ''); } else { renderTile(data); startAgoTicker(); } } export function loadForegroundProcess() { // Push-only — just render the cached state. If nothing has arrived // yet, leave the loading placeholder visible. if (latestForeground !== null) { updateForegroundUI(latestForeground); } }