# File Manager

Full-featured file browser with dual-pane layout, upload, clipboard operations, view modes, and a complete filesystem API for programmatic control.

## Live Demo

<iframe id="fm-demo" src="/online/webapp/file-manager" width="100%" height="500" frameborder="0" style="border:1px solid #ccc; border-radius:4px;"></iframe>

<script>window._wsConnect('fm-demo', 'fmSocket');</script>

## Embed

```html
<iframe src="https://sgapps.io/online/webapp/file-manager"
    width="100%" height="600" frameborder="0"></iframe>
```

## Security

External API filesystem operations are **sandboxed** to `input://` only. Any attempt to read, write, navigate, or delete outside this path via the API returns an access denied error.

- `input://` -- sandboxed area for API operations (no auth, browser-memory-only)
- The user can still browse other paths via the UI (sidebar, breadcrumbs) — only the **programmatic API** is restricted
- `input://` is browser-memory-only: isolated per tab, destroyed on refresh, never touches the server
- Server paths (`/home/user/`) require authentication and are not accessible via the API

---

## Filesystem Events Reference

These events let you create, read, write, delete, rename files and folders programmatically — perfect for building custom UIs on top of the embedded file manager.

---

### `fs:cwd` -- Get or Set Current Directory

Navigate the file manager to a different directory, or get the current path.

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string (optional) | Directory path to navigate to |
| `callback` | function | `(err, currentPath: string)` |

```js
// Navigate to input://
socket.fire("webapp::instance::request", "fs:cwd", "input://", function (err, cwd) {
    console.log("Now at:", cwd);
});

// Get current directory
socket.fire("webapp::instance::request", "fs:cwd", function (err, cwd) {
    console.log("Current:", cwd);
});
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:cwd','input://',function(e,p){if(e)alert('Error: '+e);else console.log('Navigated to:',p)})">Try: Navigate to input://</button>
<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:cwd',function(e,p){alert('Current path: '+p)})">Try: Get Current Path</button>

---

### `fs:mkdirp` -- Create Directory (with parents)

Creates a directory and any missing parent directories. Works like `mkdir -p`.

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | Directory path to create |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "fs:mkdirp", "input://my-project/src/components/", function (err) {
    console.log(err || "Directories created");
});
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/src/components/',function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:cwd','input://demo-project/');alert('Created input://demo-project/src/components/')}else alert('Error: '+e)})">Try: Create demo-project/src/components/</button>

---

### `fs:write` -- Write a File

Creates or overwrites a file with the given content.

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | File path |
| `content` | string | File content |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "fs:write",
    "input://demo-project/README.md",
    "# My Project\n\nThis is a demo project created via the API.",
    function (err) { console.log(err || "File written"); }
);
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/',function(){window.fmSocket.fire('webapp::instance::request','fs:write','input://demo-project/README.md','# Demo Project\n\nCreated via the File Manager API.\n\n## Features\n- In-memory storage\n- No server required\n- Destroyed on refresh',function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:cwd','input://demo-project/');alert('README.md created!')}else alert('Error: '+e)})})">Try: Create README.md</button>
<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:write','input://demo-project/src/index.js','console.log(&apos;Hello from the API!&apos;);\n\nfunction add(a, b) {\n    return a + b;\n}\n\nmodule.exports = { add };',function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:refresh');alert('index.js created!')}else alert('Error: '+e)})">Try: Create src/index.js</button>

---

### `fs:read` -- Read a File

Reads the content of a file as text.

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | File path |
| `callback` | function | `(err, content: string)` |

```js
socket.fire("webapp::instance::request", "fs:read", "input://demo-project/README.md",
    function (err, content) {
        console.log("Content:", content);
    }
);
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:read','input://demo-project/README.md',function(e,c){if(c)alert('File content:\n\n'+c);else alert('Error: '+(e||'file not found'))})">Try: Read README.md</button>

---

### `fs:ls` -- List Directory Contents

Returns an array of files and folders in the specified directory.

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | Directory path |
| `callback` | function | `(err, files: Array<{filename, length, metadata}>)` |

