# API Communication

Control embedded SGApps web applications from your parent page using the WindowSocket postMessage bridge.

## Setup

Include the WindowSocket library and create a connection to the iframe:

```html
<iframe id="app" src="https://sgapps.io/online/webapp/photo-editor"></iframe>

<script src="https://sgapps.io/components/window-socket/index.js"></script>
<script>
    var iframe = document.getElementById('app');
    var socket = new WindowSocket(iframe.contentWindow);
    socket.start();
</script>
```

## Sending Commands

Send commands to the app using `socket.fire()`:

```js
socket.fire("webapp::instance::request", eventName, arg1, arg2, ..., callback);
```

The `eventName` is mapped to `api-request::instance:{eventName}` inside the app. Each app defines its own set of API request events.

### Examples

```js
// Reset the photo editor canvas
socket.fire("webapp::instance::request", "reset", function (err) {
    console.log("Done");
});

// Get image data from photo editor
socket.fire("webapp::instance::request", "getDataBase64", "image/png", function (err, dataUrl) {
    // dataUrl is a base64 PNG
    document.getElementById("preview").src = dataUrl;
});

// Set editor content in code editor
socket.fire("webapp::instance::request", "active-file:codeEditor:value", "console.log('hello');", function () {
    console.log("Content set");
});
```

## Built-in Events

These events are available for all apps:

### `webapp::instance::embed-mode`

Toggle embed mode (hide/show window chrome):

```js
// Enable: hides title bar, maximizes app
socket.fire("webapp::instance::embed-mode", true, function (err, isEmbedded) {
    console.log("Embedded:", isEmbedded); // true
});

// Disable: restores window chrome
socket.fire("webapp::instance::embed-mode", false, function (err, isEmbedded) {
    console.log("Embedded:", isEmbedded); // false
});
```

### `webapp::instance::require-redraw`

Force a UI redraw (useful after container resize):

```js
socket.fire("webapp::instance::require-redraw");
```

### `webapp::connection::ping`

The app sends periodic ping messages to the parent after loading:

```js
socket.on("webapp::connection::ping", function (appName) {
    console.log("App connected:", appName);
});
```

### `webapp::instance::error`

Fires when the app fails to load:

```js
socket.on("webapp::instance::error", function (errorMessage) {
    console.error("App error:", errorMessage);
});
```

## App-Specific Events

Each app has its own set of `api-request::instance:*` events. When using `socket.fire("webapp::instance::request", ...)`, the first argument after the event string becomes the event name suffix.

### Photo Editor

| Event Name (via socket.fire) | Args | Description |
|---|---|---|
| `"reset"` | `callback` | Clear canvas |
| `"setDataBase64"` | `base64Url, callback` | Load image from base64 data URL |
| `"getDataBase64"` | `type, callback, quality` | Export canvas (e.g., `"image/png"`) |
| `"zoom"` | `level, callback` | Set zoom level |
| `"applyFilters"` | `filtersArray, callback` | Apply image filters |
| `"resize"` | `width, height, callback` | Resize image |
| `"crop"` | `x, y, w, h, callback` | Crop to rectangle |
| `"flipH"` | `callback` | Flip horizontally |
| `"flipW"` | `callback` | Flip vertically |
| `"rotateCW"` | `callback` | Rotate 90 clockwise |
| `"rotateCCW"` | `callback` | Rotate 90 counter-clockwise |
| `"history:undo"` | `callback` | Undo last action |
| `"history:redo"` | `callback` | Redo last undone action |
| `"history:save"` | `callback` | Save current state to history |
| `"getImageInfo"` | `callback` | Returns `{ width, height, layerCount, scale }` |
| `"loadFromURL"` | `url, callback` | Load image from URL directly |
| `"getScale"` | `callback` | Returns current zoom scale |
| `"fitToView"` | `callback` | Fit image to viewport and center |

```js
// Apply sepia + brightness filter chain
socket.fire("webapp::instance::request", "applyFilters", [
    { name: "sepia", params: { level: 0.8 } },
    { name: "brightness", params: { level: 0.2 } }
], function () {
    console.log("Filters applied");
});

// Export as JPEG at 80% quality
socket.fire("webapp::instance::request", "getDataBase64", "image/jpeg",
    function (err, dataUrl) {
        // upload dataUrl to your server
    },
    0.8
);
```

### SVG Editor

| Event Name | Args | Description |
|---|---|---|
| `"loadRasterImage"` | `base64, callback, options` | Import raster, auto-trace to SVG |
| `"loadSvgImage"` | `svgString, callback` | Load SVG source |
| `"generateRasterImage"` | `callback` | Export as PNG data URL |
| `"getSVGSource"` | `callback` | Get SVG source string |
| `"getSVGDynamicSource"` | `callback` | Get dynamic SVG source |
| `"getSVGAbsoluteSource"` | `callback` | Get SVG with absolute coords |

