# 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`: ```js 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 = '

Hello from custom app!

Args: ' + args.join(', ') + '

'; 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 | `{ appName: { path, name, icon, comment?, mimetypes?, args? } }` | | `callback` | function | `(err, registeredNames: string[])` | The `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 | 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. ```js 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. --- ## Example 1: Inline App via Data URI The simplest way — embed the entire app code as a base64 data URI. No server needed. ```js 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. --- ## Example 2: App from External URL Host your app on your own server and register it: ```js 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. --- ## Example 3: App from Blob URL Create the app dynamically in JavaScript and register it via a blob URL: ```js 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 = "
" +
                                JSON.stringify(json, null, 2).replace(/";
                        } catch (e) {
                            node.innerHTML = "
Invalid JSON: " + e.message + "
"; } }); }); } 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"] } }); ``` --- ## Example 4: App from input:// Storage Write the app code to in-memory storage first, then register: ```js 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, "

$1

") .replace(/^## (.+)$/gm, "

$1

") .replace(/^# (.+)$/gm, "

$1

") .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/`(.+?)`/g, "$1") .replace(/\n/g, "
"); node.innerHTML = "
" + html + "
"; }); }); } 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"] } }); }); }); ``` --- ## The Data URI Pattern The `data:application/javascript;base64,...` pattern lets you inline an entire app without any external files: ```js 'data:application/javascript;base64,' + btoa(';((' + (function () { module.exports = function () { // your app code here }; }).toString() + ')());') ``` How it works: 1. `(function () { ... }).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 () { ... }` 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