> ## Documentation Index
> Fetch the complete documentation index at: https://klyx.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# File Runners

> Run files from the editor toolbar

Extensions can claim files for the editor's **Run** button. When the user opens a file and hits Run, Klyx consults every registered `FileRunner` in descending `priority` order and delegates to the first runner whose `supports` returns `true`.

All runner types live in `com.klyx.api.data.runner`.

## FileRunnerRegistry

Resolve the registry from a plugin as a service:

```kotlin theme={null}
val runners: FileRunnerRegistry by plugin()
// or
val runners = context.service<FileRunnerRegistry>()
```

| Method               | Returns                  | Description                                              |
| -------------------- | ------------------------ | -------------------------------------------------------- |
| `register(runner)`   | `FileRunnerRegistration` | Register a runner (context receiver provides the plugin) |
| `unregister(id)`     |                          | Remove a runner by its id                                |
| `runnerFor(request)` | `FileRunner?`            | First runner that supports the request                   |
| `supports(request)`  | `Boolean`                | Whether any runner claims the file                       |
| `runners()`          | `List<FileRunner>`       | All runners, ordered by descending priority              |

Registering a runner whose `id` matches an existing one replaces the previous registration.

```kotlin theme={null}
private var registration: FileRunnerRegistration? = null

override suspend fun onStart() {
    registration = runners.register(PythonRunner())
}

override suspend fun onStop() {
    registration?.unregister()
}
```

## FileRunner

Implement this interface to handle a specific kind of file:

```kotlin theme={null}
interface FileRunner {
    val id: String                    // Unique, reverse-DNS id (e.g. "com.example.python.runner")
    val priority: Int                 // Higher runs first. Defaults to 0.

    fun supports(request: FileRunRequest): Boolean

    suspend fun run(request: FileRunRequest, runner: FileRunnerContext)
}
```

| Member     | Description                                                                                                                                   |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`       | Unique identifier. Recommended format: reverse-DNS                                                                                            |
| `priority` | Runners with a higher priority are consulted first. Defaults to 0                                                                             |
| `supports` | Return `true` if this runner can execute the file. Called to decide whether to show the **Run** button, so keep it cheap and side-effect free |
| `run`      | Execute the file using the provided `FileRunnerContext`                                                                                       |

## FileRunRequest

The request handed to every runner when the user hits **Run**:

```kotlin theme={null}
data class FileRunRequest(
    val file: KxFile,
    val uri: Uri,
    val projectUri: Uri? = null,
    val tabId: String? = null,
)
```

| Property     | Description                                                       |
| ------------ | ----------------------------------------------------------------- |
| `file`       | The file being run                                                |
| `uri`        | The URI of the file being run                                     |
| `projectUri` | The URI of the project the file belongs to, if any                |
| `tabId`      | The id of the active editor tab for this file, if any             |
| `extension`  | Lowercase file extension without the leading dot, or `""` if none |

The `extension` convenience property is the most common way to match a file:

```kotlin theme={null}
override fun supports(request: FileRunRequest): Boolean =
    request.extension == "py"
```

## FileRunnerContext

Runtime helpers handed to `run`. Use it to interact with the host:

| Method                                       | Returns              | Description                                                                                          |
| -------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------- |
| `runInTerminal(command, cwd?, sessionName?)` |                      | Open the terminal and run a shell command in a fresh session                                         |
| `openTerminal()`                             |                      | Navigate to the terminal screen without running a command                                            |
| `openScreen(screenId)`                       |                      | Navigate to a plugin-registered screen                                                               |
| `openScreen(screenId, content)`              | `ScreenRegistration` | Register the composable for the id, navigate to it, and auto-unregister it when the screen is popped |
| `openTab(tab)`                               |                      | Open a custom tab in the editor workspace                                                            |

### Running a command in the terminal

`runInTerminal` opens the terminal screen and executes the command directly — no login shell, prompt, or MOTD, only the command's stdout/stderr and stdin:

```kotlin theme={null}
override suspend fun run(request: FileRunRequest, runner: FileRunnerContext) {
    val path = request.uri.path ?: return
    runner.runInTerminal(
        command = "python3 \"$path\"",
        cwd = path.substringBeforeLast('/'),
        sessionName = "Python"
    )
}
```

These terminal helpers are only available inside a `FileRunner`. To run a command or open an interactive shell from anywhere else in your plugin (toolbar action, settings, lifecycle), use `TerminalManager.runInTerminal(...)` / `TerminalManager.openTerminal()` instead. See [Terminal](/extensions/api-reference/terminal).

### Opening a custom screen

Runners that preview a file can navigate to a screen registered via `ScreenRegistry`:

```kotlin theme={null}
override suspend fun run(request: FileRunRequest, runner: FileRunnerContext) {
    runner.openScreen(ScreenId("com.example.html.preview"))
}
```

To open a screen directly with data — no pre-registration required — pass a composable. The closure captures whatever the screen needs, typically the file being run and its URI:

```kotlin theme={null}
override suspend fun run(request: FileRunRequest, runner: FileRunnerContext) {
    runner.openScreen(ScreenId("com.example.html.preview")) {
        HtmlPreviewScreen(file = request.file, uri = request.uri)
    }
}
```

Screens opened this way are *transient*: the host auto-unregisters them when the screen is popped, so no cleanup is required. The returned `ScreenRegistration` lets you remove the screen earlier if needed.

## Example

A Python runner registered during `onLoad`:

```kotlin theme={null}
class PythonRunner : FileRunner {
    override val id = "com.example.python.runner"
    override val priority = 10

    override fun supports(request: FileRunRequest): Boolean =
        request.extension == "py"

    override suspend fun run(request: FileRunRequest, runner: FileRunnerContext) {
        val path = request.uri.path ?: return
        runner.runInTerminal(
            command = "python3 \"$path\"",
            cwd = path.substringBeforeLast('/'),
            sessionName = "Python"
        )
    }
}

// In the plugin:
private var registration: FileRunnerRegistration? = null

override suspend fun onLoad() {
    registration = runners.register(PythonRunner())
}

override suspend fun onUnload() {
    registration?.unregister()
}
```

## Best practices

* **Keep `supports` cheap**: It runs on every file open to decide whether to show the **Run** button
* **Register with a stable `id`**: Re-registering with the same id replaces the runner
* **Unregister on `onUnload`**: Keep the `FileRunnerRegistration` and call `unregister()` to clean up
* **Use `runInTerminal` for interpreters**: It handles opening the terminal, running the command, and setting the working directory