```js
// Get SVG source from the editor
socket.fire("webapp::instance::request", "getSVGSource", function (err, svgString) {
    console.log("SVG:", svgString);
});
```

### Code Editor

| Event Name | Args | Description |
|---|---|---|
| `"active-file"` | `callback` | Get active tab info |
| `"active-file:codeEditor:value"` | `value?, callback` | Get or set editor text |
| `"active-file:codeEditor:json:schema"` | `schema, callback` | Set JSON schema |
| `"active-file:codeEditor:syntax"` | `syntax?, callback` | Get or set language mode (e.g., `"javascript"`, `"html"`) |
| `"active-file:codeEditor:options"` | `options, callback` | Set Monaco editor options (readOnly, fontSize, wordWrap, etc.) |
| `"active-file:codeEditor:selection"` | `callback` | Returns `{ startLine, startCol, endLine, endCol, text }` |
| `"active-file:codeEditor:insertText"` | `text, position?, callback` | Insert text at position `{ line, col }` or at cursor |
| `"active-file:codeEditor:revealLine"` | `lineNumber, callback` | Scroll to center a specific line |
| `"active-file:save"` | `callback` | Save the active file |
| `"active-file:markers"` | `markers, callback` | Set error/warning markers on the editor |
| `"files:open"` | `fileConfig, callback` | Open a file in a new tab |
| `"files:close:all"` | `callback` | Close all tabs |
| `"files:list"` | `callback` | Returns array of open tabs `[{ index, name, path, type, title }]` |
| `"files:active:set"` | `tabIndex, callback` | Activate a tab by index |
| `"gui:sidebar:toggle"` | `visible, callback` | Show or hide the sidebar |

```js
// Set code content
socket.fire("webapp::instance::request",
    "active-file:codeEditor:value",
    "function hello() { return 'world'; }",
    function () { console.log("Set"); }
);

// Read code content
socket.fire("webapp::instance::request",
    "active-file:codeEditor:value",
    function (value) { console.log("Code:", value); }
);

// Change syntax to Python
socket.fire("webapp::instance::request", "active-file:codeEditor:syntax", "python");

// Make editor read-only
socket.fire("webapp::instance::request", "active-file:codeEditor:options", { readOnly: true });

// Insert text at line 5
socket.fire("webapp::instance::request", "active-file:codeEditor:insertText",
    "// inserted comment\n", { line: 5, col: 1 }
);

// Set error markers
socket.fire("webapp::instance::request", "active-file:markers", [
    { startLine: 10, startCol: 1, endLine: 10, endCol: 20, message: "Unused variable", severity: 4 }
]);

// Get current selection
socket.fire("webapp::instance::request", "active-file:codeEditor:selection",
    function (err, sel) { console.log("Selected:", sel.text); }
);
```

### Media Player

| Event Name | Args | Description |
|---|---|---|
| `"play"` | `callback` | Start playback |
| `"pause"` | `callback` | Pause playback |
| `"stop"` | `callback` | Pause and seek to start |
| `"seek"` | `timeSeconds, callback` | Seek to position |
| `"volume"` | `level?, callback` | Get or set volume (0-1) |
| `"mute"` | `muted?, callback` | Get or set muted state |
| `"getStatus"` | `callback` | Returns `{ paused, currentTime, duration, volume, muted, playbackRate, loop, readyState, videoWidth, videoHeight }` |
| `"source"` | `url, callback` | Set media source (URL or file path) |
| `"playbackRate"` | `rate?, callback` | Get or set playback speed |
| `"loop"` | `enabled?, callback` | Get or set loop mode |
| `"fullscreen"` | `callback` | Enter fullscreen mode |

```js
// Play a video from URL
socket.fire("webapp::instance::request", "source", "https://example.com/video.mp4");
socket.fire("webapp::instance::request", "play");

// Get playback status
socket.fire("webapp::instance::request", "getStatus", function (err, status) {
    console.log("Time:", status.currentTime, "/", status.duration);
});

// Seek to 30 seconds, set volume to 50%
socket.fire("webapp::instance::request", "seek", 30);
socket.fire("webapp::instance::request", "volume", 0.5);
```

### XTerm (Terminal)