```js
socket.fire("webapp::instance::request", "fs:ls", "input://demo-project/",
    function (err, files) {
        files.forEach(function (f) {
            console.log(f.metadata._isDirectory ? "[DIR]" : "[FILE]", f.filename, f.length + " bytes");
        });
    }
);
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:ls','input://demo-project/',function(e,f){if(f){var msg=f.map(function(x){return (x.metadata._isDirectory?'[DIR] ':'[FILE] ')+x.filename.replace(/^.*\/(?=[^\/]+\/?$)/,'')}).join('\n');alert('Contents:\n\n'+msg)}else alert('Error: '+(e||'not found'))})">Try: List demo-project/</button>

---

### `fs:mv` -- Move or Rename

Moves or renames a file or directory within the same protocol.

| Arg | Type | Description |
|-----|------|-------------|
| `src` | string | Source path |
| `dst` | string | Destination path |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "fs:mv",
    "input://demo-project/README.md",
    "input://demo-project/ABOUT.md",
    function (err) { console.log(err || "Renamed"); }
);
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:mv','input://demo-project/README.md','input://demo-project/ABOUT.md',function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:refresh');alert('Renamed README.md to ABOUT.md')}else alert('Error: '+e)})">Try: Rename README.md to ABOUT.md</button>

---

### `fs:remove` -- Delete a File

Removes a single file or empty directory. Lightweight, no UI.

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | Path to remove |
| `options` | object (optional) | `{ autoConfirm: true }` — accepted for API consistency |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "fs:remove", "input://demo-project/ABOUT.md",
    function (err) { console.log(err || "Deleted"); }
);

// With options
socket.fire("webapp::instance::request", "fs:remove", "input://temp/file.txt",
    { autoConfirm: true },
    function (err) { console.log(err || "Deleted"); }
);
```

---

### `fs:copy` -- Copy a File

Copies a single file. Reads source as blob, writes to destination. Lightweight, no UI.

| Arg | Type | Description |
|-----|------|-------------|
| `src` | string | Source file path |
| `dst` | string | Destination file path |
| `options` | object (optional) | `{ autoConfirm: true }` — accepted for API consistency |
| `callback` | function | `(err)` |

```js
// Simple copy
socket.fire("webapp::instance::request", "fs:copy",
    "input://project/config.json",
    "input://backup/config.json",
    function (err) { console.log(err || "Copied"); }
);

// With options (accepted, currently same behavior)
socket.fire("webapp::instance::request", "fs:copy",
    "input://project/config.json",
    "input://backup/config.json",
    { autoConfirm: true },
    function (err) { console.log(err || "Copied"); }
);
```

---

### `fs:archive` -- Create Archive (no UI)

Creates an archive from a directory using Packer. No window, no progress UI — runs silently in background.

| Arg | Type | Description |
|-----|------|-------------|
| `sourcePath` | string | Directory to archive |
| `outputPath` | string or null | Where to save (null = returns blob URL in callback) |
| `format` | string | `"zip"`, `"tar"`, `"tar.gz"`, `"7z"` (default: `"zip"`) |
| `options` | object (optional) | `{ autoConfirm: true }` — accepted for API consistency |
| `callback` | function | `(err, result: { size, path?, url? })` |

```js
// Archive and save to filesystem
socket.fire("webapp::instance::request", "fs:archive",
    "input://project/", "input://project.zip", "zip",
    function (err, result) {
        console.log("Archive:", result.size, "bytes");
    }
);

// Archive without saving (get blob URL for download)
socket.fire("webapp::instance::request", "fs:archive",
    "input://project/", null, "tar.gz",
    function (err, result) {
        console.log("Blob URL:", result.url);
    }
);
```

---

### Bulk Operations (with UI)

These open a bin app window with progress bars, error handling, and conflict resolution. Use for batch operations or when you want the user to see progress.

#### `fs:bulkCopy`

| Arg | Type | Description |
|-----|------|-------------|
| `sources` | string or Array | Source path(s) |
| `destination` | string | Destination directory |
| `options` | object (optional) | `{ autoConfirm: true }` to auto-resolve conflicts |
| `callback` | function | `(err)` — fires when bin/cp window closes |

```js
socket.fire("webapp::instance::request", "fs:bulkCopy",
    ["input://src/file1.txt", "input://src/file2.txt"],
    "input://backup/",
    { autoConfirm: true },
    function (err) { console.log(err || "Bulk copy done"); }
);
```

#### `fs:bulkRemove`

