init
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user