| Event Name | Args | Description |
|---|---|---|
| `"write"` | `data, callback` | Write text to the terminal display |
| `"sendInput"` | `data, callback` | Send keystrokes to the shell |
| `"getSize"` | `callback` | Returns `{ cols, rows }` |
| `"resize"` | `cols, rows, callback` | Resize terminal |
| `"clear"` | `callback` | Clear terminal screen |
| `"reset"` | `callback` | Reset terminal state |
| `"scrollToBottom"` | `callback` | Scroll to bottom |
| `"focus"` | `callback` | Focus the terminal |

```js
// Send a command to the shell
socket.fire("webapp::instance::request", "sendInput", "ls -la\r");

// Write text directly to the display
socket.fire("webapp::instance::request", "write", "\x1b[32mHello from parent page!\x1b[0m\r\n");

// Get terminal size
socket.fire("webapp::instance::request", "getSize", function (err, size) {
    console.log("Terminal:", size.cols, "x", size.rows);
});
```

### File Manager & Archive Viewer

Both apps share the same filesystem API. All operations work on the `input://` in-memory storage without server access.

| Event Name | Args | Description |
|---|---|---|
| `"fs:cwd"` | `path?, callback` | Get or set current directory |
| `"fs:ls"` | `path, callback` | List directory contents |
| `"fs:read"` | `path, callback` | Read file as text |
| `"fs:write"` | `path, content, callback` | Create or overwrite a file |
| `"fs:mkdir"` | `path, callback` | Create a directory |
| `"fs:mkdirp"` | `path, callback` | Create directory with parents |
| `"fs:remove"` | `path, options?, callback` | Delete a single file or empty directory |
| `"fs:mv"` | `src, dst, callback` | Move or rename |
| `"fs:copy"` | `src, dst, options?, callback` | Copy a single file |
| `"fs:archive"` | `sourcePath, outputPath?, format?, options?, callback` | Create archive from directory (no UI) |
| `"fs:bulkCopy"` | `sources, destination, options?, callback` | Copy with progress UI (bin/cp) |
| `"fs:bulkRemove"` | `paths, options?, callback` | Delete with confirmation UI (bin/rm) |
| `"fs:bulkArchive"` | `sources, outputPath?, format?, options?, callback` | Archive with format selection UI (bin/archive) |
| `"fs:exists"` | `path, callback` | Check if path exists |
| `"fs:stats"` | `path, callback` | Get file/directory metadata |
| `"fs:readStreamUrl"` | `path, callback` | Get blob: URL for streaming |
| `"fs:readBinary"` | `path, callback` | Read file as base64 data URL |
| `"fs:refresh"` | `callback` | Refresh current view |
| `"fs:download"` | `path, filename?, callback` | Trigger browser download |
| `"gui:sidebar:toggle"` | `visible, callback` | Show or hide the sidebar |
| `"gui:specialPaths:add"` | `entry, callback` | Add a named bookmark to sidebar (supports `paths` array for breadcrumb grouping) |
| `"gui:specialPaths:remove"` | `path, callback` | Remove an API-managed bookmark |
| `"gui:specialPaths:clear"` | `callback` | Remove all sidebar bookmarks (built-in and API-managed) |
| `"apps:register"` | `appsMap, callback` | Register custom apps (see [Building Custom Apps](/building-custom-apps.md)) |
| `"apps:unregister"` | `appName, callback, disableDefaultApps?` | Remove an app. Pass `true` as 3rd arg to also remove built-in apps |
| `"apps:list"` | `callback` | List all registered apps |
| `"gui:setHomePath"` | `path, callback` | Set default start directory |
| `"fs:createShortcut"` | `path, config, callback` | Create a `.lnk` shortcut file that launches an app |
| `"viewport:addFiles"` | `path?, files, callback` | Inject virtual clickable icons into the file listing |

> **Sandbox:** All `fs:*` operations are restricted to the `input://` protocol (browser-only in-memory storage). Server paths are not accessible via the API.
> App paths must start with `https:`, `http:`, `data:`, `blob:`, or `input:`.

```js
// Create a project structure in memory
socket.fire("webapp::instance::request", "fs:mkdirp", "input://my-project/src/");
socket.fire("webapp::instance::request", "fs:write",
    "input://my-project/index.js", "console.log('hello');");
socket.fire("webapp::instance::request", "fs:cwd", "input://my-project/");

// List files
socket.fire("webapp::instance::request", "fs:ls", "input://my-project/",
    function (err, files) { console.log(files); });
```

### Writer

