import { useEffect, useRef } from "preact/hooks";
import { signal } from "@preact/signals";
import { apiGet, apiSend } from "../lib/api.js";
const sessions = signal(null);
const error = signal(null);
async function refresh() {
try {
const data = await apiGet("/api/sessions");
sessions.value = Array.isArray(data) ? data : data.sessions || [];
error.value = null;
} catch (e) {
error.value = String(e.message || e);
}
}
export function HomePage() {
const dialogRef = useRef(null);
const nameRef = useRef(null);
useEffect(() => {
refresh();
}, []);
const open = async (name) => {
window.location.href = `/app#/s/${encodeURIComponent(name)}`;
window.location.reload();
};
const create = async (e) => {
e.preventDefault();
const name = (nameRef.current?.value || "").trim();
if (!name) return;
try {
await apiSend("/api/sessions", {
method: "POST",
body: JSON.stringify({ name }),
});
nameRef.current.value = "";
dialogRef.current?.close();
await refresh();
} catch (err) {
alert(`Create failed: ${err.message}`);
}
};
const kill = async (name) => {
if (!confirm(`Kill session '${name}'?`)) return;
try {
await apiSend(`/api/sessions/${encodeURIComponent(name)}/kill`, {
method: "POST",
});
await refresh();
} catch (err) {
alert(`Kill failed: ${err.message}`);
}
};
const rename = async (oldName) => {
const newName = prompt(`Rename '${oldName}' to:`, oldName);
if (!newName || newName === oldName) return;
try {
await apiSend(`/api/sessions/${encodeURIComponent(oldName)}/rename`, {
method: "POST",
body: JSON.stringify({ name: newName }),
});
await refresh();
} catch (err) {
alert(`Rename failed: ${err.message}`);
}
};
const swipeRow = (row) => {
if (!row) return;
const item = row.querySelector(".session-item");
if (!item || item.dataset.swipeWired) return;
item.dataset.swipeWired = "1";
let startX = 0;
let currentX = 0;
let swiping = false;
item.addEventListener(
"touchstart",
(e) => {
startX = e.touches[0].clientX;
currentX = 0;
swiping = true;
item.style.transition = "none";
},
{ passive: true },
);
item.addEventListener(
"touchmove",
(e) => {
if (!swiping) return;
currentX = Math.max(-100, Math.min(100, e.touches[0].clientX - startX));
item.style.transform = `translateX(${currentX}px)`;
},
{ passive: true },
);
item.addEventListener("touchend", () => {
swiping = false;
item.style.transition = "transform 0.2s ease";
if (currentX < -60) item.style.transform = "translateX(-100px)";
else if (currentX > 60) item.style.transform = "translateX(100px)";
else item.style.transform = "translateX(0)";
});
row.addEventListener("click", (e) => {
if (e.target.closest(".swipe-btn")) return;
if (
item.style.transform !== "translateX(0px)" &&
item.style.transform !== ""
) {
item.style.transition = "transform 0.2s ease";
item.style.transform = "translateX(0)";
}
});
};
const list = sessions.value;
return (
<>
<div id="sessionList" class="session-list">
{error.value && (
<p class="hint">Failed to load sessions: {error.value}</p>
)}
{list == null && !error.value && <p class="hint">Loading…</p>}
{list && list.length === 0 && (
<p class="hint">No tmux sessions. Tap + to create one.</p>
)}
{(list || []).map((s) => {
const name = typeof s === "string" ? s : s.name;
const meta =
typeof s === "object"
? `${s.windows ?? "?"} win · ${s.attached ?? 0} attached`
: "";
return (
<div class="swipe-row" data-name={name} key={name} ref={swipeRow}>
<div class="swipe-action swipe-left">
<button
class="swipe-btn rename-btn"
onClick={() => rename(name)}
>
Rename
</button>
</div>
<a
class="session-item"
href="#"
onClick={(e) => {
e.preventDefault();
open(name);
}}
>
<div class="session-info">
<span class="session-name">{name}</span>
{meta && <span class="session-meta">{meta}</span>}
</div>
<span class="session-arrow">›</span>
</a>
<div class="swipe-action swipe-right">
<button class="swipe-btn kill-btn" onClick={() => kill(name)}>
Kill
</button>
</div>
</div>
);
})}
</div>
<button
id="fabNew"
class="fab"
aria-label="New session"
onClick={() => {
dialogRef.current?.showModal();
nameRef.current?.focus();
}}
>
+
</button>
<dialog ref={dialogRef} id="newSessionDialog" class="session-dialog">
<form id="newSessionForm" method="dialog" onSubmit={create}>
<h3>New session</h3>
<input
ref={nameRef}
id="sessionName"
placeholder="session-name"
autocomplete="off"
required
/>
<div class="dialog-actions">
<button
type="button"
class="btn-cancel"
onClick={() => dialogRef.current?.close()}
>
Cancel
</button>
<button type="submit" class="btn-create">
Create
</button>
</div>
</form>
</dialog>
</>
);
}