diff --git a/Makefile b/Makefile index a354305..3ce1f60 100644 --- a/Makefile +++ b/Makefile @@ -125,11 +125,11 @@ build/assets/matter.css: apps/libfileview.lib/icons: apps/libfileview.lib/icons.json cd apps/libfileview.lib; bash geticons.sh -build/bin/chimerix.ajs: chimerix/src/* +bin/chimerix.ajs: chimerix/src/* mkdir -p build/bin cd chimerix; npm i cd chimerix; npx rollup -c rollup.config.js - cp chimerix/dist/chimerix.ajs build/bin/chimerix.ajs + cp chimerix/dist/chimerix.ajs bin/chimerix.ajs clean: rm -rf build @@ -193,6 +193,7 @@ static: all mkdir -p static/ cp -r aboutproxy/static/* static/ cp -r apps/ static/apps/ + cp -r bin/ static/bin/ cp -r build/* static/ cp -r public/* static/ diff --git a/apps/ashell.app/term.html b/apps/ashell.app/term.html index b91840a..675e9ae 100644 --- a/apps/ashell.app/term.html +++ b/apps/ashell.app/term.html @@ -1,7 +1,7 @@
- + diff --git a/apps/ashell.app/term.js b/apps/ashell.app/term.js index ddba95f..676ddb7 100644 --- a/apps/ashell.app/term.js +++ b/apps/ashell.app/term.js @@ -1,7 +1,67 @@ const hterm = (await anura.import("anura.hterm")).default; +const exit = env.process.kill.bind(env.process); + +const url = new URL(window.location.href); +const argv = ExternalApp.deserializeArgs(url.searchParams.get("args")); + +let scan = "none"; + +const argmap = {}; + +console.log(argv); + +for (let i = 0; i < argv.length; i++) { + let arg = argv[i]; + + if (arg === "--" || arg === "") continue; + + if (arg.startsWith("--")) { + scan = arg.slice(2); + } else if (scan !== "none") { + argmap[scan] = arg; + scan = "none"; + } else { + console.error(`Unknown argument: ${arg}`); + window.postMessage({ + type: "stderr", + message: "\x1b[31mUnknown argument: " + arg, + }); + exit(1); + } +} + +if (scan !== "none") { + console.error(`Expected argument after ${scan}`); + window.postMessage({ + type: "stderr", + message: "\x1b[31mExpected argument after " + scan, + }); + exit(1); +} + +// detect if there are arguments that dont exist +const validArgs = ["cmd"]; +for (let key in argmap) { + if (!validArgs.includes(key)) { + console.error(`Unknown argument: ${key}`); + window.postMessage({ + type: "stderr", + message: "\x1b[31mUnknown argument: " + key, + }); + exit(1); + } +} + +console.log(argmap); + const shell = anura.settings.get("shell") || "/usr/bin/chimerix.ajs"; anura.settings.set("shell", shell); +if (!argmap.cmd) { + // If no command is provided, default to the system shell + argmap.cmd = shell; +} + const config = anura.settings.get("anura-shell-config") || {}; anura.settings.set("anura-shell-config", config); @@ -35,7 +95,17 @@ term.onTerminalReady = async () => { let io = term.io.push(); - const proc = await anura.processes.execute(shell); + const cmdline = (argmap.cmd.match(/(?:[^\s"]+|"[^"]*")+/g) || []).map( + (arg) => { + // Remove surrounding quotes if they exist + if (arg.startsWith('"') && arg.endsWith('"')) { + return arg.slice(1, -1); + } + return arg; + }, + ); + + const proc = await anura.processes.execute(cmdline[0], cmdline.slice(1)); const stdinWriter = proc.stdin.getWriter(); @@ -68,6 +138,9 @@ term.onTerminalReady = async () => { proc.stdout.pipeTo( new WritableStream({ write: (chunk) => { + if (typeof chunk === "string") { + chunk = encoder.encode(chunk); + } io.writeUTF8(LF_to_CRLF(chunk)); }, }), @@ -76,6 +149,9 @@ term.onTerminalReady = async () => { proc.stderr.pipeTo( new WritableStream({ write: (chunk) => { + if (typeof chunk === "string") { + chunk = encoder.encode(chunk); + } io.writeUTF8(LF_to_CRLF(chunk)); }, }), @@ -83,12 +159,12 @@ term.onTerminalReady = async () => { const oldProcKill = proc.kill.bind(proc); proc.kill = () => { - oldProcKill(); - instanceWindow.close(); + if (proc.alive) oldProcKill(); + if (instanceWindow.alive) instanceWindow.close(); }; instanceWindow.addEventListener("close", () => { - proc.kill(); + if (proc.alive) proc.kill(); }); proc.exit = proc.kill.bind(proc); diff --git a/apps/libfileview.lib/fileHandler.js b/apps/libfileview.lib/fileHandler.js index 1b81d19..38c65e2 100644 --- a/apps/libfileview.lib/fileHandler.js +++ b/apps/libfileview.lib/fileHandler.js @@ -1,5 +1,7 @@ import { createAppView, getAppIcon } from "./pages/appview/appview.js"; +const { ShortcutApp } = await anura.import("anura.globalscope"); + const icons = await (await fetch(localPathToURL("icons.json"))).json(); export function openFile(path) { @@ -118,6 +120,23 @@ export function openFile(path) { iframe.srcdoc = data; fileView.content.appendChild(iframe); } + + async function openApp(path) { + const stat = await fs.promises.stat(path); + if (stat.isDirectory()) { + console.error("TODO: Move special folder execution to libfileview"); + anura.dialog.alert( + "Special folder execution is not yet implemented in libfileview. You should not be seeing this message unless you are a developer, if you are, please fix it. If you are not, please report this issue.", + ); + } else { + // Shortcut file + const data = await fs.promises.readFile(path); + const app = new ShortcutApp(path, JSON.parse(data)); + console.log(app); + await app.open(); + } + } + switch (path.split(".").slice("-2").join(".")) { case "app.zip": createAppView(path, "app"); @@ -139,7 +158,10 @@ export function openFile(path) { openText(path); break; case "ajs": - anura.processes.execute(path); + // anura.processes.execute(path); + const shell = anura.settings.get("shell") || "/usr/bin/chimerix.ajs"; + anura.settings.set("shell", shell); + anura.processes.execute(shell, ["--cmd", path], true); break; case "mp3": openAudio(path, "audio/mpeg"); @@ -181,13 +203,15 @@ export function openFile(path) { case "html": openHTML(path); break; + case "app": + openApp(path); + break; default: openText(path); - break; } } -export function getIcon(path) { +export async function getIcon(path) { switch (path.split(".").slice("-2").join(".")) { case "app.zip": return getAppIcon(path); @@ -197,10 +221,30 @@ export function getIcon(path) { break; } let ext = path.split(".").slice("-1")[0]; + + if (ext === "app") { + const stat = await anura.fs.promises.stat(path); + if (stat.isDirectory()) { + console.error("TODO: Move special folder execution to libfileview"); + anura.dialog.alert( + "Special folder execution is not yet implemented in libfileview. You should not be seeing this message unless you are a developer, if you are, please fix it. If you are not, please report this issue.", + ); + } else { + // Shortcut file + const app = new ShortcutApp( + path, + JSON.parse(await anura.fs.promises.readFile(path)), + ); + + console.log(app); + return new URL(app.icon, top.location.href).href; + } + } let iconObject = icons.files.find((icon) => icon.ext === ext); if (iconObject) { return localPathToURL(iconObject.icon); } + return localPathToURL(icons.default); } @@ -213,8 +257,10 @@ export function getFileType(path) { default: break; } - let ext = path.split(".").slice("-1")[0]; - let iconObject = icons.files.find((icon) => icon.ext === ext); + + const ext = path.split(".").slice("-1")[0]; + + const iconObject = icons.files.find((icon) => icon.ext === ext); if (iconObject) { return iconObject.type; } diff --git a/apps/libfileview.lib/icons.json b/apps/libfileview.lib/icons.json index 7d2f19b..433d99b 100644 --- a/apps/libfileview.lib/icons.json +++ b/apps/libfileview.lib/icons.json @@ -131,6 +131,10 @@ "icon": "icons/css.svg", "source": "papirus/Papirus/16x16/mimetypes/text-css.svg", "type": "CSS Stylesheet" + }, + { + "ext": "app", + "type": "Anura Shortcut" } ], "default": "icons/txt.svg", diff --git a/bin/anuractrl.ajs b/bin/anuractrl.ajs new file mode 100644 index 0000000..1f0dae3 --- /dev/null +++ b/bin/anuractrl.ajs @@ -0,0 +1,91 @@ +#! {"lang":"module"} +console.warn(` +// AnuraCtrl - A command-line utility for controlling Anura apps +// Eventually this should have more tightly knit integration with Anurad, +// instead of just the raw process API. For now, this is a good start.`) + +export async function main(args) { + const validArgs = new Set(["open", "close", "info", "pid", "pkg"]); + const argmap = {}; + + args.forEach((arg, i) => { + if (arg.startsWith("--")) { + const key = arg.slice(2); + if (!validArgs.has(key)) { + eprintln(`Unknown argument: --${key}`); + exit(1); + } + argmap[key] = ["open", "close", "info"].includes(key) ? true : args[i + 1] || ""; + } + }); + + const commands = ["open", "close", "info"].filter((cmd) => argmap[cmd]); + if (commands.length !== 1) { + eprintln("Provide exactly one command: --open, --close, or --info."); + exit(1); + } + + const { + open, + close, + info, + pid, + pkg + } = argmap; + + if (open) { + await anura.apps[pkg].open(); + println(`Opened app with package name: ${pkg}`); + } else if (close) { + if (pid) { + anura.processes.procs[pid].deref()?.kill(); + println(`Closed process with PID: ${pid}`); + } else if (pkg) { + anura.processes.procs.forEach((p) => { + let proc = p?.deref(); + if (proc?.app?.package === pkg) { + proc.kill(); + println(`Closed process with PID: ${proc.pid} for app: ${pkg}`); + } + }); + } else { + eprintln("Please provide either --pid or --pkg to close a process or an app's windows."); + exit(1); + } + } else if (info) { + if (pid) { + const proc = anura.processes.procs[pid]?.deref(); + if (proc) { + const isApp = proc?.app + println(`Process info for PID ${pid}:`); + println(` Title: ${proc.title}`); + println(` Belongs to app: ${isApp}`); + if (isApp) { + println(` Package: ${proc.app?.package}`); + } + } else { + eprintln(`No process found with PID: ${pid}`); + } + } else if (pkg) { + let found = false; + anura.processes.procs.forEach((p) => { + let proc = p?.deref(); + if (proc?.app?.package === pkg) { + found = true; + println(`Process info for PID ${proc.pid}:`); + println(` Title: ${proc.title}`); + println(` Belongs to app: true`); + println(` Package: ${proc.app?.package}`); + } + }); + + if (!found) { + eprintln(`No process found for package: ${pkg}`); + } + } else { + eprintln("Please provide either --pid or --pkg to get process information."); + exit(1); + } + } + exit(0); +} \ No newline at end of file diff --git a/bin/vista.ajs b/bin/vista.ajs new file mode 100644 index 0000000..014fa33 --- /dev/null +++ b/bin/vista.ajs @@ -0,0 +1,46 @@ +#! {"lang":"module"} + +export async function main(args) { + const validArgs = new Set(["alert", "confirm", "prompt", "message", "title", "default"]); + const argmap = {}; + + args.forEach((arg, i) => { + if (arg.startsWith("--")) { + const key = arg.slice(2); + if (!validArgs.has(key)) { + eprintln(`Unknown argument: --${key}`); + exit(1); + } + argmap[key] = ["alert", "confirm", "prompt"].includes(key) ? true : args[i + 1] || ""; + } + }); + + const commands = ["alert", "confirm", "prompt"].filter((cmd) => argmap[cmd]); + if (commands.length !== 1) { + eprintln("Provide exactly one command: --alert, --confirm, or --prompt."); + exit(1); + } + + const { + alert, + confirm, + prompt, + message = "No message provided", + title = "Dialog", + default: defaultValue = null, + } = argmap; + + if (alert) { + anura.dialog.alert(message, title); + println(title); + println(message); + } else if (confirm) { + const result = await anura.dialog.confirm(message, title); + println((!!result) + ""); + } else if (prompt) { + const result = await anura.dialog.prompt(message, defaultValue); + println(result + ""); + } + + exit(0); +} \ No newline at end of file diff --git a/bin/x86-run.ajs b/bin/x86-run.ajs new file mode 100644 index 0000000..7e77a63 --- /dev/null +++ b/bin/x86-run.ajs @@ -0,0 +1,55 @@ +#! {"lang":"module"} + +export async function main(args) { + let cmd = args.slice(1).join(" "); + + console.log(args, cmd); + + if (!cmd || cmd === "") { + cmd = "/bin/bash --login"; + } + + if (anura.x86 === undefined) { + println( + "\u001b[33mThe Anura x86 subsystem is not enabled. Please enable it in Settings.\u001b[0m", + ); + return; + } + if (!anura.x86.ready) { + println( + "\u001b[33mThe Anura x86 subsystem has not yet booted. Please wait for the notification that it has booted and try again.\u001b[0m", + ); + return; + } + println( + "Welcome to the Anura x86 subsystem.\nTo access your Anura files within Linux, use the /root directory.", + ); + const pty = await anura.x86.openpty( + cmd, + 80, 24, + (data) => { + print(data); + }, + ); + + addEventListener("message", (event) => { + if (event.data.type === "ioctl.set") { + console.log(event.data.windowSize); + anura.x86.resizepty(pty, event.data.windowSize.cols, event.data.windowSize.rows); + } + }); + + addEventListener("message", (event) => { + if (event.data.type === "stdin") { + console.log(event.data.message); + anura.x86.writepty(pty, event.data.message); + } + }); + + const oldkill = env.process.kill.bind(env.process); + + env.process.kill = (signal) => { + anura.x86.closepty(pty); + oldkill(signal); + }; +} \ No newline at end of file diff --git a/config.default.json b/config.default.json index 9ddad17..1fb8916 100644 --- a/config.default.json +++ b/config.default.json @@ -12,7 +12,12 @@ "/apps/libpersist.lib", "/apps/libhterm.lib" ], - "bin": ["/bin/chimerix.ajs"], + "bin": [ + "/bin/chimerix.ajs", + "/bin/x86-run.ajs", + "/bin/vista.ajs", + "/bin/anuractrl.ajs" + ], "defaultsettings": { "use-sw-cache": false, "applist": ["anura.browser", "anura.settings", "anura.fsapp"], diff --git a/public/index.html b/public/index.html index bf1c7be..826dda7 100755 --- a/public/index.html +++ b/public/index.html @@ -60,6 +60,7 @@ + @@ -119,6 +120,7 @@ + diff --git a/server/server.js b/server/server.js index a4af875..db51bfa 100644 --- a/server/server.js +++ b/server/server.js @@ -55,6 +55,7 @@ if (debugAppFolder) { app.use(express.static(__dirname + "/public")); app.use(express.static(__dirname + "/build")); +app.use("/bin", express.static(__dirname + "/bin")); app.use("/apps", express.static(__dirname + "/apps")); app.use(express.static(__dirname + "/aboutproxy/static")); diff --git a/src/AliceWM.tsx b/src/AliceWM.tsx index 47ecd43..ad3277b 100755 --- a/src/AliceWM.tsx +++ b/src/AliceWM.tsx @@ -28,6 +28,7 @@ class WindowInformation { minwidth: number; minheight: number; resizable: boolean; + args?: string[]; } class WMWindow extends EventTarget implements Process { @@ -81,6 +82,12 @@ class WMWindow extends EventTarget implements Process { this.state.title = title; } + #args: string[]; + + get args() { + return this.#args; + } + maximizeImg: HTMLOrSVGElement; maximizeSvg: HTMLOrSVGElement; restoreSvg: HTMLOrSVGElement; @@ -89,6 +96,7 @@ class WMWindow extends EventTarget implements Process { public app?: App, ) { super(); + this.#args = wininfo.args || []; this.wininfo = wininfo; this.state = $state({ title: wininfo.title, diff --git a/src/Boot.tsx b/src/Boot.tsx index add0c9f..1c2e818 100644 --- a/src/Boot.tsx +++ b/src/Boot.tsx @@ -354,6 +354,8 @@ window.addEventListener("load", async () => { } } + anura.registerLib(new AnuraGlobalsLib()); + // Register built-in Node Polyfills anura.registerLib(new NodeFS()); anura.registerLib(new NodePrelude()); @@ -788,12 +790,25 @@ async function bootUserCustomizations() { const files = await anura.fs.promises.readdir(directories["apps"]); if (files) { for (const file of files) { - try { - await anura.registerExternalApp( - `/fs/${directories["apps"]}/${file}/`, + const { type } = await anura.fs.promises.stat( + `${directories["apps"]}/${file}`, + ); + if (type === "DIRECTORY") { + try { + await anura.registerExternalApp( + `/fs/${directories["apps"]}/${file}/`, + ); + } catch (e) { + anura.logger.error("Anura failed to load an app", e); + } + } else { + // This is a shortcut file + const shortcut = JSON.parse( + ( + await anura.fs.promises.readFile(`${directories["apps"]}/${file}`) + ).toString(), ); - } catch (e) { - anura.logger.error("Anura failed to load an app", e); + anura.registerApp(new ShortcutApp(file, shortcut)); } } } diff --git a/src/anurad.tsx b/src/anurad.tsx index ae8e0de..378fb43 100644 --- a/src/anurad.tsx +++ b/src/anurad.tsx @@ -46,6 +46,7 @@ class AnuradInitScript implements Process { frame: InitScriptFrame; window: InitScriptFrame["contentWindow"]; info?: InitScriptExports; + #args: string[]; get title() { return this.info?.name as string; @@ -58,8 +59,11 @@ class AnuradInitScript implements Process { constructor( script: string, public pid: number, + args: string[] = [], ) { this.script = script; + this.#args = args; + this.frame = (