Files
lpra/internal/server/web/static/wsutil.js
T

64 lines
3.3 KiB
JavaScript

// Pure helpers of the workspace: quoting paths for a shell, what a dropped
// file does where it lands, sizes. No DOM here, so node can test them.
(function (root) {
'use strict';
function shellQuote(p) { return "'" + String(p).replace(/'/g, "'\\''") + "'"; }
function shellQuoteAll(ps) { return ps.map(shellQuote).join(' '); }
const same = (a, b) => a.kind === b.kind && a.id === b.id;
// dropAction says what dropping item on target does: a transfer between
// file panes, paths pasted into a terminal of the same place, nothing on
// its own folder, or a refusal with the reason.
function dropAction(item, target) {
if (target.kind === 'term') {
return same(item.source, target.source) ? {kind: 'paste'}
: {kind: 'deny', reason: 'Нельзя: файл на ' + item.source.label + ', а терминал на ' + target.source.label};
}
if (same(item.source, target.source) && target.dir === item.dir) return {kind: 'none'};
return {kind: 'transfer'};
}
function fmtSize(n) {
if (n < 1024) return n + ' B';
if (n < 1048576) return (n / 1024).toFixed(1) + ' K';
if (n < 1073741824) return (n / 1048576).toFixed(1) + ' M';
return (n / 1073741824).toFixed(1) + ' G';
}
// scope gives a module timers, requests and listeners that end with it;
// once ended it starts nothing new (a request's late answer cannot set a
// timer going again).
function scope() {
let ended = false;
const timers = new Set(), offs = [], ac = new AbortController();
return {
timeout(fn, ms) { if (ended) return 0; const id = setTimeout(() => { timers.delete(id); fn(); }, ms); timers.add(id); return id; },
interval(fn, ms) { if (ended) return 0; const id = setInterval(fn, ms); timers.add(id); return id; },
clear(id) { clearTimeout(id); clearInterval(id); timers.delete(id); },
fetch(url, opt) {
const o = Object.assign({credentials: 'same-origin'}, opt || {});
// A request with its own signal ends on either.
o.signal = o.signal && AbortSignal.any ? AbortSignal.any([o.signal, ac.signal]) : (o.signal || ac.signal);
return fetch(url, o);
},
on(target, ev, fn, opt) { target.addEventListener(ev, fn, opt); offs.push(() => target.removeEventListener(ev, fn, opt)); },
signal: ac.signal,
end() { ended = true; timers.forEach(id => { clearTimeout(id); clearInterval(id); }); timers.clear(); offs.forEach(f => f()); ac.abort(); }
};
}
// parseActions finds what an assistant's answer asks the panel to do:
// closed ``` blocks headed run [machine], read|ls [machine:]path or
// write [machine:]path (its body the file's text).
function parseActions(text) {
const out = [], re = /```(run|read|ls|write)([^\n]*)\n([\s\S]*?)```/g;
for (let m; (m = re.exec(text));) {
const kind = m[1], head = m[2].trim(), body = m[3].replace(/\n$/, '');
if (kind === 'run') { out.push({kind, machine: head, path: '', body}); continue; }
const c = head.indexOf(':');
const machine = c > 0 && !head.startsWith('/') ? head.slice(0, c) : '';
out.push({kind, machine, path: machine ? head.slice(c + 1) : head, body: kind === 'write' ? body : ''});
}
return out;
}
const api = {shellQuote, shellQuoteAll, dropAction, fmtSize, scope, parseActions};
if (typeof module !== 'undefined') module.exports = api; else root.WSUtil = api;
})(this);