Initial commit

parents
build/
builddir/
*.so
*.o
*.mo
# Tuner Displays
Плагин Tuner для настройки мониторов.
## Поддержка
- Hyprland: чтение через `hyprctl -j monitors all`, сохранение в `~/.config/hypr/monitors.conf`.
- GNOME: чтение и применение настроек через Mutter DisplayConfig.
Backend выбирается по XDG-переменным:
- `XDG_CURRENT_DESKTOP`;
- `XDG_SESSION_DESKTOP`, если `XDG_CURRENT_DESKTOP` пустой.
## Hyprland
Плагин пишет `monitorv2` в hyprlang-формате. Для совместимости с `nwg-displays` используется файл:
```text
~/.config/hypr/monitors.conf
```
Пример:
```text
monitorv2 {
output = DP-1
mode = 1920x1080@60.00
position = 0x0
scale = 1.00
}
```
Для монитора можно включить сохранение по `desc:...`, если имя порта нестабильно.
## GNOME
GNOME-бэкенд поддерживает обычный и зеркальный режимы, выбор основного дисплея, разрешение, масштаб, поворот, частоту обновления, VRR, underscan и доступные цветовые режимы. В зеркальном режиме отдельные настройки мониторов скрываются, а общие параметры переносятся на главную страницу.
## Сборка
```sh
meson setup build
meson compile -C build
```
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/ru/ximperlinux/tuner/Displays">
<file alias="displays-view.ui" preprocess="xml-stripblanks">ui/displays-view.ui</file>
<file alias="monitor-settings-content.ui" preprocess="xml-stripblanks">ui/monitor-settings-content.ui</file>
</gresource>
</gresources>
<?xml version="1.0" encoding="UTF-8"?>
<component type="addon">
<id>ru.ximperlinux.tuner.Displays</id>
<extends>org.altlinux.Tuner</extends>
<name>Displays</name>
<summary>Monitor settings page for Tuner</summary>
<metadata_license>MIT</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
</component>
subdir('ui')
resources = gnome.compile_resources(
'resources',
'gresource.xml',
source_dir: meson.current_build_dir(),
dependencies: custom_target(
'blueprints',
input: blueprints,
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
)
)
i18n.merge_file(
input: 'id.metainfo.xml.in',
output: id + '.metainfo.xml',
po_dir: meson.project_source_root() / 'po',
install: true,
install_dir: get_option('datadir') / 'metainfo'
)
configure_file(
input: 'plugin.in',
output: output + '.plugin',
configuration: {
'NAME': name,
'MODULE': output,
'VERSION': version,
},
install: true,
install_dir: get_option('libdir') / 'tuner' / 'plugins'
)
[Plugin]
Name=@NAME@
Module=@MODULE@
Version=@VERSION@
Authors=Fiersik Kouji
Copyright=Copyright © 2026 Etersoft
Website=https://gitlab.eterfund.ru/ximperlinux/tuner-displays
X-Priority=-5
using Gtk 4.0;
using Adw 1;
translation-domain "tuner-displays";
template $TunerDisplaysDisplaysView : Adw.PreferencesPage {
title: _("Displays");
icon-name: "video-display-symbolic";
Adw.PreferencesGroup status_group {}
Adw.PreferencesGroup layout_group {}
Adw.PreferencesGroup monitors_group {
title: _("Details");
}
}
blueprints = files(
'displays-view.blp',
'monitor-settings-content.blp',
)
using Gtk 4.0;
using Adw 1;
translation-domain "tuner-displays";
template $TunerDisplaysMonitorSettingsContent : Adw.PreferencesPage {
Adw.PreferencesGroup basic_group {
title: _("Display");
Adw.SwitchRow enabled_row {
title: _("Enabled");
}
}
Adw.PreferencesGroup hyprland_group {
title: _("Hyprland");
}
Adw.PreferencesGroup hdr_group {
title: _("HDR / EDID overrides");
}
}
project(
'Displays',
['c', 'vala'],
version: '0.1.0',
meson_version: '>= 1.0',
)
id = 'ru.ximperlinux.tuner.Displays'
name = meson.project_name()
version = meson.project_version()
output = 'displays'
po_output = 'tuner-displays'
add_project_arguments(
'-w', '-DGETTEXT_PACKAGE="@0@"'.format(po_output),
language: 'c'
)
gnome = import('gnome')
i18n = import('i18n')
config = configuration_data()
config.set('GETTEXT_PACKAGE', po_output)
config.set('LOCALEDIR', get_option('prefix') / get_option('localedir'))
subdir('data')
subdir('src')
subdir('po')
data/ui/displays-view.blp
data/ui/monitor-settings-content.blp
src/plugin.vala
src/core/display-model.vala
src/ui/displays-view.vala
src/ui/monitor-settings-content.vala
src/ui/ui-helpers.vala
i18n.gettext(po_output, preset: 'glib')
msgid ""
msgstr ""
"Project-Id-Version: tuner-displays\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-29 23:02+0300\n"
"PO-Revision-Date: 2026-05-28 00:00+0000\n"
"Last-Translator: Automatically generated\n"
"Language-Team: Russian\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: data/ui/displays-view.blp:7 src/plugin.vala:15
msgid "Displays"
msgstr "Мониторы"
#: data/ui/displays-view.blp:15
msgid "Details"
msgstr "Параметры"
#: data/ui/monitor-settings-content.blp:8
msgid "Display"
msgstr "Дисплей"
#: data/ui/monitor-settings-content.blp:11
msgid "Enabled"
msgstr "Включён"
#: data/ui/monitor-settings-content.blp:16
msgid "Hyprland"
msgstr "Hyprland"
#: data/ui/monitor-settings-content.blp:20
msgid "HDR / EDID overrides"
msgstr "Переопределения HDR / EDID"
#: src/plugin.vala:24
msgid "Refresh"
msgstr "Обновить"
#: src/plugin.vala:29 src/plugin.vala:30 src/plugin.vala:53 src/plugin.vala:54
msgid "Apply"
msgstr "Применить"
#: src/plugin.vala:41 src/plugin.vala:47
msgid "Monitor"
msgstr "Монитор"
#: src/core/display-model.vala:62
msgid "Built-in Display"
msgstr "Встроенный дисплей"
#: src/ui/displays-view.vala:67
msgid "Failed to load monitors"
msgstr "Не удалось загрузить мониторы"
#: src/ui/displays-view.vala:76
msgid "Monitor settings applied"
msgstr "Настройки мониторов применены"
#: src/ui/displays-view.vala:159
msgid "Read-only backend"
msgstr "Режим только для чтения"
#: src/ui/displays-view.vala:160
msgid "Applying monitor layouts is not supported by this backend."
msgstr "Применение раскладок мониторов не поддерживается этим бэкендом."
#: src/ui/displays-view.vala:170
msgid "Mirror Displays"
msgstr "Зеркалировать мониторы"
#: src/ui/displays-view.vala:230 src/ui/monitor-settings-content.vala:86
#: src/ui/monitor-settings-content.vala:125
msgid "Resolution"
msgstr "Разрешение"
#: src/ui/displays-view.vala:276 src/ui/monitor-settings-content.vala:263
#: src/ui/monitor-settings-content.vala:289
msgid "Scale"
msgstr "Масштаб"
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
msgid "Normal"
msgstr "Обычный"
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
msgid "90 degrees"
msgstr "90 градусов"
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
msgid "180 degrees"
msgstr "180 градусов"
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
msgid "270 degrees"
msgstr "270 градусов"
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
msgid "Flipped"
msgstr "Отражённый"
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
msgid "Flipped 90 degrees"
msgstr "Отражённый 90 градусов"
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
msgid "Flipped 180 degrees"
msgstr "Отражённый 180 градусов"
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
msgid "Flipped 270 degrees"
msgstr "Отражённый 270 градусов"
#: src/ui/displays-view.vala:301 src/ui/monitor-settings-content.vala:313
msgid "Rotation"
msgstr "Поворот"
#: src/ui/displays-view.vala:334
msgid "Primary Display"
msgstr "Основной дисплей"
#: src/ui/monitor-settings-content.vala:154
#: src/ui/monitor-settings-content.vala:162
msgid "Refresh Rate"
msgstr "Частота обновления"
#: src/ui/monitor-settings-content.vala:184
msgid "Variable Refresh Rate"
msgstr "Переменная частота обновления"
#: src/ui/monitor-settings-content.vala:333
msgid "None"
msgstr "Нет"
#: src/ui/monitor-settings-content.vala:348
msgid "Mirror"
msgstr "Зеркалирование"
#: src/ui/monitor-settings-content.vala:364
msgid "Use description"
msgstr "Использовать описание"
#: src/ui/monitor-settings-content.vala:373
msgid "Bit depth"
msgstr "Глубина цвета"
#: src/ui/monitor-settings-content.vala:374
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
msgid "Off"
msgstr "Отключено"
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
msgid "On"
msgstr "Включено"
#: src/ui/monitor-settings-content.vala:374
msgid "Fullscreen"
msgstr "Полный экран"
#: src/ui/monitor-settings-content.vala:374
msgid "Fullscreen video/game"
msgstr "Полноэкранное видео/игра"
#: src/ui/monitor-settings-content.vala:375
msgid "Color management"
msgstr "Управление цветом"
#: src/ui/monitor-settings-content.vala:376
msgid "SDR EOTF"
msgstr "SDR EOTF"
#: src/ui/monitor-settings-content.vala:377
msgid "SDR brightness"
msgstr "Яркость SDR"
#: src/ui/monitor-settings-content.vala:378
msgid "SDR saturation"
msgstr "Насыщенность SDR"
#: src/ui/monitor-settings-content.vala:380
msgid "Force wide color"
msgstr "Принудительно широкий цветовой охват"
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
msgid "Auto"
msgstr "Авто"
#: src/ui/monitor-settings-content.vala:381
msgid "Force HDR"
msgstr "Принудительно HDR"
#: src/ui/monitor-settings-content.vala:382
msgid "SDR min luminance"
msgstr "Мин. яркость SDR"
#: src/ui/monitor-settings-content.vala:383
msgid "SDR max luminance"
msgstr "Макс. яркость SDR"
#: src/ui/monitor-settings-content.vala:384
msgid "Min luminance"
msgstr "Мин. яркость"
#: src/ui/monitor-settings-content.vala:385
msgid "Max luminance"
msgstr "Макс. яркость"
#: src/ui/monitor-settings-content.vala:386
msgid "Max average luminance"
msgstr "Макс. средняя яркость"
#: src/ui/monitor-settings-content.vala:389
msgid "ICC profile"
msgstr "Профиль ICC"
#: src/ui/monitor-settings-content.vala:401
msgid "Underscanning"
msgstr "Underscan"
#: src/ui/monitor-settings-content.vala:412
msgid "HDR"
msgstr "HDR"
#: src/ui/monitor-settings-content.vala:571
#, c-format
msgid "Variable (up to %.2f Hz)"
msgstr "Переменная (до %.2f Гц)"
#: src/ui/monitor-settings-content.vala:572
msgid "Variable"
msgstr "Переменная"
#: src/ui/monitor-settings-content.vala:575 src/ui/ui-helpers.vala:50
#, c-format
msgid "%.2f Hz"
msgstr "%.2f Гц"
#~ msgid "Color Mode"
#~ msgstr "Цветовой режим"
#~ msgid "Default"
#~ msgstr "По умолчанию"
#, c-format
#~ msgid "Mode %d"
#~ msgstr "Режим %d"
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the tuner-displays package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: tuner-displays\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-29 23:02+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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: data/ui/displays-view.blp:7 src/plugin.vala:15
msgid "Displays"
msgstr ""
#: data/ui/displays-view.blp:15
msgid "Details"
msgstr ""
#: data/ui/monitor-settings-content.blp:8
msgid "Display"
msgstr ""
#: data/ui/monitor-settings-content.blp:11
msgid "Enabled"
msgstr ""
#: data/ui/monitor-settings-content.blp:16
msgid "Hyprland"
msgstr ""
#: data/ui/monitor-settings-content.blp:20
msgid "HDR / EDID overrides"
msgstr ""
#: src/plugin.vala:24
msgid "Refresh"
msgstr ""
#: src/plugin.vala:29 src/plugin.vala:30 src/plugin.vala:53 src/plugin.vala:54
msgid "Apply"
msgstr ""
#: src/plugin.vala:41 src/plugin.vala:47
msgid "Monitor"
msgstr ""
#: src/core/display-model.vala:62
msgid "Built-in Display"
msgstr ""
#: src/ui/displays-view.vala:67
msgid "Failed to load monitors"
msgstr ""
#: src/ui/displays-view.vala:76
msgid "Monitor settings applied"
msgstr ""
#: src/ui/displays-view.vala:159
msgid "Read-only backend"
msgstr ""
#: src/ui/displays-view.vala:160
msgid "Applying monitor layouts is not supported by this backend."
msgstr ""
#: src/ui/displays-view.vala:170
msgid "Mirror Displays"
msgstr ""
#: src/ui/displays-view.vala:230 src/ui/monitor-settings-content.vala:86
#: src/ui/monitor-settings-content.vala:125
msgid "Resolution"
msgstr ""
#: src/ui/displays-view.vala:276 src/ui/monitor-settings-content.vala:263
#: src/ui/monitor-settings-content.vala:289
msgid "Scale"
msgstr ""
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
msgid "Normal"
msgstr ""
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
msgid "90 degrees"
msgstr ""
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
msgid "180 degrees"
msgstr ""
#: src/ui/displays-view.vala:294 src/ui/monitor-settings-content.vala:306
msgid "270 degrees"
msgstr ""
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
msgid "Flipped"
msgstr ""
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
msgid "Flipped 90 degrees"
msgstr ""
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
msgid "Flipped 180 degrees"
msgstr ""
#: src/ui/displays-view.vala:295 src/ui/monitor-settings-content.vala:307
msgid "Flipped 270 degrees"
msgstr ""
#: src/ui/displays-view.vala:301 src/ui/monitor-settings-content.vala:313
msgid "Rotation"
msgstr ""
#: src/ui/displays-view.vala:334
msgid "Primary Display"
msgstr ""
#: src/ui/monitor-settings-content.vala:154
#: src/ui/monitor-settings-content.vala:162
msgid "Refresh Rate"
msgstr ""
#: src/ui/monitor-settings-content.vala:184
msgid "Variable Refresh Rate"
msgstr ""
#: src/ui/monitor-settings-content.vala:333
msgid "None"
msgstr ""
#: src/ui/monitor-settings-content.vala:348
msgid "Mirror"
msgstr ""
#: src/ui/monitor-settings-content.vala:364
msgid "Use description"
msgstr ""
#: src/ui/monitor-settings-content.vala:373
msgid "Bit depth"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
msgid "VRR"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
msgid "Off"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
msgid "On"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
msgid "Fullscreen"
msgstr ""
#: src/ui/monitor-settings-content.vala:374
msgid "Fullscreen video/game"
msgstr ""
#: src/ui/monitor-settings-content.vala:375
msgid "Color management"
msgstr ""
#: src/ui/monitor-settings-content.vala:376
msgid "SDR EOTF"
msgstr ""
#: src/ui/monitor-settings-content.vala:377
msgid "SDR brightness"
msgstr ""
#: src/ui/monitor-settings-content.vala:378
msgid "SDR saturation"
msgstr ""
#: src/ui/monitor-settings-content.vala:380
msgid "Force wide color"
msgstr ""
#: src/ui/monitor-settings-content.vala:380
#: src/ui/monitor-settings-content.vala:381
msgid "Auto"
msgstr ""
#: src/ui/monitor-settings-content.vala:381
msgid "Force HDR"
msgstr ""
#: src/ui/monitor-settings-content.vala:382
msgid "SDR min luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:383
msgid "SDR max luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:384
msgid "Min luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:385
msgid "Max luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:386
msgid "Max average luminance"
msgstr ""
#: src/ui/monitor-settings-content.vala:389
msgid "ICC profile"
msgstr ""
#: src/ui/monitor-settings-content.vala:401
msgid "Underscanning"
msgstr ""
#: src/ui/monitor-settings-content.vala:412
msgid "HDR"
msgstr ""
#: src/ui/monitor-settings-content.vala:571
#, c-format
msgid "Variable (up to %.2f Hz)"
msgstr ""
#: src/ui/monitor-settings-content.vala:572
msgid "Variable"
msgstr ""
#: src/ui/monitor-settings-content.vala:575 src/ui/ui-helpers.vala:50
#, c-format
msgid "%.2f Hz"
msgstr ""
namespace TunerDisplays {
public errordomain BackendError {
UNSUPPORTED,
COMMAND_FAILED,
PARSE_FAILED,
APPLY_FAILED
}
public abstract class DisplayBackend : Object {
public abstract string id { get; }
public abstract string title { owned get; }
public abstract bool can_apply { get; }
public abstract Gee.ArrayList<MonitorConfig> load() throws Error;
public abstract void apply(Gee.ArrayList<MonitorConfig> monitors) throws Error;
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 ("hyprland" in detected) {
return new HyprlandBackend();
}
if ("gnome" in detected) {
return new GnomeBackend();
}
return new GnomeBackend();
}
}
}
namespace TunerDisplays {
public class GnomeBackend : DisplayBackend {
private const string BUS_NAME = "org.gnome.Mutter.DisplayConfig";
private const string OBJECT_PATH = "/org/gnome/Mutter/DisplayConfig";
private const string INTERFACE = "org.gnome.Mutter.DisplayConfig";
private const uint METHOD_PERSISTENT = 2;
private uint32 serial;
private bool supports_changing_layout_mode;
private uint32 layout_mode = 1;
public override string id { get { return "gnome"; } }
public override string title { owned get { return "GNOME"; } }
public override bool can_apply { get { return true; } }
public override Gee.ArrayList<MonitorConfig> load() throws Error {
var state = call_display_config("GetCurrentState", null, null);
serial = state.get_child_value(0).get_uint32();
var monitors = new Gee.ArrayList<MonitorConfig>();
var monitor_array = state.get_child_value(1);
for (size_t i = 0; i < monitor_array.n_children(); i++)
monitors.add(parse_monitor(monitor_array.get_child_value(i)));
parse_logical_monitors(state.get_child_value(2), monitors);
parse_cloning_state(state.get_child_value(2), monitors);
parse_global_properties(state.get_child_value(3));
foreach (var monitor in monitors) {
if (monitor.width <= 0 || monitor.height <= 0)
apply_first_available_mode(monitor);
sanitize_monitor(monitor);
}
place_disabled_after_enabled(monitors);
return monitors;
}
public override void apply(Gee.ArrayList<MonitorConfig> monitors) throws Error {
var requested = monitors;
var applied = load();
merge_requested_state(requested, applied);
var cloning = is_cloning(applied);
if (cloning)
setup_clone_layout(applied);
else
ensure_adjacent_layout(applied);
normalize_enabled_positions(applied);
var logical_monitors = new VariantBuilder(new VariantType("a(iiduba(ssa{sv}))"));
bool primary_set = false;
foreach (var monitor in applied) {
if (monitor.enabled && monitor.primary) {
primary_set = true;
break;
}
}
bool primary_written = false;
if (cloning) {
var primary = primary_set ? primary_monitor(applied) : first_enabled_monitor(applied);
if (primary == null)
throw new BackendError.APPLY_FAILED("No enabled monitors to mirror");
logical_monitors.add(
"(iidub@*)",
0,
0,
primary.scale,
transform_to_code(primary.transform),
true,
build_clone_monitor_specs(applied)
);
} else {
foreach (var monitor in applied) {
if (!monitor.enabled)
continue;
var primary = monitor.primary;
if (!primary_set) {
primary = true;
primary_set = true;
}
logical_monitors.add(
"(iidub@*)",
monitor.x,
monitor.y,
monitor.scale,
transform_to_code(monitor.transform),
primary && !primary_written,
build_monitor_specs(monitor)
);
if (primary)
primary_written = true;
}
}
var props = new VariantBuilder(new VariantType("a{sv}"));
if (supports_changing_layout_mode)
props.add("{sv}", "layout-mode", new Variant.uint32(layout_mode));
call_display_config(
"ApplyMonitorsConfig",
new Variant("(uu@*@*)", serial, METHOD_PERSISTENT, logical_monitors.end(), props.end()),
null
);
merge_requested_state(applied, requested);
}
private MonitorConfig parse_monitor(Variant variant) {
var spec = variant.get_child_value(0);
var modes = variant.get_child_value(1);
var props = variant.get_child_value(2);
var monitor = new MonitorConfig();
monitor.name = spec.get_child_value(0).get_string();
monitor.vendor = spec.get_child_value(1).get_string();
monitor.product = spec.get_child_value(2).get_string();
monitor.serial = spec.get_child_value(3).get_string();
monitor.enabled = false;
monitor.description = read_string_property(props, "display-name", describe(monitor));
monitor.underscanning = read_bool_property(props, "is-underscanning", false);
monitor.color_mode = (int) read_uint_property(props, "color-mode", 0);
var supported_color_modes = lookup_property(props, "supported-color-modes");
if (supported_color_modes != null) {
for (size_t i = 0; i < supported_color_modes.n_children(); i++)
monitor.supported_color_modes.add((int) supported_color_modes.get_child_value(i).get_uint32());
}
for (size_t i = 0; i < modes.n_children(); i++) {
var mode_variant = modes.get_child_value(i);
var mode = parse_mode(mode_variant);
monitor.modes.add(mode);
if (mode.variable_refresh_rate)
monitor.supports_variable_refresh_rate = true;
if (read_bool_property(mode_variant.get_child_value(6), "is-current", false)) {
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
monitor.variable_refresh_rate = mode.variable_refresh_rate;
}
}
return monitor;
}
private DisplayMode parse_mode(Variant variant) {
var mode = new DisplayMode();
mode.id = variant.get_child_value(0).get_string();
mode.width = variant.get_child_value(1).get_int32();
mode.height = variant.get_child_value(2).get_int32();
mode.refresh = variant.get_child_value(3).get_double();
mode.preferred_scale = variant.get_child_value(4).get_double();
mode.variable_refresh_rate = read_string_property(variant.get_child_value(6), "refresh-rate-mode", "fixed") == "variable";
var scales = variant.get_child_value(5);
for (size_t i = 0; i < scales.n_children(); i++)
mode.supported_scales.add(scales.get_child_value(i).get_double());
return mode;
}
private void parse_logical_monitors(Variant logicals, Gee.ArrayList<MonitorConfig> monitors) {
for (size_t i = 0; i < logicals.n_children(); i++) {
var logical = logicals.get_child_value(i);
int x = logical.get_child_value(0).get_int32();
int y = logical.get_child_value(1).get_int32();
double scale = logical.get_child_value(2).get_double();
string transform = transform_from_code(logical.get_child_value(3).get_uint32());
bool primary = logical.get_child_value(4).get_boolean();
var specs = logical.get_child_value(5);
for (size_t j = 0; j < specs.n_children(); j++) {
var spec = specs.get_child_value(j);
var monitor = find_monitor(monitors, spec);
if (monitor == null)
continue;
monitor.enabled = true;
monitor.x = x;
monitor.y = y;
monitor.scale = scale > 0 ? scale : 1.0;
monitor.transform = transform;
monitor.primary = primary;
}
}
}
private void parse_cloning_state(Variant logicals, Gee.ArrayList<MonitorConfig> monitors) {
int enabled_count = 0;
foreach (var monitor in monitors) {
if (monitor.enabled)
enabled_count++;
}
var cloning = enabled_count > 1 && logicals.n_children() == 1 && logicals.get_child_value(0).get_child_value(5).n_children() > 1;
foreach (var monitor in monitors)
monitor.mirrored = cloning;
}
private void parse_global_properties(Variant props) {
supports_changing_layout_mode = read_bool_property(props, "supports-changing-layout-mode", false);
layout_mode = read_uint_property(props, "layout-mode", 1);
}
private Variant build_monitor_specs(MonitorConfig monitor) {
var specs = new VariantBuilder(new VariantType("a(ssa{sv})"));
var props = new VariantBuilder(new VariantType("a{sv}"));
props.add("{sv}", "underscanning", new Variant.boolean(monitor.underscanning));
props.add("{sv}", "color-mode", new Variant.uint32((uint32) monitor.color_mode));
specs.add("(ss@*)", monitor.name, selected_mode_id(monitor), props.end());
return specs.end();
}
private Variant build_clone_monitor_specs(Gee.ArrayList<MonitorConfig> monitors) {
var specs = new VariantBuilder(new VariantType("a(ssa{sv})"));
foreach (var monitor in monitors) {
if (!monitor.enabled)
continue;
var props = new VariantBuilder(new VariantType("a{sv}"));
props.add("{sv}", "underscanning", new Variant.boolean(monitor.underscanning));
props.add("{sv}", "color-mode", new Variant.uint32((uint32) monitor.color_mode));
specs.add("(ss@*)", monitor.name, selected_mode_id(monitor), props.end());
}
return specs.end();
}
private static string selected_mode_id(MonitorConfig monitor) {
DisplayMode? same_resolution = null;
foreach (var mode in monitor.modes) {
if (mode.width == monitor.width
&& mode.height == monitor.height
&& Math.fabs(mode.refresh - monitor.refresh) < 0.02
&& mode.variable_refresh_rate == monitor.variable_refresh_rate) {
return mode.id;
}
if (same_resolution == null
&& mode.width == monitor.width
&& mode.height == monitor.height
&& mode.variable_refresh_rate == monitor.variable_refresh_rate) {
same_resolution = mode;
}
}
foreach (var mode in monitor.modes) {
if (mode.width == monitor.width
&& mode.height == monitor.height
&& Math.fabs(mode.refresh - monitor.refresh) < 0.02) {
return mode.id;
}
if (same_resolution == null && mode.width == monitor.width && mode.height == monitor.height)
same_resolution = mode;
}
if (same_resolution != null)
return same_resolution.id;
return monitor.modes.size > 0 ? monitor.modes[0].id : "";
}
private Variant call_display_config(string method, Variant? parameters, VariantType? reply_type) throws Error {
var connection = Bus.get_sync(BusType.SESSION);
return connection.call_sync(
BUS_NAME,
OBJECT_PATH,
INTERFACE,
method,
parameters,
reply_type,
DBusCallFlags.NONE,
-1
);
}
private static MonitorConfig? find_monitor(Gee.ArrayList<MonitorConfig> monitors, Variant spec) {
var connector = spec.get_child_value(0).get_string();
var vendor = spec.get_child_value(1).get_string();
var product = spec.get_child_value(2).get_string();
var serial = spec.get_child_value(3).get_string();
foreach (var monitor in monitors) {
if (monitor.name == connector
&& monitor.vendor == vendor
&& monitor.product == product
&& monitor.serial == serial) {
return monitor;
}
}
foreach (var monitor in monitors) {
if (monitor.name == connector)
return monitor;
}
return null;
}
private static void merge_requested_state(Gee.ArrayList<MonitorConfig> source, Gee.ArrayList<MonitorConfig> target) {
foreach (var target_monitor in target) {
var source_monitor = find_monitor_by_name(source, target_monitor.name);
if (source_monitor == null)
continue;
target_monitor.enabled = source_monitor.enabled;
target_monitor.primary = source_monitor.primary;
target_monitor.x = source_monitor.x;
target_monitor.y = source_monitor.y;
target_monitor.width = source_monitor.width;
target_monitor.height = source_monitor.height;
target_monitor.refresh = source_monitor.refresh;
target_monitor.scale = source_monitor.scale;
target_monitor.transform = source_monitor.transform;
target_monitor.mirrored = source_monitor.mirrored;
target_monitor.underscanning = source_monitor.underscanning;
target_monitor.color_mode = source_monitor.color_mode;
target_monitor.variable_refresh_rate = source_monitor.variable_refresh_rate;
}
}
private static MonitorConfig? find_monitor_by_name(Gee.ArrayList<MonitorConfig> monitors, string name) {
foreach (var monitor in monitors) {
if (monitor.name == name)
return monitor;
}
return null;
}
private static Variant? lookup_property(Variant props, string key) {
for (size_t i = 0; i < props.n_children(); i++) {
var entry = props.get_child_value(i);
if (entry.get_child_value(0).get_string() == key)
return entry.get_child_value(1).get_variant();
}
return null;
}
private static string read_string_property(Variant props, string key, string fallback) {
var value = lookup_property(props, key);
return value != null ? value.get_string() : fallback;
}
private static bool read_bool_property(Variant props, string key, bool fallback) {
var value = lookup_property(props, key);
return value != null ? value.get_boolean() : fallback;
}
private static uint32 read_uint_property(Variant props, string key, uint32 fallback) {
var value = lookup_property(props, key);
return value != null ? value.get_uint32() : fallback;
}
private static void apply_first_available_mode(MonitorConfig monitor) {
if (monitor.modes.size == 0)
return;
var mode = monitor.modes[0];
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
if (monitor.scale <= 0)
monitor.scale = mode.preferred_scale > 0 ? mode.preferred_scale : 1.0;
}
private static void sanitize_monitor(MonitorConfig monitor) {
if (monitor.scale <= 0 || Math.isinf(monitor.scale) != 0 || Math.isnan(monitor.scale) != 0)
monitor.scale = 1.0;
if (monitor.width <= 0)
monitor.width = 1024;
if (monitor.height <= 0)
monitor.height = 768;
foreach (var mode in monitor.modes) {
if (mode.preferred_scale <= 0 || Math.isinf(mode.preferred_scale) != 0 || Math.isnan(mode.preferred_scale) != 0)
mode.preferred_scale = 1.0;
for (int i = mode.supported_scales.size - 1; i >= 0; i--) {
var scale = mode.supported_scales[i];
if (scale <= 0 || Math.isinf(scale) != 0 || Math.isnan(scale) != 0)
mode.supported_scales.remove_at(i);
}
if (mode.supported_scales.size == 0)
mode.supported_scales.add(1.0);
}
}
private static void normalize_enabled_positions(Gee.ArrayList<MonitorConfig> monitors) {
bool first = true;
int min_x = 0;
int min_y = 0;
foreach (var monitor in monitors) {
if (!monitor.enabled)
continue;
if (first) {
min_x = monitor.x;
min_y = monitor.y;
first = false;
} else {
min_x = int.min(min_x, monitor.x);
min_y = int.min(min_y, monitor.y);
}
}
if (first || (min_x == 0 && min_y == 0))
return;
foreach (var monitor in monitors) {
if (!monitor.enabled)
continue;
monitor.x -= min_x;
monitor.y -= min_y;
}
}
private static void ensure_adjacent_layout(Gee.ArrayList<MonitorConfig> monitors) {
var enabled = new Gee.ArrayList<MonitorConfig>();
foreach (var monitor in monitors) {
if (monitor.enabled)
enabled.add(monitor);
}
if (enabled.size <= 1 || monitors_are_adjacent(enabled))
return;
enabled.sort((a, b) => {
if (a.x != b.x)
return a.x - b.x;
return a.y - b.y;
});
int x = 0;
foreach (var monitor in enabled) {
monitor.x = x;
monitor.y = 0;
x += (int) Math.round(logical_width(monitor));
}
}
private static bool monitors_are_adjacent(Gee.ArrayList<MonitorConfig> monitors) {
var connected = new Gee.HashSet<MonitorConfig>();
connected.add(monitors[0]);
bool changed = false;
do {
changed = false;
foreach (var monitor in monitors) {
if (connected.contains(monitor))
continue;
foreach (var other in connected) {
if (monitors_touch(monitor, other)) {
connected.add(monitor);
changed = true;
break;
}
}
}
} while (changed);
return connected.size == monitors.size;
}
private static bool monitors_touch(MonitorConfig a, MonitorConfig b) {
var ax1 = (double) a.x;
var ay1 = (double) a.y;
var ax2 = ax1 + logical_width(a);
var ay2 = ay1 + logical_height(a);
var bx1 = (double) b.x;
var by1 = (double) b.y;
var bx2 = bx1 + logical_width(b);
var by2 = by1 + logical_height(b);
var vertical_overlap = ay1 < by2 && ay2 > by1;
var horizontal_overlap = ax1 < bx2 && ax2 > bx1;
var touches_left_or_right = Math.fabs(ax2 - bx1) < 0.5 || Math.fabs(bx2 - ax1) < 0.5;
var touches_top_or_bottom = Math.fabs(ay2 - by1) < 0.5 || Math.fabs(by2 - ay1) < 0.5;
return (touches_left_or_right && vertical_overlap) || (touches_top_or_bottom && horizontal_overlap);
}
private static bool is_cloning(Gee.ArrayList<MonitorConfig> monitors) {
int enabled_count = 0;
foreach (var monitor in monitors) {
if (monitor.enabled)
enabled_count++;
}
return enabled_count > 1 && monitors.size > 1 && monitors[0].mirrored;
}
private static void setup_clone_layout(Gee.ArrayList<MonitorConfig> monitors) throws Error {
var reference = first_enabled_monitor(monitors);
if (reference == null)
throw new BackendError.APPLY_FAILED("No enabled monitors to mirror");
var clone_mode = find_clone_mode(monitors, reference);
if (clone_mode == null)
throw new BackendError.APPLY_FAILED("No common mirror mode is available");
var scale = clone_mode.preferred_scale > 0 ? clone_mode.preferred_scale : reference.scale;
if (scale <= 0)
scale = 1.0;
foreach (var monitor in monitors) {
if (!monitor.enabled)
continue;
var compatible = find_compatible_mode(monitor, clone_mode);
if (compatible == null)
throw new BackendError.APPLY_FAILED("No common mirror mode is available");
monitor.width = compatible.width;
monitor.height = compatible.height;
monitor.refresh = compatible.refresh;
monitor.variable_refresh_rate = compatible.variable_refresh_rate;
monitor.scale = mode_supports_scale(compatible, scale) ? scale : compatible.preferred_scale;
if (monitor.scale <= 0)
monitor.scale = 1.0;
monitor.x = 0;
monitor.y = 0;
}
}
private static DisplayMode? find_clone_mode(Gee.ArrayList<MonitorConfig> monitors, MonitorConfig reference) {
DisplayMode? best = null;
foreach (var mode in reference.modes) {
if (!all_enabled_support_mode(monitors, mode))
continue;
if (best == null || mode.width * mode.height > best.width * best.height)
best = mode;
}
return best;
}
private static bool all_enabled_support_mode(Gee.ArrayList<MonitorConfig> monitors, DisplayMode mode) {
foreach (var monitor in monitors) {
if (!monitor.enabled)
continue;
if (find_compatible_mode(monitor, mode) == null)
return false;
}
return true;
}
private static DisplayMode? find_compatible_mode(MonitorConfig monitor, DisplayMode mode) {
foreach (var candidate in monitor.modes) {
if (candidate.width == mode.width
&& candidate.height == mode.height
&& candidate.variable_refresh_rate == mode.variable_refresh_rate) {
return candidate;
}
}
foreach (var candidate in monitor.modes) {
if (candidate.width == mode.width && candidate.height == mode.height)
return candidate;
}
return null;
}
private static bool mode_supports_scale(DisplayMode mode, double scale) {
foreach (var supported in mode.supported_scales) {
if (Math.fabs(supported - scale) < 0.01)
return true;
}
return false;
}
private static MonitorConfig? primary_monitor(Gee.ArrayList<MonitorConfig> monitors) {
foreach (var monitor in monitors) {
if (monitor.enabled && monitor.primary)
return monitor;
}
return null;
}
private static MonitorConfig? first_enabled_monitor(Gee.ArrayList<MonitorConfig> monitors) {
foreach (var monitor in monitors) {
if (monitor.enabled)
return monitor;
}
return null;
}
private static double logical_width(MonitorConfig monitor) {
if (monitor.transform.index_of("90") >= 0 || monitor.transform.index_of("270") >= 0)
return monitor.height / monitor.scale;
return monitor.width / monitor.scale;
}
private static double logical_height(MonitorConfig monitor) {
if (monitor.transform.index_of("90") >= 0 || monitor.transform.index_of("270") >= 0)
return monitor.width / monitor.scale;
return monitor.height / monitor.scale;
}
private static void place_disabled_after_enabled(Gee.ArrayList<MonitorConfig> monitors) {
int x = 0;
bool found_enabled = false;
foreach (var monitor in monitors) {
if (!monitor.enabled)
continue;
var right = monitor.x + (int) Math.round(logical_width(monitor));
if (!found_enabled || right > x)
x = right;
found_enabled = true;
}
foreach (var monitor in monitors) {
if (monitor.enabled)
continue;
monitor.x = x;
monitor.y = 0;
x += (int) Math.round(logical_width(monitor));
}
}
private static string describe(MonitorConfig monitor) {
if (monitor.vendor != "" && monitor.product != "")
return "%s %s".printf(monitor.vendor, monitor.product);
if (monitor.product != "")
return monitor.product;
if (monitor.vendor != "")
return monitor.vendor;
return monitor.name;
}
private static string transform_from_code(uint32 code) {
switch (code) {
case 1: return "90";
case 2: return "180";
case 3: return "270";
case 4: return "flipped";
case 5: return "flipped-90";
case 6: return "flipped-180";
case 7: return "flipped-270";
default: return "normal";
}
}
private static uint32 transform_to_code(string transform) {
switch (transform) {
case "90": return 1;
case "180": return 2;
case "270": return 3;
case "flipped": return 4;
case "flipped-90": return 5;
case "flipped-180": return 6;
case "flipped-270": return 7;
default: return 0;
}
}
}
}
namespace TunerDisplays {
public class HyprlandBackend : DisplayBackend {
private const string[] TRANSFORMS = {
"normal", "90", "180", "270", "flipped", "flipped-90", "flipped-180", "flipped-270"
};
private class SavedMonitor : Object {
public bool use_description { get; set; }
public bool has_mode { get; set; }
public int width { get; set; }
public int height { get; set; }
public double refresh { get; set; }
public bool has_position { get; set; }
public int x { get; set; }
public int y { get; set; }
public bool has_scale { get; set; }
public double scale { get; set; default = 1.0; }
public string mirror { get; set; default = ""; }
}
public override string id { get { return "hyprland"; } }
public override string title { owned get { return "Hyprland"; } }
public override bool can_apply { get { return true; } }
public override Gee.ArrayList<MonitorConfig> load() throws Error {
var monitors = new Gee.ArrayList<MonitorConfig>();
var active = read_active_names();
var saved = read_saved_monitors();
var root = parse_json(ShellCommand.run("hyprctl -j monitors all"));
if (root.get_node_type() != Json.NodeType.ARRAY)
throw new BackendError.PARSE_FAILED("hyprctl monitors all returned non-array JSON");
var array = root.get_array();
for (uint i = 0; i < array.get_length(); i++) {
var obj = array.get_object_element(i);
var monitor = new MonitorConfig();
monitor.name = obj.get_string_member("name");
monitor.enabled = active.contains(monitor.name);
monitor.description = get_string(obj, "description");
monitor.x = (int) get_double(obj, "x", 0);
monitor.y = (int) get_double(obj, "y", 0);
monitor.width = (int) get_double(obj, "width", 0);
monitor.height = (int) get_double(obj, "height", 0);
monitor.refresh = get_double(obj, "refreshRate", 0);
monitor.scale = get_double(obj, "scale", 1);
monitor.transform = transform_from_code((int) get_double(obj, "transform", 0));
monitor.dpms = get_bool(obj, "dpmsStatus", true);
monitor.bitdepth = is_ten_bit_format(get_string(obj, "currentFormat")) ? 10 : 8;
monitor.vrr = get_bool(obj, "vrr", false) ? 1 : 0;
monitor.color_management_preset = get_string(obj, "colorManagementPreset", "srgb");
monitor.sdr_brightness = get_double(obj, "sdrBrightness", 1);
monitor.sdr_saturation = get_double(obj, "sdrSaturation", 1);
monitor.sdr_min_luminance = get_double(obj, "sdrMinLuminance", 0.2);
monitor.sdr_max_luminance = (int) get_double(obj, "sdrMaxLuminance", 80);
if (obj.has_member("availableModes")) {
var modes = obj.get_array_member("availableModes");
for (uint j = 0; j < modes.get_length(); j++) {
var mode = parse_mode(modes.get_string_element(j));
if (mode != null)
monitor.modes.add(mode);
}
}
var saved_monitor = find_saved_monitor(saved, monitor);
if (saved_monitor != null) {
monitor.use_description = saved_monitor.use_description;
monitor.mirror = saved_monitor.mirror;
if (saved_monitor.has_position) {
monitor.x = saved_monitor.x;
monitor.y = saved_monitor.y;
}
if (saved_monitor.has_scale)
monitor.scale = saved_monitor.scale;
if (!monitor.enabled && saved_monitor.has_mode) {
monitor.width = saved_monitor.width;
monitor.height = saved_monitor.height;
monitor.refresh = saved_monitor.refresh;
}
}
if (monitor.width <= 0 || monitor.height <= 0)
apply_first_available_mode(monitor);
monitors.add(monitor);
}
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. Hyprland monitorv2 syntax.\n");
foreach (var monitor in monitors) {
builder.append("monitorv2 {\n");
builder.append(" output = %s\n".printf(output_identifier(monitor)));
if (!monitor.enabled) {
builder.append(" disabled = true\n");
}
if (monitor.width > 0 && monitor.height > 0) {
builder.append(" mode = %dx%d@%s\n".printf(
monitor.width,
monitor.height,
format_double(monitor.refresh)
));
}
builder.append(" position = %dx%d\n".printf(monitor.x, monitor.y));
builder.append(" scale = %s\n".printf(format_double(monitor.scale)));
if (monitor.mirror != "")
builder.append(" mirror = %s\n".printf(monitor.mirror));
if (monitor.bitdepth != 8)
builder.append(" bitdepth = %d\n".printf(monitor.bitdepth));
if (monitor.transform != "normal")
builder.append(" transform = %d\n".printf(transform_to_code(monitor.transform)));
if (monitor.vrr != 0)
builder.append(" vrr = %d\n".printf(monitor.vrr));
if (monitor.color_management_preset != "" && monitor.color_management_preset != "srgb")
builder.append(" cm = %s\n".printf(monitor.color_management_preset));
if (monitor.sdr_eotf != "default")
builder.append(" sdr_eotf = %s\n".printf(monitor.sdr_eotf));
if (!double_equal(monitor.sdr_brightness, 1.0))
builder.append(" sdrbrightness = %s\n".printf(format_double(monitor.sdr_brightness)));
if (!double_equal(monitor.sdr_saturation, 1.0))
builder.append(" sdrsaturation = %s\n".printf(format_double(monitor.sdr_saturation)));
if (monitor.supports_wide_color != 0)
builder.append(" supports_wide_color = %d\n".printf(monitor.supports_wide_color));
if (monitor.supports_hdr != 0)
builder.append(" supports_hdr = %d\n".printf(monitor.supports_hdr));
if (monitor.sdr_min_luminance != 0.2)
builder.append(" sdr_min_luminance = %s\n".printf(format_double(monitor.sdr_min_luminance)));
if (monitor.sdr_max_luminance != 80)
builder.append(" sdr_max_luminance = %d\n".printf(monitor.sdr_max_luminance));
if (monitor.min_luminance >= 0)
builder.append(" min_luminance = %s\n".printf(format_double(monitor.min_luminance)));
if (monitor.max_luminance >= 0)
builder.append(" max_luminance = %d\n".printf(monitor.max_luminance));
if (monitor.max_avg_luminance >= 0)
builder.append(" max_avg_luminance = %d\n".printf(monitor.max_avg_luminance));
if (monitor.icc != "")
builder.append(" icc = %s\n".printf(monitor.icc));
builder.append("}\n");
}
DirUtils.create_with_parents(Path.get_dirname(path), 0755);
FileUtils.set_contents(path, builder.str);
try {
ShellCommand.run("hyprctl reload");
} catch (Error err) {
warning("Failed to reload Hyprland after writing monitors.conf: %s", err.message);
}
}
private static Json.Node parse_json(string text) throws Error {
var parser = new Json.Parser();
parser.load_from_data(text);
return parser.get_root();
}
private static Gee.HashSet<string> read_active_names() throws Error {
var names = new Gee.HashSet<string>();
var root = parse_json(ShellCommand.run("hyprctl -j monitors"));
var array = root.get_array();
for (uint i = 0; i < array.get_length(); i++)
names.add(array.get_object_element(i).get_string_member("name"));
return names;
}
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;
foreach (var line in content.split("\n")) {
var trimmed = line.strip();
if (trimmed.has_prefix("monitor=")) {
var fields = trimmed.substring("monitor=".length).split(",");
if (fields.length >= 2) {
var key = output_key(fields[0].strip());
var monitor = monitors.has_key(key) ? monitors[key] : new SavedMonitor();
fill_output(fields[0].strip(), monitor);
parse_v1_fields(fields, monitor);
monitors[key] = monitor;
}
}
}
parse_monitorv2(content, monitors);
} catch (Error err) {
warning("Failed to parse Hyprland monitor config: %s", err.message);
}
return monitors;
}
private static void parse_monitorv2(string content, Gee.HashMap<string, SavedMonitor> monitors) {
string current_output = "";
SavedMonitor? current = null;
bool in_block = false;
foreach (var line in content.split("\n")) {
var trimmed = line.strip();
if (trimmed.has_prefix("monitorv2")) {
current_output = "";
current = new SavedMonitor();
in_block = true;
continue;
}
if (!in_block)
continue;
if (trimmed == "}") {
if (current_output != "" && current != null)
monitors[output_key(current_output)] = current;
in_block = false;
continue;
}
if (current == null)
continue;
if (trimmed.has_prefix("output")) {
current_output = value_after_equals(trimmed);
fill_output(current_output, current);
} else if (trimmed.has_prefix("mode")) {
parse_mode_setting(value_after_equals(trimmed), current);
} else if (trimmed.has_prefix("position")) {
parse_position_setting(value_after_equals(trimmed), current);
} else if (trimmed.has_prefix("scale")) {
current.scale = double.parse(value_after_equals(trimmed));
current.has_scale = true;
} else if (trimmed.has_prefix("mirror")) {
current.mirror = value_after_equals(trimmed);
}
}
}
private static void parse_v1_fields(string[] fields, SavedMonitor monitor) {
if (fields.length >= 4) {
parse_mode_setting(fields[1].strip(), monitor);
parse_position_setting(fields[2].strip(), monitor);
monitor.scale = double.parse(fields[3].strip());
monitor.has_scale = true;
}
for (int i = 1; i < fields.length - 1; i++) {
if (fields[i].strip() == "mirror")
monitor.mirror = fields[i + 1].strip();
}
}
private static void parse_mode_setting(string value, SavedMonitor monitor) {
var mode = parse_mode(value);
if (mode == null)
return;
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
monitor.has_mode = true;
}
private static void parse_position_setting(string value, SavedMonitor monitor) {
var parts = value.split("x");
if (parts.length != 2)
return;
monitor.x = int.parse(parts[0]);
monitor.y = int.parse(parts[1]);
monitor.has_position = true;
}
private static void fill_output(string output, SavedMonitor monitor) {
monitor.use_description = output.has_prefix("desc:");
}
private static SavedMonitor? find_saved_monitor(Gee.HashMap<string, SavedMonitor> saved, MonitorConfig monitor) {
var name_key = output_key(monitor.name);
if (saved.has_key(name_key))
return saved[name_key];
var desc = description_identifier(monitor);
if (desc != "") {
var desc_key = output_key(desc);
if (saved.has_key(desc_key))
return saved[desc_key];
}
return null;
}
private static void apply_first_available_mode(MonitorConfig monitor) {
if (monitor.modes.size == 0)
return;
var mode = monitor.modes[0];
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
}
private static string monitors_path() {
return Path.build_filename(Environment.get_user_config_dir(), "hypr", "monitors.conf");
}
private static DisplayMode? parse_mode(string value) {
var cleaned = value.replace("Hz", "");
var parts = cleaned.split("@");
if (parts.length != 2)
return null;
var size = parts[0].split("x");
if (size.length != 2)
return null;
return new DisplayMode() {
width = int.parse(size[0]),
height = int.parse(size[1]),
refresh = double.parse(parts[1])
};
}
private static string transform_from_code(int code) {
return code >= 0 && code < TRANSFORMS.length ? TRANSFORMS[code] : "normal";
}
private static int transform_to_code(string transform) {
for (int i = 0; i < TRANSFORMS.length; i++)
if (TRANSFORMS[i] == transform)
return i;
return 0;
}
private static string get_string(Json.Object obj, string name, string fallback = "") {
return obj.has_member(name) ? obj.get_string_member(name) : fallback;
}
private static double get_double(Json.Object obj, string name, double fallback) {
return obj.has_member(name) ? obj.get_double_member(name) : fallback;
}
private static bool get_bool(Json.Object obj, string name, bool fallback) {
return obj.has_member(name) ? obj.get_boolean_member(name) : fallback;
}
private static bool is_ten_bit_format(string format) {
return format == "XRGB2101010" || format == "XBGR2101010";
}
private static string format_double(double value) {
return "%.2f".printf(value).replace(",", ".");
}
private static string output_identifier(MonitorConfig monitor) {
if (!monitor.use_description)
return monitor.name;
var desc = description_identifier(monitor);
return desc != "" ? desc : monitor.name;
}
private static string description_identifier(MonitorConfig monitor) {
return monitor.description != "" ? "desc:" + monitor.description.replace("#", "##") : "";
}
private static string output_key(string value) {
return value.strip();
}
private static bool double_equal(double a, double b) {
return Math.fabs(a - b) < 0.0001;
}
private static string value_after_equals(string line) {
var parts = line.split("=", 2);
return parts.length == 2 ? parts[1].strip() : "";
}
}
}
namespace TunerDisplays {
private const string GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@";
private const string LOCALEDIR = "@LOCALEDIR@";
}
namespace TunerDisplays {
public class DisplayMode : Object {
public string id { get; set; default = ""; }
public int width { get; set; }
public int height { get; set; }
public double refresh { get; set; }
public double preferred_scale { get; set; default = 1.0; }
public bool variable_refresh_rate { get; set; }
public Gee.ArrayList<double?> supported_scales { get; private set; default = new Gee.ArrayList<double?>(); }
public string label {
owned get {
return "%dx%d@%.2f".printf(width, height, refresh);
}
}
}
public class MonitorConfig : Object {
public string name { get; set; default = ""; }
public string description { get; set; default = ""; }
public string vendor { get; set; default = ""; }
public string product { get; set; default = ""; }
public string serial { get; set; default = ""; }
public bool enabled { get; set; default = true; }
public bool primary { get; set; }
public int x { get; set; }
public int y { get; set; }
public int width { get; set; }
public int height { get; set; }
public double refresh { get; set; }
public double scale { get; set; default = 1.0; }
public string transform { get; set; default = "normal"; }
public string mirror { get; set; default = ""; }
public bool mirrored { get; set; }
public bool use_description { get; set; }
public int bitdepth { get; set; default = 8; }
public int vrr { get; set; }
public string color_management_preset { get; set; default = "srgb"; }
public string sdr_eotf { get; set; default = "default"; }
public double sdr_brightness { get; set; default = 1.0; }
public double sdr_saturation { get; set; default = 1.0; }
public int supports_wide_color { get; set; }
public int supports_hdr { get; set; }
public double sdr_min_luminance { get; set; default = 0.2; }
public int sdr_max_luminance { get; set; default = 80; }
public double min_luminance { get; set; default = -1; }
public int max_luminance { get; set; default = -1; }
public int max_avg_luminance { get; set; default = -1; }
public string icc { get; set; default = ""; }
public bool underscanning { get; set; }
public int color_mode { get; set; }
public bool supports_variable_refresh_rate { get; set; }
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 Gee.ArrayList<DisplayMode> modes { get; private set; default = new Gee.ArrayList<DisplayMode>(); }
public string display_name {
owned get {
if (is_builtin())
return _("Built-in Display");
return description != "" ? description : name;
}
}
public string preview_name {
owned get {
return name;
}
}
public string title {
owned get {
return display_name;
}
}
private bool is_builtin() {
return name.has_prefix("eDP-") || name.has_prefix("LVDS-") || name.has_prefix("DSI-");
}
}
}
namespace TunerDisplays {
public class ShellCommand : Object {
public static string run(string command) throws Error {
string stdout;
string stderr;
int status;
Process.spawn_command_line_sync(command, out stdout, out stderr, out status);
if (status != 0) {
throw new BackendError.COMMAND_FAILED(
"%s failed with status %d: %s".printf(command, status, stderr.strip())
);
}
return stdout;
}
public static string quote(string value) {
return Shell.quote(value);
}
}
}
deps = [
dependency('gee-0.8'),
dependency('json-glib-1.0'),
dependency('tuner-1'),
]
sources = files(
'plugin.vala',
'backends/display-backend.vala',
'backends/gnome-backend.vala',
'backends/hyprland-backend.vala',
'core/display-model.vala',
'core/shell-command.vala',
'ui/displays-view.vala',
'ui/monitor-layout.vala',
'ui/monitor-row.vala',
'ui/monitor-settings-content.vala',
'ui/ui-helpers.vala',
) + configure_file(
input: 'build.vala.in',
output: 'build.vala',
configuration: config,
)
shared_module(
output,
resources,
sources,
dependencies: deps,
install: true,
install_dir: get_option('libdir') / 'tuner' / 'plugins'
)
namespace TunerDisplays {
public class Addin : Tuner.Addin {
private DisplaysView view;
private Tuner.Page page;
private Tuner.Page monitor_page;
private Gtk.Box monitor_page_content;
private Gtk.Label monitor_page_title;
construct {
Intl.bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
view = new DisplaysView();
page = new Tuner.Page() {
title = _("Displays"),
id = "displays",
category = "system",
icon_name = "video-display-symbolic",
priority = 900,
content = view
};
var refresh = new Gtk.Button.from_icon_name("view-refresh-symbolic") {
tooltip_text = _("Refresh")
};
refresh.clicked.connect(view.reload);
page.pack_end(refresh);
var apply = new Gtk.Button.with_label(_("Apply")) {
tooltip_text = _("Apply"),
sensitive = view.can_apply,
css_classes = { "suggested-action" }
};
apply.clicked.connect(view.apply_changes);
page.pack_end(apply);
monitor_page_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0) {
hexpand = true,
vexpand = true
};
monitor_page_title = new Gtk.Label(_("Monitor")) {
single_line_mode = true,
ellipsize = Pango.EllipsizeMode.END
};
monitor_page = new Tuner.Page() {
title = _("Monitor"),
id = monitor_settings_page_id(),
content = monitor_page_content,
title_widget = monitor_page_title
};
var monitor_apply = new Gtk.Button.with_label(_("Apply")) {
tooltip_text = _("Apply"),
sensitive = view.can_apply,
css_classes = { "suggested-action" }
};
monitor_apply.clicked.connect(view.apply_changes);
monitor_page.pack_end(monitor_apply);
page.add_page(monitor_page);
view.monitor_settings_requested.connect(show_monitor_settings);
add_page(page);
}
private void show_monitor_settings(MonitorConfig monitor) {
monitor_page.title = monitor.title;
monitor_page_title.label = monitor.title;
var child = monitor_page_content.get_first_child();
while (child != null) {
var next = child.get_next_sibling();
monitor_page_content.remove(child);
child = next;
}
monitor_page_content.append(create_monitor_page_content(monitor));
}
private MonitorSettingsContent create_monitor_page_content(MonitorConfig monitor) {
var content = new MonitorSettingsContent(monitor, view.backend_id, view.monitor_configs);
content.monitor_changed.connect(view.refresh_from_monitors);
return content;
}
}
}
public void peas_register_types(TypeModule module) {
var obj = (Peas.ObjectModule) module;
obj.register_extension_type(typeof(Tuner.Addin), typeof(TunerDisplays.Addin));
}
namespace TunerDisplays {
[GtkTemplate (ui = "/ru/ximperlinux/tuner/Displays/displays-view.ui")]
public class DisplaysView : Adw.PreferencesPage {
private DisplayBackend backend;
private Gee.ArrayList<MonitorConfig> monitors = new Gee.ArrayList<MonitorConfig>();
private Gee.ArrayList<Gtk.Widget> monitor_rows = new Gee.ArrayList<Gtk.Widget>();
private Gee.ArrayList<Gtk.Widget> status_rows = new Gee.ArrayList<Gtk.Widget>();
private Gee.ArrayList<Gtk.Widget> mirror_settings_rows = new Gee.ArrayList<Gtk.Widget>();
private Gee.ArrayList<Adw.PreferencesGroup> single_monitor_groups = new Gee.ArrayList<Adw.PreferencesGroup>();
private MonitorSettingsContent? single_monitor_content;
private MonitorLayout layout;
private Adw.PreferencesRow layout_row;
private DBusConnection? session_bus;
private uint monitors_changed_id;
[GtkChild] private unowned Adw.PreferencesGroup monitors_group;
[GtkChild] private unowned Adw.PreferencesGroup layout_group;
[GtkChild] private unowned Adw.PreferencesGroup status_group;
public bool can_apply { get { return backend.can_apply; } }
public Gee.ArrayList<MonitorConfig> monitor_configs { get { return monitors; } }
public string backend_id { owned get { return backend.id; } }
public signal void monitor_settings_requested(MonitorConfig monitor);
construct {
backend = DisplayBackend.create_for_session();
layout_row = new Adw.PreferencesRow() {
activatable = false,
selectable = false,
can_focus = false,
focusable = false
};
layout = new MonitorLayout() {
height_request = 320,
hexpand = true,
can_focus = false,
focusable = false
};
layout.layout_changed.connect(sync_rows);
layout_row.child = layout;
layout_group.add(layout_row);
subscribe_monitor_changes();
reload();
}
~DisplaysView() {
if (session_bus != null && monitors_changed_id != 0)
session_bus.signal_unsubscribe(monitors_changed_id);
}
public void reload() {
clear_monitor_rows();
clear_status_rows();
clear_mirror_settings_rows();
clear_single_monitor_settings();
try {
monitors = merge_loaded_monitors(monitors, backend.load());
layout.set_monitors(monitors);
rebuild_rows();
} catch (Error err) {
layout.set_monitors(new Gee.ArrayList<MonitorConfig>());
sync_layout_visibility();
sync_group_visibility();
add_status(new Adw.ActionRow() {
title = _("Failed to load monitors"),
subtitle = err.message
});
}
}
public void apply_changes() {
try {
backend.apply(monitors);
Tuner.toast(_("Monitor settings applied"));
} catch (Error err) {
Tuner.toast(err.message);
}
}
public void refresh_from_monitors() {
layout.recenter();
sync_rows();
}
private void sync_rows() {
foreach (var widget in monitor_rows) {
var row = widget as MonitorRow;
if (row != null)
row.sync_from_monitor();
}
}
private void add_status(Gtk.Widget row) {
status_group.add(row);
status_rows.add(row);
}
private void add_mirror_setting(Gtk.Widget row) {
layout_group.add(row);
mirror_settings_rows.add(row);
}
private void clear_monitor_rows() {
foreach (var row in monitor_rows)
monitors_group.remove(row);
monitor_rows.clear();
}
private void clear_status_rows() {
foreach (var row in status_rows)
status_group.remove(row);
status_rows.clear();
}
private void clear_mirror_settings_rows() {
foreach (var row in mirror_settings_rows)
layout_group.remove(row);
mirror_settings_rows.clear();
}
private void clear_single_monitor_settings() {
foreach (var group in single_monitor_groups)
remove(group);
single_monitor_groups.clear();
single_monitor_content = null;
}
private void rebuild_rows() {
clear_monitor_rows();
clear_status_rows();
clear_mirror_settings_rows();
clear_single_monitor_settings();
sync_layout_visibility();
sync_group_visibility();
add_gnome_mirror_row();
if (gnome_mirror_enabled()) {
add_gnome_mirror_settings_rows();
} else if (single_monitor_mode()) {
add_single_monitor_settings();
} else {
add_gnome_primary_row();
foreach (var monitor in monitors) {
var row = new MonitorRow(monitor, monitor_settings_page_id(), monitors);
row.monitor_selected.connect(monitor => monitor_settings_requested(monitor));
row.monitor_changed.connect(() => layout.recenter());
monitors_group.add(row);
monitor_rows.add(row);
}
}
sync_layout_visibility();
sync_group_visibility();
if (!backend.can_apply) {
add_status(new Adw.ActionRow() {
title = _("Read-only backend"),
subtitle = _("Applying monitor layouts is not supported by this backend.")
});
}
}
private void add_gnome_mirror_row() {
if (backend.id != "gnome" || monitors.size <= 1)
return;
var row = new Adw.SwitchRow() {
title = _("Mirror Displays"),
active = monitors.size > 0 && monitors[0].mirrored
};
row.notify["active"].connect(() => {
foreach (var monitor in monitors) {
monitor.mirrored = row.active;
if (row.active) {
monitor.enabled = true;
monitor.x = 0;
monitor.y = 0;
} else {
place_monitor_after_active(monitor, monitors);
}
}
layout.recenter();
rebuild_rows();
});
add_status(row);
}
private void add_single_monitor_settings() {
if (monitors.size != 1)
return;
single_monitor_content = new MonitorSettingsContent(monitors[0], backend.id, monitors, false);
single_monitor_content.monitor_changed.connect(refresh_from_monitors);
while (single_monitor_content.get_group(0) != null) {
var group = single_monitor_content.get_group(0);
single_monitor_content.remove(group);
add(group);
single_monitor_groups.add(group);
}
}
private void add_gnome_mirror_settings_rows() {
add_gnome_mirror_mode_row();
add_gnome_mirror_scale_row();
add_gnome_mirror_transform_row();
}
private void add_gnome_mirror_mode_row() {
var modes = common_mirror_resolutions();
if (modes.size == 0)
return;
var model = new Gtk.StringList(null);
uint selected = 0;
for (int i = 0; i < modes.size; i++) {
var mode = modes[i];
model.append("%dx%d".printf(mode.width, mode.height));
if (monitors.size > 0
&& mode.width == monitors[0].width
&& mode.height == monitors[0].height) {
selected = i;
}
}
var row = new Adw.ComboRow() {
title = _("Resolution"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index < 0 || index >= modes.size)
return;
var selected_mode = modes[index];
foreach (var monitor in monitors) {
var mode = find_compatible_mirror_mode(monitor, selected_mode);
if (mode == null)
continue;
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
monitor.variable_refresh_rate = mode.variable_refresh_rate;
if (!mode_supports_scale(mode, monitor.scale))
monitor.scale = mode.preferred_scale;
}
rebuild_rows();
});
add_mirror_setting(row);
}
private void add_gnome_mirror_scale_row() {
var mode = selected_common_mirror_mode();
if (mode == null)
return;
var scales = common_mirror_scales(mode);
if (scales.size == 0)
return;
var model = new Gtk.StringList(null);
uint selected = 0;
for (int i = 0; i < scales.size; i++) {
var scale = scales[i];
model.append("%.0f%%".printf(scale * 100));
if (monitors.size > 0 && Math.fabs(scale - monitors[0].scale) < 0.01)
selected = i;
}
var row = new Adw.ComboRow() {
title = _("Scale"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index < 0 || index >= scales.size)
return;
foreach (var monitor in monitors)
monitor.scale = scales[index];
});
add_mirror_setting(row);
}
private void add_gnome_mirror_transform_row() {
var model = new Gtk.StringList(null);
string[] titles = {
_("Normal"), _("90 degrees"), _("180 degrees"), _("270 degrees"),
_("Flipped"), _("Flipped 90 degrees"), _("Flipped 180 degrees"), _("Flipped 270 degrees")
};
foreach (var title in titles)
model.append(title);
var row = new Adw.ComboRow() {
title = _("Rotation"),
model = model,
selected = monitors.size > 0 ? transform_to_index(monitors[0].transform) : 0
};
row.notify["selected"].connect(() => {
var transform = transform_from_index((int) row.selected);
foreach (var monitor in monitors)
monitor.transform = transform;
});
add_mirror_setting(row);
}
private void add_gnome_primary_row() {
if (backend.id != "gnome" || monitors.size <= 1)
return;
var model = new Gtk.StringList(null);
var values = new Gee.ArrayList<MonitorConfig>();
uint selected = 0;
foreach (var monitor in monitors) {
if (!monitor.enabled)
continue;
model.append(monitor.title);
values.add(monitor);
if (monitor.primary)
selected = (uint) values.size - 1;
}
if (values.size <= 1)
return;
var row = new Adw.ComboRow() {
title = _("Primary Display"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index < 0 || index >= values.size)
return;
foreach (var monitor in monitors)
monitor.primary = monitor == values[index];
});
monitors_group.add(row);
monitor_rows.add(row);
}
private void sync_layout_visibility() {
layout_row.visible = !gnome_mirror_enabled() && !single_monitor_mode();
layout_group.visible = (!gnome_mirror_enabled() && !single_monitor_mode()) || mirror_settings_rows.size > 0;
}
private void sync_group_visibility() {
monitors_group.visible = monitor_rows.size > 0;
}
private bool gnome_mirror_enabled() {
return backend.id == "gnome" && monitors.size > 0 && monitors[0].mirrored;
}
private bool single_monitor_mode() {
return monitors.size == 1;
}
private void subscribe_monitor_changes() {
if (backend.id != "gnome")
return;
try {
session_bus = Bus.get_sync(BusType.SESSION);
monitors_changed_id = session_bus.signal_subscribe(
"org.gnome.Mutter.DisplayConfig",
"org.gnome.Mutter.DisplayConfig",
"MonitorsChanged",
"/org/gnome/Mutter/DisplayConfig",
null,
DBusSignalFlags.NONE,
() => {
Idle.add(() => {
reload();
return false;
});
}
);
} catch (Error err) {
warning("Failed to subscribe to GNOME monitor changes: %s", err.message);
}
}
private Gee.ArrayList<DisplayMode> common_mirror_modes() {
var modes = new Gee.ArrayList<DisplayMode>();
if (monitors.size == 0)
return modes;
foreach (var mode in monitors[0].modes) {
if (all_monitors_support_mode(mode))
modes.add(mode);
}
return modes;
}
private Gee.ArrayList<DisplayMode> common_mirror_resolutions() {
var resolutions = new Gee.ArrayList<DisplayMode>();
foreach (var mode in common_mirror_modes()) {
bool exists = false;
foreach (var existing in resolutions) {
if (existing.width == mode.width && existing.height == mode.height) {
exists = true;
break;
}
}
if (!exists)
resolutions.add(mode);
}
return resolutions;
}
private DisplayMode? selected_common_mirror_mode() {
if (monitors.size == 0)
return null;
foreach (var mode in common_mirror_resolutions()) {
if (mode.width == monitors[0].width
&& mode.height == monitors[0].height) {
return mode;
}
}
var modes = common_mirror_resolutions();
return modes.size > 0 ? modes[0] : null;
}
private Gee.ArrayList<double?> common_mirror_scales(DisplayMode mode) {
var scales = new Gee.ArrayList<double?>();
foreach (var scale in mode.supported_scales) {
bool supported = true;
foreach (var monitor in monitors) {
var compatible = find_compatible_mirror_mode(monitor, mode);
if (compatible == null || !mode_supports_scale(compatible, scale)) {
supported = false;
break;
}
}
if (supported)
scales.add(scale);
}
return scales;
}
private bool all_monitors_support_mode(DisplayMode mode) {
foreach (var monitor in monitors) {
if (find_compatible_mirror_mode(monitor, mode) == null)
return false;
}
return true;
}
private static DisplayMode? find_compatible_mirror_mode(MonitorConfig monitor, DisplayMode mode) {
foreach (var candidate in monitor.modes) {
if (candidate.width == mode.width
&& candidate.height == mode.height
&& Math.fabs(candidate.refresh - mode.refresh) < 0.02) {
return candidate;
}
}
foreach (var candidate in monitor.modes) {
if (candidate.width == mode.width && candidate.height == mode.height)
return candidate;
}
return null;
}
private static Gee.ArrayList<MonitorConfig> merge_loaded_monitors(Gee.ArrayList<MonitorConfig> current, Gee.ArrayList<MonitorConfig> loaded) {
var merged = new Gee.ArrayList<MonitorConfig>();
foreach (var loaded_monitor in loaded) {
var monitor = find_monitor_by_name(current, loaded_monitor.name);
if (monitor == null)
monitor = loaded_monitor;
else
copy_monitor(loaded_monitor, monitor);
merged.add(monitor);
}
return merged;
}
private static MonitorConfig? find_monitor_by_name(Gee.ArrayList<MonitorConfig> monitors, string name) {
foreach (var monitor in monitors) {
if (monitor.name == name)
return monitor;
}
return null;
}
private static void copy_monitor(MonitorConfig source, MonitorConfig target) {
target.description = source.description;
target.vendor = source.vendor;
target.product = source.product;
target.serial = source.serial;
target.enabled = source.enabled;
target.primary = source.primary;
target.x = source.x;
target.y = source.y;
target.width = source.width;
target.height = source.height;
target.refresh = source.refresh;
target.scale = source.scale;
target.transform = source.transform;
target.mirror = source.mirror;
target.mirrored = source.mirrored;
target.use_description = source.use_description;
target.bitdepth = source.bitdepth;
target.vrr = source.vrr;
target.color_management_preset = source.color_management_preset;
target.sdr_eotf = source.sdr_eotf;
target.sdr_brightness = source.sdr_brightness;
target.sdr_saturation = source.sdr_saturation;
target.supports_wide_color = source.supports_wide_color;
target.supports_hdr = source.supports_hdr;
target.sdr_min_luminance = source.sdr_min_luminance;
target.sdr_max_luminance = source.sdr_max_luminance;
target.min_luminance = source.min_luminance;
target.max_luminance = source.max_luminance;
target.max_avg_luminance = source.max_avg_luminance;
target.icc = source.icc;
target.underscanning = source.underscanning;
target.color_mode = source.color_mode;
target.supports_variable_refresh_rate = source.supports_variable_refresh_rate;
target.variable_refresh_rate = source.variable_refresh_rate;
target.dpms = source.dpms;
target.modes.clear();
foreach (var mode in source.modes)
target.modes.add(mode);
target.supported_color_modes.clear();
foreach (var mode in source.supported_color_modes)
target.supported_color_modes.add(mode);
}
}
}
namespace TunerDisplays {
public class MonitorLayout : Gtk.DrawingArea {
private const double EDGE_PADDING = 6;
private const double MARGIN_MON = 0.66;
private const double SNAP_DISTANCE = 25;
private const double MIN_ZOOM = 0.04;
private const double MAX_ZOOM = 0.18;
private Gee.ArrayList<MonitorConfig> monitors = new Gee.ArrayList<MonitorConfig>();
private int dragged = -1;
private double zoom = 0.1;
private double offset_x = 0;
private double offset_y = 0;
private int view_width = 0;
private int view_height = 0;
private int drag_start_x = 0;
private int drag_start_y = 0;
private bool drag_active;
private bool needs_recenter = true;
public signal void layout_changed();
public MonitorLayout() {
set_draw_func(draw);
var drag = new Gtk.GestureDrag();
drag.drag_begin.connect((x, y) => {
dragged = hit_test(x, y);
if (dragged >= 0) {
drag_active = true;
drag_start_x = monitors[dragged].x;
drag_start_y = monitors[dragged].y;
}
queue_draw();
});
drag.drag_update.connect((dx, dy) => {
if (dragged < 0)
return;
var monitor = monitors[dragged];
int previous_x = monitor.x;
int previous_y = monitor.y;
int next_x = drag_start_x + (int) Math.round(dx / zoom);
int next_y = drag_start_y + (int) Math.round(dy / zoom);
snap_position(monitor, ref next_x, ref next_y);
clamp_position(monitor, ref next_x, ref next_y);
if (overlaps_any(monitor, next_x, next_y)) {
next_x = previous_x;
next_y = previous_y;
}
monitor.x = next_x;
monitor.y = next_y;
layout_changed();
queue_draw();
});
drag.drag_end.connect((dx, dy) => {
drag_active = false;
dragged = -1;
normalize_positions();
recenter();
layout_changed();
});
add_controller(drag);
}
public void set_monitors(Gee.ArrayList<MonitorConfig> monitors) {
this.monitors = monitors;
dragged = -1;
needs_recenter = true;
view_width = 0;
view_height = 0;
queue_draw();
}
public void recenter() {
needs_recenter = true;
queue_draw();
}
private void draw(Gtk.DrawingArea area, Cairo.Context cr, int width, int height) {
if (!drag_active && (needs_recenter || width != view_width || height != view_height)) {
view_width = width;
view_height = height;
calculate_view(width, height);
needs_recenter = false;
}
var dark = Adw.StyleManager.get_default().dark;
var tile = dark ? 0.145 : 0.985;
var tile_disabled = dark ? 0.105 : 0.880;
var text = dark ? 0.925 : 0.145;
var line = dark ? 1.0 : 0.0;
var line_alpha = dark ? 0.105 : 0.110;
for (int i = 0; i < monitors.size; i++) {
var monitor = monitors[i];
double x;
double y;
double w;
double h;
rect_for_monitor(monitor, out x, out y, out w, out h);
if (monitor.enabled)
cr.set_source_rgb(tile, tile, tile);
else
cr.set_source_rgb(tile_disabled, tile_disabled, tile_disabled);
rounded_rectangle(cr, x, y, w, h, 5);
cr.fill();
cr.set_line_width(1);
cr.set_source_rgba(line, line, line, monitor.enabled ? line_alpha * 1.4 : line_alpha);
rounded_rectangle(cr, x + 0.5, y + 0.5, w - 1, h - 1, 5);
cr.stroke();
draw_monitor_badge(cr, i + 1, x + 18, y + 18, dark, monitor.enabled);
cr.set_source_rgb(text, text, text);
cr.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
cr.set_font_size(13);
Cairo.TextExtents extents;
cr.text_extents(monitor.preview_name, out extents);
cr.move_to(
x + w / 2 - extents.width / 2 - extents.x_bearing,
y + h / 2 - extents.height / 2 - extents.y_bearing
);
cr.show_text(monitor.preview_name);
if (drag_active && dragged == i)
draw_position_overlay(cr, "%dx%d".printf(monitor.x, monitor.y), x + w / 2, y - 8, width, dark);
}
}
private void draw_monitor_badge(Cairo.Context cr, int number, double cx, double cy, bool dark, bool enabled) {
var radius = 10.0;
if (enabled)
cr.set_source_rgb(dark ? 0.82 : 0.88, dark ? 0.82 : 0.88, dark ? 0.82 : 0.88);
else
cr.set_source_rgba(dark ? 0.82 : 0.45, dark ? 0.82 : 0.45, dark ? 0.82 : 0.45, 0.55);
cr.arc(cx, cy, radius, 0, 2 * Math.PI);
cr.fill();
cr.set_source_rgb(0.04, 0.04, 0.04);
cr.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
cr.set_font_size(10);
var label = number.to_string();
Cairo.TextExtents extents;
cr.text_extents(label, out extents);
cr.move_to(cx - extents.width / 2 - extents.x_bearing, cy - extents.height / 2 - extents.y_bearing);
cr.show_text(label);
}
private void draw_position_overlay(Cairo.Context cr, string label, double cx, double top_y, int view_width, bool dark) {
cr.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
cr.set_font_size(16);
Cairo.TextExtents extents;
cr.text_extents(label, out extents);
var padding_x = 13.0;
var padding_y = 8.0;
var box_width = extents.width + padding_x * 2;
var box_height = extents.height + padding_y * 2;
var x = double.max(6, double.min(view_width - box_width - 6, cx - box_width / 2));
var y = double.max(6, top_y - box_height);
cr.set_source_rgba(dark ? 0.08 : 0.96, dark ? 0.08 : 0.96, dark ? 0.08 : 0.96, 0.92);
rounded_rectangle(cr, x, y, box_width, box_height, 8);
cr.fill();
cr.set_source_rgba(dark ? 1.0 : 0.0, dark ? 1.0 : 0.0, dark ? 1.0 : 0.0, 0.12);
rounded_rectangle(cr, x + 0.5, y + 0.5, box_width - 1, box_height - 1, 8);
cr.stroke();
cr.set_source_rgb(dark ? 0.92 : 0.12, dark ? 0.92 : 0.12, dark ? 0.92 : 0.12);
cr.move_to(x + padding_x - extents.x_bearing, y + padding_y - extents.y_bearing);
cr.show_text(label);
}
private int hit_test(double px, double py) {
for (int i = monitors.size - 1; i >= 0; i--) {
var monitor = monitors[i];
double x;
double y;
double w;
double h;
rect_for_monitor(monitor, out x, out y, out w, out h);
if (px >= x && px <= x + w && py >= y && py <= y + h)
return i;
}
return -1;
}
private void snap_position(MonitorConfig monitor, ref int x, ref int y) {
var width = get_logical_width(monitor);
var height = get_logical_height(monitor);
var snap_units = SNAP_DISTANCE / zoom;
double best_x_distance = snap_units + 1;
double best_y_distance = snap_units + 1;
int snapped_x = x;
int snapped_y = y;
foreach (var other in monitors) {
if (other == monitor)
continue;
var other_width = get_logical_width(other);
var other_height = get_logical_height(other);
var right = x + width;
var bottom = y + height;
var other_right = other.x + other_width;
var other_bottom = other.y + other_height;
consider_x_snap(x, other.x, ref best_x_distance, ref snapped_x, snap_units);
consider_x_snap(x, (int) Math.round(other_right), ref best_x_distance, ref snapped_x, snap_units);
consider_x_snap((int) Math.round(right), other.x, ref best_x_distance, ref snapped_x, snap_units, (int) Math.round(-width));
consider_x_snap((int) Math.round(right), (int) Math.round(other_right), ref best_x_distance, ref snapped_x, snap_units, (int) Math.round(-width));
consider_y_snap(y, other.y, ref best_y_distance, ref snapped_y, snap_units);
consider_y_snap(y, (int) Math.round(other_bottom), ref best_y_distance, ref snapped_y, snap_units);
consider_y_snap((int) Math.round(bottom), other.y, ref best_y_distance, ref snapped_y, snap_units, (int) Math.round(-height));
consider_y_snap((int) Math.round(bottom), (int) Math.round(other_bottom), ref best_y_distance, ref snapped_y, snap_units, (int) Math.round(-height));
}
x = snapped_x;
y = snapped_y;
}
private static void consider_x_snap(int current_edge, int target_edge, ref double best_distance, ref int snapped_x, double threshold, int offset = 0) {
var distance = Math.fabs(current_edge - target_edge);
if (distance <= threshold && distance < best_distance) {
best_distance = distance;
snapped_x = target_edge + offset;
}
}
private static void consider_y_snap(int current_edge, int target_edge, ref double best_distance, ref int snapped_y, double threshold, int offset = 0) {
var distance = Math.fabs(current_edge - target_edge);
if (distance <= threshold && distance < best_distance) {
best_distance = distance;
snapped_y = target_edge + offset;
}
}
private void clamp_position(MonitorConfig monitor, ref int x, ref int y) {
if (view_width <= 0 || view_height <= 0 || zoom <= 0)
return;
var logical_width = get_logical_width(monitor);
var logical_height = get_logical_height(monitor);
var min_x = (EDGE_PADDING - offset_x) / zoom;
var min_y = (EDGE_PADDING - offset_y) / zoom;
var max_x = (view_width - EDGE_PADDING - offset_x) / zoom - logical_width;
var max_y = (view_height - EDGE_PADDING - offset_y) / zoom - logical_height;
if (max_x < min_x)
max_x = min_x;
if (max_y < min_y)
max_y = min_y;
x = (int) Math.round(double.min(max_x, double.max(min_x, x)));
y = (int) Math.round(double.min(max_y, double.max(min_y, y)));
}
private bool overlaps_any(MonitorConfig monitor, int x, int y) {
var width = get_logical_width(monitor);
var height = get_logical_height(monitor);
foreach (var other in monitors) {
if (other == monitor)
continue;
if (rects_overlap(
x, y, width, height,
other.x, other.y, get_logical_width(other), get_logical_height(other)
)) {
return true;
}
}
return false;
}
private static bool rects_overlap(double ax, double ay, double aw, double ah, double bx, double by, double bw, double bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
private void normalize_positions() {
if (monitors.size == 0)
return;
int min_x = 0;
int min_y = 0;
bool first = true;
foreach (var monitor in monitors) {
if (first) {
min_x = monitor.x;
min_y = monitor.y;
first = false;
} else {
min_x = int.min(min_x, monitor.x);
min_y = int.min(min_y, monitor.y);
}
}
if (min_x == 0 && min_y == 0)
return;
foreach (var monitor in monitors) {
monitor.x -= min_x;
monitor.y -= min_y;
}
}
private void calculate_view(int width, int height) {
if (monitors.size == 0) {
zoom = 0.1;
offset_x = 0;
offset_y = 0;
return;
}
double min_x = 0;
double min_y = 0;
double max_x = 1;
double max_y = 1;
double max_monitor_width = 1;
double max_monitor_height = 1;
bool first = true;
foreach (var monitor in monitors) {
var logical_width = get_logical_width(monitor);
var logical_height = get_logical_height(monitor);
max_monitor_width = double.max(max_monitor_width, logical_width);
max_monitor_height = double.max(max_monitor_height, logical_height);
if (first) {
min_x = monitor.x;
min_y = monitor.y;
max_x = monitor.x + logical_width;
max_y = monitor.y + logical_height;
first = false;
} else {
min_x = double.min(min_x, monitor.x);
min_y = double.min(min_y, monitor.y);
max_x = double.max(max_x, monitor.x + logical_width);
max_y = double.max(max_y, monitor.y + logical_height);
}
}
var layout_width = double.max(1, max_x - min_x);
var layout_height = double.max(1, max_y - min_y);
var fit_x = width / (layout_width + max_monitor_width * 2 * MARGIN_MON);
var fit_y = height / (layout_height + max_monitor_height * 2 * MARGIN_MON);
zoom = double.min(MAX_ZOOM, double.max(MIN_ZOOM, double.min(fit_x, fit_y)));
offset_x = width / 2.0 - ((min_x + max_x) / 2.0) * zoom;
offset_y = height / 2.0 - ((min_y + max_y) / 2.0) * zoom;
}
private void rect_for_monitor(MonitorConfig monitor, out double x, out double y, out double w, out double h) {
x = offset_x + monitor.x * zoom;
y = offset_y + monitor.y * zoom;
w = double.max(48, get_logical_width(monitor) * zoom);
h = double.max(36, get_logical_height(monitor) * zoom);
}
private static double get_logical_width(MonitorConfig monitor) {
if (is_rotated(monitor))
return monitor.height / monitor.scale;
return monitor.width / monitor.scale;
}
private static double get_logical_height(MonitorConfig monitor) {
if (is_rotated(monitor))
return monitor.width / monitor.scale;
return monitor.height / monitor.scale;
}
private static bool is_rotated(MonitorConfig monitor) {
return monitor.transform.index_of("90") >= 0 || monitor.transform.index_of("270") >= 0;
}
private static void rounded_rectangle(Cairo.Context cr, double x, double y, double w, double h, double radius) {
var r = double.min(radius, double.min(w / 2, h / 2));
cr.new_sub_path();
cr.arc(x + w - r, y + r, r, -Math.PI / 2, 0);
cr.arc(x + w - r, y + h - r, r, 0, Math.PI / 2);
cr.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI);
cr.arc(x + r, y + r, r, Math.PI, 3 * Math.PI / 2);
cr.close_path();
}
}
}
namespace TunerDisplays {
public class MonitorRow : Adw.ActionRow {
private MonitorConfig monitor;
private Gee.ArrayList<MonitorConfig> all_monitors;
private string page_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) {
Object(
title: monitor.title,
subtitle: "%dx%d@%.2f scale %.2f %dx%d".printf(
monitor.width, monitor.height, monitor.refresh, monitor.scale, monitor.x, monitor.y
)
);
this.monitor = monitor;
this.all_monitors = all_monitors;
this.page_id = page_id;
build();
}
private void build() {
activatable = true;
selectable = false;
activated.connect(() => {
monitor_selected(monitor);
activate_action("navigation.push", "s", page_id);
});
enabled_switch = new Gtk.Switch() {
valign = Gtk.Align.CENTER,
active = monitor.enabled,
can_focus = true
};
enabled_switch.notify["active"].connect(() => {
if (enabled_switch.active != monitor.enabled)
place_monitor_after_active(monitor, all_monitors);
monitor.enabled = enabled_switch.active;
monitor_changed();
});
monitor.notify["enabled"].connect(() => sync_from_monitor());
add_suffix(enabled_switch);
var arrow = new Gtk.Image.from_icon_name("go-next-symbolic") {
valign = Gtk.Align.CENTER,
pixel_size = 16
};
add_suffix(arrow);
}
public void sync_from_monitor() {
if (enabled_switch.active != monitor.enabled)
enabled_switch.active = monitor.enabled;
subtitle = "%dx%d@%.2f scale %.2f %dx%d".printf(
monitor.width, monitor.height, monitor.refresh, monitor.scale, monitor.x, monitor.y
);
}
}
}
namespace TunerDisplays {
[GtkTemplate (ui = "/ru/ximperlinux/tuner/Displays/monitor-settings-content.ui")]
public class MonitorSettingsContent : Adw.PreferencesPage {
private MonitorConfig monitor;
private Gee.ArrayList<MonitorConfig> all_monitors;
private string backend_id;
private bool show_enabled;
[GtkChild] private unowned Adw.SwitchRow enabled_row;
private Adw.ExpanderRow? gnome_refresh_expander_row;
private Adw.ComboRow? gnome_refresh_rate_row;
private Adw.SwitchRow? gnome_variable_refresh_row;
private Gtk.Label? gnome_refresh_value_label;
private Gee.ArrayList<DisplayMode> gnome_refresh_values = new Gee.ArrayList<DisplayMode>();
private bool updating_gnome_refresh_row;
[GtkChild] private unowned Adw.PreferencesGroup basic_group;
[GtkChild] private unowned Adw.PreferencesGroup hyprland_group;
[GtkChild] private unowned Adw.PreferencesGroup hdr_group;
public signal void monitor_changed();
public MonitorSettingsContent(MonitorConfig monitor, string backend_id, Gee.ArrayList<MonitorConfig> all_monitors, bool show_enabled = true) {
Object(title: monitor.title);
this.monitor = monitor;
this.backend_id = backend_id;
this.all_monitors = all_monitors;
this.show_enabled = show_enabled;
build_preferences();
}
private void build_preferences() {
enabled_row.visible = show_enabled;
enabled_row.active = monitor.enabled;
enabled_row.notify["active"].connect(() => {
if (enabled_row.active != monitor.enabled)
place_monitor_after_active(monitor, all_monitors);
monitor.enabled = enabled_row.active;
emit_changed();
});
monitor.notify["enabled"].connect(sync_from_monitor);
if (backend_id == "gnome") {
add_gnome_resolution_row(basic_group);
add_gnome_refresh_rate_row(basic_group);
add_gnome_scale_row(basic_group);
} else {
add_mode_row(basic_group);
add_scale_row(basic_group);
}
add_transform_row(basic_group);
add_mirror_row(basic_group);
if (backend_id == "hyprland") {
add_hyprland_rows();
} else if (backend_id == "gnome") {
add_gnome_rows(basic_group);
hyprland_group.visible = false;
hdr_group.visible = false;
} else {
hyprland_group.visible = false;
hdr_group.visible = false;
}
}
private void add_mode_row(Adw.PreferencesGroup group) {
var model = new Gtk.StringList(null);
int selected = -1;
for (int i = 0; i < monitor.modes.size; i++) {
var mode = monitor.modes[i];
model.append(mode.label);
if (mode.width == monitor.width
&& mode.height == monitor.height
&& Math.fabs(mode.refresh - monitor.refresh) < 0.02
&& selected < 0) {
selected = i;
}
}
if (selected < 0) {
model.append("%dx%d@%.2f".printf(monitor.width, monitor.height, monitor.refresh));
selected = (int) model.get_n_items() - 1;
}
var row = new Adw.ComboRow() {
title = _("Resolution"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index >= 0 && index < monitor.modes.size) {
var mode = monitor.modes[index];
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
if (backend_id == "gnome" && !mode_supports_scale(mode, monitor.scale))
monitor.scale = mode.preferred_scale;
emit_changed();
}
});
group.add(row);
}
private void add_gnome_resolution_row(Adw.PreferencesGroup group) {
var model = new Gtk.StringList(null);
var values = new Gee.ArrayList<DisplayMode>();
uint selected = 0;
foreach (var mode in monitor.modes) {
if (has_resolution(values, mode.width, mode.height))
continue;
model.append("%dx%d".printf(mode.width, mode.height));
values.add(mode);
if (mode.width == monitor.width && mode.height == monitor.height)
selected = (uint) values.size - 1;
}
if (values.size == 0) {
model.append("%dx%d".printf(monitor.width, monitor.height));
}
var row = new Adw.ComboRow() {
title = _("Resolution"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index < 0 || index >= values.size)
return;
var mode = find_best_mode_for_resolution(values[index].width, values[index].height);
if (mode == null)
return;
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
monitor.variable_refresh_rate = mode.variable_refresh_rate;
if (!mode_supports_scale(mode, monitor.scale))
monitor.scale = mode.preferred_scale;
if (gnome_variable_refresh_row != null)
gnome_variable_refresh_row.visible = has_variable_mode_for_resolution();
update_gnome_refresh_row();
emit_changed();
});
group.add(row);
}
private void add_gnome_refresh_rate_row(Adw.PreferencesGroup group) {
gnome_refresh_expander_row = new Adw.ExpanderRow() {
title = _("Refresh Rate")
};
gnome_refresh_value_label = new Gtk.Label(null) {
valign = Gtk.Align.CENTER
};
gnome_refresh_expander_row.add_suffix(gnome_refresh_value_label);
gnome_refresh_rate_row = new Adw.ComboRow() {
title = _("Refresh Rate")
};
gnome_refresh_rate_row.notify["selected"].connect(() => {
if (updating_gnome_refresh_row)
return;
var index = (int) gnome_refresh_rate_row.selected;
if (index < 0 || index >= gnome_refresh_values.size)
return;
var mode = gnome_refresh_values[index];
monitor.refresh = mode.refresh;
monitor.variable_refresh_rate = false;
if (!mode_supports_scale(mode, monitor.scale))
monitor.scale = mode.preferred_scale;
sync_gnome_refresh_state();
emit_changed();
});
gnome_refresh_expander_row.add_row(gnome_refresh_rate_row);
if (monitor.supports_variable_refresh_rate && has_variable_mode_for_resolution()) {
gnome_variable_refresh_row = new Adw.SwitchRow() {
title = _("Variable Refresh Rate"),
active = monitor.variable_refresh_rate
};
gnome_variable_refresh_row.notify["active"].connect(() => {
if (updating_gnome_refresh_row)
return;
monitor.variable_refresh_rate = gnome_variable_refresh_row.active;
if (monitor.variable_refresh_rate) {
var mode = find_variable_mode_for_resolution();
if (mode != null) {
monitor.refresh = mode.refresh;
if (!mode_supports_scale(mode, monitor.scale))
monitor.scale = mode.preferred_scale;
}
} else {
var mode = find_best_fixed_mode_for_resolution(monitor.width, monitor.height);
if (mode != null) {
monitor.refresh = mode.refresh;
if (!mode_supports_scale(mode, monitor.scale))
monitor.scale = mode.preferred_scale;
}
}
sync_gnome_refresh_state();
emit_changed();
});
gnome_refresh_expander_row.add_row(gnome_variable_refresh_row);
}
update_gnome_refresh_row();
group.add(gnome_refresh_expander_row);
}
private void update_gnome_refresh_row() {
if (gnome_refresh_expander_row == null || gnome_refresh_rate_row == null)
return;
updating_gnome_refresh_row = true;
gnome_refresh_values.clear();
var model = new Gtk.StringList(null);
uint selected = 0;
foreach (var mode in monitor.modes) {
if (mode.width != monitor.width || mode.height != monitor.height)
continue;
if (mode.variable_refresh_rate)
continue;
model.append(refresh_rate_label(mode));
gnome_refresh_values.add(mode);
if (Math.fabs(mode.refresh - monitor.refresh) < 0.02
&& !monitor.variable_refresh_rate) {
selected = (uint) gnome_refresh_values.size - 1;
}
}
gnome_refresh_rate_row.model = model;
gnome_refresh_rate_row.selected = selected;
gnome_refresh_rate_row.sensitive = !monitor.variable_refresh_rate && gnome_refresh_values.size > 0;
if (gnome_variable_refresh_row != null)
gnome_variable_refresh_row.active = monitor.variable_refresh_rate;
if (gnome_refresh_value_label != null)
gnome_refresh_value_label.label = current_refresh_rate_label();
updating_gnome_refresh_row = false;
}
private void sync_gnome_refresh_state() {
updating_gnome_refresh_row = true;
if (gnome_refresh_rate_row != null)
gnome_refresh_rate_row.sensitive = !monitor.variable_refresh_rate && gnome_refresh_values.size > 0;
if (gnome_variable_refresh_row != null && gnome_variable_refresh_row.active != monitor.variable_refresh_rate)
gnome_variable_refresh_row.active = monitor.variable_refresh_rate;
if (gnome_refresh_value_label != null)
gnome_refresh_value_label.label = current_refresh_rate_label();
updating_gnome_refresh_row = false;
}
private void add_scale_row(Adw.PreferencesGroup group) {
group.add(create_spin_row(_("Scale"), monitor.scale, 0.25, 8.0, 0.05, 2, value => monitor.scale = value));
}
private void add_gnome_scale_row(Adw.PreferencesGroup group) {
var mode = selected_mode();
if (mode == null || mode.supported_scales.size == 0) {
add_scale_row(group);
return;
}
var labels = new Gee.ArrayList<string>();
var values = new Gee.ArrayList<double?>();
uint selected = 0;
for (int i = 0; i < mode.supported_scales.size; i++) {
var scale = mode.supported_scales[i];
labels.add("%.0f%%".printf(scale * 100));
values.add(scale);
if (Math.fabs(scale - monitor.scale) < 0.01)
selected = i;
}
var model = new Gtk.StringList(null);
foreach (var label in labels)
model.append(label);
var row = new Adw.ComboRow() {
title = _("Scale"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index >= 0 && index < values.size) {
monitor.scale = values[index];
emit_changed();
}
});
group.add(row);
}
private void add_transform_row(Adw.PreferencesGroup group) {
var model = new Gtk.StringList(null);
string[] titles = {
_("Normal"), _("90 degrees"), _("180 degrees"), _("270 degrees"),
_("Flipped"), _("Flipped 90 degrees"), _("Flipped 180 degrees"), _("Flipped 270 degrees")
};
foreach (var title in titles)
model.append(title);
var row = new Adw.ComboRow() {
title = _("Rotation"),
model = model,
selected = transform_to_index(monitor.transform)
};
row.notify["selected"].connect(() => {
monitor.transform = transform_from_index((int) row.selected);
emit_changed();
});
group.add(row);
}
private void add_mirror_row(Adw.PreferencesGroup group) {
if (backend_id != "hyprland")
return;
if (all_monitors.size <= 1)
return;
var model = new Gtk.StringList(null);
var values = new Gee.ArrayList<string>();
model.append(_("None"));
values.add("");
uint selected = 0;
foreach (var other in all_monitors) {
if (other == monitor)
continue;
model.append(other.title);
values.add(other.name);
if (other.name == monitor.mirror)
selected = (uint) values.size - 1;
}
var row = new Adw.ComboRow() {
title = _("Mirror"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index >= 0 && index < values.size) {
monitor.mirror = values[index];
emit_changed();
}
});
group.add(row);
}
private void add_hyprland_rows() {
var use_description = new Adw.SwitchRow() {
title = _("Use description"),
active = monitor.use_description
};
use_description.notify["active"].connect(() => {
monitor.use_description = use_description.active;
emit_changed();
});
hyprland_group.add(use_description);
add_int_combo(hyprland_group, _("Bit depth"), new string[] { "8", "10" }, new int[] { 8, 10 }, monitor.bitdepth, value => monitor.bitdepth = value);
add_int_combo(hyprland_group, _("VRR"), new string[] { _("Off"), _("On"), _("Fullscreen"), _("Fullscreen video/game") }, new int[] { 0, 1, 2, 3 }, monitor.vrr, value => monitor.vrr = value);
add_string_combo(hyprland_group, _("Color management"), new string[] { "auto", "srgb", "dcip3", "dp3", "adobe", "wide", "edid", "hdr", "hdredid" }, monitor.color_management_preset, value => monitor.color_management_preset = value);
add_string_combo(hyprland_group, _("SDR EOTF"), new string[] { "default", "gamma22", "srgb" }, monitor.sdr_eotf, value => monitor.sdr_eotf = value);
hyprland_group.add(create_spin_row(_("SDR brightness"), monitor.sdr_brightness, 0.1, 4.0, 0.05, 2, value => monitor.sdr_brightness = value));
hyprland_group.add(create_spin_row(_("SDR saturation"), monitor.sdr_saturation, 0.1, 4.0, 0.05, 2, value => monitor.sdr_saturation = value));
add_int_combo(hdr_group, _("Force wide color"), new string[] { _("Auto"), _("Off"), _("On") }, new int[] { 0, -1, 1 }, monitor.supports_wide_color, value => monitor.supports_wide_color = value);
add_int_combo(hdr_group, _("Force HDR"), new string[] { _("Auto"), _("Off"), _("On") }, new int[] { 0, -1, 1 }, monitor.supports_hdr, value => monitor.supports_hdr = value);
hdr_group.add(create_spin_row(_("SDR min luminance"), monitor.sdr_min_luminance, 0.0, 10.0, 0.01, 2, value => monitor.sdr_min_luminance = value));
hdr_group.add(create_spin_row(_("SDR max luminance"), monitor.sdr_max_luminance, 1, 1000, 1, 0, value => monitor.sdr_max_luminance = (int) value));
hdr_group.add(create_spin_row(_("Min luminance"), monitor.min_luminance, -1, 10.0, 0.01, 2, value => monitor.min_luminance = value));
hdr_group.add(create_spin_row(_("Max luminance"), monitor.max_luminance, -1, 10000, 1, 0, value => monitor.max_luminance = (int) value));
hdr_group.add(create_spin_row(_("Max average luminance"), monitor.max_avg_luminance, -1, 10000, 1, 0, value => monitor.max_avg_luminance = (int) value));
var icc = new Adw.EntryRow() {
title = _("ICC profile")
};
icc.text = monitor.icc;
icc.notify["text"].connect(() => {
monitor.icc = icc.text;
emit_changed();
});
hdr_group.add(icc);
}
private void add_gnome_rows(Adw.PreferencesGroup group) {
var underscanning = new Adw.SwitchRow() {
title = _("Underscanning"),
active = monitor.underscanning
};
underscanning.notify["active"].connect(() => {
monitor.underscanning = underscanning.active;
emit_changed();
});
group.add(underscanning);
if (monitor.supported_color_modes.contains(1)) {
var hdr = new Adw.SwitchRow() {
title = _("HDR"),
active = monitor.color_mode == 1
};
hdr.notify["active"].connect(() => {
monitor.color_mode = hdr.active ? 1 : 0;
emit_changed();
});
group.add(hdr);
}
}
private delegate void NumberChanged(double value);
private delegate void IntChanged(int value);
private delegate void StringChanged(string value);
private Adw.ActionRow create_spin_row(string title, double value, double min, double max, double step, uint digits, NumberChanged changed) {
var row = new Adw.ActionRow() {
title = title
};
var spin = new Gtk.SpinButton.with_range(min, max, step) {
valign = Gtk.Align.CENTER,
value = value,
digits = digits
};
spin.value_changed.connect(() => {
changed(spin.value);
emit_changed();
});
row.add_suffix(spin);
return row;
}
private void add_int_combo(Adw.PreferencesGroup group, string title, string[] labels, int[] values, int current, IntChanged changed) {
var model = new Gtk.StringList(null);
var stored_values = new Gee.ArrayList<int>();
uint selected = 0;
for (int i = 0; i < labels.length; i++) {
model.append(labels[i]);
stored_values.add(values[i]);
if (values[i] == current)
selected = i;
}
var row = new Adw.ComboRow() {
title = title,
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index >= 0 && index < stored_values.size) {
changed(stored_values[index]);
emit_changed();
}
});
group.add(row);
}
private void add_string_combo(Adw.PreferencesGroup group, string title, 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]);
stored_values.add(values[i]);
if (values[i] == current)
selected = i;
}
var row = new Adw.ComboRow() {
title = title,
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index >= 0 && index < stored_values.size) {
changed(stored_values[index]);
emit_changed();
}
});
group.add(row);
}
private void emit_changed() {
monitor_changed();
}
private void sync_from_monitor() {
if (enabled_row.active != monitor.enabled)
enabled_row.active = monitor.enabled;
}
private DisplayMode? selected_mode() {
foreach (var mode in monitor.modes) {
if (mode.width == monitor.width
&& mode.height == monitor.height
&& Math.fabs(mode.refresh - monitor.refresh) < 0.02
&& mode.variable_refresh_rate == monitor.variable_refresh_rate) {
return mode;
}
}
return monitor.modes.size > 0 ? monitor.modes[0] : null;
}
private DisplayMode? find_best_mode_for_resolution(int width, int height) {
DisplayMode? fallback = null;
foreach (var mode in monitor.modes) {
if (mode.width != width || mode.height != height)
continue;
if (Math.fabs(mode.refresh - monitor.refresh) < 0.02
&& mode.variable_refresh_rate == monitor.variable_refresh_rate) {
return mode;
}
if (fallback == null)
fallback = mode;
}
return fallback;
}
private DisplayMode? find_best_fixed_mode_for_resolution(int width, int height) {
DisplayMode? fallback = null;
foreach (var mode in monitor.modes) {
if (mode.width != width || mode.height != height || mode.variable_refresh_rate)
continue;
if (Math.fabs(mode.refresh - monitor.refresh) < 0.02)
return mode;
if (fallback == null)
fallback = mode;
}
return fallback;
}
private DisplayMode? find_variable_mode_for_resolution() {
foreach (var mode in monitor.modes) {
if (mode.width == monitor.width && mode.height == monitor.height && mode.variable_refresh_rate)
return mode;
}
return null;
}
private bool has_variable_mode_for_resolution() {
return find_variable_mode_for_resolution() != null;
}
private string current_refresh_rate_label() {
if (monitor.variable_refresh_rate) {
var mode = find_variable_mode_for_resolution();
if (mode != null)
return _("Variable (up to %.2f Hz)").printf(mode.refresh);
return _("Variable");
}
return _("%.2f Hz").printf(monitor.refresh);
}
}
}
namespace TunerDisplays {
public static string monitor_settings_page_id() {
return "display-monitor-settings";
}
private static void place_monitor_after_active(MonitorConfig monitor, Gee.ArrayList<MonitorConfig> monitors) {
bool found_active = false;
int max_x = 0;
foreach (var other in monitors) {
if (other == monitor || !other.enabled)
continue;
var right = other.x + (int) Math.round(logical_width_for_monitor(other));
if (!found_active || right > max_x)
max_x = right;
found_active = true;
}
monitor.x = found_active ? max_x : 0;
monitor.y = 0;
}
private static double logical_width_for_monitor(MonitorConfig monitor) {
if (monitor.transform.index_of("90") >= 0 || monitor.transform.index_of("270") >= 0)
return monitor.height / monitor.scale;
return monitor.width / monitor.scale;
}
private static bool mode_supports_scale(DisplayMode mode, double scale) {
foreach (var supported in mode.supported_scales) {
if (Math.fabs(supported - scale) < 0.01)
return true;
}
return false;
}
private static bool has_resolution(Gee.ArrayList<DisplayMode> modes, int width, int height) {
foreach (var mode in modes) {
if (mode.width == width && mode.height == height)
return true;
}
return false;
}
private static string refresh_rate_label(DisplayMode mode) {
return _("%.2f Hz").printf(mode.refresh);
}
private static uint transform_to_index(string transform) {
switch (transform) {
case "90": return 1;
case "180": return 2;
case "270": return 3;
case "flipped": return 4;
case "flipped-90": return 5;
case "flipped-180": return 6;
case "flipped-270": return 7;
default: return 0;
}
}
private static string transform_from_index(int index) {
switch (index) {
case 1: return "90";
case 2: return "180";
case 3: return "270";
case 4: return "flipped";
case 5: return "flipped-90";
case 6: return "flipped-180";
case 7: return "flipped-270";
default: return "normal";
}
}
}
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