API модулей Polynya
Модуль Polynya - отдельная программа на любом языке: скрипт на Node.js или Python, .exe, скрипт PowerShell. Он лежит в своей папке рядом с манифестом module.toml и общается с ядром JSON-строками через stdin/stdout.
Интерфейс рисует ядро. Модуль отдает данные: экран из строк, чип уведомления, текст для копирования. Остров рисует все это в стиле панели задач, с ее темой, анимациями, навигацией с клавиатуры и вводом через IME. Модулю не нужен UI-фреймворк, и он ничего не знает о положении и цветах панели задач. Если модуль упадет, остров продолжит работать и покажет, что случилось.
Эта страница - справочник. Готовые модули с нуля - в примерах.
1. Папка и установка
%LOCALAPPDATA%\Polynya\modules\<id>\
├── module.toml
└── main.js (или main.py, tool.exe, ...)
В портативной версии папка модуля - modules\<id>\ рядом с polynya.exe, а лог лежит там же в logs\.
- Установка: положите папку в
modules\. Ядро подхватит ее при следующем открытии поиска, без перезапуска. Чтобы перезапустить работающие процессы модулей после правки кода, выполнитеpolynya modules reload. - Включение и выключение: в настройках острова, раздел "Модули". Применяется сразу.
- Удаление: удалите папку.
- Проблемы (сломанный манифест, не найден интерпретатор, падение) видны чипом в панели задач и в логе
%LOCALAPPDATA%\Polynya\logs\polynya.log.
2. Манифест module.toml
api = 1 # версия протокола; более новую, чем знает ядро, оно не примет
id = "devtools" # уникальный id: латиница, цифры, - и _
exe = "main.js" # что запускать, см. "Как запускается модуль"
args = ["--fast"] # необязательно: аргументы перед аргументами ядра
kind = "search" # необязательно: search | zone | other
name.en = "Devtools" # имя модуля по языкам (фоллбэк на en)
name.ru = "Девтулз"
resident = false # держать процесс запущенным со старта острова
hooks = false # вызывать `exe register` / `exe unregister` при включении / выключении
taskbar = false # работать, только пока панель задач горизонтальная
experimental = false # выключен, пока пользователь не включит
[main] # необязательно: модуль в поиске; открывает его главный интерфейс
hint.ru = "Веб-утилиты"
keywords = ["devtools", "px", "rem"]
icon = "m18 16 4-4-4-4 | m6 8-4 4 4 4 | m14.5 4-5 16"
[[functions]] # необязательно: функции главного интерфейса в поиске и на сочетаниях
id = "convert" # что модуль получит в `invoke`
title.en = "Convert value" # ровно как пункт называется в меню модуля
title.ru = "Преобразовать значение"
hint.ru = "Единицы, цвета"
keywords = ["convert", "px", "rem"]
icon = "M8 3 4 7l4 4 | M4 7h16 | m16 21 4-4-4-4 | M20 17H4"
hotkey = "Ctrl+Alt+D" # необязательно: глобальное сочетание
selection = true # необязательно: по сочетанию прислать выделенный текст
search = true # необязательно: false - не показывать в поиске (только сочетание и меню)
Строки по языкам (name, title, hint) - либо обычная строка, либо таблица по языкам: ru, en, zh, ja. Если нужного языка нет, берется en.
Главный интерфейс и его функции
Модуль - одна сущность с одним интерфейсом. Поиск, сочетания клавиш и меню - разные входы в него, а не отдельные возможности:
[main]добавляет в поиск сам модуль под егоname. Выбор запускаетmain, и модуль открывает свой главный интерфейс. У модуля без интерфейса (только хуки или чипы)[main]нет.[[functions]]- пункты этого интерфейса, до которых полезно добраться напрямую. У каждой функции обязан быть двойник в интерфейсе модуля с тем жеtitle: в поиске она показывается как "Девтулз › Преобразовать значение" - путь к тому же пункту в меню, и имя модуля ядро дописывает само. Функция не может быть тем, чего нет в интерфейсе. Ядро не может это проверить, поэтому это правило API.- В меню строка функции несет
"command": "convert"(см. Экраны). Ядро показывает на ней сочетание функции в том виде, как его задал пользователь, а выбор строки отправляетinvokeс"source": "menu". Функция приходит одинаково из всех трех мест, а с чего начинать, модуль решает поsource.
Поиск сопоставляет само ядро. Строка поиска сравнивается с названием (у функции - еще и с "имя модуля + название") и keywords сразу на всех языках, и процесс модуля не запускается, пока ничего не выбрали.
Сочетания клавиш (у [main] или функции) пишутся как модификаторы плюс клавиша, регистр не важен: Ctrl+Alt+D, Ctrl+Shift+F2, Win+Alt+Space. Нужен хотя бы один из Ctrl, Alt, Shift, Win. Клавиша - буква, цифра, F1–F24, Space, Enter, Tab, Insert, Home, End, PageUp, PageDown или один из ` - = , . / ;. Сочетание, занятое другой программой, пропускается с записью в логе. Пользователь может переназначить или выключить любое сочетание в config.ini:
hotkey.devtools.convert = Ctrl+Shift+Q
hotkey.devtools.convert =
Первая строка назначает Ctrl+Shift+Q, вторая, с пустым значением, выключает сочетание. Для [main] ключ - hotkey.<модуль>.main.
Иконка - SVG-путь в сетке 24×24 в стиле Lucide, рисуется одним штрихом. Отдельные контуры разделяются |. Поддерживаются команды M L H V C S Q T A Z в обоих регистрах.
Манифест с ошибкой пропускается, а причина попадает в лог и в чип, например devtools: manifest: id = "a b": use only latin letters, digits, - and _. Функция без title - тоже ошибка.
3. Как запускается модуль
Ядро запускает exe [args] run, рабочая папка - папка модуля:
exe |
Что запускается |
|---|---|
main.js, .mjs, .cjs |
node main.js run |
main.py, .pyw |
py -3 main.py run (или python, если нет лаунчера py) |
main.ps1 |
powershell -NoProfile -ExecutionPolicy Bypass -File main.ps1 run |
| файл в папке | этот файл |
| что-то другое | программа из PATH (deno, bun, ...); скрипт добавьте в args |
Когда работает процесс. Обычный модуль запускается при первой команде и остается жить, поэтому следующие команды отвечают мгновенно. Модуль с resident = true запускается вместе с островом. Процесс останавливается, когда модуль выключают, меняют его манифест или закрывают остров.
Как завершаться. EOF на stdin значит, что ядро закрылось или остановило модуль, и ему тоже пора выходить. Процесс, который через 3 секунды все еще жив, ядро завершает принудительно.
Переменные окружения:
| Переменная | Значение |
|---|---|
POLYNYA_MODULE |
id модуля |
POLYNYA_LANG |
язык интерфейса: ru, en, zh, ja |
POLYNYA_API |
версия протокола ядра |
POLYNYA_VERSION |
версия Polynya |
Еще ядро выставляет PYTHONUNBUFFERED, PYTHONIOENCODING=utf-8 и PYTHONUTF8, чтобы модулю на Python не пришлось делать это самому.
Вывод. Каждая строка из stderr попадает в лог Polynya. Если процесс упал (ненулевой код выхода), пользователь видит чип с последней строкой stderr. Модуль с resident, который упал, проработав не меньше 10 секунд, перезапускается. Строки в stdout, которые не являются сообщениями протокола, уходят в лог с причиной, а чтение продолжается.
hooks = true: при включении модуля ядро один раз вызывает exe register, а при выключении - exe unregister. Через эти вызовы модуль прописывает свои хуки в конфиги других программ (так работает встроенный модуль agents).
4. Протокол
Каждое сообщение - одна строка JSON с \n в конце, в кодировке UTF-8. Поле op говорит, что это за сообщение; неизвестные поля игнорируются. Сбрасывайте буфер stdout после каждой строки, иначе сообщение застрянет в буфере.
От ядра: stdin
| Сообщение | Когда приходит |
|---|---|
{"op":"invoke","command":"convert","source":"hotkey","query":"","selection":"font-size: 16px","clipboard":"#fff"} |
Запуск main или функции. source - откуда: menu (строка с command), search или hotkey. query - текст в строке поиска. selection - выделение в активной программе, только для функции с selection = true, вызванной сочетанием, и пустое, если ничего не выделено. clipboard - текст из буфера, всегда. С чего начинать, решает модуль. |
{"op":"select","id":"r1"} |
Выбрана строка item, toggle или button (Enter или клик). Строка item с command вместо этого отправляет invoke. |
{"op":"submit","id":"query","text":"16px 24px"} |
Enter в строке input. |
{"op":"closed"} |
Выпадашка модуля закрылась: Esc, клик мимо или открылась другая панель. |
Ядру: stdout
| Сообщение | Что делает |
|---|---|
{"op":"open","screen":{...}} |
Открывает выпадашку модуля с этим экраном. После команды из поиска становится страницей поиска вместо списка результатов. В остальных случаях открывается под блоком поиска. С "at":[left,right] (пиксели экрана) открывается под этим отрезком панели задач. |
{"op":"push","screen":{...}} |
Следующий экран в открытой выпадашке, с кнопкой "назад". |
{"op":"update","screen":{...}} |
Заменяет текущий экран на месте: выделение и набираемый текст остаются на своих строках по id. Для результатов, которые меняются по мере ввода, или для тикающего значения. |
{"op":"close"} |
Закрывает выпадашку. Если она отвечала на команду, фокус возвращается в окно, откуда команду вызвали, и пользователь может сразу вставить результат. |
{"op":"copy","text":"1rem"} |
Кладет текст в буфер обмена. |
{"op":"chip",...}, {"op":"dismiss",...} |
Чип уведомления, см. Чипы. |
Что ядро делает само: навигация стрелками, наведение, прокрутка длинных строк, "назад" (модулю об этом не сообщается, поэтому делайте id строк уникальными между экранами), Esc, закрытие по клику мимо, подсказки с полным текстом строк, обрезанных по ширине, и скринридеры: имена, роли и состояния берутся из самих строк, так что модулю для этого ничего делать не нужно.
После invoke из поиска или по сочетанию ядро до секунды ждет ответа модуля и только потом закрывает поиск. Модуль, который просто делает свое дело (копирует, показывает чип), отвечает close. Из меню функция обычно отвечает push: следующий экран с "назад".
5. Экраны
{"title": "Резиновый размер", "rows": [
{"type": "input", "id": "sizes", "text": "16px 24px", "placeholder": "Два размера"},
{"type": "divider"},
{"type": "item", "id": "r1", "text": "clamp(1rem, 0.8333rem + 0.7407vw, 1.5rem)", "hint": "clamp()"}
]}
title необязателен. У экрана с заголовком или с местом, куда можно вернуться, есть строка заголовка с кнопкой "назад".
type |
Поля | Что это |
|---|---|---|
item |
id, text, hint?, command?, keys?, confirm?, image? |
Выбираемая строка; hint приглушенно справа. command делает строку функцией модуля: ядро показывает сочетание функции в узкой колонке и вместо select отправляет invoke с "source": "menu". keys - показать сочетание, которое не принадлежит модулю (например, Win+L). С confirm (например, "Перезагрузить сейчас?") первое нажатие показывает этот вопрос с убывающей полосой, и срабатывает только второе нажатие в течение 3 секунд. image добавляет миниатюру 56×56 DIP слева; строка сохраняет своё действие, а клик по миниатюре открывает предпросмотр, если он включён. |
image |
image, caption? |
Изображение на ширину панели с сохранением пропорций и необязательной подписью снизу. Высота ограничена 360 DIP; слишком высокие изображения уменьшаются без обрезки. |
toggle |
id, text, on |
Строка с переключателем. Нажатие отправляет select, а модуль отвечает update с новым on. |
input |
id, text, placeholder? |
Текстовое поле. Нажатие начинает ввод с курсором под мышью; протяжка выделяет, двойной клик выделяет слово, длинный текст прокручивается, работают буфер обмена, Ctrl+Z и IME. Enter отправляет submit, Esc отменяет. text - текущее значение. |
button |
id, text, plain? |
Кнопка во всю ширину. plain: true рисует ее без фона. |
text |
text |
Приглушенный абзац; \n переносит строку. |
divider |
Тонкая линия. | |
section |
title |
Мелкая подпись над группой строк; пустой title - отступ. |
header |
title, subtitle |
Крупный заголовок с подписью под ним. |
pair |
label, value |
Подпись слева, значение справа. |
art |
lines, colors? |
Моноширинный текстовый рисунок по центру. colors[строка][символ] - #rrggbb для каждого символа, пустая строка - цвет текста. |
Длинный текст в любой строке обрезается по ширине, а у выбранной строки едет бегущей строкой.
6. Изображения (API 2)
Используйте ImageRef в строке item, в строке image или в чипе. Должен быть ровно один источник: path или data.
{"path": "C:/images/chart.png", "alt": "Нагрузка за неделю", "preview": true, "width": 1200, "height": 600}
{"data": "iVBORw0KGgoAAA...", "mime": "image/png", "alt": "Нагрузка за неделю", "preview": true}
| Поле | По умолчанию | Описание |
|---|---|---|
path |
null |
Абсолютный путь к файлу изображения. Относительные пути и URL не принимаются. |
data |
null |
Байты изображения в Base64. Альтернатива path; передавать оба поля нельзя. |
mime |
null |
Обязательный для data: image/png, image/jpeg, image/svg+xml, image/gif, image/bmp, image/x-icon, image/tiff или image/webp. |
alt |
"" |
Текст для скринридеров и заглушки во время загрузки или после ошибки декодирования. |
preview |
false |
Открывает исходное изображение по клику в приложении Windows, выбранном для этого формата по умолчанию. Данные из data сначала сохраняются во временный файл. |
width, height |
null |
Необязательные размеры оригинала: позволяют сохранить пропорции до декодирования. |
Растровые форматы декодируются через Windows Imaging Component (WIC), SVG рендерится через resvg; внешние ресурсы игнорируются. Текст в SVG может использовать установленные шрифты и CJK-фоллбэки; системный каталог шрифтов загружается только при наличии текста. Декодирование происходит в фоне и кэшируется, но не повторяется на каждом кадре. Полноширинное изображение ограничено высотой 360 DIP, миниатюра обрезается по центру до 56×56 DIP.
Ограничения: 16 МиБ на сообщение канала, 12 МиБ на исходное изображение и 16 мегапикселей. Неподдерживаемый формат, ошибка Base64, отсутствующий файл или превышение лимита показывают заглушку и предупреждение в логе.
7. Чипы
Чип - уведомление в правой зоне панели задач. Модуль пишет его в свой stdout. Любая другая программа отправляет его в именованный канал \\.\pipe\Polynya-<SessionId>, по одному JSON-сообщению до 16 МиБ на подключение, или командой polynya push.
{"op": "chip", "key": "build", "title": "Сборка готова", "body": "48 с", "icon": "ok", "ttl_ms": 5000}
| Поле | По умолчанию | Описание |
|---|---|---|
key |
обязательно | Id чипа. Чип с тем же key заменяет старый на месте. |
title |
обязательно | Короткий текст на чипе. |
body |
"" |
Подробности: бегущая строка при наведении и полный текст в раскрытом чипе. |
meta |
"" |
Мелкая подпись внизу раскрытого чипа: источник, время. |
icon |
"info" |
ok, fail, info, key, claude, agent или цветной кружок color:#rrggbb. |
ttl_ms |
null |
Время жизни в мс, считается с момента, когда чип встал первым. null - висит, пока его не закроют или не заменят. |
animated |
false |
Анимированная иконка: работа идет. |
source |
key |
Серия: чипы одного источника не ждут друг друга в очереди. |
popup |
false |
Раскрыть чип компактной выпадашкой, когда он встанет первым. |
image |
null |
ImageRef с миниатюрой 56×56 DIP в раскрытом уведомлении. |
swatches |
[] |
Цвета #rrggbb в раскрытом чипе; клик копирует цвет. |
mark |
null |
Индекс выделенного цвета в swatches. |
{"op": "dismiss", "key": "build"} убирает чип. Правила очереди и серий описаны в Уведомлениях.
8. Для Rust: крейт proto
В крейте proto из репозитория есть serde-типы для всех сообщений (ModIn, ModOut, Screen, Row, Msg) и несколько помощников:
proto::send(&msg)отправляет сообщение в канал и коротко повторяет попытку, пока канал занят. Если остров не запущен, сразу возвращает ошибку.proto::language()возвращает язык интерфейса:POLYNYA_LANG, затемconfig.ini, затем язык Windows.proto::info!,warn!,error!иdebug!пишут строку в общий лог. Каждая строка пишется одним системным вызовом, поэтому строки разных процессов не перемешиваются.
9. Лицензирование
Ядро Polynya распространяется под GPLv3, а спецификация протокола и крейт proto - под MIT. Модули можно писать на любом языке и публиковать под любой лицензией, в том числе закрытой и коммерческой. Ограничение одно - бренд: "модуль для Polynya" писать можно, а выдавать свою сборку или плагин за официальный продукт Polynya нельзя (см. TRADEMARKS.md).