Files
Halcyon/web/static/view-code.js
T
2026-09-19 20:21:47 +02:00

140 lines
6.6 KiB
JavaScript

// Code editor: write assembly, check it, download it, seal it into a ship.
import { api, b64ToBytes } from './api.js';
import { h, clear, toast, confirmDialog, download } from './dom.js';
import { createEditor } from './editor.js';
const draftKey = (ship) => `wh.draft.${ship?.id ?? 0}`;
const load = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
const save = (k, v) => { try { localStorage.setItem(k, v); } catch { /* ignore */ } };
export function codeView(ctx) {
const ship = ctx.ship;
const limit = ctx.state.info?.program_bytes ?? 4096;
const key = draftKey(ship);
const canSeal = ship?.status === 'inventory';
const cursor = h('span', { class: 'dim' }, 'LN 1 COL 1');
let timer = 0;
const editor = createEditor({
value: load(key) ?? '',
onInput: (v) => { clearTimeout(timer); timer = setTimeout(() => save(key, v), 300); },
onCursor: ({ line, col }) => { cursor.textContent = `LN ${line} COL ${col}`; },
});
if (load(key) === null) api.example('telemetry').then((t) => { editor.value = t; }).catch(() => {});
// ---- console -----------------------------------------------------------
const output = h('div', { class: 'console', role: 'log', 'aria-live': 'polite' },
h('p', { class: 'dim' }, 'READY. ASSEMBLE TO CHECK YOUR PROGRAM.'));
const say = (...nodes) => clear(output).append(...nodes);
let assembled = null;
async function assemble() {
try {
const r = await api.assemble(editor.value);
editor.setError(null);
const bytes = b64ToBytes(r.program);
assembled = { bytes, ...r };
const pct = Math.min(100, Math.round((r.size / r.limit) * 100));
say(
h('p', { class: r.fits ? 'ok' : 'bad' }, r.fits ? 'ASSEMBLED OK' : (r.size === 0 ? 'NOTHING TO ASSEMBLE' : 'PROGRAM TOO LARGE FOR THIS BELT')),
h('p', null, `${r.size} BYTES / ${r.instructions} INSTRUCTIONS / LIMIT ${r.limit} BYTES`),
h('div', { class: `meter ${r.fits ? '' : 'over'}`, role: 'meter', 'aria-valuemin': '0', 'aria-valuemax': String(r.limit), 'aria-valuenow': String(r.size), 'aria-label': 'Program size' },
h('div', { class: 'fill', style: null, 'data-pct': String(pct) })),
);
output.querySelector('.fill')?.style.setProperty('width', `${pct}%`);
return assembled;
} catch (e) {
assembled = null;
const m = /^line (\d+):/.exec(e.message);
if (m) editor.setError(Number(m[1]));
say(h('p', { class: 'bad' }, `ERROR: ${e.message}`));
return null;
}
}
// ---- toolbar -----------------------------------------------------------
const btn = (label, onclick, opts = {}) =>
h('button', { class: `btn ${opts.cls ?? ''}`, type: 'button', onclick, disabled: opts.disabled, title: opts.title }, label);
const exampleSel = h('select', { id: 'example', 'aria-label': 'Example program' }, h('option', { value: '' }, 'EXAMPLES...'));
api.examples().then((names) => {
for (const n of names.filter((n) => n !== 'ports')) exampleSel.append(h('option', { value: n }, n.toUpperCase()));
});
exampleSel.addEventListener('change', async () => {
const name = exampleSel.value;
exampleSel.value = '';
if (!name) return;
if (editor.value.trim() && !(await confirmDialog({ title: 'REPLACE EDITOR CONTENTS?', body: `LOAD THE ${name.toUpperCase()} EXAMPLE OVER YOUR CURRENT TEXT.`, confirmText: 'REPLACE' }))) return;
try { editor.value = await api.example(name); say(h('p', { class: 'dim' }, `LOADED ${name.toUpperCase()}.`)); } catch (e) { toast(e.message, 'err'); }
});
const bytesOrNull = async () => {
const r = await assemble();
if (r && r.size === 0) { toast('NOTHING TO ASSEMBLE', 'err'); return null; }
return r;
};
const seal = btn(ship ? `SEAL INTO SHIP #${ship.id}` : 'NO SHIP', async () => {
const r = await bytesOrNull();
if (!r) return;
if (!r.fits) { toast(`PROGRAM IS ${r.size} BYTES; THE LIMIT IS ${r.limit}`, 'err'); return; }
try {
await api.setProgram(ship.id, r.bytes);
toast(`PROGRAM SEALED IN SHIP #${ship.id} (${r.size} BYTES). READY TO LAUNCH.`);
await ctx.refresh();
} catch (e) { toast(`UPLOAD FAILED: ${e.message}`, 'err'); }
}, { cls: 'primary', disabled: !canSeal, title: canSeal ? 'Store this program in the ship' : 'Only docked ships can be reprogrammed' });
const toolbar = h('div', { class: 'toolbar' },
exampleSel,
btn('INSERT EQUATES', async () => {
if (/\bP_UPNEW\b/.test(editor.value)) { toast('EQUATES ALREADY PRESENT'); return; }
try { editor.insertAtTop(`${await api.example('ports')}\n`); } catch (e) { toast(e.message, 'err'); }
}),
h('span', { class: 'spacer' }),
btn('ASSEMBLE', assemble),
btn('DOWNLOAD .BIN', async () => { const r = await bytesOrNull(); if (r) download(`ship-${ship?.id ?? 'x'}-program.bin`, r.bytes); }),
btn('SAVE .S', () => download(`ship-${ship?.id ?? 'x'}.s`, editor.value, 'text/plain')),
);
// ---- reference ---------------------------------------------------------
const card = h('pre', { class: 'refcard' }, 'LOADING...');
const ref = h('details', { class: 'panel ref', onToggle: async (e) => {
if (!e.target.open || card.dataset.loaded) return;
card.dataset.loaded = '1';
try {
const txt = await (await fetch('/manual.txt')).text();
const a = txt.indexOf('APPENDIX E QUICK REFERENCE CARD');
const b = txt.indexOf('* * * END OF MANUAL');
card.textContent = txt.slice(a, b).split('\n').slice(3).join('\n').trim();
} catch { card.textContent = 'COULD NOT LOAD THE MANUAL.'; }
} },
h('summary', { class: 'panel-title' }, 'QUICK REFERENCE (HC-33 MANUAL, APPENDIX E)'),
card,
h('p', { class: 'hint pad' }, h('a', { href: '/manual.txt', target: '_blank', rel: 'noopener' }, 'OPEN THE FULL MANUAL')),
);
const note = !ship
? 'YOU HAVE NO SHIP TO PROGRAM.'
: canSeal
? `PROGRAMMING SHIP #${ship.id}. SEALING REPLACES ANY EARLIER PROGRAM UNTIL LAUNCH.`
: `SHIP #${ship.id} IS ${ship.status.toUpperCase()}: ITS PROGRAM IS SEALED. YOU CAN STILL EDIT AND DOWNLOAD HERE.`;
const view = h('div', { class: 'stack-lg' },
h('section', { class: 'panel' },
h('div', { class: 'panel-title' }, 'FLIGHT PROGRAM EDITOR', h('span', { class: 'spacer' }), cursor),
h('div', { class: 'panel-body' },
toolbar,
editor.el,
h('div', { class: 'toolbar sealrow' }, h('p', { class: 'hint grow' }, note), seal),
h('p', { class: 'hint' }, 'DRAFTS ARE KEPT IN THIS BROWSER ONLY. TAB INDENTS; ESC THEN TAB LEAVES THE EDITOR.'),
output,
),
),
ref,
);
queueMicrotask(() => editor.render());
return view;
}