Building Custom Apps

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.

How It Works

  1. Register your app via the apps:register API event
  2. The app gets added to the window manager's application library
  3. When a user opens a file matching your app's MIME types, your app launches
  4. Your app's module is loaded via Application.require(path) — the path can be an HTTPS URL, blob URL, or data URI

App Module Format

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

Registration

apps:register -- Register Custom Apps

Arg Type Description
appsMap object &#x7B; appName: &#x7B; path, name, icon, comment&#x3F;, mimetypes&#x3F;, args&#x3F; &#x7D; &#x7D;
callback function (err, registeredNames: string[])

The path field can be:
Path Type Example Description
HTTPS URL &#x22;https://example.com/my-app.js&#x22; Loaded from your server
Blob URL &#x22;blob:https://...&#x22; Created via URL.createObjectURL()
Data URI &#x22;data:application/javascript;base64,...&#x22; Inline JavaScript encoded in base64
input:// &#x22;input://apps/my-app.js&#x22; 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

By default, only API-injected apps can be removed. Pass true as the third argument to also remove built-in apps — useful for replacing default apps with custom implementations.

apps:list -- List All Registered Apps

Returns 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)");
    });
});

Live Demo

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>


    Example 1: Inline App via Data URI

    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>


    Example 2: App from External URL

    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 () &#x7B; ... &#x7D; format.


    Example 3: App from Blob URL

    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>


    Example 4: App from input:// Storage

    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 URI Pattern

    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:

    1. (function () &#x7B; ... &#x7D;).toString() — converts your function to a string
    2. ;(( ... )()); — wraps it as an IIFE that executes immediately when loaded
    3. btoa(...) — base64-encodes the JavaScript code
    4. data:application/javascript;base64,... — creates a valid data URI that Application.require() can load as a module

    The IIFE wrapper is needed because Application.require() wraps the loaded code in a module scope. The module.exports = function () &#x7B; ... &#x7D; is what the window manager calls to instantiate your app.

    App Interface Requirements

    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

    Available Inside Your App

    Your app code runs inside the SGApps environment with access to:

    • ApplicationPrototype — global, for creating event-driven objects
    • Application.require(module) — load any module
    • Application.icon(name) — resolve icon paths
    • Application.translate(key, default) — i18n
    • _() — DOM builder (from extensions/prototype, available globally)
    • app.window().env().args — arguments passed to your app
    • app.window().title(str) / icon(str) / height(n) / width(n) — window controls

    Security Notes

    • App path must start with https:, http:, data:, blob:, or input: — other protocols are rejected
    • By default, only API-injected apps can be unregistered. Pass disableDefaultApps: true to also remove built-in apps
    • Built-in apps cannot be removed or overwritten via the API
    • Apps have the same access as any code running in the iframe — they can use Application.require() to load any module
    sgapps.io