ctx — the plugin context¶
register(ctx) receives one argument: a PluginContext. It exposes the
services below (the whole IPluginContext surface that fits the
out-of-process model). Each is a small Python class the worker builds
on top of the RPC channel; the source is Python/mte/.
def register(ctx):
ctx.commands # register/invoke commands
ctx.menus # add menu items & separators
ctx.events # subscribe to editor events
ctx.log # write to the host's diagnostics stream
ctx.status # post transient status-bar messages
ctx.editor # buffer / caret / selection / files / search / bookmarks / groups
ctx.languages # list languages, read / switch a document's language, contribute one
ctx.settings # per-plugin key/value store
ctx.app_settings # editor-wide read-only settings view (keys: mte.keys)
ctx.document_settings # .editorconfig-layered per-document reads
ctx.theme # themeable colour categories for your own UI
ctx.diagnostics # publish squiggles + margin glyphs
ctx.compare # open the side-by-side compare view
ctx.completions # register auto-completion providers
ctx.docks # declarative dock panels (+ set_panel_visible, is_panel_visible)
ctx.line_markers # margin glyphs / line tints that follow edits; the margin-click claim
ctx.settings_page # declarative Preferences pages
ctx.ui # runtime get/set for panel controls
Two parts of the native contract have no Python counterpart yet: hover providers (so the hover priority bands do not apply to Python plugins) and editor context-menu providers (so neither does the context menu's text / margin area). Both are answered while the user waits, which needs the same deadline treatment completion providers have before a round trip to the worker is safe.
Do not store ctx in a global expecting to use it later — while the
attributes are stable for the plugin's lifetime, treat ctx as
per-registration. Keep references to the individual services if you
need them (log = ctx.log, for example).
commands¶
Decorator that registers a command and binds the wrapped callable as its handler.
id(str, required): reverse-DNS-style unique identifier (e.g."wordcount.count"). Same rules as inplugin.json— must be globally unique.title(str, required): human-readable label shown wherever the command is surfaced (menus, command palette).shortcut(str, optional): portable default shortcut string, e.g."Ctrl+Alt+W".Ctrlmaps to the platform's primary modifier automatically (Cmdon macOS). Leave empty for none. The user can rebind or unbind it in Preferences ▸ Shortcuts; for menu-bound commands the host applies the remap for you.category(str, optional): free-form grouping label — shown as the command's category in the Shortcut Mapper.scope(str, optional):""/"application"(default — the host binds the shortcut on the command's menu action) or"pluginWindow"for chords living in plugin-owned UI: the host only lists the command in the mapper; readctx.commands.effective_shortcut(id)when building your widgets and re-apply on the"command.shortcutChanged"event.
The shortcut currently in effect after user remapping and conflict
resolution ("" = unbound or unknown). Safe to call from handlers.
The wrapped function takes no arguments and returns nothing. Exceptions inside it are caught by the worker and logged to stderr; they do not crash the editor.
@ctx.commands.command(id="wordcount.count",
title="Count Words",
shortcut="Ctrl+Alt+W",
category="Text")
def _count():
ctx.status.show("counted")
Under the hood: the Python side allocates a handlerId locally,
registers the callback on the RPC connection, and fires
commands.registerCommand at the host. When the user activates the
command (menu click, shortcut, palette), the host sends a callback
RPC and your function runs.
Command ids are process-wide, not per-plugin. Prefix your ids with
your reverse-DNS namespace (wordcount.…) to avoid collisions.
menus¶
Menu paths use / as separator. The leftmost segment is a top-level
menu (Plugins, Tools, …); intermediate segments create submenus on
demand; the final segment is the item label.
ctx.menus.add_item(path, command_id)¶
path(str):"Top/Sub/…/Label". Intermediate submenus are created if they don't exist.command_id(str): id of a previously-registered command. Clicking the item calls the command.
Returns an opaque handlerId string. Keep it if you'll need to
unsubscribe the item later (rare in Phase 1).
The leftmost segment ("Plugins" above) must match the title of one
of your menus[] entries in plugin.json so the menu-bar placement is
picked up.
ctx.menus.add_separator(path)¶
Inserts a separator into an existing submenu.
ctx.menus.set_item_checked(handler_id, checked)¶
Turns the previously-added item into a checkable menu entry and sets its checked state — the standard idiom for a persistent on/off toggle that shows a visible check mark.
item = ctx.menus.add_item("View/Auto-open Preview",
"myplugin.toggleAutoOpen")
# Seed the check mark from the persisted setting so it matches
# reality before the user opens the menu.
ctx.menus.set_item_checked(item, ctx.settings.get("autoOpen", True))
@ctx.commands.command(id="myplugin.toggleAutoOpen",
title="Toggle Auto-Open")
def _toggle():
on = not ctx.settings.get("autoOpen", True)
ctx.settings.set("autoOpen", on)
ctx.menus.set_item_checked(item, on)
The first call promotes the item to checkable; subsequent calls just
flip the mark. Passing a handler_id the host doesn't know is a
silent no-op (safe to call after your token has been dropped, e.g.
from a delayed callback firing during shutdown).
ctx.menus.set_item_enabled(handler_id, enabled)¶
Enables or greys out a previously-added item. Use it when the command needs a precondition — something to close, something to restore — so the menu shows it is unavailable instead of offering an entry that quietly does nothing.
item = ctx.menus.add_item("File/Close Thing", "myplugin.closeThing")
# Nothing to close yet:
ctx.menus.set_item_enabled(item, False)
# ... and from every place that changes the state the command needs:
ctx.menus.set_item_enabled(item, thing_is_open)
A disabled item stays visible but cannot be triggered, by click or by
its shortcut. The call is idempotent, so you never have to remember the
previous state, and an unknown handler_id is a silent no-op as above.
Items start out enabled: one call right after add_item seeds an item
that is unavailable at start-up.
events¶
Subscribe to editor events. Two equivalent APIs — the sugar decorators
are recommended; the raw subscribe() is available if you want to
compute the topic at runtime.
Sugar decorators¶
@ctx.events.on_document_saved
def _saved(ev):
ctx.log.info(f"saved doc {ev.document}")
@ctx.events.on_document_opened
def _opened(ev):
ctx.log.info(f"opened {ev.path}")
The wrapped function takes one argument: a typed dataclass corresponding to the topic. See the Events reference for the full topic ↔ dataclass table.
Raw API¶
handler_id = ctx.events.subscribe("document.saved", _saved)
# later:
ctx.events.unsubscribe(handler_id)
Available sugar properties (all decorators):
| Property | Fires on |
|---|---|
on_document_opened |
Document opened in a tab |
on_document_closed |
Document closed |
on_document_saved |
Document written to disk |
on_selection_changed |
Caret / selection moved in the active document |
on_active_document_changed |
Foreground tab changed |
on_document_modified_changed |
The clean/dirty flag toggled (modified: bool) |
on_text_changed |
Every edit (position, added_bytes, removed_bytes) — keep it trivial |
on_settings_changed |
Preferences applied (scope: "" editor-wide, "plugins/<id>") |
on_theme_changed |
Theme switched or edited (variant) — re-read ctx.theme colours |
on_workspace_root_changed |
Workspace folder opened / closed (root) |
on_workspace_files_changed |
Workspace file set changed (files) |
on_host_window_shown |
Once per run, after the first frame — schedule slow startup work here |
on_app_about_to_quit |
Editor is closing — last chance to run |
on_command_shortcut_changed |
A command's effective shortcut changed (user remap / keymap import / conflict resolution); payload: command_id, sequence ("" = unbound) |
Unknown topic names raise ValueError at subscribe time. Subscribers
may run after the corresponding native handler; treat event
delivery as informational, not authoritative.
Publishing¶
ctx.events.publish(topic, payload) broadcasts to native and Python
subscribers alike — accepted only for the two workspace topics
(workspace.rootChanged, workspace.filesChanged); anything else raises
ValueError. See the Events reference.
log¶
ctx.log.trace ("very-fine-grained")
ctx.log.debug ("dev-only detail")
ctx.log.info ("routine event")
ctx.log.warning("recoverable weirdness")
ctx.log.error ("something is wrong")
Each level maps to the host's ILogger. Lines are prefixed
[python:<plugin.id>] so your output is discoverable in the editor's
diagnostics log:
Never print() from a plugin — worker stdout is the RPC channel, and
writing to it corrupts the protocol. ctx.log.* is the only sanctioned
way to emit diagnostic text.
status¶
Post a transient notification to the status bar.
message(str): what to show. Short strings only — the status bar has a fixed width.timeout_ms(int, default 0): how long to keep it on screen;0lets the host pick a sensible default (a few seconds).level(str, default"info"):"info"/"warning"/"error". Higher levels may hold the on-screen minimum a little longer so an error is not instantly wiped by a routine info message.
ctx.status.show is fire-and-forget. If the host has no visible status
bar (headless / minimal build), the call is silently dropped.
ctx.status.clear() wipes whatever transient notification is on screen.
Persistent segments¶
For live state (a word count, a server status) use a segment you own and refresh — it never clobbers a notification and vice versa:
seg = ctx.status.add_segment("wordcount.total", priority=100)
seg.set_text("2,431 words")
seg.set_visible(False) # hide without giving it up
seg.remove() # take it off the bar (idempotent)
priority orders plugin segments left to right (lower = further left;
ties keep registration order). Plugin shutdown removes anything still
held.
editor¶
Full sync access to the active document(s). Every method blocks the
calling thread until the host responds (typically microseconds when the
worker and editor share a machine). Legal from any command / event
handler; never from an async def callback (see the threading note
at the top of this page).
Document enumeration¶
doc = ctx.editor.active_document() # 0 when no document is open
docs = ctx.editor.open_documents() # list[int]
ctx.editor.set_active_document(doc)
path = ctx.editor.file_path(doc) # str or None for untitled
Buffer access¶
text = ctx.editor.text(doc) # whole buffer
slice = ctx.editor.text_range(doc, start, end) # [start, end) bytes
n = ctx.editor.length(doc) # int64
ctx.editor.set_text(doc, "...")
text_range avoids pulling the whole buffer across the RPC when you
only need a slice — prefer it for sort/transform on big files.
Selection¶
sel = ctx.editor.selection_range(doc) # mte.MatchRange(start, end)
s = ctx.editor.selected_text(doc)
ctx.editor.replace_selection(doc, "new text")
ctx.editor.set_selection(doc, mte.MatchRange(4, 9))
ranges = ctx.editor.selection_ranges(doc) # list[MatchRange]: one per caret
block = ctx.editor.is_rectangular_selection(doc) # Alt+drag column block?
Caret¶
pos = ctx.editor.caret(doc) # mte.EditorPosition(line, column)
ctx.editor.set_caret(doc, mte.EditorPosition(line=7, column=3))
File ops¶
new_doc = ctx.editor.open_file("/path/to/file") # doc-id or 0 on failure
here = ctx.editor.open_file_at("/path/to/file", mte.EditorPosition(41, 0))
ctx.editor.reveal_line(doc, 120) # scroll there; caret and focus stay
ok = ctx.editor.save_document(doc)
closed = ctx.editor.close_document(doc) # prompts if modified; False = user cancelled
dirty = ctx.editor.is_modified(doc)
ctx.editor.set_modified(doc, True) # force the tab asterisk
ro = ctx.editor.is_read_only(doc) # e.g. Monitoring (tail -f) mode
enc = ctx.editor.document_encoding(doc) # mte.DocumentEncoding, e.g. UTF8_BOM
ctx.editor.set_document_encoding(doc, mte.DocumentEncoding.UTF8) # takes effect on the next save
eol = ctx.editor.line_ending(doc) # mte.LineEnding, e.g. CRLF
ctx.editor.set_line_ending(doc, mte.LineEnding.LF) # Enter inserts it; the next save writes it
scratch = ctx.editor.create_untitled("text", "Untitled-3") # new buffer, doc-id
sid = ctx.editor.stable_id(doc) # survives Save As; compare with ==
clip = ctx.editor.clipboard_text() # system clipboard, "" if no text
# Declare that you snapshot unsaved buffers (the host then stops asking
# "save changes?" when the window closes):
ctx.editor.set_unsaved_changes_preserved(True)
ctx.editor.unsaved_changes_preserved() # bool, global flag
open_file_at is the one call for "take the user there" (a build error, a
stack frame, a search hit): it opens the file or activates its tab, places the
caret with no selection, unfolds a fold that hides the line, scrolls the line
into view — centred when it was off-screen — and focuses the editor. Line and
column are 0-based (the column in UTF-8 bytes) and clamped, never refused; it
returns the document id, or 0 when the file could not be opened, and then
nothing moved. reveal_line only shows a line: it does not move the caret,
switch tabs or take focus.
Bookmarks, marks, URL highlight¶
ctx.editor.toggle_bookmark(doc, line) # 0-based line
lines = ctx.editor.bookmarked_lines(doc) # list[int]
nxt = ctx.editor.next_bookmark(doc, from_line, wrap=True) # -1 = none
prv = ctx.editor.previous_bookmark(doc, from_line, wrap=True)
ctx.editor.clear_bookmarks(doc)
ctx.editor.mark_range(doc, mte.MatchRange(4, 9)) # "mark" indicator
ctx.editor.clear_marks(doc)
ctx.editor.set_url_highlight_enabled(doc, True)
ctx.editor.is_url_highlight_enabled(doc)
url = ctx.editor.url_at_caret(doc) # "" when none
ctx.editor.open_url_at_caret(doc) # bool
Search¶
opts = mte.SearchOptions(match_case=True, mode="regex")
hit = ctx.editor.find_in_range(doc, r"TODO\(.*\)", 0,
ctx.editor.length(doc), opts)
if hit.valid:
new_end = ctx.editor.replace_target(doc, hit, "DONE()", opts)
SearchOptions.mode: "normal" / "extended" / "regex".
SearchOptions.direction: "forward" / "backward". See
mte.SearchOptions for the full field list.
Incremental search (grow the pattern as the user types, from a pinned anchor):
ctx.editor.set_incremental_search_anchor(doc)
hit = ctx.editor.incremental_search(doc, "TOD", opts) # MatchRange
Indentation and syntax¶
style = ctx.editor.indentation(doc) # IndentationStyle(use_tabs, width)
ctx.editor.set_indentation(doc,
mte.IndentationStyle(use_tabs=False, width=2))
# Where word-wrapped continuation rows start:
# "left" (window edge) / "same" (align with the line; default) / "indent"
ctx.editor.set_wrap_indent(doc, "same")
mode = ctx.editor.wrap_indent(doc) # same strings back
lang = ctx.editor.colorizer_id(doc) # e.g. "python", "" if none
# Semantic classification from the active colorizer (cheap, no re-lex):
# list[mte.CategorySpan(start, length, category)] with category names
# "comment" / "string" / "keyword" / ... -- a spell checker keeps to
# comments and strings this way.
spans = ctx.editor.text_categories(doc, 0, ctx.editor.length(doc))
ctx.editor.trigger_completion(doc) # pop the list as if Ctrl+Space
Split view / tab groups¶
n = ctx.editor.group_count() # >= 1
g = ctx.editor.group_of(doc) # -1 if not open
w = ctx.editor.group_weights() # relative widths
ctx.editor.move_document_to_group(doc, 1) # creates trailing groups on demand
ctx.editor.set_group_weights([2, 1])
ctx.editor.set_document_tab_index(doc, 0) # reorder within its pane
gs = ctx.editor.groups_of(doc) # EVERY pane showing it
ctx.editor.show_document_in_group(doc, 1) # split it into pane 1
A document shown in two panes is one document with one id, so
group_of() can name only one of its seats. groups_of() reports them
all, and show_document_in_group() creates a second shared view --
the same thing View > Split Document Right does, so no
document_opened event is raised.
ctx.editor now mirrors the native IEditorService one to one.
languages¶
Syntax colouring is a core feature — the host detects a document's
language and colours it without any plugin. ctx.languages lets you join
in: list what the host knows, read or override a document's language,
re-run detection, and contribute whole language definitions.
for lang in ctx.languages.available(): # list[mte.LanguageInfo(id, name)]
...
lang_id = ctx.languages.language_for(doc) # "" when uncoloured
ok = ctx.languages.set_language_for(doc, "json") # "" clears; False = unknown
ctx.languages.auto_detect_for(doc) # undo a manual override
handler = ctx.languages.register(
id="mylang", name="My Language", lexer="cpp",
extensions=["mylang", "ml2"],
keywords=[{"group": "Primary keywords and identifiers",
"words": "if else while"}],
styles={"SCE_C_COMMENT": "comment", "SCE_C_STRING": "string"})
...
ctx.languages.unregister(handler) # restores what it displaced
register takes the same shape as a shipped colorizers/*.json file
(or a whole definition= dict): lexer names an existing backend lexer
(the host gains no new lexers at runtime), styles maps that lexer's own
style names onto semantic categories, and keywords addresses its word
lists by the lexer's own slot description — the host resolves both exactly
as it does for a shipped file, so you never spell out engine ids.
Registering an id that already exists replaces it for as long as you hold
the handler id; plugin shutdown withdraws anything still held.
document_settings¶
Read-only, document-scoped settings that layer a project's .editorconfig
over your own ctx.settings. For key k and document doc, a read
resolves mte.<plugin.id>.<k> from the .editorconfig that applies to that
document, then falls back to ctx.settings, then to your default:
wrap = ctx.document_settings.get_bool(doc, "wrap", False)
width = ctx.document_settings.get_int(doc, "width", 80)
ratio = ctx.document_settings.get_double(doc, "ratio", 1.0)
name = ctx.document_settings.get_string(doc, "profile", "")
ctx.document_settings.contains(doc, "wrap") # set in either layer?
Writes go through ctx.settings — never back into .editorconfig.
diagnostics¶
Push squiggles and margin glyphs for a document — from a linter, a
language server, a build log. The host renders them; you never touch the
view. Publishing replaces everything you previously published for that
(document, source) pair, so send the complete set each time:
ctx.diagnostics.publish(doc, "mylinter", [
mte.Diagnostic(start=12, end=18, message="unused variable",
severity=mte.SEVERITY_WARNING, code="W0612"),
])
ctx.diagnostics.publish(doc, "mylinter", []) # retract only your own
ctx.diagnostics.clear(doc) # clears EVERY producer's set on doc
source is required — it is what keeps your set separate from other
producers' (the LSP plugin, the spell checker) on the same document.
Severities: SEVERITY_ERROR / SEVERITY_WARNING / SEVERITY_INFORMATION
/ SEVERITY_HINT. Offsets are half-open [start, end) byte ranges.
theme¶
Register the colours your own UI paints as named, themeable categories; the Theme Editor lists them automatically and the user's overrides round-trip in the theme file:
h = ctx.theme.register_category("com.example.my/match", "Match highlight",
default_light=0xFFF3B0, default_dark=0x8A6D00)
rgb = ctx.theme.resolve_color("com.example.my/match") # 0xRRGGBB int
themed = ctx.theme.has_override("com.example.my/match")
variant = ctx.theme.active_variant() # "light" / "dark" / ""
cats = ctx.theme.categories() # list[mte.ThemeCategory]
ctx.theme.unregister_category(h)
Re-read resolve_color on ctx.events.on_theme_changed.
compare¶
Open (or focus) the editor's side-by-side compare view over two sources.
Each side is a file on disk (path=), literal text (content=), or both
(content is shown, the path gives session persistence a restorable
identity):
shown = ctx.compare.open(
mte.CompareSource("before", path="/tmp/a.txt"),
mte.CompareSource("after", content=new_text),
ignore_whitespace=True) # ignore_case / ignore_eol too
Returns False when a side cannot be read.
settings¶
Persistent typed key/value store, scoped to plugins/<your.id>/ on the
host. All methods synchronous; get/set are inferred from the default's
Python type.
ctx.settings.set("wrap", True)
ctx.settings.set("count", 42)
ctx.settings.set("theme", "dark")
wrap = ctx.settings.get("wrap", False) # returns bool
count = ctx.settings.get("count", 0) # returns int
theme = ctx.settings.get("theme", "light") # returns str
Types:
Python type of default |
Wire type | Return type from get() |
|---|---|---|
bool |
bool |
bool |
int (non-bool) |
int |
int |
float |
double |
float |
str (or None) |
string |
str |
Also:
ctx.settings.contains("wrap") # bool
ctx.settings.remove("stale_key")
ctx.settings.sync() # optional; host also syncs on shutdown
React to changes made in the Preferences dialog by subscribing to
settings.changed (ctx.events.on_settings_changed; see the
Events reference).
app_settings¶
Read-only view of the editor's app-wide config. The well-known key names
are exported as constants in mte.keys (mirroring the C++ header
PluginApi/AppSettingsKeys.h, which stays the authoritative list):
import mte
size = ctx.app_settings.get(mte.keys.EDITOR_FONT_SIZE, mte.keys.DEFAULT_EDITOR_FONT_SIZE)
family = ctx.app_settings.get(mte.keys.EDITOR_FONT_FAMILY, "") # "" = host default
theme = ctx.app_settings.get(mte.keys.UI_THEME, mte.keys.DEFAULT_UI_THEME) # "system"/"light"/"dark"
Setters, remove(), and sync() on ctx.app_settings raise
RuntimeError — the view is intentionally read-only.
completions¶
Register a completion provider that participates in the editor's popup:
@ctx.completions.provider(languages=["python"])
def _complete(req: mte.CompletionRequest):
if not req.prefix:
return []
return [mte.CompletionItem(text=w, kind=mte.KIND_KEYWORD)
for w in ("hello", "hello_world", "helper")
if w.startswith(req.prefix)]
The wrapped callable is invoked for every keystroke (or Ctrl+Space)
whose document matches the language filter. Fields on req:
| Field | Type | Meaning |
|---|---|---|
document |
int |
Document being edited |
prefix |
str |
Word fragment immediately before the caret |
position |
int |
Caret byte offset |
language_id |
str |
Colorizer id ("python", "cpp", …) |
manual |
bool |
True when the user pressed Ctrl+Space; False for auto-trigger |
Return a list of mte.CompletionItem (or plain strings, or dicts with
{text, detail, kind}). Kinds: mte.KIND_KEYWORD / KIND_WORD /
KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.
Deadline
The host sends your provider a keystroke request with a ~30 ms deadline. If your callback misses it, the popup shows without your candidates for that keystroke. Build any expensive index in the background and answer from cache — never do I/O in the callback. LSP-style network completion belongs in a future async surface, not here.
languages=[] (or omitted) means "every document". Multiple providers
per plugin are allowed; each decorator registers its own.
data_dir¶
Returns the per-plugin writable directory the host has already created
for you. Use for caches, indexes, and any data you own.
ctx.diagnostics_dir() similarly returns the shared crash/log
directory (read-only for you — the crash reporter writes there).
docks¶
Declarative dock panels — a side-panel UI built from a form spec,
not from Qt widgets. See mte.ui for the builder
vocabulary.
from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button
def register(ctx):
def _on_needle_changed(value: str) -> None:
ctx.log.info(f"needle -> {value!r}")
needle_handler = ctx.docks.on_change(_on_needle_changed)
panel_id = ctx.docks.add_panel(
id="renamer.panel",
title="Renamer",
area="right", # left / right / top / bottom
form=Column([
Label(text="Search:"),
LineEdit(id="needle", placeholder="text...",
binds="settings:lastNeedle",
on_change=needle_handler),
Checkbox(id="cs", label="Case sensitive",
binds="settings:caseSensitive"),
Row([Button(text="Run", on_click="renamer.run")]),
]),
)
ctx.docks.add_panel(*, id=None, title="", area="right", form, initially_visible=True, proportional_size=0.0, tab_group="") -> str¶
Registers a dock panel. Returns the panel id (auto-allocated when
id is None).
id(str, optional) — stable identifier used byctx.ui.get / ctx.ui.setto address controls inside this panel. If you don't supply one, the host allocates apy-dock/Nid and returns it.title(str) — shown on the dock header.area(str) —"left"/"right"/"top"/"bottom". Unknown values fall back to"right".form(dict) — a spec built with themte.uibuilders.initially_visible(bool) — whether the dock is shown when the plugin loads. Defaults toTrue. PassFalsefor a panel that should not pop up on startup and reveal it later withset_panel_visible.proportional_size(float) — the panel's default size as a share of the main window: its width in the"left"/"right"areas, its height in"top"/"bottom".0.0(the default) keeps the pixel behaviour a tool panel wants — the panel holds its size and a window resize leaves it alone. A fraction such as0.4is what a preview wants: maximising the window widens the panel with it. Only the default is yours; once the user drags the panel's separator the host adopts and remembers their share. Out-of-range values are clamped, and the host keeps a minimum extent for both the panel and the editor. SeeproportionalSizefor the full rules.tab_group(str) — panels with the same non-empty group id in the sameareashare one dock as tabs instead of sitting side by side. Namespace it like a panel id ("renamer/panels"). Only the default arrangement: a layout the user saved wins. SeetabGroup.
ctx.docks.is_panel_visible(handler_id) -> bool¶
Whether a panel added with add_panel is open — a panel tabbed behind
another counts as open; one registered with initially_visible=False and
never shown, a closed one and an unknown id do not. Pair it with
set_panel_visible to show your panels for a task and put back what the user
had:
was_open = ctx.docks.is_panel_visible(panel)
ctx.docks.set_panel_visible(panel, True)
# ... when the task ends:
ctx.docks.set_panel_visible(panel, was_open)
ctx.docks.set_panel_visible(handler_id, visible)¶
Reveals or hides a panel added with add_panel. Showing also raises the
panel above the docks it shares an area with, so a command that "opens"
its panel really puts it in front of the user. Idempotent — repeat the
same value freely, no state to track; silently ignored for an unknown
id.
panel = ctx.docks.add_panel(id="mine.panel", title="Mine", form=form,
initially_visible=False)
@ctx.commands.command("mine.show", title="Show My Panel")
def _show():
ctx.docks.set_panel_visible(panel, True)
ctx.docks.on_change(callback) -> str¶
Allocates a handler id for wiring a control's on_change field to
a Python callable. The callback receives the new value as a
string; boolean controls deliver "true" / "false", numeric
controls deliver the integer as a string.
def _on_volume(value: str) -> None:
ctx.log.info(f"volume={int(value)}")
handler = ctx.docks.on_change(_on_volume)
form = Column([NumberSpin(id="volume", default=5, on_change=handler)])
on_change handler ids are prefixed py-dock-cb/N and never
collide with command / event / completion handler ids.
The callback is invoked on the worker's event loop, NOT the thread
pool — keep it fast. Use ctx.log, mutate a small in-memory state,
or fire a follow-up command; don't do heavy work here.
line_markers¶
Margin glyphs and whole-line tints attached to document lines that follow the user's edits — breakpoints, a current-statement arrow, change strips — and the claim on the click in the bookmark column. The native service and all its rules are described under Line markers & the margin click.
bp_colour = ctx.theme.register_category("myplugin.breakpoint", "Breakpoint",
default_light=0xCC0000,
default_dark=0xFF5555)
bp = ctx.line_markers.register_kind(glyph=mte.GLYPH_CIRCLE,
glyph_color="myplugin.breakpoint",
line_tint="", z_order=50)
marker = ctx.line_markers.add_marker(bp, doc, line) # 0 = refused
ctx.line_markers.line_of(marker) # where it is NOW; -1 = gone
ctx.line_markers.markers(bp, doc) # [mte.LineMarkerPlacement(id, line)]
ctx.line_markers.remove_marker(marker)
ctx.line_markers.clear_markers(bp, doc) # doc 0 = every document
ctx.line_markers.release(bp) # removes the kind and its markers
register_kind(glyph=, glyph_color=, line_tint=, z_order=)—glyphis one ofmte.GLYPH_NONE(tint only),GLYPH_CIRCLE,GLYPH_CIRCLE_HOLLOW,GLYPH_CIRCLE_BAR,GLYPH_ARROW,GLYPH_STRIP; the colours are ids of categories you registered withctx.theme. The editor has only a few marker slots for all plugins together, so this can fail — it raisesRpcErrorthen. Register your kinds once, inregister().add_marker(kind, doc, line)— 0-based line, not clamped: a line outside the document returns 0 and places nothing.- Markers move with their lines, survive a reload on the same line number,
and disappear with their document — re-add yours on
ctx.events.on_document_opened.
The margin click¶
@ctx.events.on_margin_clicked
def _clicked(ev): # mte.MarginClicked
if ev.modifiers & mte.MODIFIER_CONTROL:
edit_condition(ev.document, ev.line)
else:
toggle_breakpoint(ev.document, ev.line)
claim = ctx.line_markers.claim_margin_click()
Every left click in the bookmark column is published as margin.clicked
(document, line, margin = "symbol", modifiers — bits
mte.MODIFIER_SHIFT / MODIFIER_CONTROL / MODIFIER_ALT /
MODIFIER_META). By default the editor also toggles a bookmark on it; while
any plugin holds claim_margin_click() it does not, and leaves the click to
the plugins. ctx.line_markers.release(claim) gives the click back.
settings_page¶
Adds a page under the editor's Preferences dialog. Same form
spec vocabulary as dock panels; same binds="settings:<key>"
persistence.
from mte.ui import Column, Checkbox, NumberSpin
def register(ctx):
ctx.settings_page.register(
category="Plugins/Renamer", # slash-separated tree path
title="Renamer",
form=Column([
Checkbox(id="cs", label="Case sensitive by default",
binds="settings:caseSensitive"),
NumberSpin(id="results", default=100, min=1, max=10_000,
binds="settings:resultCap"),
]),
)
ctx.settings_page.register(*, category, title, form, scope="") -> str¶
category(str, required) — slash-separated path in the Preferences tree, e.g."Plugins/My Plugin". Nested categories arrive as tree nodes.title(str, required) — heading shown above the page.form(dict, required) — a spec built with themte.uibuilders.scope(str, optional) — event scope emitted onSettingsChangedafter the user hits OK / Apply.
Returns the handler id.
Cancel does NOT roll back changes
Controls with binds="settings:<key>" persist eagerly as the
user edits, so hitting Cancel in the Preferences dialog
does not undo them. This is a known Phase 3 limitation; a
future refinement will wrap the underlying settings in a
buffering proxy.
ui¶
Reads and writes control values inside panels you already registered. Values are always strings on the wire.
def register(ctx):
panel_id = ctx.docks.add_panel(id="renamer.panel", ...)
@ctx.commands.command(id="renamer.echo", title="Echo needle")
def _echo():
needle = ctx.ui.get(panel_id, "needle") or ""
cs = (ctx.ui.get(panel_id, "cs") or "false") == "true"
ctx.status.show(f"needle={needle!r} case={cs}")
@ctx.commands.command(id="renamer.clear", title="Clear needle")
def _clear():
ctx.ui.set(panel_id, "needle", "")
ctx.ui.get(panel_id, control_id) -> Optional[str]¶
Reads the current value of a control. Returns None when the
panel or control is unknown (not an error — treat both None
and "" as "empty").
- Label / Button / LineEdit / ComboBox — returns the text.
- Checkbox — returns
"true"or"false". - NumberSpin — returns the integer as a decimal string.
- List — returns the currently selected item's text (empty when nothing is selected).
ctx.ui.set(panel_id, control_id, value) -> bool¶
Writes a value into a control programmatically. Returns True
when the write succeeded, False when the panel/control is
unknown or the value can't be coerced (e.g. a non-integer sent to
a NumberSpin).
Python bool True / False are converted to "true" / "false"
before being sent; everything else goes through str().
Host-driven set does NOT re-fire the control's on_change
callback, so you can update your own widgets from a command
handler without infinite loops.
Runtime bindings summary (Phase 3)¶
| Form spec field | Runtime behaviour |
|---|---|
id="…" |
Address the control from ctx.ui.get / ctx.ui.set. |
binds="settings:<key>" |
Reads initial value from ctx.settings; writes back on every user edit. |
on_click="<cmd>" |
Buttons invoke the registered command id on click. |
on_change=handler |
Fires the callback registered via ctx.docks.on_change(...) on user edit. |
Same rules apply on dock panels and Preferences pages.