mirror of
https://github.com/MercuryWorkshop/anuraOS.git
synced 2025-09-04 09:14:30 +00:00
process args support, bin loading overhaul, shortcuts, utilities
This commit is contained in:
5
Makefile
5
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/
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<body style="margin: 0px; padding: 0px">
|
||||
<div id="terminal"></div>
|
||||
<script src="term.js" type="module"></script>
|
||||
</body>
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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",
|
||||
|
91
bin/anuractrl.ajs
Normal file
91
bin/anuractrl.ajs
Normal file
@@ -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);
|
||||
}
|
46
bin/vista.ajs
Normal file
46
bin/vista.ajs
Normal file
@@ -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);
|
||||
}
|
55
bin/x86-run.ajs
Normal file
55
bin/x86-run.ajs
Normal file
@@ -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);
|
||||
};
|
||||
}
|
@@ -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"],
|
||||
|
@@ -60,6 +60,7 @@
|
||||
<script type="text/javascript" src="lib/libs/AnuradHelpersLib.js"></script>
|
||||
|
||||
<script type="text/javascript" src="lib/coreapps/App.js"></script>
|
||||
<script type="text/javascript" src="lib/coreapps/ShortcutApp.js"></script>
|
||||
|
||||
<script type="text/javascript" src="lib/coreapps/XAppStub.js"></script>
|
||||
<script type="text/javascript" src="lib/coreapps/XFrogApp.js"></script>
|
||||
@@ -119,6 +120,7 @@
|
||||
<script type="text/javascript" src="lib/api/Systray.js"></script>
|
||||
<!-- <script type="text/javascript" src="libs/bare-mux/index.js"></script> -->
|
||||
<script type="text/javascript" src="libs/bare-mux/bare.cjs"></script>
|
||||
<script type="text/javascript" src="lib/libs/AnuraGlobalsLib.js"></script>
|
||||
<script type="text/javascript" src="lib/Boot.js"></script>
|
||||
<script type="module" src="lib/bcc.js"></script>
|
||||
|
||||
|
@@ -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"));
|
||||
|
||||
|
@@ -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,
|
||||
|
25
src/Boot.tsx
25
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 = (
|
||||
<iframe
|
||||
id={`proc-${pid}`}
|
||||
@@ -69,7 +73,7 @@ class AnuradInitScript implements Process {
|
||||
<html>
|
||||
<head>
|
||||
<script type="module">
|
||||
globalThis.initScript = await import("data:text/javascript;base64,${btoa(script)}");
|
||||
globalThis.initScript = await import("data:text/javascript;base64,${utoa(script)}");
|
||||
window.postMessage({ type: "init" });
|
||||
</script>
|
||||
</head>
|
||||
@@ -196,6 +200,10 @@ class AnuradInitScript implements Process {
|
||||
return this.frame.isConnected;
|
||||
}
|
||||
|
||||
get args(): string[] {
|
||||
return this.#args;
|
||||
}
|
||||
|
||||
kill(): void {
|
||||
this.info!.stop();
|
||||
this.frame.remove();
|
||||
|
@@ -1,3 +1,11 @@
|
||||
interface ModuleProcessExports {
|
||||
main?: (args: string[]) => void;
|
||||
}
|
||||
|
||||
type ModuleProcessFrame = HTMLIFrameElement & {
|
||||
contentWindow: Window & { moduleProcess: ModuleProcessExports };
|
||||
};
|
||||
|
||||
class Processes {
|
||||
processesDiv = (<div id="processes"></div>);
|
||||
constructor() {
|
||||
@@ -28,13 +36,17 @@ class Processes {
|
||||
this.state.procs = this.state.procs;
|
||||
}
|
||||
|
||||
create(script: string, type: "common" | "module" = "common"): IframeProcess {
|
||||
const proc = new IframeProcess(script, type, this.procs.length);
|
||||
create(
|
||||
script: string,
|
||||
type: "common" | "module" = "common",
|
||||
args: string[] = [],
|
||||
): IframeProcess {
|
||||
const proc = new IframeProcess(script, type, this.procs.length, args);
|
||||
this.register(proc);
|
||||
return proc;
|
||||
}
|
||||
|
||||
async execute(path: string) {
|
||||
async execute(path: string, args: string[] = [], useLogger: boolean = false) {
|
||||
const data = await anura.fs.promises.readFile(path);
|
||||
// Read the file until the first newline
|
||||
let i = 0;
|
||||
@@ -58,9 +70,19 @@ class Processes {
|
||||
|
||||
if (["common", "module"].includes(options.lang)) {
|
||||
const script = new TextDecoder().decode(payload);
|
||||
const proc = this.create(script, options.lang);
|
||||
const proc = this.create(script, options.lang, args);
|
||||
|
||||
// Whether to pipe to the built-in logger, useful if you are lazy and want
|
||||
// devtools to contain the std streams without any effort
|
||||
if (useLogger) {
|
||||
const { stdout, stderr } = anura.logger.createStreams(proc.title);
|
||||
proc.stdout.pipeTo(stdout);
|
||||
proc.stderr.pipeTo(stderr);
|
||||
}
|
||||
|
||||
return proc;
|
||||
}
|
||||
|
||||
throw new Error("Invalid shebang");
|
||||
}
|
||||
}
|
||||
@@ -73,35 +95,40 @@ abstract class Process {
|
||||
stderr: ReadableStream<Uint8Array>;
|
||||
stdin: WritableStream<Uint8Array>;
|
||||
|
||||
kill() {
|
||||
kill(code?: number) {
|
||||
anura.processes.remove(this.pid);
|
||||
}
|
||||
|
||||
get args(): string[] {
|
||||
return [];
|
||||
}
|
||||
abstract get alive(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumb hack to convert utf-8 to base64
|
||||
*/
|
||||
function utoa(data: string) {
|
||||
return btoa(unescape(encodeURIComponent(data)));
|
||||
}
|
||||
|
||||
class IframeProcess extends Process {
|
||||
script: string;
|
||||
title = "Process";
|
||||
frame: HTMLIFrameElement;
|
||||
|
||||
#args: string[] = [];
|
||||
|
||||
constructor(
|
||||
script: string,
|
||||
type: "common" | "module" = "common",
|
||||
public pid: number,
|
||||
args: string[] = [],
|
||||
) {
|
||||
super();
|
||||
this.title = `Process ${pid}`;
|
||||
console.log(
|
||||
"/display?content=" +
|
||||
encodeURIComponent(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script ${type === "module" ? 'type="module"' : ""}>
|
||||
${script}
|
||||
</script>
|
||||
</head>
|
||||
</html>`),
|
||||
);
|
||||
this.#args = args;
|
||||
|
||||
this.frame = (
|
||||
<iframe
|
||||
id={`proc-${pid}`}
|
||||
@@ -112,13 +139,13 @@ class IframeProcess extends Process {
|
||||
<html>
|
||||
<head>
|
||||
<script ${type === "module" ? 'type="module"' : ""}>
|
||||
${script}
|
||||
${type === "module" ? `globalThis.moduleProcess = await import("data:text/javascript;base64,${utoa(script)}"); if ( typeof moduleProcess?.main === "function" ) { await moduleProcess.main(${JSON.stringify(args)}); }` : script}
|
||||
</script>
|
||||
</head>
|
||||
</html>`)
|
||||
}
|
||||
></iframe>
|
||||
) as HTMLIFrameElement;
|
||||
) as ModuleProcessFrame;
|
||||
|
||||
anura.processes.processesDiv.appendChild(this.frame);
|
||||
|
||||
@@ -145,12 +172,26 @@ class IframeProcess extends Process {
|
||||
message,
|
||||
});
|
||||
},
|
||||
// Alias for printerr
|
||||
eprint: (message: string) => {
|
||||
this.window.postMessage({
|
||||
type: "stderr",
|
||||
message,
|
||||
});
|
||||
},
|
||||
printlnerr: (message: string) => {
|
||||
this.window.postMessage({
|
||||
type: "stderr",
|
||||
message: message + "\n",
|
||||
});
|
||||
},
|
||||
// Alias for printlnerr
|
||||
eprintln: (message: string) => {
|
||||
this.window.postMessage({
|
||||
type: "stderr",
|
||||
message: message + "\n",
|
||||
});
|
||||
},
|
||||
read: () => {
|
||||
return new Promise((resolve) => {
|
||||
this.window.addEventListener(
|
||||
@@ -180,6 +221,10 @@ class IframeProcess extends Process {
|
||||
this.window.addEventListener("message", listener);
|
||||
});
|
||||
},
|
||||
// Exit codes are not implemented yet but it is good practice to include them anyways
|
||||
exit: (code?: number) => {
|
||||
this.kill(code);
|
||||
},
|
||||
env: {
|
||||
process: this,
|
||||
},
|
||||
@@ -197,11 +242,17 @@ class IframeProcess extends Process {
|
||||
this.stderr = new ReadableStream({
|
||||
start: (controller) => {
|
||||
this.window.addEventListener("error", (e) => {
|
||||
controller.enqueue(e.error);
|
||||
const en = new TextEncoder();
|
||||
controller.enqueue(en.encode(e.error.message + "\n"));
|
||||
});
|
||||
|
||||
this.window.addEventListener("message", (e) => {
|
||||
if (e.data.type === "stderr") {
|
||||
if (typeof e.data.message === "string") {
|
||||
const en = new TextEncoder();
|
||||
e.data.message = en.encode(e.data.message);
|
||||
}
|
||||
|
||||
controller.enqueue(e.data.message);
|
||||
}
|
||||
});
|
||||
@@ -212,6 +263,11 @@ class IframeProcess extends Process {
|
||||
start: (controller) => {
|
||||
this.window.addEventListener("message", (e) => {
|
||||
if (e.data.type === "stdout") {
|
||||
if (typeof e.data.message === "string") {
|
||||
const en = new TextEncoder();
|
||||
e.data.message = en.encode(e.data.message);
|
||||
}
|
||||
|
||||
controller.enqueue(e.data.message);
|
||||
}
|
||||
});
|
||||
@@ -219,13 +275,32 @@ class IframeProcess extends Process {
|
||||
});
|
||||
}
|
||||
|
||||
kill() {
|
||||
this.frame.remove();
|
||||
super.kill();
|
||||
#closing = false;
|
||||
|
||||
kill(code?: number) {
|
||||
// Make sure all messages are received by sending a dummy message and waiting
|
||||
|
||||
if (code) {
|
||||
console.warn("Exit codes are not implemented yet, ignoring");
|
||||
}
|
||||
|
||||
this.#closing = true;
|
||||
|
||||
this.window.addEventListener("message", (e) => {
|
||||
if (e.data.type === "kill") {
|
||||
this.frame.remove();
|
||||
super.kill();
|
||||
}
|
||||
});
|
||||
this.window.postMessage({ type: "kill" });
|
||||
}
|
||||
|
||||
get args() {
|
||||
return this.#args;
|
||||
}
|
||||
|
||||
get alive() {
|
||||
return this.frame.isConnected;
|
||||
return !this.#closing || !this.frame.isConnected;
|
||||
}
|
||||
|
||||
get window() {
|
||||
|
93
src/coreapps/ShortcutApp.tsx
Normal file
93
src/coreapps/ShortcutApp.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
interface AnuraShortcut {
|
||||
name: string;
|
||||
command: string;
|
||||
icon?: string;
|
||||
console?: boolean;
|
||||
}
|
||||
|
||||
// mangle file path to a valid package id component
|
||||
function b26(s: string) {
|
||||
return [...s]
|
||||
.map((c) =>
|
||||
[...c.charCodeAt(0).toString(26)]
|
||||
.map((d) => String.fromCharCode(parseInt(d, 36) + 97))
|
||||
.join(""),
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Virtual app that represents a shortcut, used when a shortcut file is placed in the apps directory
|
||||
class ShortcutApp extends App implements AnuraShortcut {
|
||||
static async launchShortcut(props: AnuraShortcut) {
|
||||
// Manually parse the cmdline string. Eventually we should have a proper
|
||||
// system shell that can handle this, but for now we will just use a regex
|
||||
// to split the command line into arguments.
|
||||
const cmdline = (props.command!.match(/(?:[^\s"]+|"[^"]*")+/g) || []).map(
|
||||
(arg) => {
|
||||
// Remove surrounding quotes if they exist
|
||||
if (arg.startsWith('"') && arg.endsWith('"')) {
|
||||
return arg.slice(1, -1);
|
||||
}
|
||||
return arg;
|
||||
},
|
||||
);
|
||||
|
||||
const streams = anura.logger.createStreams(
|
||||
"Shortcut: " + props.name + " (" + props.command + ") ",
|
||||
);
|
||||
|
||||
if (props.console) {
|
||||
const terminal = anura.settings.get("terminal") || "anura.ashell";
|
||||
anura.settings.set("terminal", terminal);
|
||||
|
||||
const proc = await anura.apps[terminal].open([
|
||||
"--cmd",
|
||||
cmdline.join(" "),
|
||||
]);
|
||||
if (proc instanceof WMWindow || proc instanceof Process) {
|
||||
proc.stdout.pipeTo(streams.stdout);
|
||||
proc.stderr.pipeTo(streams.stderr);
|
||||
}
|
||||
return proc;
|
||||
} else {
|
||||
anura.processes.execute(cmdline[0]!, cmdline.slice(1)).then((proc) => {
|
||||
proc.stdout.pipeTo(streams.stdout);
|
||||
proc.stderr.pipeTo(streams.stderr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
name = "Shortcut";
|
||||
package = "anura.shortcut";
|
||||
icon = "/assets/icons/generic.svg";
|
||||
console = false;
|
||||
command =
|
||||
'/usr/bin/vista.ajs --alert --message "Anura Shortcuts: This shortcut is not configured properly." --title Error';
|
||||
|
||||
constructor(filePath: string, props: AnuraShortcut) {
|
||||
super();
|
||||
Object.assign(this, props);
|
||||
this.package = "anura.shortcut." + b26(filePath);
|
||||
if (anura.apps[this.package]) {
|
||||
if (anura.apps[this.package] instanceof ShortcutApp) {
|
||||
// If the app is already a shortcut app, just return it
|
||||
return anura.apps[this.package];
|
||||
}
|
||||
|
||||
this.package += "." + Date.now();
|
||||
console.warn(
|
||||
"ShortcutApp: Mitigating package collision, please investigate as this is a bug.",
|
||||
);
|
||||
anura.notifications.add({
|
||||
title: "ShortcutApp",
|
||||
description:
|
||||
"Package collision detected, renaming package, please investigate or report this.",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async open() {
|
||||
await ShortcutApp.launchShortcut(this);
|
||||
}
|
||||
}
|
50
src/libs/AnuraGlobalsLib.tsx
Normal file
50
src/libs/AnuraGlobalsLib.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Export helpful global objects from the anura top level window
|
||||
*/
|
||||
class AnuraGlobalsLib extends Lib {
|
||||
icon = "/assets/icons/generic.svg";
|
||||
package = "anura.globalscope";
|
||||
name = "Anura Global Objects";
|
||||
latestVersion = anura.version.pretty;
|
||||
|
||||
versions = {
|
||||
[anura.version.pretty]: {
|
||||
/**
|
||||
* Run a top level eval to get a global object,
|
||||
* this is how you would get an object from the top level
|
||||
* before this library was created but this helper method
|
||||
* is more verbose and easier to explain.
|
||||
*/
|
||||
getWithPath: eval.bind(top),
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.versions[anura.version.pretty] = new Proxy<any>(
|
||||
this.versions[anura.version.pretty],
|
||||
{
|
||||
get: (target, prop) => {
|
||||
if (prop in target) {
|
||||
return target[prop];
|
||||
} else {
|
||||
try {
|
||||
return this.versions[anura.version.pretty]?.getWithPath(prop);
|
||||
} catch (_) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getImport(version: string): Promise<any> {
|
||||
if (!version) version = this.latestVersion;
|
||||
if (!this.versions[version]) {
|
||||
throw new Error("Version not found");
|
||||
}
|
||||
return this.versions[version];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user