import { useEffect, useRef } from 'preact/hooks';
import { signal } from '@preact/signals';
import { useLocation } from 'wouter-preact';
import { apiGet, apiSend } from '../lib/api.js';
const sessions = signal(null);
const error = signal(null);
function currentPeer() {
try {
return window.MobuxMesh ? window.MobuxMesh.getPeer() : '';
} catch (_) {
return '';
}
}
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 [, navigate] = useLocation();
const dialogRef = useRef(null);
const nameRef = useRef(null);
useEffect(() => {
refresh();
window.addEventListener('mobux:peer-changed', refresh);
window.refreshSessions = refresh;
return () => {
window.removeEventListener('mobux:peer-changed', refresh);
if (window.refreshSessions === refresh) delete window.refreshSessions;
};
}, []);
const open = (name) => {
const peer = currentPeer();
const href = peer
? `/s/${encodeURIComponent(peer)}/${encodeURIComponent(name)}`
: `/s/${encodeURIComponent(name)}`;
navigate(href);
};
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>
</>
);
}