Metadata-Version: 2.4
Name: rune-sdk
Version: 0.1.0b1
Summary: Python SDK for extending the Rune IDE
Project-URL: Homepage, https://rune.build
Project-URL: Documentation, https://docs.rune.build/develop/sdk/python
Project-URL: Repository, https://github.com/unstablebuild/rune-python-sdk
Project-URL: Issues, https://github.com/unstablebuild/rune-python-sdk/issues
Author-email: "Unstable Build, LLC" <hi@unstable.build>
License: Apache-2.0
License-File: LICENSE
Keywords: extension,grpc,ide,rune,sdk,tui
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Text Editors
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: googleapis-common-protos>=1.60
Requires-Dist: grpcio>=1.60
Requires-Dist: pymongo>=4
Provides-Extra: rich
Requires-Dist: rich<16,>=15; extra == 'rich'
Provides-Extra: textual
Requires-Dist: textual<9,>=8.2; extra == 'textual'
Description-Content-Type: text/markdown

# rune-python-sdk

The Python SDK for building **extensions** for [Rune](https://rune.build).

## Installation

```bash
pip install rune-sdk
```

Requires Python 3.11 or newer. The optional TUI adapters
([Beyond extensions](#beyond-extensions)) are extras:

```bash
pip install "rune-sdk[rich]"     # RichAdapter
pip install "rune-sdk[textual]"  # TextualAdapter
```

## What is an extension?

An extension is a standalone program that Rune launches as a child process and
connects to over a local unix socket. The SDK handles the handshake for you:

1. You describe your extension with `Metadata`: its id, version, and the
   `Permission`s it needs. `serve_workspace_extension` writes that to Rune over
   stdout and reads back the connection config from stdin.
2. Your setup coroutine receives a `Workspace`, whose accessors
   (`storage()`, `editor()`, `file_system()`, `window_manager()`,
   `notifications()`, `lsp()`, `llm()`, `debugger()`, and more) are gRPC clients
   into the host that share one `grpc.aio` channel. Every call is gated on a
   `Permission` you declared, so an undeclared capability is rejected.
3. You wire capabilities, register commands and event handlers, and return.
   `serve_workspace_extension` owns the lifetime and serves until SIGINT/SIGTERM.

## Quick Start

The example below is the wiring half of [`examples/snippets`](examples/snippets),
a complete extension that adds a `snippets` command for storing and reusing
text. `main.py` does only metadata and setup. The logic lives in a `Snippets`
handler typed against SDK clients so it stays testable.

```python
# main.py
import logging
import sys

from examples.snippets.handler import extend
from rune_sdk.extension import Metadata, Permission, serve_workspace_extension

META = Metadata(
    developer_id="rune-sdk-examples",
    developer_email="your@email.com",
    developer_key="1234",
    extension_id="snippets",
    extension_name="Snippets",
    extension_version="0.1.0",
    permissions=frozenset(
        {
            Permission.STORAGE,
            Permission.EDITOR,
            Permission.COMMANDS,
            Permission.FILE_SYSTEM,
            Permission.BROWSER_RESOURCE_OPENER,
            Permission.BROWSER_WINDOW_MANAGER,
            Permission.NOTIFICATIONS,
        }
    ),
)

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, stream=sys.stderr)
    serve_workspace_extension(extend, META)
```

```python
# handler.py (the setup body: wire and register, then return)
import asyncio
from typing import Any

from rune_sdk.api import text
from rune_sdk.extension import Workspace


async def extend(w: Workspace, _config: dict[str, Any]) -> None:
    s = Snippets(w)  # holds w.storage(), w.editor(), w.file_system(), etc.

    # React to editor events in a background task.
    sub = await w.editor().subscribe_events(
        text.EventType.SELECTION,
        text.EventType.FLUSH,
        text.EventType.CLOSE,
    )
    asyncio.create_task(s.handle_events(sub))

    manual = text.CommandManual(
        name="snippets",
        summary="Insert, edit, copy, or delete reusable text snippets.",
        synopsis="[insert|edit|copy|delete] <name>",
    )
    await w.register_command(manual, s.handle_command, s.complete)
```

The `Snippets` handler (`handler.py`) drives the clients: `snippets insert`
writes a stored body at the cursor, `snippets edit`/`copy` open a scratch buffer
that is persisted when saved or closed, and `snippets delete` removes one, all
with tab completion over stored names. See the
[`examples/snippets` README](examples/snippets/README.md) for the full
walkthrough and the [extension SDK guide](https://docs.rune.build/develop/sdk)
for the complete authoring guide.

## Extension API

`rune_sdk.extension` handles the handshake and hands you a `Workspace`. Its
accessors return `rune_sdk.api` gRPC clients that share one channel:

| Package | Capability | Accessor |
| --- | --- | --- |
| `rune_sdk.extension` | Handshake, `Workspace`, command registration | (none) |
| `api.text` | Editor and text operations, editor events, commands | `editor()`, `commands()`, `register_command()` |
| `api.storage` | Document storage and persistence | `storage()` |
| `api.browser` | Windows, tabs, resource opening, notifications, interrupts | `window_manager()`, `resource_opener()`, `notifications()`, `interrupter()` |
| `api.workspace` | URI resolution, file operations, watchers, processes, ptys | `file_system()`, `executor()`, `terminal()` |
| `api.semantic` | Language-server operations (definition, references, rename, diagnostics, and more) | `lsp()` |
| `api.llm` | LLM access (models, token counting, messages) | `llm()` |
| `api.debug` | Debug-adapter (DAP) control | `debugger()` |
| `api.syntax` | Tree-sitter structural search and queries | `parser()` |
| `api.config` | Workspace configuration | `config()` |

Every accessor is gated on the matching `Permission` you declared in
`Metadata`, so an undeclared capability is rejected by the host. Each API
package ships a `testing` module with fake-service harnesses; the vendored
`.proto` definitions live in [`proto/`](proto) and the generated stubs in
`rune_sdk._pb`.

## Running and debugging your extension

Rune launches an extension as a child process and connects to it over a private
local socket, so you never run your script directly during development. Start it
in a workspace from the [Rune console](https://docs.rune.build/learn/console)
with the `extensions` command, pointing it at your interpreter and entry script:

```
extensions start snippets python3 /path/to/examples/snippets/main.py --config '{"key":"value"}'
```

`extensions start <id> <cmdAndArgs> [--config <json>]` runs your program as the
extension `id`, so you can iterate without publishing a package. The rest of the
lifecycle is managed from the same console command:

- `extensions status` lists every workspace extension with its status, pid, and uptime.
- `extensions info <id>` shows detailed state, including restart counts.
- `extensions logs <id> [--tail <N>]` prints what your extension wrote to stderr, where startup failures and crashes show up. Configure `logging` to write there.
- `extensions restart <id>` relaunches with the current config, the fastest way to pick up a change.
- `extensions stop <id>` stops it.

Permissions are enforced at two layers: a call into a capability you did not
declare in `Metadata.permissions` is rejected outright, and the first time your
extension uses a declared capability Rune prompts the user to allow or deny it.
If a capability seems unreachable, check `authorizer list` for a lingering
**deny always** decision and clear it with `authorizer revoke <permission>`. See
the [extensions guide](https://docs.rune.build/develop/extensions) for the full
workflow.

## Packaging your extension

You distribute an extension as a Rune package: a gzipped tar archive that Rune
extracts into the user's data directory and installs with `pkg install`. A
package holds three kinds of content:

- **Executables.** Any file with the execute bit set (by convention under
  `bin/`) is copied onto the `PATH` by its base name. An executable can be a
  compiled binary or a script with a shebang, so a launcher script that runs
  your entry module works here.
- **Payload.** Any other files stay in the extracted package directory, reachable
  at `$RUNE_DATADIR/lib/$RUNE_PKG_ID`. Bundle your Python sources (and, if you
  vendor them, dependencies) here.
- **A config overlay.** A top-level `config.yaml` is deep-merged into the user's
  config at install time. This is how your extension registers itself: add an
  `extensions.<id>` entry with a `path` to your launcher and optional `config`
  defaults.

```yaml
# config.yaml
extensions:
  snippets:
    path: '/bin/snippets'
    config:
      # defaults surfaced in the user's config, editable like any other setting
```

Rune launches the executable named by `path` and passes the `config` block to
your `extend` coroutine as its config map. Because a Python extension needs an
interpreter, ship a small launcher script (with a shebang) under `bin/` that
runs your entry module from the payload, and rely on a Python present on the
target or a bundled interpreter. A portable script can ship as a single archive;
anything native (a bundled interpreter) needs one archive per OS and
architecture. See the [packages guide](https://docs.rune.build/develop/packages)
for the full format and how to publish so `pkg install snippets` resolves.

## Beyond extensions

Extensions are the richest way to add new functionality to Rune, but if all you
need is simple one-shot functionality that can be packaged as a TUI, you can
also use this SDK for that.

### TUIs (window content over a handler stream)

An extension can serve cell-grid text UIs into Rune windows over a
bidirectional handler stream. Window content is drawn by handlers (defined in
`rune_sdk.tui`):

1. **Component**: a basic UI element that can be drawn and resized.
2. **Handler**: a component that can also handle keyboard and mouse events and manage the cursor/selection.
3. **Handler stream**: the host drives installed handlers over a bidirectional gRPC stream (resize, draw, handle, cursor, selection, close).

`serve_handler` installs a handler in a window created according to a placement
(`SplitPlacement`, `TabPlacement`, `FloatingPlacement`, `BarPlacement`,
`WindowContentPlacement`) and serves it until the host closes it.

Instead of a widget library, the SDK adapts the Python TUI ecosystem
(`rune_sdk.adapt`):

- **`RichAdapter`** hosts any Rich renderable (tables, markdown, syntax,
  tracebacks) as a Component. Render-only: swap content with `set_renderable`,
  which fires `on_dirty` so the embedder can publish a redraw interrupt.
- **`TextualAdapter`** hosts a full interactive Textual app headless as a
  Handler, translating term events to Textual key/mouse events and signaling
  repaints through `on_dirty`.

See [`examples/hello_rich`](examples/hello_rich/main.py) for a live-updating
Rich table served into a window split, and
[`examples/textual_app`](examples/textual_app/main.py) for an interactive
Textual repl (Input + Button + DataTable) served as a browser tab.

### runectl (CLI)

If you only need a scripted, one-shot integration rather than a full extension,
`runectl` is the fast path. It is Rune's command-line companion: a single
executable that reaches into a running workspace to drive the editor, windows,
storage, language servers, and more from a shell script, a Makefile, or an
editor hook. It works regardless of the language you write extensions in.

Install it from the [Rune console](https://docs.rune.build/learn/console):

```
pkg install runectl
```

Once installed, `runectl` is on your `PATH` inside Rune's terminals and targets
the current workspace automatically. It is also the fastest way to sample the
data Rune returns for each API while you build an extension: run the equivalent
`runectl` command with `--format json` to see the exact response shape before
writing code against it. See the
[runectl guide](https://docs.rune.build/develop/runectl) for the full command
reference.

## Development

Common Make targets:

```bash
make generate  # Regenerate protobuf/gRPC code
make lint      # ruff check, ruff format --check, mypy --strict
make test      # pytest
make format    # ruff format + autofix
```

Run the full pre-commit validation before committing:

```bash
make generate && make lint && make test
```

Tests follow a table-driven approach with `pytest.mark.parametrize`.
Handlers are tested with the `rune_sdk.tui.testing` harness
(`draw_handler`, `run_handler_sequence`) and extensions end to end against
in-process fake hosts (see [`tests/e2e`](tests/e2e)).

## License

Apache License 2.0. See [LICENSE](LICENSE).
