process args support, bin loading overhaul, shortcuts, utilities

This commit is contained in:
endercass
2025-05-10 17:34:44 -04:00
parent d0f645608a
commit 76e1d606a3
17 changed files with 618 additions and 42 deletions

View File

@@ -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/

View File

@@ -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>

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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
View 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
View 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
View 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);
};
}

View File

@@ -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"],

View File

@@ -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>

View File

@@ -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"));

View File

@@ -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,

View File

@@ -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));
}
}
}

View File

@@ -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();

View File

@@ -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() {

View 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);
}
}

View 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];
}
}