# Kiosk Extension Development

Extensions are optional, in-process Python modules that extend Kiosk's own UI
(menubar entries, source-context actions, file-context actions). Kiosk core runs
fully without any extensions. They are **not shipped in the installer** — users
install them on demand from the **Extension Manager** (App → Extensions →
Extension Manager…), which fetches the online catalog when it opens.

Each extension is a folder with a `manifest.json` and a Python entry point that
exposes a top-level `register(api)` function. See `video_converter/` for a
complete reference.

## `manifest.json`

```json
{
    "id": "my_extension",
    "name": "My Extension",
    "version": "1.0.0",
    "icon": "builtin:convert",
    "entry_point": "extension.py",
    "description": "What it does, in one line.",
    "author": "Your Name",
    "homepage": "https://example.com",
    "kiosk_min_version": "1.2.0",
    "hooks": ["menubar", "source_context", "file_context"]
}
```

| Field | Required | Notes |
|-------|----------|-------|
| `id` | yes | Simple identifier (`[A-Za-z0-9_][A-Za-z0-9_.-]*`). Also the install folder name. |
| `name` | yes | Display name. |
| `version` | yes | Dotted numeric (`1.0.0`). Drives the Manager's update detection. |
| `icon` | yes | See **Icons** below. |
| `entry_point` | yes | Python file with `register(api)`, relative to the folder. |
| `description`, `author`, `homepage` | no | Shown in the Manager. |
| `kiosk_min_version` | no | Extension is skipped (with a warning) on older Kiosk. |
| `hooks` | no | Advisory list: `menubar`, `source_context`, `file_context`. |

## Icons

The `icon` field resolves three ways, in order:

1. **Built-in core icon** — `"icon": "builtin:<name>"` references an icon Kiosk
   ships under `Source/Icons/extensions/`. These resolve **without downloading
   the extension**, so they render in the Manager's catalog *before* install.
   Use this if you don't want to draw your own icon. The `.png` suffix is
   optional (`builtin:convert` == `builtin:convert.png`).

   Available names: `convert`, `converter`, `video`, `light`, `texture`,
   `filter`, `database`, `cloud`, `tools`, `usd_viewer`, `puzzle`. (`_default`
   is the fallback and is used automatically.)

2. **Bundled icon** — any other value (e.g. `"icon": "icon.png"`) is a path
   relative to your extension folder, for custom branding. It only renders once
   the extension is installed (the catalog shows the default placeholder until
   then).

3. **Fallback** — if the icon can't be resolved, the extension still loads and
   uses the core `_default.png`. A missing icon is a warning, never a load
   failure.

## `register(api)`

`register` is called once at startup with the `KioskExtensionAPI` facade — the
only surface extensions should touch. Register UI hooks here:

```python
def register(api):
    api.register_menubar_entry("Do Thing…", on_do_thing)
    api.register_file_context_action("Process…", on_process, predicate=is_image)
    api.register_source_context_action("Batch…", on_batch)
```

Failures in `register()` are trapped and surfaced in the Extension Manager
footer — a broken extension never crashes Kiosk.

## The `api` object

The `api` passed to `register(api)` is a `KioskExtensionAPI` instance — the only
supported contract. Internal Kiosk objects are private on purpose, so the core
can change without breaking shipped extensions. It has four areas.

### Query — read library state

| Method | Returns |
|--------|---------|
| `get_categories()` | All category names, in display order. |
| `get_current_category()` | The category currently active in the UI. |
| `get_sources(category=None)` | List of source dicts for a category (current category if omitted). |
| `get_files(category=None, source=None)` | List of `file_info` dicts — one source, a whole category, or the current category. |
| `get_file_info(file_path)` | The `file_info` record for one file. |
| `get_selected_files()` | `file_info` dicts for the currently selected tiles. |
| `is_polyhaven_texture(file_info)` | `True` if the file is a Poly Haven texture set that isn't downloaded yet. |

A `file_info` is Kiosk's database record for a file — keys include `file_path`,
`file_name`, `base_name`, `extension`, `source_path`, `category`,
`thumbnail_path`, and grouping flags (`is_texture_set`, `is_image_sequence`, …).

### Tooling — bundled binaries & helpers

| Method | Returns / does |
|--------|----------------|
| `get_ffmpeg_path()` · `get_ffprobe_path()` | Absolute path to the bundled `ffmpeg` / `ffprobe`. |
| `get_oiio()` | The already-loaded OpenImageIO module. |
| `get_user_data_dir()` | `%APPDATA%\Kiosk`. Create a subfolder for your settings/presets — it survives updates. |
| `get_logger(suffix='')` | A logger namespaced to your extension (`kiosk.ext.<id>`). |
| `get_task_helpers()` | Dict of reusable helpers: image/video metadata and video thumbnail sequences. |
| `run_task(fn, *args, on_done=None, on_progress=None, on_error=None, **kwargs)` | Run `fn` on Kiosk's thread pool; callbacks fire on the GUI thread. |
| `resolve_texture_set(file_info, on_done=None, on_progress=None, on_error=None)` | Ensure a texture set is local (downloads a Poly Haven set if needed), then call `on_done` with the updated `file_info`. |

Anything slow (ffmpeg, OIIO, downloads) should run through `run_task` so the UI
stays responsive — never block inside a callback.

### Mutation — change the library

| Method | Does |
|--------|------|
| `rename_files(renames)` | Rename a list of `(old_path, new_path)` pairs on disk and keep the database **and** thumbnails in sync. Returns `{'renamed': int, 'errors': [(path, reason), …]}`. |

### Registration — add UI

Call these **only inside `register(api)`**. Each `icon` is optional (a `builtin:`
name or a path inside your extension folder).

| Method | Adds |
|--------|------|
| `register_menubar_entry(label, callback, icon='', shortcut='')` | An entry under your submenu in the Extensions menu. `callback()` takes no arguments. |
| `register_source_context_action(label, callback, icon='', predicate=None)` | An action in a source's context menu. `predicate(source_dict) -> bool` filters which sources show it; `callback(ctx)` receives `{source_name, category, source}`. |
| `register_file_context_action(label, callback, icon='', predicate=None)` | An action in the file context menu. `predicate(file_info_list) -> bool` gates visibility; `callback(file_info_list)` receives the file(s) the menu opened against. |
| `register_file_context_submenu(label, provider, icon='', predicate=None)` | A nested submenu built fresh each time it opens: `provider()` returns a list of `(item_label, item_callback)`, and each `item_callback(file_info_list)` runs when chosen. Use this when items change at runtime (e.g. presets read from disk). |

### A worked example

```python
def register(api):
    log = api.get_logger()

    # Only show for image files.
    def is_image(file_info_list):
        return all(fi.get("extension", "").lower() in ("png", "jpg", "exr", "tif")
                   for fi in file_info_list)

    # Read resolution off the UI thread, then log it.
    def show_resolution(file_info_list):
        helpers = api.get_task_helpers()
        for fi in file_info_list:
            api.run_task(
                helpers["get_image_resolution"], fi["file_path"],
                on_done=lambda res, p=fi["file_path"]: log.info(f"{p}: {res}"),
            )

    api.register_file_context_action("Show resolution", show_resolution, predicate=is_image)
```

## Loading locally

You don't need to publish anything to use an extension. In the Extension Manager,
click **Browse** and point Kiosk at a folder on disk — the same workflow as custom
plugins. Every subfolder with a valid `manifest.json` is loaded on startup, so a
dev checkout, a network share, or a synced pipeline folder all work. Restart Kiosk
after changes.
