// deck-it - windows panel // Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. async function post(action, body = {}) { try { await fetch(`/api/windows/${action}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); } catch { // the next poll shows the real state } } function tile(label, onclick) { const b = document.createElement("button"); b.className = "wtile"; b.textContent = label; b.onclick = onclick; return b; } let pop = null; function closePop() { if (pop) { pop.remove(); pop = null; } } function openMenu(anchor, win, workspaces) { closePop(); pop = document.createElement("div"); pop.className = "wpop"; pop.innerHTML = `
Move to desktop
`; pop.querySelector(".wpop-title").textContent = win.title || win.class; pop.querySelector(".wpop-close").onclick = () => { closePop(); post("close", { address: win.address }); }; const targets = pop.querySelector(".wpop-ws"); for (const n of workspaces ?? []) { const b = document.createElement("button"); b.textContent = String(n); b.onclick = () => { closePop(); post("move", { address: win.address, n }); }; targets.append(b); } document.body.append(pop); const r = anchor.getBoundingClientRect(); const width = pop.offsetWidth; const height = pop.offsetHeight; pop.style.top = `${Math.min(r.bottom + 6, window.innerHeight - height - 8)}px`; pop.style.left = `${Math.max(8, Math.min(r.left, window.innerWidth - width - 8))}px`; } document.addEventListener("pointerdown", (e) => { if (pop && !pop.contains(e.target) && !e.target.closest(".wmenu")) closePop(); }); function appIcon(w) { if (!w.icons?.length) return null; const img = document.createElement("img"); img.className = "wicon"; img.alt = ""; let i = 0; const next = () => { if (i >= w.icons.length) { img.remove(); return; } img.src = `/icons/${w.icons[i++]}.svg`; }; img.addEventListener("error", next); next(); return img; } export default { title: "Windows", tab: "Windows", interval: 1, mount(el) { el.innerHTML = `

Workspaces

Launches

Windows

`; }, update(st, el) { // Polled every second: rebuilding unchanged rows reloads every icon // through its fallbacks, which flickers, so only a change re-renders. const key = JSON.stringify(st); if (key === el._last) return; el._last = key; const ws = el.querySelector(".ws"); ws.textContent = ""; for (const id of st.workspaces ?? []) { const b = tile(String(id), () => post("workspace", { n: id })); b.classList.toggle("on", (st.windows ?? []).some((w) => w.workspace === id)); ws.append(b); } const launches = el.querySelector(".launches"); launches.textContent = ""; for (const item of st.launches ?? []) { const b = document.createElement("button"); b.className = "wtile launch"; b.setAttribute("aria-label", item.label || item.key); b.title = item.label || item.key; if (item.icon) { const icon = document.createElement("img"); icon.className = "laticon"; icon.src = `/icons/${item.icon}.svg`; icon.alt = ""; // An app whose icon the theme lacks shows its name instead. icon.onerror = () => { b.textContent = item.label || item.key; }; b.append(icon); } else { b.textContent = item.label || item.key; } b.onclick = () => post("launch", { key: item.key }); launches.append(b); } const list = el.querySelector(".winlist"); list.textContent = ""; for (const w of (st.windows ?? []).slice(0, 12)) { const row = document.createElement("div"); row.className = "wrow"; row.classList.toggle("on", Boolean(w.active)); const icon = appIcon(w); if (icon) row.append(icon); const title = document.createElement("span"); title.className = "wt"; title.textContent = w.title || w.class; title.onclick = () => post("focus", { address: w.address }); const menu = document.createElement("button"); menu.className = "wmenu"; menu.textContent = "⋮"; menu.setAttribute("aria-label", "Window actions"); menu.onclick = () => openMenu(menu, w, st.workspaces); row.append(title, menu); list.append(row); } }, };