// ============================================================
// 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);
}
}