backends: add niri

parent 2feb9cd2
......@@ -6,6 +6,7 @@
- Hyprland: чтение через `hyprctl -j monitors all`, сохранение в `~/.config/hypr/monitors.conf`.
- GNOME: чтение и применение настроек через Mutter DisplayConfig.
- niri: чтение через `niri msg -j outputs`, сохранение в `~/.config/niri/monitor.kdl`.
Backend выбирается по XDG-переменным:
......@@ -37,6 +38,18 @@ monitorv2 {
GNOME-бэкенд поддерживает обычный и зеркальный режимы, выбор основного дисплея, разрешение, масштаб, поворот, частоту обновления, VRR, underscan и доступные цветовые режимы. В зеркальном режиме отдельные настройки мониторов скрываются, а общие параметры переносятся на главную страницу.
## niri
niri-бэкенд пишет отдельный KDL-файл:
```text
~/.config/niri/monitor.kdl
```
Плагин не подключает этот файл в основной конфиг и не вызывает ручную перезагрузку niri. Поддерживаются `mode`, `scale`, `position`, `transform`, `off`, VRR, `focus-at-startup`, `hot-corners` и `backdrop-color`.
При выключении монитора остальные параметры сохраняются рядом с `off`, чтобы не терять позицию и настройки при повторном включении.
## Сборка
```sh
......
data/ui/displays-view.blp
data/ui/monitor-settings-content.blp
src/plugin.vala
src/backends/niri-backend.vala
src/core/display-model.vala
src/ui/displays-view.vala
src/ui/monitor-settings-content.vala
......
......@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: tuner-displays\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-29 23:02+0300\n"
"POT-Creation-Date: 2026-05-30 14:03+0300\n"
"PO-Revision-Date: 2026-05-28 00:00+0000\n"
"Last-Translator: Automatically generated\n"
"Language-Team: Russian\n"
......@@ -47,7 +47,7 @@ msgstr "Применить"
msgid "Monitor"
msgstr "Монитор"
#: src/core/display-model.vala:62
#: src/core/display-model.vala:65
msgid "Built-in Display"
msgstr "Встроенный дисплей"
......@@ -71,49 +71,49 @@ msgstr "Применение раскладок мониторов не подд
msgid "Mirror Displays"
msgstr "Зеркалировать мониторы"
#: src/ui/displays-view.vala:230 src/ui/monitor-settings-content.vala:86
#: src/ui/monitor-settings-content.vala:125
#: src/ui/displays-view.vala:230 src/ui/monitor-settings-content.vala:90
#: src/ui/monitor-settings-content.vala:129
msgid "Resolution"
msgstr "Разрешение"
#: src/ui/displays-view.vala:276 src/ui/monitor-settings-content.vala:263
#: src/ui/monitor-settings-content.vala:289
#: src/ui/displays-view.vala:276 src/ui/monitor-settings-content.vala:267
#: src/ui/monitor-settings-content.vala:293
msgid "Scale"
msgstr "Масштаб"
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:310
msgid "Normal"
msgstr "Обычный"
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:310
msgid "90 degrees"
msgstr "90 градусов"
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:310
msgid "180 degrees"
msgstr "180 градусов"
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:310
msgid "270 degrees"
msgstr "270 градусов"
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:311
msgid "Flipped"
msgstr "Отражённый"
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:311
msgid "Flipped 90 degrees"
msgstr "Отражённый 90 градусов"
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:311
msgid "Flipped 180 degrees"
msgstr "Отражённый 180 градусов"
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:311
msgid "Flipped 270 degrees"
msgstr "Отражённый 270 градусов"
#: src/ui/displays-view.vala:301 src/ui/monitor-settings-content.vala:313
#: src/ui/displays-view.vala:301 src/ui/monitor-settings-content.vala:317
msgid "Rotation"
msgstr "Поворот"
......@@ -121,136 +121,180 @@ msgstr "Поворот"
msgid "Primary Display"
msgstr "Основной дисплей"
#: src/ui/monitor-settings-content.vala:154
#: src/ui/monitor-settings-content.vala:162
#: src/ui/monitor-settings-content.vala:158
#: src/ui/monitor-settings-content.vala:166
msgid "Refresh Rate"
msgstr "Частота обновления"
#: src/ui/monitor-settings-content.vala:184
#: src/ui/monitor-settings-content.vala:188
#: src/ui/monitor-settings-content.vala:404
msgid "Variable Refresh Rate"
msgstr "Переменная частота обновления"
#: src/ui/monitor-settings-content.vala:333
#: src/ui/monitor-settings-content.vala:337
msgid "None"
msgstr "Нет"
#: src/ui/monitor-settings-content.vala:348
#: src/ui/monitor-settings-content.vala:352
msgid "Mirror"
msgstr "Зеркалирование"
#: src/ui/monitor-settings-content.vala:364
#: src/ui/monitor-settings-content.vala:368
msgid "Use description"
msgstr "Использовать описание"
#: src/ui/monitor-settings-content.vala:373
#: src/ui/monitor-settings-content.vala:377
msgid "Bit depth"
msgstr "Глубина цвета"
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:378
msgid "VRR"
msgstr "VRR"
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
#: src/ui/monitor-settings-content.vala:378
#: src/ui/monitor-settings-content.vala:384
#: src/ui/monitor-settings-content.vala:385
#: src/ui/monitor-settings-content.vala:405
#: src/ui/monitor-settings-content.vala:429
msgid "Off"
msgstr "Отключено"
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
#: src/ui/monitor-settings-content.vala:378
#: src/ui/monitor-settings-content.vala:384
#: src/ui/monitor-settings-content.vala:385
#: src/ui/monitor-settings-content.vala:405
msgid "On"
msgstr "Включено"
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:378
msgid "Fullscreen"
msgstr "Полный экран"
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:378
msgid "Fullscreen video/game"
msgstr "Полноэкранное видео/игра"
#: src/ui/monitor-settings-content.vala:375
#: src/ui/monitor-settings-content.vala:379
msgid "Color management"
msgstr "Управление цветом"
#: src/ui/monitor-settings-content.vala:376
#: src/ui/monitor-settings-content.vala:380
msgid "SDR EOTF"
msgstr "SDR EOTF"
#: src/ui/monitor-settings-content.vala:377
#: src/ui/monitor-settings-content.vala:381
msgid "SDR brightness"
msgstr "Яркость SDR"
#: src/ui/monitor-settings-content.vala:378
#: src/ui/monitor-settings-content.vala:382
msgid "SDR saturation"
msgstr "Насыщенность SDR"
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:384
msgid "Force wide color"
msgstr "Принудительно широкий цветовой охват"
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
#: src/ui/monitor-settings-content.vala:384
#: src/ui/monitor-settings-content.vala:385
msgid "Auto"
msgstr "Авто"
#: src/ui/monitor-settings-content.vala:381
#: src/ui/monitor-settings-content.vala:385
msgid "Force HDR"
msgstr "Принудительно HDR"
#: src/ui/monitor-settings-content.vala:382
#: src/ui/monitor-settings-content.vala:386
msgid "SDR min luminance"
msgstr "Мин. яркость SDR"
#: src/ui/monitor-settings-content.vala:383
#: src/ui/monitor-settings-content.vala:387
msgid "SDR max luminance"
msgstr "Макс. яркость SDR"
#: src/ui/monitor-settings-content.vala:384
#: src/ui/monitor-settings-content.vala:388
msgid "Min luminance"
msgstr "Мин. яркость"
#: src/ui/monitor-settings-content.vala:385
#: src/ui/monitor-settings-content.vala:389
msgid "Max luminance"
msgstr "Макс. яркость"
#: src/ui/monitor-settings-content.vala:386
#: src/ui/monitor-settings-content.vala:390
msgid "Max average luminance"
msgstr "Макс. средняя яркость"
#: src/ui/monitor-settings-content.vala:389
#: src/ui/monitor-settings-content.vala:393
msgid "ICC profile"
msgstr "Профиль ICC"
#: src/ui/monitor-settings-content.vala:401
#: src/ui/monitor-settings-content.vala:405
msgid "On demand"
msgstr "По требованию"
#: src/ui/monitor-settings-content.vala:409
msgid "Focus at startup"
msgstr "Фокус при запуске"
#: src/ui/monitor-settings-content.vala:419
msgid "Backdrop color"
msgstr "Цвет фона"
#: src/ui/monitor-settings-content.vala:428
msgid "Hot corners"
msgstr "Активные углы"
#: src/ui/monitor-settings-content.vala:429
msgid "Default"
msgstr "По умолчанию"
#: src/ui/monitor-settings-content.vala:429
msgid "All"
msgstr "Все"
#: src/ui/monitor-settings-content.vala:429
msgid "Top left"
msgstr "Сверху слева"
#: src/ui/monitor-settings-content.vala:429
msgid "Top right"
msgstr "Сверху справа"
#: src/ui/monitor-settings-content.vala:429
msgid "Bottom left"
msgstr "Снизу слева"
#: src/ui/monitor-settings-content.vala:429
msgid "Bottom right"
msgstr "Снизу справа"
#: src/ui/monitor-settings-content.vala:437
msgid "Underscanning"
msgstr "Underscan"
#: src/ui/monitor-settings-content.vala:412
#: src/ui/monitor-settings-content.vala:448
msgid "HDR"
msgstr "HDR"
#: src/ui/monitor-settings-content.vala:571
#: src/ui/monitor-settings-content.vala:611
#, c-format
msgid "Variable (up to %.2f Hz)"
msgstr "Переменная (до %.2f Гц)"
#: src/ui/monitor-settings-content.vala:572
#: src/ui/monitor-settings-content.vala:612
msgid "Variable"
msgstr "Переменная"
#: src/ui/monitor-settings-content.vala:575 src/ui/ui-helpers.vala:50
#: src/ui/monitor-settings-content.vala:615 src/ui/ui-helpers.vala:50
#, c-format
msgid "%.2f Hz"
msgstr "%.2f Гц"
#~ msgid "Max BPC"
#~ msgstr "Макс. BPC"
#~ msgid "Color Mode"
#~ msgstr "Цветовой режим"
#~ msgid "Default"
#~ msgstr "По умолчанию"
#, c-format
#~ msgid "Mode %d"
#~ msgstr "Режим %d"
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: tuner-displays\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-29 23:02+0300\n"
"POT-Creation-Date: 2026-05-30 14:03+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -53,7 +53,7 @@ msgstr ""
msgid "Monitor"
msgstr ""
#: src/core/display-model.vala:62
#: src/core/display-model.vala:65
msgid "Built-in Display"
msgstr ""
......@@ -77,49 +77,49 @@ msgstr ""
msgid "Mirror Displays"
msgstr ""
#: src/ui/displays-view.vala:230 src/ui/monitor-settings-content.vala:86
#: src/ui/monitor-settings-content.vala:125
#: src/ui/displays-view.vala:230 src/ui/monitor-settings-content.vala:90
#: src/ui/monitor-settings-content.vala:129
msgid "Resolution"
msgstr ""
#: src/ui/displays-view.vala:276 src/ui/monitor-settings-content.vala:263
#: src/ui/monitor-settings-content.vala:289
#: src/ui/displays-view.vala:276 src/ui/monitor-settings-content.vala:267
#: src/ui/monitor-settings-content.vala:293
msgid "Scale"
msgstr ""
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:310
msgid "Normal"
msgstr ""
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:310
msgid "90 degrees"
msgstr ""
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:310
msgid "180 degrees"
msgstr ""
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:310
msgid "270 degrees"
msgstr ""
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:311
msgid "Flipped"
msgstr ""
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:311
msgid "Flipped 90 degrees"
msgstr ""
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:311
msgid "Flipped 180 degrees"
msgstr ""
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:311
msgid "Flipped 270 degrees"
msgstr ""
#: src/ui/displays-view.vala:301 src/ui/monitor-settings-content.vala:313
#: src/ui/displays-view.vala:301 src/ui/monitor-settings-content.vala:317
msgid "Rotation"
msgstr ""
......@@ -127,126 +127,170 @@ msgstr ""
msgid "Primary Display"
msgstr ""
#: src/ui/monitor-settings-content.vala:154
#: src/ui/monitor-settings-content.vala:162
#: src/ui/monitor-settings-content.vala:158
#: src/ui/monitor-settings-content.vala:166
msgid "Refresh Rate"
msgstr ""
#: src/ui/monitor-settings-content.vala:184
#: src/ui/monitor-settings-content.vala:188
#: src/ui/monitor-settings-content.vala:404
msgid "Variable Refresh Rate"
msgstr ""
#: src/ui/monitor-settings-content.vala:333
#: src/ui/monitor-settings-content.vala:337
msgid "None"
msgstr ""
#: src/ui/monitor-settings-content.vala:348
#: src/ui/monitor-settings-content.vala:352
msgid "Mirror"
msgstr ""
#: src/ui/monitor-settings-content.vala:364
#: src/ui/monitor-settings-content.vala:368
msgid "Use description"
msgstr ""
#: src/ui/monitor-settings-content.vala:373
#: src/ui/monitor-settings-content.vala:377
msgid "Bit depth"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:378
msgid "VRR"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
#: src/ui/monitor-settings-content.vala:378
#: src/ui/monitor-settings-content.vala:384
#: src/ui/monitor-settings-content.vala:385
#: src/ui/monitor-settings-content.vala:405
#: src/ui/monitor-settings-content.vala:429
msgid "Off"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
#: src/ui/monitor-settings-content.vala:378
#: src/ui/monitor-settings-content.vala:384
#: src/ui/monitor-settings-content.vala:385
#: src/ui/monitor-settings-content.vala:405
msgid "On"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:378
msgid "Fullscreen"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:378
msgid "Fullscreen video/game"
msgstr ""
#: src/ui/monitor-settings-content.vala:375
#: src/ui/monitor-settings-content.vala:379
msgid "Color management"
msgstr ""
#: src/ui/monitor-settings-content.vala:376
#: src/ui/monitor-settings-content.vala:380
msgid "SDR EOTF"
msgstr ""
#: src/ui/monitor-settings-content.vala:377
#: src/ui/monitor-settings-content.vala:381
msgid "SDR brightness"
msgstr ""
#: src/ui/monitor-settings-content.vala:378
#: src/ui/monitor-settings-content.vala:382
msgid "SDR saturation"
msgstr ""
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:384
msgid "Force wide color"
msgstr ""
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
#: src/ui/monitor-settings-content.vala:384
#: src/ui/monitor-settings-content.vala:385
msgid "Auto"
msgstr ""
#: src/ui/monitor-settings-content.vala:381
#: src/ui/monitor-settings-content.vala:385
msgid "Force HDR"
msgstr ""
#: src/ui/monitor-settings-content.vala:382
#: src/ui/monitor-settings-content.vala:386
msgid "SDR min luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:383
#: src/ui/monitor-settings-content.vala:387
msgid "SDR max luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:384
#: src/ui/monitor-settings-content.vala:388
msgid "Min luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:385
#: src/ui/monitor-settings-content.vala:389
msgid "Max luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:386
#: src/ui/monitor-settings-content.vala:390
msgid "Max average luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:389
#: src/ui/monitor-settings-content.vala:393
msgid "ICC profile"
msgstr ""
#: src/ui/monitor-settings-content.vala:401
#: src/ui/monitor-settings-content.vala:405
msgid "On demand"
msgstr ""
#: src/ui/monitor-settings-content.vala:409
msgid "Focus at startup"
msgstr ""
#: src/ui/monitor-settings-content.vala:419
msgid "Backdrop color"
msgstr ""
#: src/ui/monitor-settings-content.vala:428
msgid "Hot corners"
msgstr ""
#: src/ui/monitor-settings-content.vala:429
msgid "Default"
msgstr ""
#: src/ui/monitor-settings-content.vala:429
msgid "All"
msgstr ""
#: src/ui/monitor-settings-content.vala:429
msgid "Top left"
msgstr ""
#: src/ui/monitor-settings-content.vala:429
msgid "Top right"
msgstr ""
#: src/ui/monitor-settings-content.vala:429
msgid "Bottom left"
msgstr ""
#: src/ui/monitor-settings-content.vala:429
msgid "Bottom right"
msgstr ""
#: src/ui/monitor-settings-content.vala:437
msgid "Underscanning"
msgstr ""
#: src/ui/monitor-settings-content.vala:412
#: src/ui/monitor-settings-content.vala:448
msgid "HDR"
msgstr ""
#: src/ui/monitor-settings-content.vala:571
#: src/ui/monitor-settings-content.vala:611
#, c-format
msgid "Variable (up to %.2f Hz)"
msgstr ""
#: src/ui/monitor-settings-content.vala:572
#: src/ui/monitor-settings-content.vala:612
msgid "Variable"
msgstr ""
#: src/ui/monitor-settings-content.vala:575 src/ui/ui-helpers.vala:50
#: src/ui/monitor-settings-content.vala:615 src/ui/ui-helpers.vala:50
#, c-format
msgid "%.2f Hz"
msgstr ""
......@@ -15,11 +15,76 @@ namespace TunerDisplays {
public abstract Gee.ArrayList<MonitorConfig> load() throws Error;
public abstract void apply(Gee.ArrayList<MonitorConfig> monitors) throws Error;
protected static Json.Node backend_parse_json(string text) throws Error {
var parser = new Json.Parser();
parser.load_from_data(text);
return parser.get_root();
}
protected static string backend_get_string(Json.Object obj, string name, string fallback = "") {
return obj.has_member(name) && !obj.get_member(name).is_null() ? obj.get_string_member(name) : fallback;
}
protected static Json.Object? backend_get_object_or_null(Json.Object obj, string name) {
return obj.has_member(name) && !obj.get_member(name).is_null() ? obj.get_object_member(name) : null;
}
protected static bool backend_is_null_member(Json.Object obj, string name) {
return !obj.has_member(name) || obj.get_member(name).is_null();
}
protected static double backend_get_double(Json.Object obj, string name, double fallback) {
return obj.has_member(name) && !obj.get_member(name).is_null() ? obj.get_double_member(name) : fallback;
}
protected static bool backend_get_bool(Json.Object obj, string name, bool fallback) {
return obj.has_member(name) && !obj.get_member(name).is_null() ? obj.get_boolean_member(name) : fallback;
}
protected static void backend_apply_mode(MonitorConfig monitor, DisplayMode mode) {
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
}
protected static void backend_apply_first_available_mode(MonitorConfig monitor) {
if (monitor.modes.size > 0)
backend_apply_mode(monitor, monitor.modes[0]);
}
protected static double backend_logical_width(MonitorConfig monitor) {
if (monitor.scale <= 0)
return monitor.width;
return backend_is_rotated(monitor) ? monitor.height / monitor.scale : monitor.width / monitor.scale;
}
protected static double backend_logical_height(MonitorConfig monitor) {
if (monitor.scale <= 0)
return monitor.height;
return backend_is_rotated(monitor) ? monitor.width / monitor.scale : monitor.height / monitor.scale;
}
protected static bool backend_is_rotated(MonitorConfig monitor) {
return monitor.transform.index_of("90") >= 0 || monitor.transform.index_of("270") >= 0;
}
protected static string backend_format_double(double value, int precision = 2) {
return ("%." + precision.to_string() + "f").printf(value).replace(",", ".");
}
protected static string backend_escape_quoted_string(string value) {
return value.replace("\\", "\\\\").replace("\"", "\\\"");
}
public static DisplayBackend create_for_session() {
var desktop = (Environment.get_variable("XDG_CURRENT_DESKTOP") ?? "").down();
var session = (Environment.get_variable("XDG_SESSION_DESKTOP") ?? "").down();
var detected = desktop != "" ? desktop : session;
if ("niri" in detected) {
return new NiriBackend();
}
if ("hyprland" in detected) {
return new HyprlandBackend();
}
......
namespace TunerDisplays {
public class NiriBackend : DisplayBackend {
private const string[] NIRI_TRANSFORMS = {
"Normal", "90", "180", "270", "Flipped", "Flipped90", "Flipped180", "Flipped270"
};
private const string[] TRANSFORMS = {
"normal", "90", "180", "270", "flipped", "flipped-90", "flipped-180", "flipped-270"
};
private class SavedMonitor : Object {
public bool has_mode { get; set; }
public int width { get; set; }
public int height { get; set; }
public double refresh { get; set; }
public bool has_scale { get; set; }
public double scale { get; set; default = 1.0; }
public bool has_position { get; set; }
public int x { get; set; }
public int y { get; set; }
public bool has_transform { get; set; }
public string transform { get; set; default = "normal"; }
public bool has_vrr { get; set; }
public int vrr { get; set; }
public bool focus_at_startup { get; set; }
public string backdrop_color { get; set; default = ""; }
public string hot_corners { get; set; default = ""; }
}
public override string id { get { return "niri"; } }
public override string title { owned get { return "niri"; } }
public override bool can_apply { get { return true; } }
public override Gee.ArrayList<MonitorConfig> load() throws Error {
var monitors = new Gee.ArrayList<MonitorConfig>();
var saved = read_saved_monitors();
var root = backend_parse_json(ShellCommand.run("niri msg -j outputs"));
if (root.get_node_type() != Json.NodeType.OBJECT)
throw new BackendError.PARSE_FAILED("niri msg outputs returned non-object JSON");
var outputs = root.get_object();
var names = outputs.get_members();
foreach (var name in names) {
var obj = outputs.get_object_member(name);
var monitor = new MonitorConfig();
monitor.name = name;
monitor.enabled = true;
monitor.vendor = backend_get_string(obj, "make");
monitor.product = backend_get_string(obj, "model");
monitor.serial = backend_get_string(obj, "serial");
monitor.description = build_description(monitor.vendor, monitor.product, monitor.serial);
monitor.supports_variable_refresh_rate = backend_get_bool(obj, "vrr_supported", false);
monitor.vrr = backend_get_bool(obj, "vrr_enabled", false) ? 1 : 0;
apply_saved_monitor(saved, monitor);
var logical = backend_get_object_or_null(obj, "logical");
monitor.enabled = logical != null && !backend_is_null_member(obj, "current_mode");
if (logical != null) {
monitor.x = (int) backend_get_double(logical, "x", 0);
monitor.y = (int) backend_get_double(logical, "y", 0);
monitor.scale = backend_get_double(logical, "scale", 1);
monitor.transform = transform_from_niri(backend_get_string(logical, "transform", "Normal"));
}
var modes = obj.has_member("modes") ? obj.get_array_member("modes") : null;
if (modes != null) {
for (uint i = 0; i < modes.get_length(); i++) {
var mode = parse_mode(modes.get_object_element(i));
if (mode != null)
monitor.modes.add(mode);
}
}
var mode_index = backend_is_null_member(obj, "current_mode") ? -1 : (int) backend_get_double(obj, "current_mode", -1);
if (monitor.enabled && mode_index >= 0 && mode_index < monitor.modes.size) {
backend_apply_mode(monitor, monitor.modes[mode_index]);
} else {
backend_apply_first_available_mode(monitor);
}
if (!monitor.enabled)
apply_saved_layout(saved, monitor);
monitors.add(monitor);
}
place_disabled_after_enabled(monitors);
return monitors;
}
public override void apply(Gee.ArrayList<MonitorConfig> monitors) throws Error {
var path = monitors_path();
var builder = new StringBuilder();
builder.append("// Generated by Tuner displays. niri monitor configuration.\n\n");
foreach (var monitor in monitors) {
builder.append("output \"%s\" {\n".printf(backend_escape_quoted_string(monitor.name)));
if (!monitor.enabled)
builder.append(" off\n");
if (monitor.width > 0 && monitor.height > 0) {
builder.append(" mode \"%dx%d@%s\"\n".printf(
monitor.width,
monitor.height,
backend_format_double(monitor.refresh, 3)
));
}
builder.append(" scale %s\n".printf(backend_format_double(monitor.scale)));
if (monitor.transform != "normal")
builder.append(" transform \"%s\"\n".printf(transform_to_niri(monitor.transform)));
builder.append(" position x=%d y=%d\n".printf(monitor.x, monitor.y));
if (monitor.vrr != 0)
builder.append(" variable-refresh-rate%s\n".printf(monitor.vrr == 2 ? " on-demand=true" : ""));
if (monitor.niri_focus_at_startup)
builder.append(" focus-at-startup\n");
if (monitor.niri_backdrop_color != "")
builder.append(" backdrop-color \"%s\"\n".printf(backend_escape_quoted_string(monitor.niri_backdrop_color)));
append_hot_corners(builder, monitor.niri_hot_corners);
builder.append("}\n\n");
}
DirUtils.create_with_parents(Path.get_dirname(path), 0755);
FileUtils.set_contents(path, builder.str);
}
private static Gee.HashMap<string, SavedMonitor> read_saved_monitors() {
var monitors = new Gee.HashMap<string, SavedMonitor>();
try {
string content;
if (!FileUtils.get_contents(monitors_path(), out content))
return monitors;
string current_name = "";
SavedMonitor? current = null;
bool in_output = false;
bool in_hot_corners = false;
var corners = new Gee.ArrayList<string>();
foreach (var raw_line in content.split("\n")) {
var line = strip_comment(raw_line).strip();
if (line == "")
continue;
if (!in_output && line.has_prefix("output ")) {
current_name = parse_quoted(line);
current = new SavedMonitor();
in_output = true;
continue;
}
if (!in_output || current == null)
continue;
if (in_hot_corners) {
if (line == "}") {
current.hot_corners = corners_to_value(corners);
corners.clear();
in_hot_corners = false;
continue;
}
corners.add(line);
continue;
}
if (line == "}") {
if (current_name != "")
monitors[current_name] = current;
current_name = "";
current = null;
in_output = false;
} else if (line.has_prefix("variable-refresh-rate")) {
current.has_vrr = true;
current.vrr = line.index_of("on-demand=true") >= 0 ? 2 : 1;
} else if (line.has_prefix("mode ")) {
parse_saved_mode(line, current);
} else if (line.has_prefix("scale ")) {
current.scale = parse_double_token(line.substring(6), 1.0);
current.has_scale = true;
} else if (line.has_prefix("position ")) {
parse_saved_position(line, current);
} else if (line.has_prefix("transform ")) {
current.transform = transform_from_niri(parse_quoted(line));
current.has_transform = true;
} else if (line == "focus-at-startup") {
current.focus_at_startup = true;
} else if (line.has_prefix("backdrop-color")) {
current.backdrop_color = parse_quoted(line);
} else if (line.has_prefix("hot-corners")) {
in_hot_corners = true;
}
}
} catch (Error err) {
warning("Failed to parse niri monitor config: %s", err.message);
}
return monitors;
}
private static void apply_saved_monitor(Gee.HashMap<string, SavedMonitor> saved, MonitorConfig monitor) {
var saved_monitor = lookup_saved_monitor(saved, monitor);
if (saved_monitor == null)
return;
if (saved_monitor.has_vrr)
monitor.vrr = saved_monitor.vrr;
monitor.niri_focus_at_startup = saved_monitor.focus_at_startup;
monitor.niri_backdrop_color = saved_monitor.backdrop_color;
monitor.niri_hot_corners = saved_monitor.hot_corners;
}
private static void apply_saved_layout(Gee.HashMap<string, SavedMonitor> saved, MonitorConfig monitor) {
var saved_monitor = lookup_saved_monitor(saved, monitor);
if (saved_monitor == null)
return;
if (saved_monitor.has_mode) {
monitor.width = saved_monitor.width;
monitor.height = saved_monitor.height;
monitor.refresh = saved_monitor.refresh;
}
if (saved_monitor.has_scale)
monitor.scale = saved_monitor.scale;
if (saved_monitor.has_transform)
monitor.transform = saved_monitor.transform;
if (saved_monitor.has_position) {
monitor.x = saved_monitor.x;
monitor.y = saved_monitor.y;
monitor.niri_has_saved_position = true;
}
}
private static SavedMonitor? lookup_saved_monitor(Gee.HashMap<string, SavedMonitor> saved, MonitorConfig monitor) {
if (saved.has_key(monitor.name))
return saved[monitor.name];
if (monitor.description != "" && saved.has_key(monitor.description))
return saved[monitor.description];
return null;
}
private static DisplayMode? parse_mode(Json.Object obj) {
var width = (int) backend_get_double(obj, "width", 0);
var height = (int) backend_get_double(obj, "height", 0);
if (width <= 0 || height <= 0)
return null;
return new DisplayMode() {
width = width,
height = height,
refresh = backend_get_double(obj, "refresh_rate", 60000) / 1000.0
};
}
private static string monitors_path() {
return Path.build_filename(Environment.get_user_config_dir(), "niri", "monitor.kdl");
}
private static void append_hot_corners(StringBuilder builder, string value) {
if (value == "")
return;
builder.append(" hot-corners {\n");
if (value == "off") {
builder.append(" off\n");
} else if (value == "all") {
builder.append(" top-left\n");
builder.append(" top-right\n");
builder.append(" bottom-left\n");
builder.append(" bottom-right\n");
} else {
builder.append(" %s\n".printf(value));
}
builder.append(" }\n");
}
private static string build_description(string vendor, string product, string serial) {
var builder = new StringBuilder();
append_description_part(builder, vendor);
append_description_part(builder, product);
append_description_part(builder, serial);
return builder.str;
}
private static void append_description_part(StringBuilder builder, string value) {
if (value == "")
return;
if (builder.len > 0)
builder.append_c(' ');
builder.append(value);
}
private static string transform_from_niri(string transform) {
for (int i = 0; i < NIRI_TRANSFORMS.length; i++) {
if (NIRI_TRANSFORMS[i] == transform)
return TRANSFORMS[i];
}
return "normal";
}
private static string transform_to_niri(string transform) {
for (int i = 0; i < TRANSFORMS.length; i++) {
if (TRANSFORMS[i] == transform)
return NIRI_TRANSFORMS[i];
}
return "Normal";
}
private static void place_disabled_after_enabled(Gee.ArrayList<MonitorConfig> monitors) {
int right = 0;
bool found_enabled = false;
foreach (var monitor in monitors) {
if (!monitor.enabled)
continue;
var monitor_right = monitor.x + (int) Math.round(backend_logical_width(monitor));
if (!found_enabled || monitor_right > right)
right = monitor_right;
found_enabled = true;
}
foreach (var monitor in monitors) {
if (monitor.enabled)
continue;
if (monitor.niri_has_saved_position)
continue;
monitor.x = found_enabled ? right : 0;
monitor.y = 0;
right += (int) Math.round(backend_logical_width(monitor));
}
}
private static string strip_comment(string line) {
var index = line.index_of("//");
return index >= 0 ? line.substring(0, index) : line;
}
private static string parse_quoted(string line) {
var start = line.index_of("\"");
if (start < 0)
return "";
var builder = new StringBuilder();
bool escaped = false;
for (int i = start + 1; i < line.length; i++) {
var c = line[i];
if (escaped) {
builder.append_c(c);
escaped = false;
} else if (c == '\\') {
escaped = true;
} else if (c == '"') {
break;
} else {
builder.append_c(c);
}
}
return builder.str;
}
private static void parse_saved_mode(string line, SavedMonitor monitor) {
var value = parse_quoted(line);
var at = value.index_of("@");
var x = value.index_of("x");
if (at <= 0 || x <= 0 || x >= at)
return;
monitor.width = int.parse(value.substring(0, x));
monitor.height = int.parse(value.substring(x + 1, at - x - 1));
monitor.refresh = parse_double_token(value.substring(at + 1), 0);
monitor.has_mode = monitor.width > 0 && monitor.height > 0 && monitor.refresh > 0;
}
private static void parse_saved_position(string line, SavedMonitor monitor) {
var parts = line.split(" ");
foreach (var part in parts) {
if (part.has_prefix("x="))
monitor.x = int.parse(part.substring(2));
else if (part.has_prefix("y="))
monitor.y = int.parse(part.substring(2));
}
monitor.has_position = true;
}
private static double parse_double_token(string value, double fallback) {
return double.parse(value.strip().replace(",", "."));
}
private static string corners_to_value(Gee.ArrayList<string> corners) {
if (corners.size == 0)
return "";
if (corners.size == 1)
return corners[0];
bool top_left = corners.contains("top-left");
bool top_right = corners.contains("top-right");
bool bottom_left = corners.contains("bottom-left");
bool bottom_right = corners.contains("bottom-right");
if (top_left && top_right && bottom_left && bottom_right)
return "all";
return "";
}
}
}
......@@ -54,6 +54,10 @@ namespace TunerDisplays {
public bool variable_refresh_rate { get; set; }
public Gee.ArrayList<int> supported_color_modes { get; private set; default = new Gee.ArrayList<int>(); }
public bool dpms { get; set; default = true; }
public bool niri_focus_at_startup { get; set; }
public string niri_backdrop_color { get; set; default = ""; }
public string niri_hot_corners { get; set; default = ""; }
public bool niri_has_saved_position { get; set; }
public Gee.ArrayList<DisplayMode> modes { get; private set; default = new Gee.ArrayList<DisplayMode>(); }
public string display_name {
......
......@@ -9,6 +9,7 @@ sources = files(
'backends/display-backend.vala',
'backends/gnome-backend.vala',
'backends/hyprland-backend.vala',
'backends/niri-backend.vala',
'core/display-model.vala',
'core/shell-command.vala',
'ui/displays-view.vala',
......
......@@ -144,7 +144,7 @@ namespace TunerDisplays {
} else {
add_gnome_primary_row();
foreach (var monitor in monitors) {
var row = new MonitorRow(monitor, monitor_settings_page_id(), monitors);
var row = new MonitorRow(monitor, monitor_settings_page_id(), monitors, backend.id);
row.monitor_selected.connect(monitor => monitor_settings_requested(monitor));
row.monitor_changed.connect(() => layout.recenter());
monitors_group.add(row);
......@@ -541,6 +541,10 @@ namespace TunerDisplays {
target.supports_variable_refresh_rate = source.supports_variable_refresh_rate;
target.variable_refresh_rate = source.variable_refresh_rate;
target.dpms = source.dpms;
target.niri_focus_at_startup = source.niri_focus_at_startup;
target.niri_backdrop_color = source.niri_backdrop_color;
target.niri_hot_corners = source.niri_hot_corners;
target.niri_has_saved_position = source.niri_has_saved_position;
target.modes.clear();
foreach (var mode in source.modes)
......
......@@ -4,12 +4,13 @@ namespace TunerDisplays {
private MonitorConfig monitor;
private Gee.ArrayList<MonitorConfig> all_monitors;
private string page_id;
private string backend_id;
private Gtk.Switch enabled_switch;
public signal void monitor_changed();
public signal void monitor_selected(MonitorConfig monitor);
public MonitorRow(MonitorConfig monitor, string page_id, Gee.ArrayList<MonitorConfig> all_monitors) {
public MonitorRow(MonitorConfig monitor, string page_id, Gee.ArrayList<MonitorConfig> all_monitors, string backend_id) {
Object(
title: monitor.title,
subtitle: "%dx%d@%.2f scale %.2f %dx%d".printf(
......@@ -19,6 +20,7 @@ namespace TunerDisplays {
this.monitor = monitor;
this.all_monitors = all_monitors;
this.page_id = page_id;
this.backend_id = backend_id;
build();
}
......@@ -36,7 +38,7 @@ namespace TunerDisplays {
can_focus = true
};
enabled_switch.notify["active"].connect(() => {
if (enabled_switch.active != monitor.enabled)
if (enabled_switch.active != monitor.enabled && backend_id != "niri")
place_monitor_after_active(monitor, all_monitors);
monitor.enabled = enabled_switch.active;
monitor_changed();
......
......@@ -32,7 +32,7 @@ namespace TunerDisplays {
enabled_row.visible = show_enabled;
enabled_row.active = monitor.enabled;
enabled_row.notify["active"].connect(() => {
if (enabled_row.active != monitor.enabled)
if (enabled_row.active != monitor.enabled && backend_id != "niri")
place_monitor_after_active(monitor, all_monitors);
monitor.enabled = enabled_row.active;
emit_changed();
......@@ -52,6 +52,10 @@ namespace TunerDisplays {
if (backend_id == "hyprland") {
add_hyprland_rows();
} else if (backend_id == "niri") {
add_niri_rows(basic_group);
hyprland_group.visible = false;
hdr_group.visible = false;
} else if (backend_id == "gnome") {
add_gnome_rows(basic_group);
hyprland_group.visible = false;
......@@ -396,6 +400,72 @@ namespace TunerDisplays {
hdr_group.add(icc);
}
private void add_niri_rows(Adw.PreferencesGroup group) {
if (monitor.supports_variable_refresh_rate) {
add_int_combo(group, _("Variable Refresh Rate"), new string[] {
_("Off"), _("On"), _("On demand")
}, new int[] { 0, 1, 2 }, monitor.vrr, value => monitor.vrr = value);
}
var focus = new Adw.SwitchRow() {
title = _("Focus at startup"),
active = monitor.niri_focus_at_startup
};
focus.notify["active"].connect(() => {
monitor.niri_focus_at_startup = focus.active;
emit_changed();
});
group.add(focus);
add_niri_backdrop_color_row(group);
add_string_combo_with_labels(group, _("Hot corners"), new string[] {
_("Default"), _("Off"), _("All"), _("Top left"), _("Top right"), _("Bottom left"), _("Bottom right")
}, new string[] {
"", "off", "all", "top-left", "top-right", "bottom-left", "bottom-right"
}, monitor.niri_hot_corners, value => monitor.niri_hot_corners = value);
}
private void add_niri_backdrop_color_row(Adw.PreferencesGroup group) {
var row = new Adw.ActionRow() {
title = _("Backdrop color")
};
var enabled = new Gtk.Switch() {
valign = Gtk.Align.CENTER,
active = monitor.niri_backdrop_color != ""
};
Gdk.RGBA color = { 0, 0, 0, 1 };
if (monitor.niri_backdrop_color != "")
color.parse(monitor.niri_backdrop_color);
var button = new Gtk.ColorButton.with_rgba(color) {
valign = Gtk.Align.CENTER,
sensitive = enabled.active,
title = _("Backdrop color"),
use_alpha = true
};
enabled.notify["active"].connect(() => {
button.sensitive = enabled.active;
var selected = button.rgba;
monitor.niri_backdrop_color = enabled.active ? rgba_to_css(selected) : "";
emit_changed();
});
button.notify["rgba"].connect(() => {
if (!enabled.active)
return;
var selected = button.rgba;
monitor.niri_backdrop_color = rgba_to_css(selected);
emit_changed();
});
row.add_suffix(button);
row.add_suffix(enabled);
group.add(row);
}
private void add_gnome_rows(Adw.PreferencesGroup group) {
var underscanning = new Adw.SwitchRow() {
title = _("Underscanning"),
......@@ -468,11 +538,15 @@ namespace TunerDisplays {
}
private void add_string_combo(Adw.PreferencesGroup group, string title, string[] values, string current, StringChanged changed) {
add_string_combo_with_labels(group, title, values, values, current, changed);
}
private void add_string_combo_with_labels(Adw.PreferencesGroup group, string title, string[] labels, string[] values, string current, StringChanged changed) {
var model = new Gtk.StringList(null);
var stored_values = new Gee.ArrayList<string>();
uint selected = 0;
for (int i = 0; i < values.length; i++) {
model.append(values[i]);
model.append(labels[i]);
stored_values.add(values[i]);
if (values[i] == current)
selected = i;
......@@ -493,6 +567,24 @@ namespace TunerDisplays {
group.add(row);
}
private static string rgba_to_css(Gdk.RGBA rgba) {
if (rgba.alpha >= 0.999) {
return "#%02x%02x%02x".printf(
(int) Math.round(rgba.red * 255),
(int) Math.round(rgba.green * 255),
(int) Math.round(rgba.blue * 255)
);
}
var alpha = "%.3f".printf(rgba.alpha).replace(",", ".");
return "rgba(%d, %d, %d, %s)".printf(
(int) Math.round(rgba.red * 255),
(int) Math.round(rgba.green * 255),
(int) Math.round(rgba.blue * 255),
alpha
);
}
private void emit_changed() {
monitor_changed();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment