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:
- You describe your extension with
Metadata: its id, version, and thePermissions it needs.serve_workspace_extensionwrites that to Rune over stdout and reads back the connection config from stdin. - 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 onegrpc.aiochannel. Every call is gated on aPermissionyou declared, so an undeclared capability is rejected. - You wire capabilities, register commands and event handlers, and return.
serve_workspace_extensionowns 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 statuslists 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. Configureloggingto 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 thePATHby 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.yamlis deep-merged into the user's config at install time. This is how your extension registers itself: add anextensions.<id>entry with apathto your launcher and optionalconfigdefaults.
# 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):
- Component: a basic UI element that can be drawn and resized.
- Handler: a component that can also handle keyboard and mouse events and manage the cursor/selection.
- 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):
RichAdapterhosts any Rich renderable (tables, markdown, syntax, tracebacks) as a Component. Render-only: swap content withset_renderable, which fireson_dirtyso the embedder can publish a redraw interrupt.TextualAdapterhosts a full interactive Textual app headless as a Handler, translating term events to Textual key/mouse events and signaling repaints throughon_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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69eff3f67d6876855741c6c16292c2d5b5f11df59c7e2890c571ab6d857b20f5
|
|
| MD5 |
6bee1b2646e0feaa82a18329805b051f
|
|
| BLAKE2b-256 |
7ad9091881367a9b7e54b6a86a5764b910193f4759151b4bad0fbd5114aa5206
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bf4bb55ffa78ad16a7779ad3fe7d69d58eb700954c00170c57bbd4ddf1ab611
|
|
| MD5 |
af09c21f3bcd168739df9a6199bd4e09
|
|
| BLAKE2b-256 |
3771e46acd611227d1c109d7fb7491184747f01fb70c6e8d851fb71179817ced
|