Плагин 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-конфигов.