| Arg | Type | Description |
|-----|------|-------------|
| `paths` | string or Array | Path(s) to delete |
| `options` | object (optional) | `{ autoConfirm: true }` to skip confirmation |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "fs:bulkRemove",
    ["input://old-dir/", "input://temp.txt"],
    { autoConfirm: true },
    function (err) { console.log(err || "Bulk delete done"); }
);
```

#### `fs:bulkArchive`

| Arg | Type | Description |
|-----|------|-------------|
| `sources` | string or Array | Path(s) to archive |
| `outputPath` | string or null | Where to save (null = download) |
| `format` | string | `"zip"`, `"tar"`, `"tar.gz"`, `"7z"` |
| `options` | object (optional) | `{ autoConfirm: true }` to skip format selection |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "fs:bulkArchive",
    "input://project/", "input://project.zip", "zip",
    { autoConfirm: true },
    function (err) { console.log(err || "Bulk archive done"); }
);
```

> **When to use which:** Use `fs:copy`, `fs:remove`, `fs:archive` for single-file operations or scripted pipelines. Use `fs:bulkCopy`, `fs:bulkRemove`, `fs:bulkArchive` when you want the user to see progress, resolve conflicts, or when operating on many files at once.

---

### `fs:exists` -- Check if Path Exists

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | Path to check |
| `callback` | function | `(err, exists: boolean)` |

```js
socket.fire("webapp::instance::request", "fs:exists", "input://demo-project/",
    function (err, exists) { console.log("Exists:", exists); }
);
```

---

### `fs:stats` -- Get File/Directory Info

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | Path to inspect |
| `callback` | function | `(err, stats: object)` |

---

### `fs:readStreamUrl` -- Get Streaming URL

Returns a URL that can be used in `<img>`, `<video>`, `<a href>` etc. For `input://` files, this returns a `blob:` URL.

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | File path |
| `callback` | function | `(err, url: string)` |

---

### `fs:refresh` -- Refresh View

Refreshes the file manager's current directory listing.

```js
socket.fire("webapp::instance::request", "fs:refresh");
```

---

### `gui:sidebar:toggle` -- Show or Hide Sidebar

Toggle the sidebar panel visibility. Pass `false` to hide, `true` to show.

| Arg | Type | Description |
|-----|------|-------------|
| `visible` | boolean | `true` to show, `false` to hide |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "gui:sidebar:toggle", false); // hide
socket.fire("webapp::instance::request", "gui:sidebar:toggle", true);  // show
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:sidebar:toggle',false)">Try: Hide Sidebar</button>
<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:sidebar:toggle',true)">Try: Show Sidebar</button>

---

### `gui:specialPaths:add` -- Add Custom Path to Sidebar

Adds a named bookmark to the path bar / sidebar. Useful for creating labeled shortcuts to directories within your sandbox. These entries are tagged as `__apiManaged` and can be removed later.

| Arg | Type | Description |
|-----|------|-------------|
| `entry` | object | `{ path, name, icon?, description?, paths?, bookmark? }` |
| `callback` | function | `(err)` |

#### Icon Format

The `icon` field supports three formats:

| Format | Example | Description |
|--------|---------|-------------|
| Icon library name | `"breeze/icons/places/64/folder-blue"` | Resolved via `Application.icon()` to `/scripts/modules/icons/...` |
| Absolute URL | `"https://example.com/icon.png"` | Used as-is (also `http://`, `//`, `data:`) |
| Wildcard match | `"paper*folder"` | `*` matches any path segment in the icon library |

