feat: file panes follow the machine going offline and back, reconnect a lost SSH host; zip and unzip over SSH where the host has them; phone keys only for terminals; version 0.2.0, README

This commit is contained in:
redtea committed 2026-10-15 21:58:09 +03:00
1 parent d92696226e
commit 01dc45ecad
8 files changed
+164 -29

No files matched your search

+20 -9
View File
@@ -111,6 +111,12 @@
}
function setActivePane(s) {
activePane = s;
// The phone's extra keys are a terminal's: a file pane goes without.
const term = s.kind === 'term' || s.kind === 'ssh';
if (touch && document.body.classList.contains('term-active') !== term) {
document.body.classList.toggle('term-active', term);
setTimeout(() => active && layout(active, false), 0);
}
document.querySelectorAll('.pane.active').forEach(p => p.classList.toggle('active', p === s.el));
s.el.classList.add('active');
showStatus();
@@ -133,7 +139,8 @@
function placeFloat() {
const p = tabs.length === 1 ? tabs[0].panes.find(s => s.el.classList.contains('under-plus')) : null;
const bars = p && /files$/.test(p.kind) ? p.el.querySelectorAll('.fm-bar') : [];
const on = bars.length > 1 && bars[0].offsetParent !== null;
// A bar folded in two (a phone) keeps the corner "+".
const on = bars.length > 1 && bars[0].offsetParent !== null && bars[0].offsetHeight < 48;
floatAdd.classList.toggle('in-files', on);
if (!on) { floatAdd.style.top = floatAdd.style.right = ''; return; }
const a = area.getBoundingClientRect(), top = bars[0].getBoundingClientRect(), row = bars[1].getBoundingClientRect();
@@ -160,7 +167,7 @@
function closeTab(t) {
const i = tabs.indexOf(t);
if (i < 0) return;
if (t.panes.length > 1 && !confirm('Закрыть вкладку с ' + t.panes.length + ' терминалами?')) return;
if (t.panes.length > 1 && !confirm('Закрыть вкладку с ' + t.panes.length + ' панелями?')) return;
tabs.splice(i, 1);
t.panes.forEach(s => s.close());
t.el.classList.add('leave');
@@ -170,7 +177,7 @@
active = null;
activate(tabs[Math.min(i, tabs.length - 1)]);
}
if (!tabs.length) { activePane = null; showStatus(); $('empty').hidden = false; }
if (!tabs.length) { activePane = null; showStatus(); $('empty').hidden = false; document.body.classList.remove('term-active'); }
updateMode();
}
function rename(t) {
@@ -525,12 +532,14 @@
// ---------- the SSH dialog ----------
const KEYI = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="4.5"/><path d="m21 2-9.6 9.6M15.5 7.5l3 3L22 7l-3-3"/></svg>';
let sshDlg = null, sshMode = 'term';
let sshInto = null, sshDlg = null, sshMode = 'term';
// openSSH asks where to connect; mode says what opens there: a terminal
// ('term') or a file manager ('files', through Workspace.sshFiles).
function openSSH(here, mode) {
// into: a file pane whose SSH connection is lost - the dialog comes with
// its host, port and user, and connects that pane again.
function openSSH(here, mode, into) {
if (!sshDlg) sshDlg = makeSSHDialog();
sshDlg.show(here, mode || 'term');
sshDlg.show(here, mode || 'term', into);
}
function makeSSHDialog() {
const d = document.createElement('dialog');
@@ -605,7 +614,7 @@
const btn = f.querySelector('button[type=submit]');
btn.disabled = true;
msg.textContent = 'подключение…';
window.Workspace.sshFiles(a, t).then(r => {
window.Workspace.sshFiles(a, t, sshInto).then(r => {
btn.disabled = false;
if (!r.ok) { msg.textContent = r.error || 'не подключилось'; return; }
msg.textContent = '';
@@ -616,8 +625,9 @@
close(() => openTerminal(a, t));
};
return {
show(here, mode) {
show(here, mode, into) {
sshMode = mode;
sshInto = into || null;
d.querySelector('.dlg-h strong').textContent = mode === 'files' ? 'Файлы по SSH' : 'SSH-подключение';
msg.textContent = '';
let last = {};
@@ -639,6 +649,7 @@
if (here && !agents.some(a => a.id === here.id)) agents.unshift(here);
fill();
}).catch(() => {});
if (into) last = { agent: here.id, host: into.ssh.host, port: into.ssh.port, user: into.ssh.user, auth: last.auth };
if (last.host) { f.host.value = last.host; f.port.value = last.port || 22; f.user.value = last.user || ''; }
setAuth(last.auth === 'key' || last.auth === 'agent' ? last.auth : 'password');
d.showModal();
@@ -692,7 +703,7 @@
panes: () => tabs.flatMap(t => t.panes),
activePane: () => activePane,
renderPane: p => { const t = tabOf(p); if (t) renderTab(t); if (p === activePane) showStatus(); },
setActivePane, tabOf, renderTab, showStatus, closePane, startDrag,
setActivePane, tabOf, renderTab, showStatus, closePane, startDrag, openSSH,
// paneAt, tabAt: the pane of the shown tab, the tab title at a point.
paneAt: (x, y) => active && active.panes.find(p => in_(p.el, x, y)),
tabAt: (x, y) => document.body.classList.contains('multi') ? tabs.find(t => in_(t.el, x, y)) : null