backlight: add DDC/CI support for external monitors

parent 5a27e306
option('systemd-service', type: 'boolean', value: true, description: 'Install systemd user service unit.')
option('scripting', type: 'boolean', value: true, description: 'Enable notification scripting.')
option('pulse-audio', type: 'boolean', value: true, description: 'Provide PulseAudio Widget.')
option('ddc', type: 'boolean', value: true, description: 'Provide DDC/CI monitor brightness control via libddcutil.')
option('man-pages', type: 'boolean', value: true, description: 'Install all man pages.')
option('zsh-completions', type: 'boolean', value: true, description: 'Install zsh shell completions.')
option('bash-completions', type: 'boolean', value: true, description: 'Install bash shell completions.')
......
......@@ -794,14 +794,19 @@
},
"subsystem": {
"type": "string",
"description": "Kernel subsystem for brightness control",
"description": "Kernel subsystem for brightness control. Use 'ddc' for external monitors via DDC/CI (requires build with -Dddc=true).",
"default": "backlight",
"enum": ["backlight", "leds"]
"enum": ["backlight", "leds", "ddc"]
},
"min": {
"type": "integer",
"default": 0,
"description": "Lowest possible value for brightness"
},
"display-number": {
"type": "integer",
"default": 0,
"description": "DDC display number (from ddcutil detect). 0 means first available. Only used with subsystem 'ddc'."
}
}
},
......
......@@ -9,6 +9,10 @@ namespace SwayNotificationCenter.Widgets {
}
BacklightUtil client;
#if HAVE_DDC
DdcUtil[] ddc_clients = {};
bool use_ddc = false;
#endif
Gtk.Label label_widget = new Gtk.Label (null);
Gtk.Scale slider = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
......@@ -27,9 +31,17 @@ namespace SwayNotificationCenter.Widgets {
switch (subsystem) {
default :
case "backlight":
if (subsystem != "backlight") {
if (subsystem != "backlight"
#if HAVE_DDC
&& subsystem != "ddc"
#endif
) {
info ("Invalid subsystem %s for device %s. " +
"Use 'backlight' or 'leds'. Using default: 'backlight'",
"Use 'backlight', 'leds'" +
#if HAVE_DDC
", 'ddc'" +
#endif
". Using default: 'backlight'",
subsystem, device);
}
client = new BacklightUtil ("backlight", device);
......@@ -39,8 +51,89 @@ namespace SwayNotificationCenter.Widgets {
client = new BacklightUtil ("leds", device);
slider.set_range (min, this.client.get_max_value ());
break;
#if HAVE_DDC
case "ddc":
int display_number = int.max (0, get_prop<int> (config, "display-number"));
use_ddc = true;
if (display_number > 0) {
ddc_clients += new DdcUtil (display_number);
slider.set_range (min, 100);
} else {
// Auto-detect all DDC displays
var displays = DdcUtil.detect_displays ();
if (displays.length == 0) {
warning ("No DDC displays found");
hide ();
return;
}
for (int i = 0; i < displays.length; i++) {
var ddc = new DdcUtil (displays[i].dispno);
ddc_clients += ddc;
}
}
break;
#endif
}
}
#if HAVE_DDC
if (use_ddc) {
if (ddc_clients.length == 1) {
// Single DDC display — use the existing label + slider
if (label_widget.get_label () == "Brightness") {
label_widget.set_label ("󰍹");
label_widget.set_tooltip_text (ddc_clients[0].get_display_name ());
}
ddc_clients[0].brightness_change.connect ((percent) => {
if (percent < 0) {
hide ();
} else {
slider.set_value (percent);
}
});
slider.set_draw_value (false);
slider.set_round_digits (0);
slider.set_hexpand (true);
slider.value_changed.connect (() => {
ddc_clients[0].set_brightness.begin ((float) slider.get_value ());
slider.tooltip_text = ((int) slider.get_value ()).to_string ();
});
append (label_widget);
append (slider);
} else {
// Multiple DDC displays — create a row per display
set_orientation (Gtk.Orientation.VERTICAL);
for (int i = 0; i < ddc_clients.length; i++) {
var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
var lbl = new Gtk.Label ("󰍹 %d".printf (i + 1));
lbl.set_tooltip_text (ddc_clients[i].get_display_name ());
var scl = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
scl.set_draw_value (false);
scl.set_round_digits (0);
scl.set_hexpand (true);
int idx = i;
ddc_clients[i].brightness_change.connect ((percent) => {
if (percent < 0) {
row.hide ();
} else {
scl.set_value (percent);
}
});
scl.value_changed.connect (() => {
ddc_clients[idx].set_brightness.begin ((float) scl.get_value ());
scl.tooltip_text = ((int) scl.get_value ()).to_string ();
});
row.append (lbl);
row.append (scl);
append (row);
}
}
return;
}
#endif
this.client.brightness_change.connect ((percent) => {
if (percent < 0) { // invalid device path
......@@ -63,6 +156,18 @@ namespace SwayNotificationCenter.Widgets {
}
public override void on_cc_visibility_change (bool val) {
#if HAVE_DDC
if (use_ddc) {
foreach (var ddc in ddc_clients) {
if (val) {
ddc.start ();
} else {
ddc.close ();
}
}
return;
}
#endif
if (val) {
this.client.start ();
} else {
......
namespace SwayNotificationCenter.Widgets {
public struct DdcDisplayInfo {
int dispno;
string model_name;
}
class DdcUtil {
const uint8 VCP_BRIGHTNESS = 0x10;
void* handle = null;
string display_name = "DDC Display";
static bool lib_initialized = false;
uint poll_source = 0;
public signal void brightness_change (int percent);
public DdcUtil (int display_number) {
if (!init_library ()) return;
open_display (display_number);
}
private static bool init_library () {
if (lib_initialized) return true;
string[]? msgs;
int rc = Ddcutil.init2 (
null,
Ddcutil.SyslogLevel.NEVER,
Ddcutil.InitOptions.DISABLE_CONFIG_FILE,
out msgs);
if (rc != 0) {
warning ("ddcutil init failed: %d", rc);
return false;
}
lib_initialized = true;
return true;
}
public static DdcDisplayInfo[] detect_displays () {
if (!init_library ()) return {};
Ddcutil.DisplayInfoList? dlist;
int rc = Ddcutil.get_display_info_list2 (false, out dlist);
if (rc != 0 || dlist == null || dlist.ct == 0) {
return {};
}
DdcDisplayInfo[] result = {};
for (int i = 0; i < dlist.ct; i++) {
result += DdcDisplayInfo () {
dispno = dlist.info[i].dispno,
model_name = ((string) dlist.info[i].model_name).dup (),
};
}
return result;
}
private void open_display (int display_number) {
Ddcutil.DisplayInfoList? dlist;
int rc = Ddcutil.get_display_info_list2 (false, out dlist);
if (rc != 0 || dlist == null || dlist.ct == 0) {
warning ("No DDC displays found");
brightness_change (-1);
return;
}
int idx = 0;
if (display_number > 0) {
bool found = false;
for (int i = 0; i < dlist.ct; i++) {
if (dlist.info[i].dispno == display_number) {
idx = i;
found = true;
break;
}
}
if (!found) {
warning ("DDC display %d not found, using first", display_number);
}
}
display_name = ((string) dlist.info[idx].model_name).dup ();
void* dh;
rc = Ddcutil.open_display2 (dlist.info[idx].dref, true, out dh);
if (rc != 0) {
warning ("Failed to open DDC display: %d", rc);
brightness_change (-1);
return;
}
handle = dh;
}
public void start () {
get_brightness ();
start_polling ();
}
public void close () {
stop_polling ();
}
private void start_polling () {
stop_polling ();
poll_source = Timeout.add_seconds (5, () => {
get_brightness ();
return Source.CONTINUE;
});
}
private void stop_polling () {
if (poll_source != 0) {
Source.remove (poll_source);
poll_source = 0;
}
}
private void get_brightness () {
if (handle == null) {
brightness_change (-1);
return;
}
Ddcutil.NonTableVcpValue val;
int rc = Ddcutil.get_non_table_vcp_value (handle, VCP_BRIGHTNESS, out val);
if (rc != 0) {
warning ("Failed to read DDC brightness: %d", rc);
return;
}
int current = (val.sh << 8) | val.sl;
int max = (val.mh << 8) | val.ml;
if (max == 0) {
brightness_change (-1);
return;
}
int percent = (int) Math.round (current * 100.0 / max);
brightness_change (percent);
}
public async void set_brightness (float percent) {
if (handle == null) return;
int value = (int) Math.round (percent);
if (value < 0) value = 0;
if (value > 100) value = 100;
int rc = Ddcutil.set_non_table_vcp_value (handle, VCP_BRIGHTNESS, 0, (uint8) value);
if (rc != 0) {
warning ("Failed to set DDC brightness: %d", rc);
}
}
public string get_display_name () {
return display_name;
}
~DdcUtil () {
stop_polling ();
if (handle != null) {
Ddcutil.close_display (handle);
handle = null;
}
}
}
}
[CCode (cheader_filename = "ddcutil_c_api.h")]
namespace Ddcutil {
[CCode (cname = "DDCA_Syslog_Level", cprefix = "DDCA_SYSLOG_")]
public enum SyslogLevel {
[CCode (cname = "DDCA_SYSLOG_NOT_SET")]
NOT_SET,
[CCode (cname = "DDCA_SYSLOG_NEVER")]
NEVER
}
[CCode (cname = "DDCA_Init_Options", cprefix = "DDCA_INIT_OPTIONS_")]
[Flags]
public enum InitOptions {
[CCode (cname = "DDCA_INIT_OPTIONS_NONE")]
NONE,
[CCode (cname = "DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE")]
DISABLE_CONFIG_FILE
}
[CCode (cname = "DDCA_Non_Table_Vcp_Value", has_type_id = false)]
public struct NonTableVcpValue {
public uint8 mh;
public uint8 ml;
public uint8 sh;
public uint8 sl;
}
// All fields must be present to match C struct layout
[CCode (cname = "DDCA_Display_Info", has_type_id = false, destroy_function = "")]
public struct DisplayInfo {
public char marker[4];
public int dispno;
[CCode (cname = "path")]
public uint8 _path[8];
public int usb_bus;
public int usb_device;
public char mfg_id[4];
public char model_name[14];
public char sn[14];
public uint16 product_code;
public uint8 edid_bytes[128];
[CCode (cname = "vcp_version")]
public uint8 _vcp_version[2];
[CCode (cname = "dref")]
public void* dref;
}
[CCode (cname = "DDCA_Display_Info_List", has_type_id = false,
free_function = "ddca_free_display_info_list")]
[Compact]
public class DisplayInfoList {
public int ct;
[CCode (cname = "info", array_length_cname = "ct")]
public DisplayInfo info[0];
}
[CCode (cname = "ddca_init2")]
public static int init2 (
string? libopts,
SyslogLevel syslog_level,
InitOptions opts,
[CCode (array_length = false)]
out string[]? infomsg);
[CCode (cname = "ddca_get_display_info_list2")]
public static int get_display_info_list2 (
bool include_invalid,
out DisplayInfoList? dlist);
[CCode (cname = "ddca_open_display2")]
public static int open_display2 (
void* dref,
bool wait,
out void* dh);
[CCode (cname = "ddca_close_display")]
public static int close_display (void* dh);
[CCode (cname = "ddca_get_non_table_vcp_value")]
public static int get_non_table_vcp_value (
void* dh,
uint8 feature_code,
out NonTableVcpValue valrec);
[CCode (cname = "ddca_set_non_table_vcp_value")]
public static int set_non_table_vcp_value (
void* dh,
uint8 feature_code,
uint8 hi_byte,
uint8 lo_byte);
}
......@@ -120,6 +120,19 @@ if get_option('pulse-audio')
]
endif
# Checks if the user wants to compile with DDC/CI support
if get_option('ddc')
add_project_arguments('-D', 'HAVE_DDC', language: 'vala')
ddcutil_vapi_dir = meson.current_source_dir() / 'controlCenter' / 'widgets' / 'backlight'
app_deps += [
cc.find_library('ddcutil', required: true),
vala.find_library('ddcutil', dirs: ddcutil_vapi_dir),
]
app_sources += [
'controlCenter/widgets/backlight/ddcUtil.vala',
]
endif
args = [
'--target-glib=2.82',
]
......
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