| Event Name | Args | Description |
|---|---|---|
| `"getHTML"` | `callback` | Get document as HTML |
| `"setHTML"` | `html, callback` | Set HTML content |
| `"getMarkdown"` | `callback` | Get as Markdown |
| `"setMarkdown"` | `md, callback` | Set Markdown content |
| `"getText"` | `callback` | Get plain text |
| `"clearContent"` | `callback` | Clear document |
| `"setMode"` | `mode, callback` | `"wysiwyg"`, `"markdown"`, `"source"` |
| `"setTheme"` | `theme, callback` | `"light"`, `"dark"`, `"google-docs"`, `"word"` |
| `"usePreset"` | `preset, callback` | `"google-docs"`, `"wysiwyg"`, `"minimal"` |
| `"setZoom"` | `level, callback` | Set zoom level |
| `"readOnly"` | `val?, callback` | Get/set read-only |
| `"insertBlock"` | `type, options?, callback` | Insert table, image, code, quote, etc. |
| `"find"` | `query, opts?, callback` | Find text |
| `"replace"` | `query, repl, opts?, callback` | Find and replace |
| `"undo"` / `"redo"` | `callback` | History navigation |
| `"export"` | `format?, callback` | Export as html/markdown/pdf/docx |
| `"download"` | `format?, filename?, callback` | Download as file |
| `"getWordCount"` | `callback` | Word/char/paragraph counts |
| `"isDirty"` | `callback` | Check unsaved changes |

### Spreadsheet

| Event Name | Args | Description |
|---|---|---|
| `"getValue"` | `ref, callback` | Get cell value |
| `"setValue"` | `ref, val, callback` | Set cell value |
| `"setFormula"` | `ref, expr, callback` | Set formula (e.g., `"=SUM(A1:A10)"`) |
| `"setCellStyle"` | `ref, styles, callback` | Style cell (bold, color, borders) |
| `"getSelection"` | `callback` | Get selected range |
| `"setSelection"` | `ref, callback` | Select range |
| `"addSheet"` / `"deleteSheet"` / `"renameSheet"` | varies | Sheet management |
| `"insertRow"` / `"insertCol"` / `"deleteRow"` / `"deleteCol"` | `at, count?, callback` | Row/column ops |
| `"sort"` | `range, options?, callback` | Sort data |
| `"zoom"` | `level?, callback` | Get/set zoom |
| `"readOnly"` | `val?, callback` | Get/set read-only |
| `"exportCSV"` | `sheet?, callback` | Export as CSV string |
| `"toJSON"` / `"fromJSON"` | varies | Full state export/import |
| `"undo"` / `"redo"` | `callback` | History navigation |

### GCode Editor

| Event Name | Args | Description |
|---|---|---|
| `"reset"` | -- | Clear and relaunch simulation |

## Complete Integration Example

```html
<!DOCTYPE html>
<html>
<head>
    <title>My Image Editor</title>
    <style>
        #editor-frame { width: 100%; height: 500px; border: 1px solid #ccc; }
        .toolbar { padding: 10px; display: flex; gap: 8px; }
    </style>
</head>
<body>
    <div class="toolbar">
        <button id="btn-sepia">Apply Sepia</button>
        <button id="btn-export">Export PNG</button>
        <button id="btn-embed">Toggle Embed Mode</button>
    </div>

    <iframe id="editor-frame"
        src="https://sgapps.io/online/webapp/photo-editor"
        allow="clipboard-write"
    ></iframe>

    <img id="preview" style="max-width: 300px; margin: 10px;">

    <script src="https://sgapps.io/components/application-prototype/ApplicationPrototype.js"></script>
    <script src="https://sgapps.io/components/window-socket/index.js"></script>
    <script>
        var socket = new WindowSocket();
        socket.start();

        var embedded = false;

        document.getElementById('btn-sepia').addEventListener('click', function () {
            socket.fire("webapp::instance::request", "applyFilters", [
                { name: "sepia", params: { level: 0.9 } }
            ], function () {
                console.log("Sepia applied");
            });
        });

        document.getElementById('btn-export').addEventListener('click', function () {
            socket.fire("webapp::instance::request", "getDataBase64", "image/png",
                function (err, dataUrl) {
                    document.getElementById('preview').src = dataUrl;
                }
            );
        });

        document.getElementById('btn-embed').addEventListener('click', function () {
            embedded = !embedded;
            socket.fire("webapp::instance::embed-mode", embedded, function (err, state) {
                console.log("Embed mode:", state);
            });
        });
    </script>
</body>
</html>
```

## WindowSocket Reference

| Method | Description |
|--------|-------------|
| `new WindowSocket(targetWindow)` | Create socket to a window/iframe |
| `socket.start()` | Begin listening for messages |
| `socket.fire(event, ...args, callback)` | Send event with optional callback |
| `socket.on(event, handler)` | Listen for events from the app |
| `socket.off(event, handler)` | Remove event listener |
