Inject your own applications into the SGApps File Manager so they appear alongside the built-in apps. Your apps can open files by MIME type, receive arguments, and use the full windowed UI.
apps:register API eventApplication.require(path) — the path can be an HTTPS URL, blob URL, or data URIYour app must export a constructor function that returns an object implementing the WindowApplicationInterface:
module.exports = function () {
var config = {
window: null,
ready: new Application.Promise(),
rendered: false,
node: document.createElement("div")
};
var app = new ApplicationPrototype();
app.bind("node", function () { return config.node; }, "");
app.bind("handleWindow", function (win) {
config.window = win;
win.emit("api-request::attached", []);
win.on("api-request::ready", function (cb) { cb(config.ready); });
});
app.bind("window", function () { return config.window; }, "");
app.bind("ready", function () { return config.ready; });
app.bind("render", function (cb) {
if (config.rendered) return cb();
config.rendered = true;
// --- your UI code here ---
var args = app.window().env().args || [];
config.node.innerHTML = '<h2>Hello from custom app!</h2><p>Args: ' + args.join(', ') + '</p>';
cb();
});
app.bind("destroy", function () {
// cleanup
});
app.bind("init", function () {
if (!config.rendered) {
app.render(function (err) {
if (err) return config.ready.reject(err);
config.ready.resolve(app);
});
}
return app;
});
return app;
};apps:register -- Register Custom Apps| Arg | Type | Description |
|---|---|---|
appsMap | object | { appName: { path, name, icon, comment?, mimetypes?, args? } } |
callback | function | (err, registeredNames: string[]) |
path field can be:| Path Type | Example | Description |
|---|---|---|
| HTTPS URL | "https://example.com/my-app.js" | Loaded from your server |
| Blob URL | "blob:https://..." | Created via URL.createObjectURL() |
| Data URI | "data:application/javascript;base64,..." | Inline JavaScript encoded in base64 |
| input:// | "input://apps/my-app.js" | From in-memory storage |
apps:unregister -- Remove a Registered App| Arg | Type | Description |
|---|---|---|
appName | string | Name of the app to remove |
callback | function | (err) |
disableDefaultApps | boolean | If true, also allows removing built-in apps |
true as the third argument to also remove built-in apps — useful for replacing default apps with custom implementations.apps:list -- List All Registered AppsReturns all apps in the window manager library.
socket.fire("webapp::instance::request", "apps:list", function (err, apps) {
Object.keys(apps).forEach(function (name) {
console.log(name, apps[name].__apiInjected ? "(custom)" : "(built-in)");
});
});All examples below use this embedded File Manager. The "Try" buttons create sample files and register custom apps — then you can double-click the files to open them with the custom app.
<iframe id="ca-demo" src="/online/webapp/file-manager" width="100%" height="500" frameborder="0" style="border:1px solid #ccc; border-radius:4px;"></iframe>
<script>window._wsConnect('ca-demo', 'caSocket');
window._caFire = function () {
var s = window._ws('caSocket');
if (!s) return;
var args = Array.prototype.slice.call(arguments);
s.fire.apply(s, ['webapp::instance::request'].concat(args));};
window._caSeq = function (cmds, done) {
var s = window._ws('caSocket');
if (!s) return;
var i = 0;
var next = function () {
if (i >= cmds.length) { if (done) done(); return; }
var cmd = cmds[i++];
var args = cmd.concat([function () { next(); }]);
s.fire.apply(s, ['webapp::instance::request'].concat(args));
};
next();};
// Builds a complete app module source string from a render function body stringwindow._caAppCode = function (renderFnStr) {
return 'module.exports = function () {\n' +
'var app = new ApplicationPrototype();\n' +
'var node = document.createElement("div");\n' +
'var win = null;\n' +
'var ready = new Application.Promise();\n' +
'app.bind("node", function () { return node; }, "");\n' +
'app.bind("window", function () { return win; }, "");\n' +
'app.bind("ready", function () { return ready; });\n' +
'app.bind("handleWindow", function (w) {\n' +
' win = w;\n' +
' w.emit("api-request::attached", []);\n' +
' w.on("api-request::ready", function (cb) { cb(ready); });\n' +
'});\n' +
'app.bind("render", function (cb) {\n' +
' (' + renderFnStr + ')(app, win, node, cb);\n' +
'});\n' +
'app.bind("destroy", function () {});\n' +
'app.bind("init", function () {\n' +
' app.render(function (err) {\n' +
' if (err) return ready.reject(err);\n' +
' ready.resolve(app);\n' +
' });\n' +
' return app;\n' +
'});\n' +
'return app;\n' +
'};';};
// --- Demo 1: Text Viewer ---window._caDemo1 = function () {
var renderFn = function (app, win, node, cb) {
win.title('Text Viewer');
win.icon('paper/icons/apps/accessories-text-editor');
win.height(350); win.width(500);
var args = app.window().env().args || [];
if (args[0]) {
node.innerHTML = 'Loading...
';
Application.require('/files/api/modules/fs.js').then(function (fsModule) {
fsModule().read(args[0], null, true).response('text').then(function (text) {
node.innerHTML = '' + text.replace(/';
});
});
} else {
node.innerHTML = 'No file specified
';
}
cb();
};
var code = window._caAppCode(renderFn.toString());
window._caSeq([
['gui:specialPaths:clear'],
['fs:mkdirp', 'input://apps/'],
['fs:mkdirp', 'input://demo-files/'],
['fs:write', 'input://apps/text-viewer.js', code],
['fs:write', 'input://demo-files/hello.txt', 'Hello World!\nThis is a plain text file.\nCreated by the API.\n\nDouble-click to open with the custom Text Viewer.'],
['fs:write', 'input://demo-files/data.csv', 'Name,Age,City\nAlice,30,Paris\nBob,25,London\nCharlie,35,Berlin'],
['apps:register', {'my-text-viewer': {path: 'input://apps/text-viewer.js', name: 'Text Viewer', icon: 'paper/icons/apps/accessories-text-editor', comment: 'Custom text viewer', mimetypes: ['text/plain','text/csv','text/log'], args: ['%U']}}],
['gui:specialPaths:add', {path:'input://demo-files/', name:'Demo Files', icon:'breeze/icons/places/64/folder-blue', bookmark:{category:'demos',name:'Demo Files',icon:'breeze/icons/places/64/folder-blue'}}],
['fs:cwd', 'input://demo-files/']
], function () { alert('Done! Double-click hello.txt or data.csv to open with the Text Viewer.'); });};
// --- Demo 3: JSON Viewer ---window._caDemo3 = function () {
var renderFn = function (app, win, node, cb) {
win.title('JSON Viewer');
win.icon('breeze/icons/mimetypes/64/application-json');
win.height(400); win.width(600);
var args = app.window().env().args || [];
if (args[0]) {
node.innerHTML = 'Loading JSON...
';
Application.require('/files/api/modules/fs.js').then(function (fsModule) {
fsModule().read(args[0], null, true).response('text').then(function (text) {
try {
var json = JSON.parse(text);
var pretty = JSON.stringify(json, null, 2).replace(/&/g, '&').replace(/"$1":');
pretty = pretty.replace(/: (".*?")/g, ': $1');
pretty = pretty.replace(/: (\d+)/g, ': $1');
node.innerHTML = '' + pretty + '
';
} catch (e) {
node.innerHTML = 'Invalid JSON
' + e.message + '
';
}
});
});
}
cb();
};
var code = window._caAppCode(renderFn.toString());
window._caSeq([
['fs:mkdirp', 'input://apps/'],
['fs:mkdirp', 'input://demo-files/'],
['fs:write', 'input://apps/json-viewer.js', code],
['fs:write', 'input://demo-files/config.json', JSON.stringify({name:'my-app',version:'1.0.0',description:'A demo project',dependencies:{lodash:'^4.17.21',express:'^4.18.0'},scripts:{start:'node index.js',test:'jest'}}, null, 2)],
['apps:register', {'json-viewer': {path: 'input://apps/json-viewer.js', name: 'JSON Viewer', icon: 'breeze/icons/mimetypes/64/application-json', comment: 'Pretty-print JSON', mimetypes: ['application/json'], args: ['%U']}}],
['fs:cwd', 'input://demo-files/']
], function () { alert('Done! Double-click config.json to open with the JSON Viewer.'); });};
// --- Demo 4: Markdown Preview ---window._caDemo4 = function () {
var renderFn = function (app, win, node, cb) {
win.title('Markdown Preview');
win.icon('breeze/icons/mimetypes/16/text-x-markdown');
win.height(450); win.width(650);
var args = app.window().env().args || [];
if (args[0]) {
node.innerHTML = 'Rendering...
';
Application.require('/files/api/modules/fs.js').then(function (fsModule) {
fsModule().read(args[0], null, true).response('text').then(function (text) {
var html = text
.replace(/^### (.+)$/gm, '$1
')
.replace(/^## (.+)$/gm, '$1
')
.replace(/^# (.+)$/gm, '$1
')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/`(.+?)`/g, '$1')
.replace(/^- (.+)$/gm, '$1 ')
.replace(/\n\n/g, '
')
.replace(/\n/g, '
');
node.innerHTML = '' + html + '';
});
});
}
cb();
};
var code = window._caAppCode(renderFn.toString());
window._caSeq([
['fs:mkdirp', 'input://apps/'],
['fs:mkdirp', 'input://demo-files/'],
['fs:write', 'input://apps/md-preview.js', code],
['fs:write', 'input://demo-files/README.md', '# My Project\n\nThis is a **demo project** created via the API.\n\n## Features\n\n- Custom app injection\n- In-memory file storage\n- Markdown rendering\n\n## Getting Started\n\nRun `npm install` then `npm start`.\n\n### Notes\n\nThis file was created *programmatically* and is rendered by a custom Markdown Preview app.'],
['apps:register', {'md-preview': {path: 'input://apps/md-preview.js', name: 'Markdown Preview', icon: 'breeze/icons/mimetypes/16/text-x-markdown', comment: 'Markdown renderer', mimetypes: ['text/markdown','text/x-markdown'], args: ['%U']}}],
['fs:cwd', 'input://demo-files/']
], function () { alert('Done! Double-click README.md to open with the Markdown Preview.'); });};</script>
The simplest way — embed the entire app code as a base64 data URI. No server needed.
var appCode = ';((' + (function () {
module.exports = function () {
var config = {
window: null,
ready: new Application.Promise(),
rendered: false,
node: document.createElement("div")
};
var app = new ApplicationPrototype();
app.bind("node", function () { return config.node; }, "");
app.bind("window", function () { return config.window; }, "");
app.bind("ready", function () { return config.ready; });
app.bind("handleWindow", function (win) {
config.window = win;
win.emit("api-request::attached", []);
win.on("api-request::ready", function (cb) { cb(config.ready); });
});
app.bind("render", function (cb) {
if (config.rendered) return cb();
config.rendered = true;
var args = app.window().env().args || [];
var p = _();
app.window().title("My Text Viewer");
app.window().icon("paper/icons/apps/accessories-text-editor");
app.window().height(400);
app.window().width(600);
// If a file path was passed, read and display it
if (args[0]) {
p.E(config.node).t("Loading " + args[0] + "...");
// Access the file manager's fs via the window manager
Application.require("/files/api/modules/fs.js").then(function (fsModule) {
var fs = fsModule();
fs.read(args[0], null, true).response("text").then(function (text) {
config.node.innerHTML = "";
p.E(config.node)
.e("pre", "~")
.F({ padding: "16px", margin: 0, whiteSpace: "pre-wrap", fontFamily: "monospace" })
.t(text);
});
});
} else {
p.E(config.node)
.e("div", "~")
.F({ padding: "20px", textAlign: "center" })
.e("h2", "~").t("No file specified").U()
.e("p", "~").t("Open a text file to view it here");
}
cb();
});
app.bind("destroy", function () {});
app.bind("init", function () {
if (!config.rendered) {
app.render(function (err) {
if (err) return config.ready.reject(err);
config.ready.resolve(app);
});
}
return app;
});
return app;
};
}).toString() + ')());');
var dataUri = 'data:application/javascript;base64,' + btoa(appCode);
// Register the app
socket.fire("webapp::instance::request", "apps:register", {
"my-text-viewer": {
path: dataUri,
name: "My Text Viewer",
icon: "paper/icons/apps/accessories-text-editor",
comment: "Simple text file viewer",
mimetypes: ["text/plain", "text/csv", "text/log"],
args: ["%U"]
}
}, function (err, registered) {
console.log("Registered:", registered);
});After registration, when you double-click a .txt, .csv, or .log file in the file manager, your custom text viewer app opens.
<button onclick="window._caDemo1()">Try: Register Text Viewer + Create Sample Files</button>
Host your app on your own server and register it:
socket.fire("webapp::instance::request", "apps:register", {
"my-image-annotator": {
path: "https://my-cdn.com/apps/image-annotator/index.js",
name: "Image Annotator",
icon: "breeze/icons/apps/48/kolourpaint",
comment: "Annotate images with arrows, text, and shapes",
mimetypes: ["image/png", "image/jpeg", "image/webp"],
args: ["%U"]
}
});Your index.js file must use module.exports = function () { ... } format.
Create the app dynamically in JavaScript and register it via a blob URL:
var code = ';((' + (function () {
module.exports = function () {
var app = new ApplicationPrototype();
var node = document.createElement("div");
var win = null;
var ready = new Application.Promise();
app.bind("node", function () { return node; }, "");
app.bind("window", function () { return win; }, "");
app.bind("ready", function () { return ready; });
app.bind("handleWindow", function (w) {
win = w;
w.emit("api-request::attached", []);
w.on("api-request::ready", function (cb) { cb(ready); });
});
app.bind("render", function (cb) {
win.title("JSON Viewer");
win.height(500);
win.width(700);
var args = app.window().env().args || [];
if (args[0]) {
Application.require("/files/api/modules/fs.js").then(function (fsModule) {
fsModule().read(args[0], null, true).response("text").then(function (text) {
try {
var json = JSON.parse(text);
node.innerHTML = "<pre style='padding:16px;margin:0'>" +
JSON.stringify(json, null, 2).replace(/</g, "<") + "</pre>";
} catch (e) {
node.innerHTML = "<div style='padding:20px;color:red'>Invalid JSON: " + e.message + "</div>";
}
});
});
}
cb();
});
app.bind("destroy", function () {});
app.bind("init", function () {
app.render(function (err) {
if (err) return ready.reject(err);
ready.resolve(app);
});
return app;
});
return app;
};
}).toString() + ')());');
var blob = new Blob([code], { type: "application/javascript" });
var blobUrl = URL.createObjectURL(blob);
socket.fire("webapp::instance::request", "apps:register", {
"json-viewer": {
path: blobUrl,
name: "JSON Viewer",
icon: "breeze/icons/mimetypes/64/application-json",
comment: "Pretty-print JSON files",
mimetypes: ["application/json"],
args: ["%U"]
}
});<button onclick="window._caDemo3()">Try: Register JSON Viewer + Create config.json</button>
Write the app code to in-memory storage first, then register:
var appCode = ';((' + (function () {
module.exports = function () {
var app = new ApplicationPrototype();
var node = document.createElement("div");
var win = null;
var ready = new Application.Promise();
app.bind("node", function () { return node; }, "");
app.bind("window", function () { return win; }, "");
app.bind("ready", function () { return ready; });
app.bind("handleWindow", function (w) {
win = w;
w.emit("api-request::attached", []);
w.on("api-request::ready", function (cb) { cb(ready); });
});
app.bind("render", function (cb) {
win.title("Markdown Preview");
win.height(500);
win.width(700);
var args = app.window().env().args || [];
if (args[0]) {
Application.require("/files/api/modules/fs.js").then(function (fsModule) {
fsModule().read(args[0], null, true).response("text").then(function (text) {
// simple markdown: headers, bold, code
var html = text
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>")
.replace(/`(.+?)`/g, "<code>$1</code>")
.replace(/\n/g, "<br>");
node.innerHTML = "<div style='padding:20px'>" + html + "</div>";
});
});
}
cb();
});
app.bind("destroy", function () {});
app.bind("init", function () {
app.render(function (err) {
if (err) return ready.reject(err);
ready.resolve(app);
});
return app;
});
return app;
};
}).toString() + ')());';
// Write to input:// storage
socket.fire("webapp::instance::request", "fs:mkdirp", "input://apps/", function () {
socket.fire("webapp::instance::request", "fs:write", "input://apps/md-preview.js", appCode, function () {
// Register
socket.fire("webapp::instance::request", "apps:register", {
"md-preview": {
path: "input://apps/md-preview.js",
name: "Markdown Preview",
icon: "breeze/icons/mimetypes/16/text-x-markdown",
comment: "Simple markdown renderer",
mimetypes: ["text/markdown", "text/x-markdown"],
args: ["%U"]
}
});
});
});<button onclick="window._caDemo4()">Try: Register Markdown Preview + Create README.md</button>
The data:application/javascript;base64,... pattern lets you inline an entire app without any external files:
'data:application/javascript;base64,' + btoa(';((' + (function () {
module.exports = function () {
// your app code here
};
}).toString() + ')());')How it works:
(function () { ... }).toString() — converts your function to a string;(( ... )()); — wraps it as an IIFE that executes immediately when loadedbtoa(...) — base64-encodes the JavaScript codedata:application/javascript;base64,... — creates a valid data URI that Application.require() can load as a moduleThe IIFE wrapper is needed because Application.require() wraps the loaded code in a module scope. The module.exports = function () { ... } is what the window manager calls to instantiate your app.
| Method | Required | Description |
|---|---|---|
node() | Yes | Returns the root DOM element |
handleWindow(win) | Yes | Receives the window, must emit api-request::attached |
window() | Yes | Returns the window reference |
ready() | Yes | Returns a Promise that resolves when ready |
render(cb) | Yes | Build UI, call cb() when done |
init() | Yes | Trigger render, return self |
destroy() | Yes | Cleanup on close |
Your app code runs inside the SGApps environment with access to:
ApplicationPrototype — global, for creating event-driven objectsApplication.require(module) — load any moduleApplication.icon(name) — resolve icon pathsApplication.translate(key, default) — i18n_() — DOM builder (from extensions/prototype, available globally)app.window().env().args — arguments passed to your appapp.window().title(str) / icon(str) / height(n) / width(n) — window controlspath must start with https:, http:, data:, blob:, or input: — other protocols are rejecteddisableDefaultApps: true to also remove built-in appsApplication.require() to load any module