brightness: add sysfs and DDC/CI monitor support

parent 6788b189
option('pulse-audio', type: 'boolean', value: true, description: 'Monitor PulseAudio volume changes.') option('pulse-audio', type: 'boolean', value: true, description: 'Monitor PulseAudio volume changes.')
option('ddc', type: 'boolean', value: true, description: 'Monitor DDC/CI brightness via libddcutil.')
...@@ -17,6 +17,10 @@ namespace XimperShellOsd { ...@@ -17,6 +17,10 @@ namespace XimperShellOsd {
#if HAVE_PULSE_AUDIO #if HAVE_PULSE_AUDIO
private PulseMonitor? pulse_monitor; private PulseMonitor? pulse_monitor;
#endif #endif
private BacklightMonitor? backlight_monitor;
#if HAVE_DDC
private DdcMonitor? ddc_monitor;
#endif
public Application () { public Application () {
Object ( Object (
...@@ -54,14 +58,34 @@ namespace XimperShellOsd { ...@@ -54,14 +58,34 @@ namespace XimperShellOsd {
if (is_muted) { if (is_muted) {
osd_window.show_osd (OsdType.VOLUME_MUTE, 0, "Muted"); osd_window.show_osd (OsdType.VOLUME_MUTE, 0, "Muted");
} else { } else {
var icon = get_volume_icon (percent); osd_window.show_osd (OsdType.VOLUME, percent / 100.0, "%d%%".printf ((int) percent), get_volume_icon (percent));
osd_window.show_osd (OsdType.VOLUME, percent / 100.0, "%d%%".printf ((int) percent), icon);
} }
}); });
pulse_monitor.start (); pulse_monitor.start ();
} }
#endif #endif
if (config.monitor_brightness) {
var device = BacklightMonitor.detect_device ();
if (device != null) {
backlight_monitor = new BacklightMonitor (device);
backlight_monitor.brightness_changed.connect ((percent) => {
osd_window.show_osd (OsdType.BRIGHTNESS, percent / 100.0, "%d%%".printf (percent));
});
backlight_monitor.start ();
}
}
#if HAVE_DDC
if (config.monitor_ddc) {
ddc_monitor = new DdcMonitor (config.ddc_poll_interval_sec);
ddc_monitor.brightness_changed.connect ((percent) => {
osd_window.show_osd (OsdType.BRIGHTNESS, percent / 100.0, "%d%%".printf (percent));
});
ddc_monitor.start ();
}
#endif
hold (); hold ();
} }
...@@ -78,6 +102,16 @@ namespace XimperShellOsd { ...@@ -78,6 +102,16 @@ namespace XimperShellOsd {
pulse_monitor = null; pulse_monitor = null;
} }
#endif #endif
if (backlight_monitor != null) {
backlight_monitor.close ();
backlight_monitor = null;
}
#if HAVE_DDC
if (ddc_monitor != null) {
ddc_monitor.close ();
ddc_monitor = null;
}
#endif
base.shutdown (); base.shutdown ();
} }
......
...@@ -6,6 +6,9 @@ namespace XimperShellOsd { ...@@ -6,6 +6,9 @@ namespace XimperShellOsd {
public int margin_bottom { get; set; default = 80; } public int margin_bottom { get; set; default = 80; }
public int width { get; set; default = 300; } public int width { get; set; default = 300; }
public bool monitor_volume { get; set; default = true; } public bool monitor_volume { get; set; default = true; }
public bool monitor_brightness { get; set; default = true; }
public bool monitor_ddc { get; set; default = true; }
public int ddc_poll_interval_sec { get; set; default = 5; }
private string config_path; private string config_path;
...@@ -34,6 +37,12 @@ namespace XimperShellOsd { ...@@ -34,6 +37,12 @@ namespace XimperShellOsd {
width = (int) root.get_int_member ("width"); width = (int) root.get_int_member ("width");
if (root.has_member ("monitor_volume")) if (root.has_member ("monitor_volume"))
monitor_volume = root.get_boolean_member ("monitor_volume"); monitor_volume = root.get_boolean_member ("monitor_volume");
if (root.has_member ("monitor_brightness"))
monitor_brightness = root.get_boolean_member ("monitor_brightness");
if (root.has_member ("monitor_ddc"))
monitor_ddc = root.get_boolean_member ("monitor_ddc");
if (root.has_member ("ddc_poll_interval_sec"))
ddc_poll_interval_sec = (int) root.get_int_member ("ddc_poll_interval_sec");
} catch (Error e) { } catch (Error e) {
warning ("Failed to load OSD config: %s", e.message); warning ("Failed to load OSD config: %s", e.message);
} }
......
...@@ -20,5 +20,9 @@ namespace XimperShellOsd { ...@@ -20,5 +20,9 @@ namespace XimperShellOsd {
); );
} }
} }
public void show_brightness_osd (double percent) throws DBusError, IOError {
app.show_osd_external (OsdType.BRIGHTNESS, percent / 100.0, "%d%%".printf ((int) percent));
}
} }
} }
cc = meson.get_compiler('c')
osd_deps = [ osd_deps = [
dependency('gtk4', version: '>= 4.12'), dependency('gtk4', version: '>= 4.12'),
dependency('libadwaita-1', version: '>= 1.4'), dependency('libadwaita-1', version: '>= 1.4'),
dependency('gtk4-layer-shell-0', version: '>= 1.0'), dependency('gtk4-layer-shell-0', version: '>= 1.0'),
dependency('json-glib-1.0'), dependency('json-glib-1.0'),
dependency('gio-unix-2.0'), dependency('gio-unix-2.0'),
cc.find_library('m', required: false),
] ]
osd_sources = files( osd_sources = files(
...@@ -13,6 +16,7 @@ osd_sources = files( ...@@ -13,6 +16,7 @@ osd_sources = files(
'osd-window.vala', 'osd-window.vala',
'osd-content.vala', 'osd-content.vala',
'dbus-service.vala', 'dbus-service.vala',
'monitors/backlight-monitor.vala',
) )
if get_option('pulse-audio') if get_option('pulse-audio')
...@@ -26,6 +30,18 @@ if get_option('pulse-audio') ...@@ -26,6 +30,18 @@ if get_option('pulse-audio')
) )
endif endif
if get_option('ddc')
add_project_arguments('-D', 'HAVE_DDC', language: 'vala')
ddcutil_vapi_dir = meson.current_source_dir() / 'monitors'
osd_deps += [
cc.find_library('ddcutil', required: true),
meson.get_compiler('vala').find_library('ddcutil', dirs: ddcutil_vapi_dir),
]
osd_sources += files(
'monitors/ddc-monitor.vala',
)
endif
executable('ximper-shell-osd', osd_sources, executable('ximper-shell-osd', osd_sources,
dependencies: osd_deps, dependencies: osd_deps,
c_args: ['-w'], c_args: ['-w'],
......
namespace XimperShellOsd {
public class BacklightMonitor : Object {
private string path_current;
private string path_max;
private File fd;
private FileMonitor? monitor;
private int max_value;
private int last_percent = -1;
public signal void brightness_changed (int percent);
public BacklightMonitor (string device) {
path_current = Path.build_filename ("/sys", "class", "backlight", device, "brightness");
path_max = Path.build_filename ("/sys", "class", "backlight", device, "max_brightness");
fd = File.new_for_path (path_current);
}
public bool start () {
if (!fd.query_exists ()) {
warning ("Backlight device not found: %s", path_current);
return false;
}
read_max ();
read_brightness_silent ();
try {
monitor = fd.monitor (FileMonitorFlags.NONE, null);
monitor.changed.connect (() => read_brightness ());
} catch (Error e) {
warning ("Failed to monitor backlight: %s", e.message);
return false;
}
return true;
}
public void close () {
if (monitor != null) {
monitor.cancel ();
monitor = null;
}
}
private void read_max () {
try {
var dis = new DataInputStream (File.new_for_path (path_max).read (null));
max_value = int.parse (dis.read_line (null));
} catch (Error e) {
warning ("Failed to read max brightness: %s", e.message);
}
}
private void read_brightness_silent () {
try {
var dis = new DataInputStream (fd.read (null));
int val = int.parse (dis.read_line (null));
last_percent = (int) Math.round (val * 100.0 / max_value);
} catch (Error e) {
warning ("Failed to read brightness: %s", e.message);
}
}
private void read_brightness () {
try {
var dis = new DataInputStream (fd.read (null));
int val = int.parse (dis.read_line (null));
int percent = (int) Math.round (val * 100.0 / max_value);
if (percent != last_percent) {
last_percent = percent;
brightness_changed (percent);
}
} catch (Error e) {
warning ("Failed to read brightness: %s", e.message);
}
}
public static string? detect_device () {
try {
var dir = Dir.open ("/sys/class/backlight");
string? name;
while ((name = dir.read_name ()) != null) {
return name;
}
} catch (FileError e) {}
return null;
}
}
}
namespace XimperShellOsd {
public class DdcMonitor : Object {
private void* handle = null;
private uint poll_source = 0;
private int last_percent = -1;
private int poll_interval;
private static bool lib_initialized = false;
public signal void brightness_changed (int percent);
public DdcMonitor (int poll_interval_sec = 5) {
this.poll_interval = poll_interval_sec;
}
public bool start () {
if (!init_library ()) return false;
if (!open_display ()) return false;
read_brightness_silent ();
poll_source = Timeout.add_seconds (poll_interval, () => {
read_brightness ();
return Source.CONTINUE;
});
return true;
}
public void close () {
if (poll_source != 0) {
Source.remove (poll_source);
poll_source = 0;
}
}
~DdcMonitor () {
close ();
if (handle != null) {
Ddcutil.close_display (handle);
handle = null;
}
}
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;
}
private bool open_display () {
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");
return false;
}
void* dh;
rc = Ddcutil.open_display2 (dlist.info[0].dref, true, out dh);
if (rc != 0) {
warning ("Failed to open DDC display: %d", rc);
return false;
}
handle = dh;
return true;
}
private void read_brightness_silent () {
if (handle == null) return;
Ddcutil.NonTableVcpValue val;
int rc = Ddcutil.get_non_table_vcp_value (handle, 0x10, out val);
if (rc != 0) return;
int current = (val.sh << 8) | val.sl;
int max = (val.mh << 8) | val.ml;
if (max > 0) {
last_percent = (int) Math.round (current * 100.0 / max);
}
}
private void read_brightness () {
if (handle == null) return;
Ddcutil.NonTableVcpValue val;
int rc = Ddcutil.get_non_table_vcp_value (handle, 0x10, out val);
if (rc != 0) return;
int current = (val.sh << 8) | val.sl;
int max = (val.mh << 8) | val.ml;
if (max == 0) return;
int percent = (int) Math.round (current * 100.0 / max);
if (percent != last_percent) {
last_percent = percent;
brightness_changed (percent);
}
}
}
}
[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);
}
...@@ -10,6 +10,7 @@ namespace XimperShellOsd { ...@@ -10,6 +10,7 @@ namespace XimperShellOsd {
private string? default_sink_name; private string? default_sink_name;
private double last_volume = -1; private double last_volume = -1;
private bool last_muted = false; private bool last_muted = false;
private bool initialized = false;
public signal void volume_changed (double percent, bool is_muted); public signal void volume_changed (double percent, bool is_muted);
...@@ -100,6 +101,13 @@ namespace XimperShellOsd { ...@@ -100,6 +101,13 @@ namespace XimperShellOsd {
bool is_muted = info.mute == 1; bool is_muted = info.mute == 1;
double volume = volume_to_double (info.volume.max ()); double volume = volume_to_double (info.volume.max ());
if (!initialized) {
last_volume = volume;
last_muted = is_muted;
initialized = true;
return;
}
if (volume != last_volume || is_muted != last_muted) { if (volume != last_volume || is_muted != last_muted) {
last_volume = volume; last_volume = volume;
last_muted = is_muted; last_muted = is_muted;
......
...@@ -2,7 +2,8 @@ namespace XimperShellOsd { ...@@ -2,7 +2,8 @@ namespace XimperShellOsd {
public enum OsdType { public enum OsdType {
VOLUME, VOLUME,
VOLUME_MUTE; VOLUME_MUTE,
BRIGHTNESS;
} }
public class OsdContent : Gtk.Box { public class OsdContent : Gtk.Box {
...@@ -40,10 +41,16 @@ namespace XimperShellOsd { ...@@ -40,10 +41,16 @@ namespace XimperShellOsd {
} }
public void update (OsdType type, double value, string text, string? icon_name = null) { public void update (OsdType type, double value, string text, string? icon_name = null) {
if (type == OsdType.VOLUME_MUTE) { switch (type) {
icon.icon_name = "audio-volume-muted-symbolic"; case OsdType.VOLUME_MUTE:
} else { icon.icon_name = "audio-volume-muted-symbolic";
icon.icon_name = icon_name ?? "audio-volume-high-symbolic"; break;
case OsdType.BRIGHTNESS:
icon.icon_name = icon_name ?? "display-brightness-symbolic";
break;
default:
icon.icon_name = icon_name ?? "audio-volume-high-symbolic";
break;
} }
progress.fraction = value.clamp (0.0, 1.0); progress.fraction = value.clamp (0.0, 1.0);
......
...@@ -8,6 +8,10 @@ namespace XimperShellOsd { ...@@ -8,6 +8,10 @@ namespace XimperShellOsd {
private Adw.TimedAnimation? fade_in_anim; private Adw.TimedAnimation? fade_in_anim;
private Adw.TimedAnimation? fade_out_anim; private Adw.TimedAnimation? fade_out_anim;
private bool is_showing = false; private bool is_showing = false;
private OsdType last_type;
private double last_value = -1;
private int64 last_show_time = 0;
private const int64 DEDUP_WINDOW_US = 500000;
public OsdWindow (Application app) { public OsdWindow (Application app) {
this.app = app; this.app = app;
...@@ -34,6 +38,14 @@ namespace XimperShellOsd { ...@@ -34,6 +38,14 @@ namespace XimperShellOsd {
} }
public void show_osd (OsdType type, double value, string label, string? icon_name = null) { public void show_osd (OsdType type, double value, string label, string? icon_name = null) {
int64 now = GLib.get_monotonic_time ();
if (type == last_type && value == last_value && (now - last_show_time) < DEDUP_WINDOW_US) {
return;
}
last_type = type;
last_value = value;
last_show_time = now;
content.update (type, value, label, icon_name); content.update (type, value, label, icon_name);
cancel_hide (); cancel_hide ();
......
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