```js
socket.fire("webapp::instance::request", "gui:specialPaths:add", {
    path: "input://my-project/",
    name: "My Project",
    icon: "breeze/icons/places/64/folder-blue",        // icon library name
    description: "Project files",
    bookmark: {
        category: "personal",
        name: "My Project",
        icon: "breeze/icons/places/64/folder-blue"
    }
}, function (err) { console.log(err || "Path added"); });

// Using an external icon URL
socket.fire("webapp::instance::request", "gui:specialPaths:add", {
    path: "input://uploads/",
    name: "Uploads",
    icon: "https://example.com/my-custom-icon.svg",    // absolute URL
    bookmark: { category: "personal", name: "Uploads", icon: "https://example.com/my-custom-icon.svg" }
});
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:specialPaths:add',{path:'input://demo-project/',name:'Demo Project',description:'Files created via API',bookmark:{category:'personal',name:'Demo Project'}},function(e){alert(e||'Special path added! Check the sidebar.')})">Try: Add "Demo Project" to Sidebar</button>

### `gui:specialPaths:remove` -- Remove Custom Path

Removes a previously added API-managed special path by its path string.

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | The path that was added |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "gui:specialPaths:remove",
    "input://my-project/",
    function (err) { console.log(err || "Removed"); }
);
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:specialPaths:remove','input://demo-project/',function(e){alert(e||'Special path removed')})">Try: Remove "Demo Project" from Sidebar</button>

---

### `gui:specialPaths:clear` -- Clear All Sidebar Bookmarks

Removes all existing bookmarks from the sidebar (both built-in and API-managed). Useful when you want to set up a clean custom sidebar for your embedded widget.

```js
// Clear everything, then add your own bookmarks
socket.fire("webapp::instance::request", "gui:specialPaths:clear", function () {
    socket.fire("webapp::instance::request", "gui:specialPaths:add", {
        path: "input://my-app/",
        name: "My App Files",
        icon: "breeze/icons/places/64/folder-blue",
        bookmark: { category: "project", name: "My App Files", icon: "breeze/icons/places/64/folder-blue" }
    });
});
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:specialPaths:clear',function(e){alert(e||'Sidebar cleared!')})">Try: Clear All Sidebar Bookmarks</button>

---

### Path Bar Breadcrumb Grouping

Special paths can include a `paths` array with sub-rules that group multiple directory segments into a single named breadcrumb. This is how the navigation bar shows "My Project" instead of `input:// / demo-project /`.

Each sub-rule has:
- `rule` -- regex pattern matching the remaining path after the root
- `name` -- display name (supports `{param-1}`, `{param-2}`, etc. from regex capture groups)
- `icon` -- optional icon for this breadcrumb segment

```js
socket.fire("webapp::instance::request", "gui:specialPaths:add", {
    path: "input://demo-project/",
    name: "Demo Project",
    icon: "/scripts/modules/icons/breeze/icons/places/64/folder-red.svg",
    // Sub-path grouping rules for the breadcrumb bar
    paths: [
        {
            rule: "src\\/",
            name: "Source Code",
            icon: "/scripts/modules/icons/breeze/icons/places/64/folder-green.svg"
        },
        {
            rule: "test\\/",
            name: "Tests"
        },
        {
            rule: "docs\\/",
            name: "Documentation"
        }
    ],
    bookmark: {
        category: "personal",
        name: "Demo Project",
        icon: "breeze/icons/places/64/folder-blue"
    }
});
```

With this configuration, the breadcrumb bar shows:

- `input://demo-project/` -> **Demo Project**
- `input://demo-project/src/` -> **Demo Project** / **Source Code**
- `input://demo-project/src/components/` -> **Demo Project** / **Source Code** / `components`
- `input://demo-project/test/` -> **Demo Project** / **Tests**

Without the `paths` rules, it would show: **Demo Project** / `src` / `components` (each dir is a separate breadcrumb).

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:specialPaths:clear',function(){window.fmSocket.fire('webapp::instance::request','gui:specialPaths:add',{path:'input://demo-project/',name:'Demo Project',icon:'/scripts/modules/icons/breeze/icons/places/64/folder-red.svg',paths:[{rule:'src\\/',name:'Source Code', icon:'/scripts/modules/icons/breeze/icons/places/64/folder-green.svg'},{rule:'test\\/',name:'Tests'},{rule:'docs\\/',name:'Documentation'}],bookmark:{category:'project',name:'Demo Project',icon:'breeze/icons/places/64/folder-blue'}},function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/src/components/',function(){window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/test/',function(){window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/docs/',function(){window.fmSocket.fire('webapp::instance::request','fs:cwd','input://demo-project/');alert('Project created with grouped breadcrumbs! Navigate to src/, test/, docs/ to see the named breadcrumbs.')})})})}})})">Try: Create Project with Grouped Breadcrumbs</button>

---

## Custom App Injection

Register your own applications that open files by MIME type. Apps can be loaded from URLs, blob URLs, or inline data URIs. See the full guide: **[Building Custom Apps](/building-custom-apps.md)**.

### `apps:register` -- Register Custom Apps

```js
socket.fire("webapp::instance::request", "apps:register", {
    "my-viewer": {
        path: "https://example.com/apps/viewer.js",
        name: "My Viewer",
        icon: "paper/icons/apps/accessories-text-editor",
        comment: "Custom text viewer",
        mimetypes: ["text/plain", "text/csv"],
        args: ["%U"]
    }
}, function (err, registered) {
    console.log("Registered:", registered);
});
```

