import { async_run } from 'framerock' const run_process_gui = (runtime_conf, cwd_for_spawn, args_for_spawn) => { const jsbuild_app_frontend = async function () { return ` const streams = {} const elems_terminals = {} const async_process_stream = async function (stream_id, stream_key, callback) { for await (let chunk of streams[stream_id][stream_key].readable) { callback(chunk) } return } const setup_dom = function () { document.body.style = 'height: 100%;' document.head.insertAdjacentHTML('beforeend', '') const elem_root = document.createElement('div') elem_root.style = 'height: 100%; width: 100%; display: flex; font-size: 16px;' for (let [ stream_key, style ] of [ ['stderr', { backgroundColor: '#333' }], ['stdout', { backgroundColor: '#000' }] ]) { const elem_terminal = document.createElement('div') elems_terminals[stream_key] = elem_terminal Object.assign(elem_terminal.style, { flex: '1', fontFamily: 'monospace', color: '#FFF', height: '100%', overflow: 'auto', padding: '20px', boxSizing: 'border-box', whiteSpace: 'pre', ...style }) elem_root.appendChild(elem_terminal) } document.body.appendChild(elem_root) return } const on_open = function () { const main_streamid = '1' streams[main_streamid] = { prev_message_idx: -1, stdout: new TextDecoderStream(), stderr: new TextDecoderStream(), } for (let stream_key of ['stdout', 'stderr']) { async_process_stream(main_streamid, stream_key, (chunk) => {elems_terminals[stream_key].textContent += chunk;}).then(()=>{}).catch(console.error) } FRAMEROCK_UTILS.transport_send_bytes(new TextEncoder().encode(JSON.stringify([ 'SPAWN_PROCESS_INIT', { stream_id: main_streamid } ]))) console.info(['Process Spawned']) return } const on_message = function (event) { const [ evt_type, evt_data ] = JSON.parse(new TextDecoder().decode(event.data)) if (evt_type === 'SPAWN_PROCESS_EVENT') { const [ stream_id, msg_idx, payload ] = evt_data const stream = streams[stream_id] if (stream === undefined) { console.error(['Unexpected: missing ref to stream:', stream_id]) return } if (msg_idx !== stream.prev_message_idx+1) { console.error('received message out of order, skipping') return } stream.prev_message_idx = msg_idx*1 const [ evt_subtype, evt_subdata ] = payload if (evt_subtype === 'stream_chunk') { const [ stream_key, stream_data ] = evt_subdata if (stream_data === null) { stream[stream_key].writable.close() } else { const writer = stream[stream_key].writable.getWriter() writer.write(new Uint8Array(stream_data)) writer.releaseLock() } } else if (evt_subtype === 'exited') { console.info(['Process Exited', evt_subdata]) } } return } setup_dom() FRAMEROCK_UTILS.setup_transport({ on_open, on_message }) `.trim() } const handle_transport_bytes = function (utils, message) { const [ evt_type, evt_data ] = JSON.parse(new TextDecoder().decode(message)) if (evt_type === 'SPAWN_PROCESS_INIT') { let msg_idx = 0 const wrap_send = (payload) => { utils.transport_send_bytes(new TextEncoder().encode(JSON.stringify([ 'SPAWN_PROCESS_EVENT', [ evt_data.stream_id, msg_idx*1, payload ] ]))) msg_idx++ return } const onExit = (p, exitCode, signalCode, error) => { wrap_send(['exited', { exitCode, signalCode }]) return } const proc = Bun.spawn(args_for_spawn, { cwd: cwd_for_spawn, stdout: 'pipe', stderr: 'pipe', onExit }) const async_handle_chunks = async (stream_key) => { for await (const chunk of proc[stream_key]) { wrap_send(['stream_chunk', [ stream_key, [ ...chunk ] ]]) } wrap_send(['stream_chunk', [ stream_key, null ]]) return } Promise.all([ async_handle_chunks('stdout'), async_handle_chunks('stderr') ]).then(()=>{}).catch(console.error) } return } async_run({ config: { ...runtime_conf }, jsbuild_app_frontend, handle_transport_bytes }).then(()=>{}).catch(console.error) return } export { run_process_gui }