Skip to main content

Python SDK for extending the Rune IDE

Project description

rune-python-sdk

The Python SDK for building extensions for Rune.

Installation

pip install rune-sdk

Requires Python 3.11 or newer. The optional TUI adapters (Beyond extensions) are extras:

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 Permissions 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, 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.

# 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)
# 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 for the full walkthrough and the extension SDK guide 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/ 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 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 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.
# 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 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 for a live-updating Rich table served into a window split, and examples/textual_app 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:

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 for the full command reference.

Development

Common Make targets:

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:

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

License

Apache License 2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rune_sdk-0.1.0.tar.gz (382.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rune_sdk-0.1.0-py3-none-any.whl (297.9 kB view details)

Uploaded Python 3

File details

Details for the file rune_sdk-0.1.0.tar.gz.

File metadata

  • Download URL: rune_sdk-0.1.0.tar.gz
  • Upload date:
  • Size: 382.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.22 {"installer":{"name":"uv","version":"0.11.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for rune_sdk-0.1.0.tar.gz
Algorithm Hash digest
SHA256 69eff3f67d6876855741c6c16292c2d5b5f11df59c7e2890c571ab6d857b20f5
MD5 6bee1b2646e0feaa82a18329805b051f
BLAKE2b-256 7ad9091881367a9b7e54b6a86a5764b910193f4759151b4bad0fbd5114aa5206

See more details on using hashes here.

File details

Details for the file rune_sdk-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: rune_sdk-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 297.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.22 {"installer":{"name":"uv","version":"0.11.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for rune_sdk-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7bf4bb55ffa78ad16a7779ad3fe7d69d58eb700954c00170c57bbd4ddf1ab611
MD5 af09c21f3bcd168739df9a6199bd4e09
BLAKE2b-256 3771e46acd611227d1c109d7fb7491184747f01fb70c6e8d851fb71179817ced

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page