### Inline App (Data URI)

```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("Hello App");
            win.height(300); win.width(400);
            node.innerHTML = "<div style='padding:20px;text-align:center'>" +
                "<h2>Hello!</h2><p>Args: " + (app.window().env().args || []).join(", ") + "</p></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() + ')());';

socket.fire("webapp::instance::request", "apps:register", {
    "hello-app": {
        path: "data:application/javascript;base64," + btoa(appCode),
        name: "Hello App",
        icon: "paper/icons/apps/system-software-install",
        mimetypes: [],
        args: []
    }
});
```

### `apps:unregister` -- Remove a Registered App

| Arg | Type | Description |
|-----|------|-------------|
| `appName` | string | Name of the app to remove |
| `callback` | function | `(err)` |
| `disableDefaultApps` | boolean | If `true`, allows removing built-in apps too (not just API-injected ones) |

By default, only apps registered via the API (`__apiInjected: true`) can be unregistered. Pass `true` as the third argument to also remove built-in apps like `code-editor`, `photo-editor`, etc. This is useful when you want to fully customize the app environment.

```js
// Remove a custom app
socket.fire("webapp::instance::request", "apps:unregister", "my-viewer", function (err) {
    console.log(err || "Removed");
});

// Remove a built-in app (e.g., disable the default code editor)
socket.fire("webapp::instance::request", "apps:unregister", "code-editor", function (err) {
    console.log(err || "Removed");
}, true);

// Replace a built-in app with your own
socket.fire("webapp::instance::request", "apps:unregister", "code-editor", function () {
    socket.fire("webapp::instance::request", "apps:register", {
        "code-editor": {
            path: "input://apps/my-editor.js",
            name: "My Code Editor",
            icon: "paper/icons/apps/accessories-text-editor",
            mimetypes: ["text/plain", "application/x-javascript", "text/html", "text/css"],
            args: ["%U"]
        }
    });
}, true);
```

### `apps:list` -- List All Registered Apps

```js
socket.fire("webapp::instance::request", "apps:list", function (err, apps) {
    Object.keys(apps).forEach(function (name) {
        var app = apps[name];
        console.log(name, app.__apiInjected ? "(custom)" : "(built-in)", app.name);
    });
});
```

---

## Shortcuts (.lnk files)

Create clickable shortcut files that launch any app with predefined arguments. Double-clicking a `.lnk` file reads its JSON content and opens the specified app.

### `fs:createShortcut` -- Create a Shortcut

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | File path for the shortcut (must end in `.lnk`, must be `input://`) |
| `config` | object | `{ app, args?, name?, icon? }` |
| `callback` | function | `(err)` |

The `config` fields:

| Field | Type | Description |
|-------|------|-------------|
| `app` | string | App name from the registry (e.g., `"photo-editor"`, `"code-editor"`, or a custom app name) |
| `args` | array | Arguments to pass to the app (optional, default `[]`) |
| `name` | string | Display name (optional) |
| `icon` | string | Icon name (optional) |

```js
// Create a shortcut to the Photo Editor
socket.fire("webapp::instance::request", "fs:createShortcut",
    "input://desktop/Photo Editor.lnk",
    {
        app: "photo-editor",
        args: [],
        name: "Photo Editor",
        icon: "paper/*/multimedia-photo-viewer.png"
    },
    function (err) { console.log(err || "Shortcut created"); }
);

// Create a shortcut that opens a specific file in Code Editor
socket.fire("webapp::instance::request", "fs:createShortcut",
    "input://desktop/Edit Config.lnk",
    {
        app: "code-editor",
        args: ["input://project/config.json"],
        name: "Edit Config"
    }
);

// Create a shortcut to a custom registered app
socket.fire("webapp::instance::request", "fs:createShortcut",
    "input://desktop/View Logs.lnk",
    {
        app: "log-viewer",
        args: ["input://logs/server.log"],
        name: "Server Logs"
    }
);
```

### How It Works

1. `fs:createShortcut` writes a JSON file at the given path:
   ```json
   {
       "__shortcut": true,
       "app": "photo-editor",
       "args": [],
       "name": "Photo Editor",
       "icon": "paper/*/multimedia-photo-viewer.png"
   }
   ```

2. A built-in **shortcut runner** app is auto-registered for `.lnk` files via `file-handlers`

