> ## 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.

# Creating an Extension

> Step-by-step guide to building a Klyx extension

This guide covers the full process of creating a Klyx extension, from project setup through the plugin manifest annotation, to advanced features like screens, toolbar actions, file openers, events, and language registries.

## Project structure

A well-organized extension looks like this:

```
my-extension/
├── app/
│   ├── build.gradle.kts
│   └── src/main/
│       ├── AndroidManifest.xml
│       └── java/com/myext/
│           ├── MyExtension.kt   -- Plugin entry class
│           └── ui/
│               ├── MainScreen.kt   -- Custom screens
│               └── DetailScreen.kt
├── build.gradle.kts
├── settings.gradle.kts
├── gradle/
│   └── libs.versions.toml
└── icon.png                      -- Optional plugin icon (auto-detected)
```

There is no `plugin.json` file in the project. The `@PluginManifest` annotation generates it at compile time.

## The entry class

Your extension's entry point is a class that implements `KlyxPlugin` and is annotated with `@PluginManifest`. The compiler plugin reads the annotation and generates a `PluginDescriptor` companion property plus a `plugin.json` file — the `entryClass` is derived from the class's fully qualified name automatically.

```kotlin theme={null}
package com.myext

import androidx.compose.material3.Text
import com.klyx.api.plugin.KlyxPlugin
import com.klyx.api.plugin.PluginManifest
import com.klyx.api.plugin.PluginInfo
import com.klyx.api.plugin.runtime
import com.klyx.api.service.plugin
import com.klyx.api.ui.Screen
import com.klyx.api.ui.ScreenId
import com.klyx.api.ui.ScreenRegistry
import com.klyx.api.ui.ToolbarAction
import com.klyx.api.ui.ToolbarCategory
import com.klyx.api.ui.ToolbarIcon
import com.klyx.api.ui.ToolbarRegistry
import com.klyx.api.Navigator
import com.klyx.api.NavDestination

@PluginManifest(
    id = "com.myext.helloworld",
    version = "1.0.0",
    name = "Hello World",
    description = "A minimal Klyx extension",
    minAppVersion = "4.2.0",
    author = Author(name = "Your Name"),
    license = "MIT"
)
class MyExtension : KlyxPlugin {

    private val screens: ScreenRegistry by plugin()
    private val toolbar: ToolbarRegistry by plugin()
    private val navigator: Navigator by plugin()
    private val info: PluginInfo by runtime()

    override suspend fun onLoad() {
        screens.register(Screen(ScreenId("myext.main")) {
            Text("Hello from ${info.descriptor.name}!")
        })

        toolbar.register(ToolbarAction(
            id = "myext.show_main",
            label = "Hello World",
            icon = ToolbarIcon.ImageVector(Icons.Default.Star),
            category = ToolbarCategory("My Plugin"),
            priority = 100,
            onClick = { navigator.navigateTo(NavDestination.Custom(ScreenId("myext.main"))) }
        ) {})
    }

    override suspend fun onStart() {}

    override suspend fun onStop() {}

    override suspend fun onUnload() {
        screens.unregister(ScreenId("myext.main"))
        toolbar.unregister("myext.show_main")
    }
}
```

Notice how `register` and `toolbar.register` do not take `this` as an argument. They use Kotlin context receivers — the `KlyxPlugin` is provided as a context parameter.

## Registering resources

### Screens

Register screens during `onLoad()` using `ScreenRegistry`. Each screen gets a unique `ScreenId` and a composable lambda.

```kotlin theme={null}
screens.register(Screen(ScreenId("myext.detail")) {
    DetailScreen()
})

// Or use the set operator to update content later:
screens[ScreenId("myext.detail")] = { UpdatedDetailScreen() }
```

### Toolbar actions

Add actions to Klyx's toolbar. Each action has an ID, label, icon, category, priority, and click handler. Higher priority values place the action first within its category.

```kotlin theme={null}
toolbar.register(ToolbarAction(
    id = "myext.action1",
    label = "Do Something",
    icon = ToolbarIcon.ImageVector(Icons.Default.PlayArrow),
    category = ToolbarCategory("My Plugin"),
    priority = 100,
    onClick = { /* handle action */ }
) {})
```

`ToolbarIcon` supports four source types:

| Type                                   | Description                              |
| -------------------------------------- | ---------------------------------------- |
| `ToolbarIcon.Resource(path)`           | Drawable resource path in the plugin APK |
| `ToolbarIcon.File(file)`               | Load from a file on disk                 |
| `ToolbarIcon.Painter(painter)`         | A Compose Painter instance               |
| `ToolbarIcon.ImageVector(imageVector)` | A Compose ImageVector                    |

Predefined categories are available: `ToolbarCategory.CurrentFile`, `ToolbarCategory.Workspace`, `ToolbarCategory.Run`, `ToolbarCategory.Tools`, and `ToolbarCategory.Plugins` (the default).

### File openers

Register a `FileOpener` to handle custom file types. The opener returns a `WorkspaceTab` if it can handle the file, or `null` to let other openers try.

