tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

io-terminal-view.ts (4924B)


      1 import { Terminal, type Reply } from './io-terminal.js';
      2 import type { Event } from './io-events.js';
      3 // One identity-bound UI per run. Callbacks enqueue cloneable replies/stop requests;
      4 // they must not synchronously re-enter this widget or throw.
      5 export function terminalView(parent: HTMLElement, run: string, reply: (value: Reply) => void, stop: () => void) {
      6   const terminal = new Terminal(run), doc = parent.ownerDocument;
      7   const root = doc.createElement('section'), status = doc.createElement('p');
      8   const outputs = ['output', 'error'].map(name => {
      9     const section = doc.createElement('section'), title = doc.createElement('h4'); title.textContent = name === 'output' ? 'Standard output' : 'Standard error';
     10     const text = doc.createElement('pre'), details = doc.createElement('details'), summary = doc.createElement('summary'), hex = doc.createElement('pre');
     11     summary.textContent = 'Exact bytes (hexadecimal)'; details.append(summary, hex); section.append(title, text, details); root.append(section);
     12     text.dataset.stream = name; hex.dataset.hex = name;
     13     return { text, hex };
     14   });
     15   let form = doc.createElement('form'), clearInput = () => {}, waitingId: number | undefined;
     16   const stopping = doc.createElement('button');
     17   stopping.textContent = 'Stop (cancel and wait for cleanup)'; stopping.type = 'button';
     18   root.append(form, stopping, status); parent.append(root);
     19   let stopped = false, finished = false;
     20   const onStop = () => { if (stopped || finished) return; stopped = true; stopping.disabled = true; status.textContent = 'Cancelling; waiting for cleanup…'; stop(); };
     21   stopping.addEventListener('click', onStop);
     22   function inputView(): void {
     23     const wait = terminal.waiting;
     24     if (wait?.id === waitingId) { form.hidden = !wait; return; }
     25     clearInput();
     26     const fresh = doc.createElement('form'); form.replaceWith(fresh); form = fresh; form.hidden = !wait; waitingId = wait?.id;
     27     if (!wait) return;
     28     const label = doc.createElement('label'), input = doc.createElement('input'), send = doc.createElement('button'), eof = doc.createElement('button');
     29     label.textContent = 'One line, at most 256 Unicode scalars (Send appends LF)'; input.maxLength = 512; label.append(input);
     30     send.textContent = 'Send'; send.type = 'submit'; eof.textContent = 'End input (EOF)'; eof.type = 'button';
     31     form.append(label, send, eof);
     32     function submit(end: boolean): void {
     33       const result = end ? terminal.endInput(wait!.run, wait!.id) : terminal.line(wait!.run, wait!.id, input.value);
     34       if (!result) { status.textContent = 'Enter at most 256 Unicode scalars without CR or LF.'; return; }
     35       render(); reply(result);
     36     }
     37     const onSubmit = (event: SubmitEvent) => { event.preventDefault(); submit(false); }, onEOF = () => submit(true);
     38     form.addEventListener('submit', onSubmit); eof.addEventListener('click', onEOF);
     39     const owned = form;
     40     clearInput = () => { owned.removeEventListener('submit', onSubmit); eof.removeEventListener('click', onEOF); input.value = ''; };
     41   }
     42   function render(): void {
     43     const snapshot = terminal.output.snapshot();
     44     [snapshot.output, snapshot.error].forEach((view, index) => {
     45       const target = outputs[index]!; target.text.replaceChildren(); target.hex.textContent = view.hex;
     46       for (const token of view.tokens) {
     47         if (token.kind === 'text') target.text.append(doc.createTextNode(token.text));
     48         else {
     49           const span = doc.createElement('mark'); span.dataset.byteReason = token.reason;
     50           span.title = `${token.reason} UTF-8 bytes at stream offset ${token.offset}; see exact hex view`;
     51           span.textContent = token.bytes.map(b => `\\x${b.toString(16).padStart(2, '0')}`).join(''); target.text.append(span);
     52         }
     53       }
     54     });
     55     inputView();
     56     if (!stopped) status.textContent = terminal.waiting ? 'Waiting for a line or explicit EOF.' : 'Running…';
     57   }
     58   render();
     59   return {
     60     root,
     61     handle(event: Event): void {
     62       if (finished) throw new Error('terminal view retired');
     63       const result = terminal.handle(event);
     64       // Physically hide/clear an abandoned input BEFORE acknowledging cancel.
     65       render(); if (result) reply(result);
     66     },
     67     finish(): void {
     68       terminal.finish(); finished = true; render();
     69       clearInput(); stopping.removeEventListener('click', onStop);
     70       form.remove(); stopping.disabled = true; status.textContent = 'Settled.';
     71     },
     72     // Fatal runtime loss/navigation is NOT a physical cleanup acknowledgement.
     73     // Retire DOM callbacks without fabricating a settled interpreter outcome.
     74     abandon(): void {
     75       if (finished) return;
     76       finished = true; clearInput(); form.remove();
     77       stopping.removeEventListener('click', onStop); stopping.disabled = true;
     78       status.textContent = 'Runtime lost; cleanup was not acknowledged.';
     79     },
     80     snapshot: () => terminal.output.snapshot()
     81   };
     82 }