3. When you double-click the `.lnk` file, xdg-open matches it to the shortcut runner, which:
   - Reads the JSON content
   - Extracts `app` and `args`
   - Calls `windowManager.open({ run: [app].concat(args) })`
   - Closes itself

### Live Demo

<button onclick="window._ws('fmSocket')&&(function(s){s.fire('webapp::instance::request','fs:mkdirp','input://shortcuts/',function(){s.fire('webapp::instance::request','fs:createShortcut','input://shortcuts/Open Photo Editor.lnk',{app:'photo-editor',args:[],name:'Photo Editor',icon:'paper/*/multimedia-photo-viewer.png'},function(){s.fire('webapp::instance::request','fs:createShortcut','input://shortcuts/Open Code Editor.lnk',{app:'code-editor',args:[],name:'Code Editor',icon:'breeze*book-edit'},function(){s.fire('webapp::instance::request','fs:createShortcut','input://shortcuts/Open Recorder.lnk',{app:'recorder',args:[],name:'Recorder',icon:'breeze/icons/actions/16/media-record'},function(){s.fire('webapp::instance::request','fs:cwd','input://shortcuts/',function(){alert('Shortcuts created! Double-click any .lnk file to launch the app.')})})})})})})(window.fmSocket)">Try: Create App Shortcuts</button>

This creates 3 shortcuts in `input://shortcuts/`:
- **Open Photo Editor.lnk** — launches the Photo Editor
- **Open Code Editor.lnk** — launches the Code Editor  
- **Open Recorder.lnk** — launches the Recorder

Double-click any of them to launch the corresponding app in a new window.

### `gui:setHomePath` -- Set Default Start Directory

Sets the starting directory when the file manager opens or when navigating "home".

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string | Directory path (must be `input://`) |
| `callback` | function | `(err)` |

```js
socket.fire("webapp::instance::request", "gui:setHomePath", "input://my-workspace/");
```

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:setHomePath','input://shortcuts/',function(e){alert(e||'Home path set to input://shortcuts/')})">Try: Set Home to input://shortcuts/</button>

---

## Virtual File Icons (`viewport:addFiles`)

Inject custom clickable icons into the file manager's current view. These are purely visual — they don't create actual files in the filesystem. Each icon can launch an app, open a URL, or show a custom context menu.

### `viewport:addFiles` -- Add Virtual Icons to Current View

| Arg | Type | Description |
|-----|------|-------------|
| `path` | string (optional) | Directory path to show the icons in (defaults to current directory) |
| `files` | Array | Array of virtual file definitions |
| `callback` | function | `(err, count)` |

Each file definition:

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Display name (required) |
| `icon` | string | Icon name or URL |
| `title` | string | Tooltip text |
| `app` | string | App to launch on double-click |
| `args` | array | Arguments for the app |
| `url` | string | URL to open on double-click (alternative to `app`) |
| `contextmenu` | array | Custom right-click menu items: `[{ text, icon, app?, args?, url? }]` |

```js
// Add app launcher icons to the current directory
socket.fire("webapp::instance::request", "viewport:addFiles", [
    {
        name: "Open Photo Editor",
        icon: "paper/*/multimedia-photo-viewer.png",
        title: "Launch the Photo Editor",
        app: "photo-editor",
        args: []
    },
    {
        name: "Visit SGApps.IO",
        icon: "paper/icons/apps/internet-web-browser",
        title: "Open website in new tab",
        url: "https://sgapps.io"
    },
    {
        name: "Project Tools",
        icon: "breeze/icons/categories/32/applications-development",
        title: "Development tools",
        app: "code-editor",
        contextmenu: [
            { text: "Open Code Editor", icon: "breeze*book-edit", app: "code-editor" },
            { text: "Open Photo Editor", icon: "paper*multimedia-photo-viewer", app: "photo-editor" },
            { text: "Open Recorder", icon: "breeze/icons/actions/16/media-record", app: "recorder" }
        ]
    }
]);

// Add icons to a specific path
socket.fire("webapp::instance::request", "viewport:addFiles",
    "input://my-folder/",
    [{ name: "My App", icon: "paper/icons/apps/system-software-install", app: "my-custom-app" }]
);
```

Virtual icons are:
- **Per-path** — each directory can have its own set of virtual icons
- **Persistent per session** — icons reappear when navigating back to the directory
- **Non-destructive** — they don't create actual files, just visual entries
- **Replaced by name** — adding a file with the same name overwrites the previous one

