Зачем это плагину

У API несколько сценариев использования:

  • получить текущую тему shell;
  • получить список тем;
  • создать (временную) тему прямо из плагина, а не просить пользователя руками забивать цвета;
  • переключить тему из команды, сценария или по алгоритму;
  • синхронизировать внешний UI или webview с палитрой MinaChan.

Встроенные темы сейчас такие:

  • light
  • dark
  • brand
Светлая, тёмная и фирменная тема MinaChan
Наглядное отображение светлой, темной и фирменной темы.

Что есть в API

В shell зарегистрированы шесть runtime-команд:

  • ui:theme-current
  • ui:theme-list
  • ui:theme-get
  • ui:theme-add
  • ui:theme-delete
  • ui:theme-activate

Пример ответа ui:theme-get:

{
  "theme": {
    "id": "brand",
    "name": "Brand",
    "brightness": "dark",
    "builtIn": true,
    "colors": {
      "background": "#0B0F19",
      "surface": "#1A1330",
      "primary": "#B794F4",
      "secondary": "#FF9CF1",
      "text": "#EDE7FF",
      "hint": "#9CA3AF",
      "appBarBackground": "#0B0F19",
      "outline": "#43375F"
    }
  }
}

Пример запроса ui:theme-get:

{
  "id": "Brand"
}

Пример на Python SDK

Отдельной high-level обёртки под Theme API в sdk_python пока нет и я не уверен, что она нужна.

Ниже минимальный рабочий паттерн для запроса текущей темы:

def _request_runtime(self, tag: str, payload, callback) -> None:
    responded = {'value': False}

    def _finish(data) -> None:
        if responded['value']:
            return
        responded['value'] = True
        callback(data)

    def _on_response(sender: str, data, in_tag: str) -> None:
        _finish(data if isinstance(data, dict) else None)

    def _on_complete(sender: str, data, in_tag: str) -> None:
        _finish(None)

    seq = self.send_message_with_response(
        tag,
        payload,
        on_response=_on_response,
        on_complete=_on_complete,
    )
    if seq < 0:
        _finish(None)


def load_shell_theme(self) -> None:
    def _on_theme(payload) -> None:
        if not isinstance(payload, dict):
            return
        theme = payload.get('theme')
        if not isinstance(theme, dict):
            return

        colors = theme.get('colors') or {}
        self.set_property('lastThemeId', theme.get('id'))
        self.set_property('lastPrimaryColor', colors.get('primary'))
        self.save_properties()

    self._request_runtime('ui:theme-current', {}, _on_theme)

Что здесь важно:

  • запрос идёт в shell через message bus, как и любой другой runtime-командой;
  • ответ прилетает асинхронно;
  • плагину лучше сразу проверять типы payload, а не верить, что там всегда будет корректный dict;
  • если тема нужна только для отображения, её обычно достаточно запросить один раз при открытии окна или панели.

Тот же подход работает и в sdk_golang, и в sdk_groovy: там тоже используется обычный callback-ответ через SendMessageWithResponse / sendMessageWithResponse.

Создание своей темы

Самый приятный сценарий у нового API не чтение, а запись. Плагин теперь может принести в MinaChan свою тему, а потом сразу активировать её.

Payload для ui:theme-add:

{
  "name": "Night Shift",
  "id": "night-shift",
  "brightness": "dark",
  "colors": {
    "background": "#0B1020",
    "surface": "#151B2F",
    "primary": "#7DD3FC",
    "secondary": "#C084FC",
    "text": "#E6EEF8",
    "hint": "#94A3B8",
    "appBarBackground": "#0A0F1D",
    "outline": "#334155"
  }
}

После успешного ответа можно сразу переключиться:

{
  "themeId": "night-shift"
}

Это уже payload для ui:theme-activate.

Надо помнить об ограничениях:

  • id должен быть уникальным;
  • минимум одна тема должна оставаться в системе.

Типовые ошибки API уже предусмотрены: theme_not_found, theme_id_exists, theme_builtin, theme_last_remaining и несколько ошибок валидации id и name.

Что прилетает в UI-окна

Если плагин работает через gui:set-panel и ui:window-create, в state прилетают поля примерно такого вида:

{
  "shellTheme": "midnight-rose",
  "shellThemeData": {
    "id": "midnight-rose",
    "name": "Midnight Rose",
    "brightness": "dark",
    "builtIn": false,
    "colors": {
      "background": "#0B0F19",
      "surface": "#1A1330",
      "primary": "#B794F4",
      "secondary": "#FF9CF1",
      "text": "#EDE7FF",
      "hint": "#9CA3AF",
      "appBarBackground": "#0B0F19",
      "outline": "#43375F"
    }
  }
}

Тонкость здесь такая:

  • для встроенных тем часто достаточно одного shellTheme;
  • для кастомных тем shell дополнительно передаёт shellThemeData, чтобы окно не потеряло палитру, если это не brand/light/dark.

Послесловие

Моя главная задумка заключалась в том, чтобы дать возможность гибко настраивать цветовые палитры для тем, обмениваться ими, возможно применять какие-то ИИ-шные штуки.