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 = ``;
},
update(st, el) {
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 = "";
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);
}
},
};