tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

io-v2-terminal-view.js (5544B)


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