// 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; }