<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://desktop/',function(){window.fmSocket.fire('webapp::instance::request','viewport:addFiles','input://desktop/',[{name:'Photo Editor',icon:'paper/*/multimedia-photo-viewer.png',title:'Launch Photo Editor',app:'photo-editor',args:[]},{name:'Code Editor',icon:'breeze*book-edit',title:'Launch Code Editor',app:'code-editor',args:[]},{name:'Recorder',icon:'breeze/icons/actions/16/media-record',title:'Launch Recorder',app:'recorder',args:[]},{name:'SGApps Website',icon:'paper/icons/apps/internet-web-browser',title:'Open sgapps.io',url:'https://sgapps.io'},{name:'Tools',icon:'breeze/icons/categories/32/applications-development',title:'Dev tools menu',app:'code-editor',contextmenu:[{text:'Code Editor',icon:'breeze*book-edit',app:'code-editor'},{text:'Photo Editor',icon:'paper*multimedia-photo-viewer',app:'photo-editor'},{text:'File Manager',icon:'breeze/icons/apps/48/system-file-manager',app:'file-manager'}]}],function(e,n){window.fmSocket.fire('webapp::instance::request','fs:cwd','input://desktop/');alert(e||('Added '+n+' virtual icons to desktop! Double-click any icon to launch the app. Right-click Tools for a menu.'))})})">Try: Add App Icons to Desktop</button>

---

## Complete Example: Build a Project Structure via API

This example creates a complete project with multiple files and directories using only the API:

```html
<!DOCTYPE html>
<html>
<head><title>File Manager API Demo</title></head>
<body>
    <div style="display:flex;gap:8px;padding:10px;flex-wrap:wrap;">
        <button id="create-project">Create Project</button>
        <button id="list-files">List Files</button>
        <button id="read-file">Read package.json</button>
    </div>

    <iframe id="fm" src="https://sgapps.io/online/webapp/file-manager"
        width="100%" height="500" frameborder="0"></iframe>

    <pre id="output" style="background:#f6f8fa;padding:10px;max-height:200px;overflow:auto;"></pre>

    <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 ready = false;
        socket.on("webapp::connection::ping", function () { ready = true; });

        function req(event) {
            var args = Array.prototype.slice.call(arguments, 1);
            return new Promise(function (resolve, reject) {
                args.push(function (err, result) {
                    if (err) reject(err); else resolve(result);
                });
                socket.fire.apply(socket, ["webapp::instance::request", event].concat(args));
            });
        }

        document.getElementById('create-project').onclick = async function () {
            if (!ready) return alert("Not ready yet");
            await req("fs:mkdirp", "input://my-app/src/");
            await req("fs:mkdirp", "input://my-app/test/");
            await req("fs:write", "input://my-app/package.json",
                JSON.stringify({ name: "my-app", version: "1.0.0", main: "src/index.js" }, null, 2));
            await req("fs:write", "input://my-app/src/index.js",
                "module.exports = function greet(name) {\n    return 'Hello, ' + name + '!';\n};");
            await req("fs:write", "input://my-app/test/test.js",
                "var greet = require('../src/index');\nconsole.log(greet('World'));");
            await req("fs:write", "input://my-app/README.md",
                "# my-app\n\nA demo project created via the File Manager API.");
            await req("fs:cwd", "input://my-app/");
            document.getElementById('output').textContent = "Project created!";
        };

        document.getElementById('list-files').onclick = async function () {
            if (!ready) return;
            var files = await req("fs:ls", "input://my-app/");
            document.getElementById('output').textContent = files.map(function (f) {
                return (f.metadata._isDirectory ? "[DIR] " : "[FILE] ") + f.filename;
            }).join("\n");
        };

        document.getElementById('read-file').onclick = async function () {
            if (!ready) return;
            var content = await req("fs:read", "input://my-app/package.json");
            document.getElementById('output').textContent = content;
        };
    </script>
</body>
</html>
```

## Features

- **Sidebar** with categorized bookmarks (Home, Public, Applications, SVFS connections)
- **View modes:** icon grid, detail list, large icons
- **File operations:** upload, download, rename, delete, copy, paste
- **Context menus** for all operations
- **Permission management** with chmod UI
- **Terminal integration** for SFTP directories
- **In-memory storage** (`input://`) for temporary files
- **Archive support** -- create ZIP/TAR/7Z from context menu
