diff --git a/src/phoenix/src/puter-shell/main.js b/src/phoenix/src/puter-shell/main.js index 774d3454..007cf31c 100644 --- a/src/phoenix/src/puter-shell/main.js +++ b/src/phoenix/src/puter-shell/main.js @@ -37,6 +37,7 @@ import { CompositeCommandProvider } from './providers/CompositeCommandProvider.j import { ScriptCommandProvider } from './providers/ScriptCommandProvider.js'; import { PuterAppCommandProvider } from './providers/PuterAppCommandProvider.js'; import { EmuCommandProvider } from './providers/EmuCommandProvider.js'; +import { PDECommandProvider } from './providers/PDECommandProvider.js'; const argparser_registry = { [SimpleArgParser.name]: SimpleArgParser @@ -90,6 +91,7 @@ export const launchPuterShell = async (ctx) => { ...(ctx.platform.name === 'puter' ? [new PuterAppCommandProvider()] : []), new ScriptCommandProvider(), new EmuCommandProvider(), + new PDECommandProvider() ]); ctx = ctx.sub({ diff --git a/src/phoenix/src/puter-shell/providers/PDECommandProvider.js b/src/phoenix/src/puter-shell/providers/PDECommandProvider.js new file mode 100644 index 00000000..676db896 --- /dev/null +++ b/src/phoenix/src/puter-shell/providers/PDECommandProvider.js @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from '../coreutils/coreutil_lib/exit.js'; +import { signals } from '../../ansi-shell/signals.js'; + +const BUILT_IN_APPS = [ + 'explorer', +]; + +const lookup_app = async (id) => { + // if (BUILT_IN_APPS.includes(id)) { + // return { success: true, path: null }; + // } + + const request = await fetch(`${puter.APIOrigin}/drivers/call`, { + "headers": { + "Content-Type": "application/json", + "Authorization": `Bearer ${puter.authToken}`, + }, + "body": JSON.stringify({ interface: 'puter-apps', method: 'read', args: { id: { name: id } } }), + "method": "POST", + }); + + const { success, result } = await request.json(); + return { success, path: result?.index_url }; +}; + +export class PDECommandProvider { + + async lookup (id) { + try { + await puter.fs.stat("/admin/Public/bin/" + id + ".pde") + } catch (e) { + return false; + } + const { success, path } = await lookup_app("pderunner"); + + return { + name: id, + path: path ?? 'Built-in Puter app', + // TODO: Let apps expose option/positional definitions like builtins do, and parse them here? + async execute(ctx) { + console.log(ctx) + const args = { + command_line: { + args: ["/admin/Public/bin/" + id + ".pde" , ...ctx.locals.args], + }, + env: {...ctx.env}, + }; + console.log(args) + const child = await puter.ui.launchApp("pderunner", args); + + const resize_listener = evt => { + child.postMessage({ + $: 'ioctl.set', + windowSize: { + rows: evt.detail.rows, + cols: evt.detail.cols, + } + }); + }; + ctx.shell.addEventListener('signal.window-resize', resize_listener); + + // Wait for app to close. + // console.log('waiting for app to close (phoenix)', window, { + // child_appid: child.targetAppInstanceID, + // phoen_appid: puter.appInstanceID, + // }); + const app_close_promise = new Promise((resolve, reject) => { + child.on('close', (data) => { + if ((data.statusCode ?? 0) != 0) { + reject(new Exit(data.statusCode)); + } else { + resolve({ done: true }); + } + }); + }); + + // Wait for SIGINT + const sigint_promise = new Promise((resolve, reject) => { + ctx.externs.sig.on((signal) => { + if (signal === signals.SIGINT) { + child.close(); + reject(new Exit(130)); + } + }); + }); + + // We don't connect stdio to non-SDK apps, because they won't make use of it. + if (child.usesSDK) { + const decoder = new TextDecoder(); + child.on('message', message => { + if (message.$ === 'stdout') { + ctx.externs.out.write(decoder.decode(message.data)); + } + if (message.$ === 'chtermios') { + if ( message.termios.echo !== undefined ) { + if ( message.termios.echo ) { + ctx.externs.echo.on(); + } else { + ctx.externs.echo.off(); + } + } + } + }); + + // Repeatedly copy data from stdin to the child, while it's running. + // DRY: Initially copied from PathCommandProvider + let data, done; + const next_data = async () => { + ({ value: data, done } = await Promise.race([ + app_close_promise, sigint_promise, ctx.externs.in_.read(), + ])); + if (data) { + child.postMessage({ + $: 'stdin', + data: data, + }); + if (!done) setTimeout(next_data, 0); + } + }; + setTimeout(next_data, 0); + } + + // TODO: propagate sigint to the app + const exit = await Promise.race([ app_close_promise, sigint_promise ]); + ctx.shell.removeEventListener('signal.window-resize', resize_listener); + return exit; + } + }; + } + + // Only a single Puter app can match a given name + async lookupAll (...a) { + const result = await this.lookup(...a); + if ( result ) { + return [ result ]; + } + return undefined; + } + + async complete (query, { ctx }) { + if (query === '') return []; + + const results = []; + + // for (const app_name of BUILT_IN_APPS) { + // if (app_name.startsWith(query)) { + // results.push(app_name); + // } + // } + + // const request = await fetch(`${puter.APIOrigin}/drivers/call`, { + // "headers": { + // "Content-Type": "application/json", + // "Authorization": `Bearer ${puter.authToken}`, + // }, + // "body": JSON.stringify({ interface: 'puter-apps', method: 'select', args: { predicate: [ 'name-like', query + '%' ] } }), + // "method": "POST", + // }); + + // const json = await request.json(); + // if (json.success) { + // for (const app of json.result) { + // results.push(app.name); + // } + // } + + return results; + } +}