Git

Плагин Tuner на Vala

Плагин Tuner это обычный репозиторий с небольшим набором файлов. Он добавляет в центр настроек одну или несколько страниц. Ниже полный рабочий пример: страница «Привет» с переключателем, полем ввода и кнопкой «Применить». Пример собран и проверен в контейнере.

Как устроен Tuner#

Tuner это центр настроек на GTK4 и libadwaita. Он загружает плагины через libpeas 2. Нативный плагин на Vala это общая библиотека .so и файл описания .plugin. Плагин регистрирует расширение Tuner.Addin, а расширение добавляет страницы Tuner.Page.

Главные типы:

ТипРоль
Tuner.AddinТочка входа плагина. В блоке construct вы создаёте страницы и вызываете add_page()
Tuner.PageСтраница в боковом меню: заголовок, значок, описание, порядок, содержимое
page.contentЛюбой виджет GTK: можно собрать страницу из Adw.PreferencesPage, Adw.SwitchRow и других
page.pack_end()Положить кнопку в заголовок страницы

Самый простой путь: поставить в content обычный виджет libadwaita и не использовать Tuner.Group, Tuner.Widget и привязки. Так работает большинство плагинов Atomic Niri.

Имя репозитория#

Принято имя TunerИмя: PascalCase с префиксом Tuner. Владелец любой, общего пространства имён нет. Примеры: TunerNitiAudio, TunerNiriWallpapers.

Структура проекта#

TunerHello/ meson.build Containerfile data/ meson.build plugin.in src/ meson.build plugin.vala hello-page.vala

Файлы#

meson.build#

project('TunerHello', ['c', 'vala'], version: '0.1.0', meson_version: '>= 1.0') name = meson.project_name() output = 'tuner-hello' subdir('data') subdir('src')

output это имя модуля. Оно должно совпасть в plugin.in и в имени библиотеки.

data/plugin.in#

[Plugin] Name=@NAME@ Module=@MODULE@ Version=@VERSION@ Authors=Ваше имя Website=https://altlinux.space/ВАШ_ЛОГИН/TunerHello

Для нативного плагина на Vala ключ Loader не нужен. Он нужен только для плагинов на Python (Loader=python).

data/meson.build#

configure_file( input: 'plugin.in', output: output + '.plugin', configuration: { 'NAME': name, 'MODULE': output, 'VERSION': meson.project_version(), }, install: true, install_dir: get_option('libdir') / 'tuner' / 'plugins', )

src/meson.build#

deps = [ dependency('tuner-1'), dependency('libadwaita-1'), dependency('gio-2.0'), ] shared_module( output, files('hello-page.vala', 'plugin.vala'), dependencies: deps, install: true, install_dir: get_option('libdir') / 'tuner' / 'plugins', )

src/plugin.vala#

using Peas; namespace TunerHello { public class Addin : Tuner.Addin { construct { var page = new Tuner.Page () { icon_name = "emoji-people-symbolic", title = "Привет", id = "tuner-hello", description = "Пример плагина Tuner на Vala.", category = "system", priority = 6000, }; var hello = new HelloPage (); page.content = hello; var apply = new Gtk.Button.with_label ("Применить"); apply.add_css_class ("suggested-action"); weak HelloPage weak_hello = hello; apply.clicked.connect (() => { if (weak_hello != null) weak_hello.save (); }); page.pack_end (apply); add_page (page); } } } public void peas_register_types (TypeModule module) { var object_module = (Peas.ObjectModule) module; object_module.register_extension_type (typeof (Tuner.Addin), typeof (TunerHello.Addin)); }

Функция peas_register_types обязательна: по ней libpeas находит расширение.

src/hello-page.vala#

