io-terminal-view.js (5541B)
1 import { Terminal } from './io-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 }