Polynya

Активная разработка

Меню документации

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