```kotlin theme={null}
private var openerRegistration: FileOpenerRegistration? = null

override suspend fun onLoad() {
    openerRegistration = openers.register(object : FileOpener {
        override val id = "myext.opener"
        override val priority = 50

        override suspend fun open(request: FileOpenRequest): WorkspaceTab? {
            if (request.extension != "svg") return null
            return WorkspaceTab.Custom(
                title = request.fileName,
                id = request.uri.toString(),
                content = { SvgViewer(request.uri) }
            )
        }
    } {})
}

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

### WorkspaceTab types

| Type                     | Description                        |
| ------------------------ | ---------------------------------- |
| `WorkspaceTab.TextFile`  | Text file with syntax highlighting |
| `WorkspaceTab.ImageFile` | Image viewer                       |
| `WorkspaceTab.Welcome`   | Welcome screen                     |
| `WorkspaceTab.Custom`    | Any custom composable content      |

The `Custom` tab accepts additional optional parameters: `onClose` (a suspend lambda called when the tab is closed) and `pluginId` (for ownership tracking).

### File runners

Register a `FileRunner` to claim files for the editor's **Run** button. The registry consults runners in descending `priority` order and delegates to the first one whose `supports` returns `true`.

```kotlin theme={null}
class PythonRunner : FileRunner {
    override val id = "myext.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"
        )
    }
}

private var runnerRegistration: FileRunnerRegistration? = null

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

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

Runners that preview a file instead of running a command can navigate to a plugin screen with `runner.openScreen(...)`. See [File Runners](/extensions/api-reference/runners) for the full API.

### Installing tooling interactively

To install or configure tooling that prompts the user (e.g. `rustup component add rust-analyzer`), use `TerminalManager` from anywhere in your plugin — not just the Run button:

```kotlin theme={null}
private val terminalManager: TerminalManager by plugin()

// Run the install command in the terminal; stdin stays interactive,
// so the user can answer confirmation prompts.
pluginScope.launch {
    terminalManager.runInTerminal("rustup component add rust-analyzer")
}

// Or just open an interactive login shell and let the user type it.
terminalManager.openTerminal()
```

`runInTerminal` runs the command in a fresh session (no login shell or MOTD) with its stdin wired to the terminal. `openTerminal` opens an interactive login shell. See [Terminal](/extensions/api-reference/terminal) for details.

## Responding to events

Use the event bus to react to app events. Subscribe in `onLoad()` and keep a reference to the subscription for cleanup.

```kotlin theme={null}
private var subscription: EventSubscription? = null

override suspend fun onLoad() {
    val bus = currentPluginContext().eventBus

    subscription = bus.subscribe<FileOpenedEvent>(
        priority = Priority.Normal
    ) { event ->
        showToast("File opened: ${event.fileName}")
    }
}

override suspend fun onUnload() {
    subscription?.cancel()
}
```

The event bus also supports subscribing within a coroutine scope (auto-cancels when the scope ends):

```kotlin theme={null}
pluginScope.launch {
    currentPluginContext().eventBus.subscribe<NewSessionEvent> { event ->
        // Handle terminal session creation
    }
}
```

See [Events](/extensions/api-reference/events) for all available event types and the full event bus API.

## Navigation

Use the `Navigator` service to navigate users to different destinations:

```kotlin theme={null}
navigator.navigateTo(NavDestination.Home)
navigator.navigateTo(NavDestination.Settings)
navigator.navigateTo(NavDestination.Terminal)
navigator.navigateTo(NavDestination.Custom(ScreenId("myext.main")))
navigator.navigateBack()
```

## Language servers

Register LSP providers for custom file types. Multiple providers can be registered for the same extension, and they will all be queried in parallel:

```kotlin theme={null}
private var lspRegistration: LanguageServerRegistration? = null

override suspend fun onLoad() {
    lspRegistration = languageServers.register("py") { client ->
        PythonLanguageServer(client)
    }
}

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

See [Language Server Protocol](/extensions/api-reference/language-server) for details.

## Language grammars

Register tree-sitter grammar providers to add syntax highlighting for custom languages:

```kotlin theme={null}
languageRegistry.register(
    descriptor = LanguageDescriptor(
        name = "mylang",
        extensions = listOf("myl", "mylang")
    ),
    grammarProvider = LanguageGrammarProvider {
        // Return the tree-sitter language pointer (Long)
        nativeGetLanguage()
    },
    queries = object : QueryProvider {
        override fun highlights() = buildQuery {
            capture("keyword", "fn")
            capture("keyword", "return")
            capture("string", "\"[^\"]*\"")
        }
    }
) {}
```

See [Language Grammars](/extensions/api-reference/language-registry) for the full grammar registration API.

## Process execution

Run shell commands and system programs:

```kotlin theme={null}
override suspend fun onStart() {
    pluginScope.launch {
        // Synchronous execution
        val result = command("git", "status").output()
        showToast(result.stdoutText)

        // Streaming output
        command("ping", "-c", "4", "example.com").streamLines().collect { line ->
            // Handle each output line
        }
    }
}
```

See [Process Execution](/extensions/api-reference/processes) for the full command and pipeline API.

## Best practices

* **Keep `onLoad()` fast**: Defer heavy work to `onStart()` or launch a coroutine on `pluginScope`
* **Unregister everything**: Always clean up screens, toolbar actions, file openers, LSP providers, and event subscriptions in `onUnload()`
* **Handle file openers gracefully**: Return `null` from `open()` if you cannot handle the file
* **Use the right coroutine scope**: Use `pluginScope` for tasks that should persist across start/stop cycles, and `currentLifecycleOwner().lifecycleScope` for tasks tied to the active state
* **Use `withResources`**: Wrap Compose content with `withResources { }` so resource lookups resolve against the plugin's own APK resources
* **Bundle icons as ImageVector**: They scale well at any size
* **Test with a real plugin**: Build and install a plugin to verify it works end-to-end
