Pisanie wtyczki Pythona dla Modular Text Editor z Claude¶
Odbiorca: asystent AI (Claude / ChatGPT / itp.). Ta strona to samowystarczalny zestaw instrukcji: przekaż ją asystentowi razem z zadaniem („napisz mi wtyczkę, która robi X"), a asystent powinien wyprodukować kompletny, poprawny, działający folder wtyczki bez czytania reszty dokumentacji.
Jeśli jesteś człowiekiem piszącym wtyczkę ręcznie, zacznij tutaj — ta strona jest zoptymalizowana pod konsumpcję przez AI i pomija przyjazne przykłady.
1. Co produkujesz¶
Folder z dokładnie dwoma plikami (plus opcjonalne dodatki):
Zwróć folder jako blok kodu per plik. Użytkownik albo wrzuca folder do
katalogu wtyczek edytora, albo zipuje folder, zmienia nazwę archiwum na
<nazwa>.mteplugin i instaluje przez
Preferencje → Wtyczki → Zainstaluj…. Obie ścieżki wymagają restartu do
załadowania.
Oba pliki są opisane wyczerpująco niżej. Trzymaj się reguł dosłownie — host wykonuje statyczną sondę i odrzuca wtyczki, które je łamią.
2. plugin.json — szablon kanoniczny¶
{
"id": "org.example.<short-name>",
"name": "<Human Display Name>",
"version": "1.0.0",
"vendor": "<Author or Team>",
"description": "<One sentence about what it does.>",
"apiVersion": 1,
"entry": "python",
"module": "main",
"order": 900,
"menus": [
{ "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
}
Twarde reguły¶
"apiVersion"musi być1(bieżąca wersja hosta)."entry"musi być"python"— nic innego nie kieruje do tego backendu."module"musi być"main", chyba że istnieje jawny powód, by nazwać moduł inaczej — i nigdy ścieżka z kropkami (Faza 1 wspiera tylko płaskie nazwy modułów)."id"musi być w stylu odwróconego DNS (org.<vendor>.<name>) i globalnie unikalne wśród wtyczek użytkownika. Jeśli użytkownik nie podał vendora, użyjorg.example..menus[]musi zawierać co najmniej jeden wpis, któregotitleodpowiada skrajnie lewemu segmentowi każdej ścieżki menu użytej wmain.py. Dla rzeczy skierowanych do użytkownika, które nie pasują oczywiście do menu rdzenia, preferuj"Plugins".
Pola opcjonalne¶
"pyRequires": []— tablica łańcuchów wymagań pip (np.["requests>=2.31"]). Gdy niepusta, edytor przygotowuje venv per wtyczka przy pierwszym starcie (pip install— ten jeden raz potrzebna sieć) i uruchamia wtyczkę pod nim. Preferuj bibliotekę standardową, gdy wystarcza (brak opóźnienia provisioning-u, działa offline); deklarujpyRequires, gdy zadanie naprawdę wymaga pakietu zewnętrznego, i powiedz użytkownikowi, że pierwszy start zatrzyma się na instalację."permissions": []— zgrubne deklaracje możliwości (np.["network"]), pokazywane tylko do odczytu na stronie Wtyczek. Deklaruj"network", gdy wtyczka wykonuje połączenia wychodzące; to uczciwa etykieta dla użytkownika, nie egzekwowanie.
Wartości, których NIE wymyślaj¶
- Żadnych pól spoza listy powyżej. Host ignoruje nieznane klucze; dodawanie ich to ryzyko utrzymaniowe.
3. main.py — szkielet kanoniczny¶
"""<Short description of what this plugin does.>"""
import mte # noqa: F401 -- pulls in dataclasses for event payloads
def register(ctx):
# Register commands, menu items, and event subscriptions here.
# Everything below is illustrative -- delete what you don't need.
@ctx.commands.command(id="<id>.<verb>",
title="<Human Label>",
shortcut="Ctrl+Alt+<letter>", # optional
category="<Group>") # optional
def _handler():
ctx.status.show("hello", timeout_ms=2000)
ctx.log.info("<id>: handler ran")
ctx.menus.add_item("Plugins/<Human Label>/<Item>", "<id>.<verb>")
Twarde reguły¶
register(ctx)MUSI być funkcją najwyższego poziomu. Statyczna sonda wymaga linii zaczynającej się oddef register(w kolumnie 0. Zagnieżdżenie w klasie, dekorowanie na poziomie modułu alboasync defpowoduje ciche odrzucenie wtyczki przy wykrywaniu.- Importuj z
mte, gdy subskrybujesz zdarzenia — typowane dataclassy (mte.DocumentSaveditd.) pochodzą z tego pakietu.import mtena górze jest bezpieczny nawet nieużywany wprost;# noqa: F401wyżej ucisza linter. - Nigdy
print(). Stdout workera to kanał RPC; pisanie do niego psuje protokół. Używajctx.log.info / warning / error. - Nigdy nie
importuj niczego niezadeklarowanego. Poza biblioteką standardową imtekażdy import zewnętrzny musi wystąpić wpyRequireswplugin.json(edytor pip-instaluje go do venv wtyczki przy pierwszym starcie) albo być vendorowany w folderze wtyczki. Niezadeklarowany import rzucaModuleNotFoundErrorna maszynie użytkownika. - Handlery poleceń / zdarzeń mogą krótko blokować — działają w puli
wątków workera, więc synchroniczne wywołania
ctx.editor/ctx.settings/ctx.data_dirsą legalne. Ale callbacki dostawców uzupełnień (@ctx.completions.provider(...)) NIE MOGĄ blokować: działają na pętli zdarzeń z deadlinem ~30 ms. Bez I/O, bez wywołańctx.editor.*, tylko cache. Indeksy buduj gdzie indziej (np. nadocument.opened) i odpowiadaj z dicta. - Nie importuj Qt, PySide, PyQt ani Scintilli. Nie są dostępne w workerze.
- Identyfikatory poleceń muszą mieć prefiks przestrzeni nazw Twojej
wtyczki, aby uniknąć kolizji z rdzeniem / innymi wtyczkami.
<id>.<verb>wyżej; np.wordcount.count,hello.sayHi. - Ścieżki menu muszą zaczynać się od menu najwyższego poziomu
zadeklarowanego też w
menus[]wplugin.json. Konwencja:"Plugins/<Nazwa wtyczki>/<Czasownik>".
4. Kompletne API — wszystko na ctx¶
Masz dwanaście usług (Fazy 1 + 2 + 3). Cokolwiek niewymienione tutaj
nie istnieje — nie wymyślaj usług. (Venvy per wtyczka deklaruje się przez
pyRequires, nie usługę ctx; substrat protokołu streaming/cancel nie ma
jeszcze API dla wtyczek.)
ctx.commands¶
Dekorator. Rejestruje polecenie i wiąże _handler (funkcję bezargumentową
zwracającą None). Wyjątki w _handler łapie worker i pisze na stderr;
nie wywracają edytora.
ctx.menus¶
ctx.menus.add_item(path, command_id) # -> handler_id (str)
ctx.menus.add_separator(path) # -> handler_id (str)
ctx.menus.set_item_checked(handler_id, checked) # -> None
path to "Top/Sub/…/Etykieta". Pośrednie podmenu powstają na żądanie.
set_item_checked zamienia pozycję w zaznaczalny wpis menu i ustawia
znacznik. Używaj do trwałych przełączników wł./wył. Pierwsze wywołanie
promuje; kolejne tylko przełączają. Cichy no-op na nieznanych id.
ctx.events¶
Sześć tematów, każdy z dekoratorem-cukrem i formą surową.
| Cukier | Surowy temat | Ładunek |
|---|---|---|
@ctx.events.on_document_opened |
document.opened |
mte.DocumentOpened(document: int, path: str) |
@ctx.events.on_document_closed |
document.closed |
mte.DocumentClosed(document: int) |
@ctx.events.on_document_saved |
document.saved |
mte.DocumentSaved(document: int) |
@ctx.events.on_selection_changed |
selection.changed |
mte.SelectionChanged(document: int) |
@ctx.events.on_active_document_changed |
document.activeChanged |
mte.ActiveDocumentChanged(document: int) |
@ctx.events.on_app_about_to_quit |
app.aboutToQuit |
mte.AppAboutToQuit() |
Forma surowa:
handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id) # to unbind
Nie wymyślaj nowych tematów — forma surowa je waliduje i rzuca
ValueError na wszystko spoza tabeli.
ctx.log¶
Wyślij i zapomnij. Linie mają prefiks [python:<plugin.id>] w logu
diagnostycznym hosta.
ctx.status¶
level musi być "info" / "warning" / "error". timeout_ms=0 znaczy,
że host wybiera domyślny.
ctx.editor (Faza 2)¶
Każda metoda jest synchroniczna: blokuje wątek wywołujący. W porządku z
callbacku polecenia / zdarzenia def (worker odpala handlery sync na puli
wątków); nielegalne z handlerów async def i z dostawców
ctx.completions (działających wprost na pętli zdarzeń).
Dokumenty adresują nieprzezroczyste identyfikatory int (0 = brak
dokumentu).
doc = ctx.editor.active_document()
docs = ctx.editor.open_documents()
ctx.editor.set_active_document(doc)
path = ctx.editor.file_path(doc) # str or None
text = ctx.editor.text(doc)
slice = ctx.editor.text_range(doc, start, end) # [start, end) bytes
ctx.editor.set_text(doc, "new text")
n = ctx.editor.length(doc)
sel = ctx.editor.selection_range(doc) # mte.MatchRange
s = ctx.editor.selected_text(doc)
ctx.editor.replace_selection(doc, "hi")
ctx.editor.set_selection(doc, mte.MatchRange(4, 9))
pos = ctx.editor.caret(doc) # mte.EditorPosition
ctx.editor.set_caret(doc, mte.EditorPosition(line=5, column=0))
new_doc = ctx.editor.open_file("/path/to/file") # 0 on failure
ok = ctx.editor.save_document(doc)
dirty = ctx.editor.is_modified(doc)
hit = ctx.editor.find_in_range(doc, "TODO", 0,
ctx.editor.length(doc),
mte.SearchOptions(match_case=True))
new_end = ctx.editor.replace_target(doc, hit, "DONE",
mte.SearchOptions())
style = ctx.editor.indentation(doc) # IndentationStyle
ctx.editor.set_indentation(doc,
mte.IndentationStyle(use_tabs=False, width=2))
lang = ctx.editor.colorizer_id(doc) # "python", "cpp", ...
mte.SearchOptions.mode: "normal" / "extended" / "regex".
mte.SearchOptions.direction: "forward" / "backward".
ctx.settings / ctx.app_settings (Faza 2)¶
Typowany magazyn klucz/wartość per wtyczka (ctx.settings) plus widok
tylko do odczytu ustawień całej aplikacji (ctx.app_settings). Typ
wnioskowany z pythonowego typu wartości domyślnej.
ctx.settings.set("wrap", True)
ctx.settings.set("count", 42)
ctx.settings.set("theme", "dark")
wrap = ctx.settings.get("wrap", False) # bool
count = ctx.settings.get("count", 0) # int
theme = ctx.settings.get("theme", "light") # str
ctx.settings.contains("wrap")
ctx.settings.remove("stale")
# App-wide (read-only)
tw = ctx.app_settings.get("editor.tabWidth", 4)
ctx.app_settings.set/remove/sync rzucają RuntimeError. We własnej
logice odczytu sprawdzaj bool przed int (w Pythonie
isinstance(True, int) == True).
ctx.completions (Faza 2)¶
@ctx.completions.provider(languages=["python"])
def _complete(req: mte.CompletionRequest):
return [mte.CompletionItem(text=w, kind=mte.KIND_KEYWORD)
for w in ("hello", "hello_world")
if w.startswith(req.prefix)]
Pola req: document, prefix, position, language_id, manual.
Rodzaje: KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET /
KIND_UNKNOWN. Zwracaj list[CompletionItem], gołe łańcuchy albo dicty.
Callback MUSI wrócić w ~30 ms, inaczej host porzuca Twoje wyniki dla
tego naciśnięcia — bez I/O, bez ctx.editor.text(...), tylko cache.
Indeksy buduj gdzie indziej (np. na document.opened).
ctx.data_dir() / ctx.diagnostics_dir() (Faza 2)¶
Używaj data_dir() na cache i indeksy. diagnostics_dir() jest tylko do
odczytu (pisze tam raporter awarii).
ctx.docks — deklaratywne panele dokowane (Faza 3)¶
from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button
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="myplugin.panel", # returned; use it for ctx.ui.get / ctx.ui.set
title="My Plugin",
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="myplugin.run")]),
]),
)
Specyfikację formularza buduj pomocnikami mte.ui. Słownik
kontrolek jest stały: Label / Button / Checkbox / LineEdit / NumberSpin /
ComboBox / List w Column / Row / Group.
binds="settings:<klucz>"— wartość kontrolki utrwala się automatycznie przezctx.settings(ten sam magazyn coctx.settings.set).on_click="<cmd.id>"— tylko Button; wywołuje zarejestrowane polecenie.on_change=<handler>— alokuj przezctx.docks.on_change(callback); odpala się na edycjach użytkownika.
Callbacki on_change działają na pętli zdarzeń workera (NIE na puli
wątków). Niech będą tanie — bez wywołań edytora, bez długiej pracy. Zmień
mały stan w pamięci, zaloguj albo odpal polecenie; ciężką pracę rób z
polecenia.
ctx.settings_page — deklaratywne strony Preferencji (Faza 3)¶
from mte.ui import Column, Checkbox, NumberSpin
ctx.settings_page.register(
category="Plugins/My Plugin", # slash-separated tree path
title="My Plugin",
form=Column([
Checkbox(id="verbose", label="Verbose logging",
binds="settings:verbose"),
NumberSpin(id="cap", default=100, min=1, max=10_000,
binds="settings:cap"),
]),
)
Te same kreatory, ta sama semantyka binds. Dwie kontrolki dzielące
binds="settings:<klucz>" odzwierciedlają się w czasie rzeczywistym —
panel dokowany i strona Preferencji mogą obie trzymać checkbox tego samego
ustawienia, a przełączenie jednego przełącza drugi.
Zastrzeżenie Fazy 3
Wartości utrwalają się na bieżąco przy każdej edycji użytkownika — Anuluj nie wycofuje tego, co zmienił. Poprawi się to w przyszłym udoskonaleniu.
ctx.ui.get / ctx.ui.set — dostęp runtime do kontrolek panelu (Faza 3)¶
# Read the current value; returns str or None.
current = ctx.ui.get(panel_id, "needle")
# Write. Returns bool (False when panel/control unknown, or when the
# value can't be coerced to the target type).
ctx.ui.set(panel_id, "needle", "hello")
ctx.ui.set(panel_id, "cs", True) # bool -> "true"/"false"
get zwraca wartość główną jako łańcuch:
- Label / Button / LineEdit / ComboBox → tekst
- Checkbox →
"true"/"false" - NumberSpin → liczba dziesiętnie
- List → tekst wybranej pozycji (pusty łańcuch, gdy brak)
set sterowany przez hosta jest cichy: NIE odpala ponownie on_change
kontrolki, więc możesz aktualizować własne widżety z handlera polecenia bez
nieskończonych pętli.
5. Pełny przykład — wtyczka wordcount¶
Skopiuj dosłownie i zmień id, name i title pod swój przypadek.
Ćwiczy każdą usługę Fazy 1.
{
"id": "org.example.wordcount",
"name": "Word Count",
"version": "1.0.0",
"vendor": "Example, Inc.",
"description": "Counts words the user has typed since launch and logs saves.",
"apiVersion": 1,
"entry": "python",
"module": "main",
"order": 900,
"menus": [
{ "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
}
"""Reports a session word-count total via a menu command and logs saves."""
import mte # noqa: F401
# Module-level state is fine -- one worker per plugin, one register() call.
_state = {"saves": 0}
def register(ctx):
ctx.log.info("wordcount: register() ran")
@ctx.commands.command(id="wordcount.report",
title="Report save count",
shortcut="Ctrl+Alt+W",
category="Text")
def _report():
n = _state["saves"]
ctx.status.show(f"{n} save(s) this session", timeout_ms=2500)
ctx.log.info(f"wordcount: user asked for count; {n} saves so far")
ctx.menus.add_item("Plugins/Word Count/Report save count",
"wordcount.report")
@ctx.events.on_document_saved
def _saved(ev: mte.DocumentSaved):
_state["saves"] += 1
ctx.log.info(f"wordcount: doc {ev.document} saved -- total now {_state['saves']}")
@ctx.events.on_app_about_to_quit
def _quit(_ev):
ctx.log.info(f"wordcount: shutting down after {_state['saves']} saves")
Oczekiwany przepływ: otwórz edytor, zapisz plik trzy razy, potem kliknij
Plugins → Word Count → Report save count. Pasek stanu pokaże
3 save(s) this session. Log rejestruje każdy krok.
6. Częste zadania — przepisy kopiuj-wklej¶
Każdy przepis to snippet samego ciała: wklej wewnątrz def register(ctx):.
Dodaj polecenie w menu¶
@ctx.commands.command(id="mine.hello", title="Say Hi", shortcut="Ctrl+Alt+Y")
def _hi():
ctx.status.show("hi")
ctx.menus.add_item("Plugins/Mine/Say Hi", "mine.hello")
Reaguj na zapis¶
Ostrzeż przy wyjściu¶
@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
ctx.log.warning("mine: last chance to persist state")
Loguj na konkretnym poziomie¶
ctx.log.trace ("very fine")
ctx.log.debug ("dev")
ctx.log.info ("routine")
ctx.log.warning("odd")
ctx.log.error ("wrong")
Separator menu¶
Kilka powiązanych poleceń¶
@ctx.commands.command(id="mine.a", title="Do A")
def _a(): ...
@ctx.commands.command(id="mine.b", title="Do B")
def _b(): ...
ctx.menus.add_item("Plugins/Mine/Do A", "mine.a")
ctx.menus.add_item("Plugins/Mine/Do B", "mine.b")
Trwały przełącznik wł./wył. z widocznym znacznikiem¶
KEY = "verbose"
item = ctx.menus.add_item("View/Verbose Logging", "mine.toggleVerbose")
# Seed the mark from the persisted state so it matches on launch.
ctx.menus.set_item_checked(item, ctx.settings.get(KEY, False))
@ctx.commands.command(id="mine.toggleVerbose", title="Verbose Logging")
def _toggle():
on = not ctx.settings.get(KEY, False)
ctx.settings.set(KEY, on)
ctx.menus.set_item_checked(item, on)
Panel boczny z polem tekstowym, checkboxem i dwoma przyciskami¶
from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button
def _on_needle(value): ctx.log.info(f"needle={value!r}")
needle_h = ctx.docks.on_change(_on_needle)
panel = ctx.docks.add_panel(
id="mine.panel", title="Mine", area="right",
form=Column([
Label(text="Search:"),
LineEdit(id="needle", placeholder="text...",
binds="settings:needle", on_change=needle_h),
Checkbox(id="cs", label="Case", binds="settings:cs"),
Row([Button(text="Run", on_click="mine.run"),
Button(text="Clear", on_click="mine.clear")]),
]),
)
@ctx.commands.command(id="mine.run", title="Run")
def _run():
needle = ctx.ui.get(panel, "needle") or ""
cs = ctx.ui.get(panel, "cs") == "true"
ctx.status.show(f"searching for {needle!r} (case={cs})")
@ctx.commands.command(id="mine.clear", title="Clear")
def _clear():
ctx.ui.set(panel, "needle", "")
Strona Preferencji lustrzana do ustawień doku¶
from mte.ui import Column, Checkbox
# Both controls bind to the SAME settings key -- toggling one
# flips the other in real time.
ctx.docks.add_panel(id="mine.panel", title="Mine", area="right",
form=Column([Checkbox(id="cs", label="Case",
binds="settings:cs")]))
ctx.settings_page.register(category="Plugins/Mine", title="Mine",
form=Column([Checkbox(id="cs", label="Case sensitive",
binds="settings:cs")]))
7. Antywzorce — NIE rób tego¶
print() do diagnostyki¶
Źle:
Dobrze:
register w klasie¶
Źle:
Dobrze:
async def register¶
Źle:
Dobrze:
Importy zewnętrzne bez dostarczenia pakietu¶
Źle:
Dobrze (Faza 1): trzymaj się biblioteki standardowej. Jeśli naprawdę
potrzebujesz czegoś zewnętrznego, dostarcz wheel w folderze wtyczki i
rozszerz sys.path w pierwszych liniach main.py; zobacz
Pakowanie.
Importy Qt¶
Źle:
Dobrze: żadnego Qt. Całe UI, jakie możesz wnieść, idzie przez ctx.menus,
ctx.status i (od Fazy 3) deklaratywne panele dok/ustawień. Nigdy surowy
widżet.
Blokowanie w handlerze¶
Źle:
def _handler():
time.sleep(30) # <-- stalls the worker for 30s
result = requests.get("https://slow.example")
Dobrze (Faza 1): oddeleguj do wątku, komunikuj przez ctx.log:
def _handler():
import threading
def _work():
time.sleep(30)
ctx.log.info("done")
threading.Thread(target=_work, daemon=True).start()
ctx.status.show("started")
Ścieżka menu, której górny segment nie jest w plugin.json¶
Źle:
Dobrze: trzymaj górny segment ścieżki zgodny z menus[]. Powyżej użyj
"Plugins/Mine/Do".
Przechowywanie ctx na później¶
Źle:
_ctx = None
def register(ctx):
global _ctx
_ctx = ctx # <-- fine for the lifetime of this worker,
# but confusing and unnecessary
def some_other_function():
_ctx.log.info("...") # <-- called from where?
Dobrze: przechwyć potrzebne usługi w domknięciu, które ich używa.
register to miejsce okablowania; wszystko inne to handler, który już ma
dostęp przez domknięcie.
8. Checklist debugowania dla użytkownika¶
Dołącz to na końcu odpowiedzi, gdy produkujesz wtyczkę, aby użytkownik wiedział, co sprawdzić, gdy nie działa:
- Uruchom edytor z powłoki, w której interpreter Pythona 3.9+ jest w
PATH(alboMTE_PYTHONna niego wskazuje). - Poszukaj w logu
PluginHost: Python plugin backend enabled; interpreter=…. Jeśli brak, skocz do Rozwiązywania problemów. - Poszukaj
[python:<twoje.id>] register() ran(jeśli poszedłeś za przykłademwordcount) — dowód, żeregistersię wykonał. - Jeśli brakuje pozycji menu, sprawdź w logu wyjaśnienia
PythonPluginBackend: skipping '<id>': …. - Jeśli klik w menu nic nie robi, sprawdź w logu linie
[worker stderr] …— tam lądują pythonowe tracebacki.
9. Ograniczenia do powtórzenia na końcu każdej odpowiedzi¶
Gdy produkujesz wtyczkę, zawsze powiedz użytkownikowi:
- Zainstaluj na jeden z dwóch sposobów:
- zzipuj zawartość folderu jako
<nazwa>.mteplugini użyj Preferencje → Wtyczki → Zainstaluj… (waliduje pakiet, zanim czegokolwiek dotknie), albo - wrzuć folder do katalogu wtyczek użytkownika
(
%LOCALAPPDATA%/MTE/Plugins/na Windows;~/Library/Application Support/MTE/Plugins/na macOS;~/.local/share/MTE/Plugins/na Linuksie).
- zzipuj zawartość folderu jako
- Zrestartuj edytor.
- Ustaw
MTE_PYTHON, jeśli log mówi „no Python interpreter found". - Odeślij linię logu
[python:<id>] …, abyśmy potwierdzili działanie.
10. Źródło prawdy¶
Jeśli cokolwiek na tej stronie kłóci się z tym, co faktycznie implementuje
pakiet klienta Pythona, wygrywa pakiet. Leży obok edytora w
<exe>/Python/mte/ — przeczytaj to źródło przed rozstrzygnięciem
zachowania. Strona C++ protokołu jest w
Components/PluginHost/Src/PyServiceBridge.cpp, a specyfikacja przewodowa
w Python/PROTOCOL.md.