namespace TunerHello { public class HelloPage : Adw.Bin { private Adw.SwitchRow enabled_row; private Adw.EntryRow name_row; private string config_path; construct { config_path = Path.build_filename (Environment.get_user_config_dir (), "tuner-hello", "config.ini"); var cfg = new KeyFile (); try { cfg.load_from_file (config_path, KeyFileFlags.NONE); } catch (Error e) { // файла ещё нет, остаются значения по умолчанию } bool enabled = false; string name = ""; try { enabled = cfg.get_boolean ("hello", "enabled"); } catch (Error e) { } try { name = cfg.get_string ("hello", "name"); } catch (Error e) { } enabled_row = new Adw.SwitchRow () { title = "Включить приветствие", subtitle = "Пример настройки с переключателем", active = enabled, }; name_row = new Adw.EntryRow () { title = "Имя", text = name, }; var group = new Adw.PreferencesGroup () { title = "Приветствие", description = "Значения сохраняются кнопкой «Применить».", }; group.add (enabled_row); group.add (name_row); var page = new Adw.PreferencesPage (); page.add (group); child = page; } public void save () { var cfg = new KeyFile (); cfg.set_boolean ("hello", "enabled", enabled_row.active); cfg.set_string ("hello", "name", name_row.text); try { DirUtils.create_with_parents (Path.get_dirname (config_path), 0700); cfg.save_to_file (config_path); } catch (Error e) { warning ("Не удалось сохранить настройки: %s", e.message); } } } }

Здесь настройки лежат в обычном INI-файле, а читает и пишет его GLib.KeyFile. Для JSON подойдёт Json.Parser из json-glib-1.0, для настроек GNOME GLib.Settings.

Containerfile#

Сборочное окружение удобно держать в контейнере: на неизменяемой системе тулчейн на хост не ставят.

FROM altlinux.space/actions/runner/containers:sisyphus RUN apt-get update -qq && apt-get install -y -qq \ meson vala gcc pkgconfig \ tuner libtuner-devel libtuner-gir-devel libpeas2-gir \ libadwaita-devel libadwaita-gir-devel libgtk4-devel \ && rm -rf /var/cache/apt WORKDIR /src

Важно. Для сборки нужен именно libtuner-devel: он даёт tuner-1.vapi и tuner-1.pc. Одного libtuner-gir недостаточно.

Сборка#

podman build -t tuner-hello-dev -f Containerfile . podman run --rm -v $PWD:/src:Z tuner-hello-dev sh -c \ 'meson setup build --prefix=/usr && meson compile -C build'

На выходе build/src/libtuner-hello.so и build/data/tuner-hello.plugin. Как проверить плагин в живом Tuner, описано в разделе проверка без пересборки.

Важно. Всегда указывайте --prefix=/usr. Если плагин использует gettext, без этого в бинарник зашьётся /usr/local/share/locale, и перевод молча не заработает.

Свойства страницы#

СвойствоЧто значит
titleНазвание в боковом меню
icon_nameИмя значка, обычно символьный (...-symbolic)
idУникальный идентификатор страницы
descriptionПодпись под заголовком
categoryСтрока для группировки. Tuner рисует разделитель между соседними страницами с разной категорией. Сторонние плагины используют "system", и если нужен общий список без линий, берите её же
priorityПорядок: чем меньше число, тем выше страница. Все страницы всех плагинов попадают в общий список
contentВиджет с содержимым страницы

Порядок страниц#

Если у нескольких плагинов одинаковый priority, порядок между ними непредсказуем. Давайте каждому плагину свой диапазон. В образе заняты, например: Игровой режим около 1130, Диагностика 5010, «Мониторы» 900, «Обои» 2000, «Приложения по умолчанию» 5000, «Меню питания» 8000, «Информация о системе» 9000. Выберите значение в свободном месте.

В файле .plugin можно задать X-Priority=<число>: он влияет только на порядок загрузки плагинов, а не на положение страниц в меню.

Кнопка действия в заголовке#

Кнопки уровня страницы («Применить», «Сбросить») кладут в заголовок через page.pack_end(кнопка), а не в конец содержимого. Так сделано во всех плагинах образа, например в «Мониторах».

Несколько страниц в одном плагине#

Один плагин может добавить сколько угодно страниц: вызовите add_page() несколько раз в construct. Так сделан TunerNitiSettings: «Настройки панели» и «Быстрое меню» живут в одном плагине.

Иконки и ресурсы#

Если нужны собственные значки или интерфейс из Blueprint, подключайте ресурсы через gnome.compile_resources() в meson и регистрируйте путь значков при активации:

unowned Gdk.Display? display = Gdk.Display.get_default (); if (display != null) Gtk.IconTheme.get_for_display (display).add_resource_path ("/org/example/TunerHello/icons");

Если страница описана в .ui или Blueprint вместе с собственными виджетами, перед загрузкой ресурса вызовите typeof (МойВиджет).ensure ();, иначе GtkBuilder не найдёт тип.

Плагин на Python#

Плагины можно писать и на Python. В plugin.in добавляется Loader=python, а класс наследуется от Tuner.Addin и реализует do_activate. Но все плагины образа написаны на Vala, и новые лучше писать на нём.

Что почитать дальше#

  • Готовые плагины: небольшие TunerNitiDiag, TunerNiriNightLight, побольше TunerNitiGameMode.
  • Апстрим Tuner: alt-gnome/Tuner, примеры TunerBase (Vala) и TunerPython (Python).
  • Общая библиотека TunerNitiPluginLib с безопасным чтением и записью JSON-конфигов.