backlight: rewrite as unified brightness widget with auto-detection

parent 441e4f55
...@@ -697,29 +697,13 @@ ...@@ -697,29 +697,13 @@
}, },
"backlight": { "backlight": {
"type": "object", "type": "object",
"description": "Slider to control monitor brightness", "description": "Unified brightness control widget. Auto-detects display backlights (/sys/class/backlight), DDC/CI monitors, and keyboard backlights (/sys/class/leds/*kbd*).",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"device": {
"type": "string",
"description": "Name of monitor (find possible devices using `ls /sys/class/backlight` or `ls /sys/class/leds`)",
"default": "intel_backlight"
},
"subsystem": {
"type": "string",
"description": "Kernel subsystem for brightness control. Use 'ddc' for external monitors via DDC/CI (requires build with -Dddc=true).",
"default": "backlight",
"enum": ["backlight", "leds", "ddc"]
},
"min": { "min": {
"type": "integer", "type": "integer",
"default": 0, "default": 0,
"description": "Lowest possible value for brightness" "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'."
} }
} }
}, },
......
...@@ -8,175 +8,344 @@ namespace XimperShellNotificationCenter.Widgets { ...@@ -8,175 +8,344 @@ namespace XimperShellNotificationCenter.Widgets {
} }
} }
BacklightUtil client; private struct BrightnessSource {
BacklightUtil ?sysfs;
#if HAVE_DDC #if HAVE_DDC
DdcUtil[] ddc_clients = {}; DdcUtil ?ddc;
bool use_ddc = false;
#endif #endif
Gtk.Box row;
Gtk.Scale scale;
string icon_name;
string tooltip;
}
Gtk.Image icon_widget = new Gtk.Image.from_icon_name ( BrightnessSource[] display_sources = {};
"display-brightness-symbolic"); BrightnessSource[] kbd_sources = {};
Gtk.Scale slider = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
Gtk.Box ?displays_box = null;
Gtk.Box ?kbd_box = null;
Gtk.Separator ?separator = null;
Gtk.Revealer ?revealer = null;
Gtk.ToggleButton ?reveal_btn = null;
int min_brightness = 0;
public Backlight (string suffix) { public Backlight (string suffix) {
base (suffix); base (suffix);
set_orientation (Gtk.Orientation.VERTICAL);
Json.Object ?config = get_config (this); Json.Object ?config = get_config (this);
if (config != null) { if (config != null) {
string device = (get_prop<string> (config, "device") ?? "intel_backlight"); min_brightness = int.max (0, get_prop<int> (config, "min"));
string subsystem = (get_prop<string> (config, "subsystem") ?? "backlight"); }
int min = int.max (0, get_prop<int> (config, "min"));
switch (subsystem) {
default :
info ("Invalid subsystem %s for device %s. " +
"Use 'backlight', 'leds'" +
#if HAVE_DDC
", 'ddc'" +
#endif
". Using default: 'backlight'",
subsystem, device);
client = new BacklightUtil ("backlight", device);
break;
case "backlight":
client = new BacklightUtil ("backlight", device);
slider.set_range (min, 100);
break;
case "leds":
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) { detect_sources ();
ddc_clients += new DdcUtil (display_number);
slider.set_range (min, 100); int total = display_sources.length + kbd_sources.length;
} else { if (total == 0) {
// Auto-detect all DDC displays
var displays = DdcUtil.detect_displays ();
if (displays.length == 0) {
warning ("No DDC displays found");
hide (); hide ();
return; return;
} }
for (int i = 0; i < displays.length; i++) {
var ddc = new DdcUtil (displays[i].dispno); build_ui ();
ddc_clients += ddc;
}
} }
break;
private void detect_sources () {
// Scan /sys/class/backlight/ for display backlights
try {
var dir = Dir.open ("/sys/class/backlight");
string ?name;
while ((name = dir.read_name ()) != null) {
var util = new BacklightUtil ("backlight", name);
display_sources += BrightnessSource () {
sysfs = util,
#if HAVE_DDC
ddc = null,
#endif #endif
icon_name = "display-brightness-symbolic",
tooltip = name,
};
} }
} catch (FileError e) {
debug ("No /sys/class/backlight: %s", e.message);
} }
#if HAVE_DDC #if HAVE_DDC
if (use_ddc) { // Scan DDC displays
if (ddc_clients.length == 1) { var ddc_displays = DdcUtil.detect_displays ();
// Single DDC display — use the existing icon + slider for (int i = 0; i < ddc_displays.length; i++) {
icon_widget.set_tooltip_text ( var ddc = new DdcUtil (ddc_displays[i].dispno);
ddc_clients[0].get_display_name ()); display_sources += BrightnessSource () {
ddc_clients[0].brightness_change.connect ((percent) => { sysfs = null,
if (percent < 0) { ddc = ddc,
icon_name = "display-brightness-symbolic",
tooltip = ddc_displays[i].display_name,
};
}
#endif
// Scan /sys/class/leds/ for keyboard backlights
try {
var dir = Dir.open ("/sys/class/leds");
string ?name;
while ((name = dir.read_name ()) != null) {
if (!name.contains ("kbd")) {
continue;
}
var util = new BacklightUtil ("leds", name);
kbd_sources += BrightnessSource () {
sysfs = util,
#if HAVE_DDC
ddc = null,
#endif
icon_name = "keyboard-brightness-symbolic",
tooltip = name,
};
}
} catch (FileError e) {
debug ("No /sys/class/leds: %s", e.message);
}
}
private static string prettify_source_name (string name) {
// DDC names are already nice (e.g. "DELL U2723QE")
// Sysfs backlight: "amdgpu_bl1" → "AMDGPU BL1"
// Kbd: "asus::kbd_backlight" → "Asus Keyboard"
string result = name;
// Keyboard backlight cleanup
if (result.contains ("kbd")) {
result = result.replace ("_backlight", "");
result = result.replace ("::kbd", "");
result = result.replace ("::", " ");
if (result.strip ().length > 0) {
string first = result.substring (0, 1).up ();
result = first + result.substring (1);
}
return result.strip ();
}
// Sysfs backlight: replace _ with space, uppercase
result = result.replace ("_", " ").up ();
return result;
}
private int hidden_count = 0;
private void source_hidden () {
hidden_count++;
int total = display_sources.length + kbd_sources.length;
if (hidden_count >= total) {
hide (); hide ();
} else {
slider.set_value (percent);
} }
}); }
slider.set_draw_value (false);
slider.set_round_digits (0); private Gtk.Box create_slider_row (ref BrightnessSource src,
slider.set_hexpand (true); Gtk.SizeGroup ?sg,
slider.value_changed.connect (() => { bool show_label) {
ddc_clients[0].set_brightness.begin ((float) slider.get_value ()); var outer = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
slider.tooltip_text = ((int) slider.get_value ()).to_string ();
}); if (show_label) {
append (icon_widget); var name_label = new Gtk.Label (
append (slider); prettify_source_name (src.tooltip));
} else { name_label.set_halign (Gtk.Align.START);
// Multiple DDC displays — create a row per display name_label.add_css_class ("dim-label");
set_orientation (Gtk.Orientation.VERTICAL); name_label.add_css_class ("caption");
var size_group = new Gtk.SizeGroup ( outer.append (name_label);
Gtk.SizeGroupMode.HORIZONTAL); }
for (int i = 0; i < ddc_clients.length; i++) {
var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
var scl = new Gtk.Scale.with_range (
Gtk.Orientation.HORIZONTAL, min_brightness, 100, 1);
scl.set_draw_value (false);
scl.set_round_digits (0);
scl.set_hexpand (true);
var icon = new Gtk.Image.from_icon_name (src.icon_name);
icon.set_tooltip_text (src.tooltip);
if (sg != null) {
var label_box = new Gtk.Box ( var label_box = new Gtk.Box (
Gtk.Orientation.HORIZONTAL, 6); Gtk.Orientation.HORIZONTAL, 6);
label_box.set_halign (Gtk.Align.CENTER); label_box.set_halign (Gtk.Align.CENTER);
size_group.add_widget (label_box); sg.add_widget (label_box);
var icon = new Gtk.Image.from_icon_name (
"display-brightness-symbolic");
var num_label = new Gtk.Label (
(i + 1).to_string ());
icon.set_tooltip_text (
ddc_clients[i].get_display_name ());
label_box.append (icon); label_box.append (icon);
label_box.append (num_label); row.append (label_box);
var scl = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1); } else {
scl.set_draw_value (false); row.append (icon);
scl.set_round_digits (0); }
scl.set_hexpand (true);
int idx = i; row.append (scl);
ddc_clients[i].brightness_change.connect ((percent) => { outer.append (row);
src.row = outer;
src.scale = scl;
// Connect signals
if (src.sysfs != null) {
unowned BacklightUtil util = src.sysfs;
util.brightness_change.connect ((percent) => {
if (percent < 0) { if (percent < 0) {
row.set_visible (false); outer.set_visible (false);
source_hidden ();
} else { } else {
scl.set_value (percent); scl.set_value (percent);
} }
}); });
scl.value_changed.connect (() => { scl.value_changed.connect (() => {
ddc_clients[idx].set_brightness.begin ((float) scl.get_value ()); util.set_brightness.begin (
scl.tooltip_text = ((int) scl.get_value ()).to_string (); (float) scl.get_value ());
scl.tooltip_text =
((int) scl.get_value ()).to_string ();
}); });
row.append (label_box);
row.append (scl);
append (row);
} }
#if HAVE_DDC
if (src.ddc != null) {
unowned DdcUtil ddc = src.ddc;
ddc.brightness_change.connect ((percent) => {
if (percent < 0) {
outer.set_visible (false);
source_hidden ();
} else {
scl.set_value (percent);
}
});
scl.value_changed.connect (() => {
ddc.set_brightness.begin (
(float) scl.get_value ());
scl.tooltip_text =
((int) scl.get_value ()).to_string ();
});
} }
#endif
return outer;
}
private void build_ui () {
bool need_collapse = display_sources.length > 1
|| kbd_sources.length > 0;
if (!need_collapse && display_sources.length == 1) {
// Single display — simple row
var row = create_slider_row (
ref display_sources[0], null, false);
append (row);
return; return;
} }
#endif
this.client.brightness_change.connect ((percent) => { // Multiple sources — collapsible layout
if (percent < 0) { // invalid device path if (need_collapse) {
hide (); var topbar = new Gtk.Box (
Gtk.Orientation.HORIZONTAL, 6);
var topbar_icon = new Gtk.Image.from_icon_name (
"display-brightness-symbolic");
var topbar_label = new Gtk.Label (
_("Brightness"));
topbar_label.set_hexpand (true);
topbar_label.set_halign (Gtk.Align.START);
reveal_btn = new Gtk.ToggleButton ();
reveal_btn.set_icon_name (
"ximper-shell-notification-center-up-small-symbolic");
topbar.append (topbar_icon);
topbar.append (topbar_label);
topbar.append (reveal_btn);
append (topbar);
revealer = new Gtk.Revealer ();
revealer.set_transition_type (
Gtk.RevealerTransitionType.SLIDE_DOWN);
revealer.set_transition_duration (200);
var content = new Gtk.Box (
Gtk.Orientation.VERTICAL, 0);
revealer.set_child (content);
append (revealer);
reveal_btn.toggled.connect (() => {
revealer.set_reveal_child (reveal_btn.active);
if (reveal_btn.active) {
reveal_btn.set_icon_name (
"ximper-shell-notification-center-down-small-symbolic");
add_css_class ("expanded");
} else { } else {
slider.set_value (percent); reveal_btn.set_icon_name (
"ximper-shell-notification-center-up-small-symbolic");
remove_css_class ("expanded");
} }
}); });
slider.set_draw_value (false); // Display sources
slider.set_round_digits (0); if (display_sources.length > 0) {
slider.set_hexpand (true); displays_box = new Gtk.Box (
slider.value_changed.connect (() => { Gtk.Orientation.VERTICAL, 0);
this.client.set_brightness.begin ((float) slider.get_value ()); var sg = new Gtk.SizeGroup (
slider.tooltip_text = ((int) slider.get_value ()).to_string (); Gtk.SizeGroupMode.HORIZONTAL);
}); for (int i = 0; i < display_sources.length; i++) {
var row = create_slider_row (
ref display_sources[i], sg, true);
displays_box.append (row);
}
content.append (displays_box);
}
// Separator
if (display_sources.length > 0
&& kbd_sources.length > 0) {
separator = new Gtk.Separator (
Gtk.Orientation.HORIZONTAL);
separator.set_margin_top (8);
separator.set_margin_bottom (8);
content.append (separator);
}
append (icon_widget); // Keyboard sources
append (slider); if (kbd_sources.length > 0) {
kbd_box = new Gtk.Box (
Gtk.Orientation.VERTICAL, 0);
var sg = new Gtk.SizeGroup (
Gtk.SizeGroupMode.HORIZONTAL);
for (int i = 0; i < kbd_sources.length; i++) {
var row = create_slider_row (
ref kbd_sources[i], sg, true);
kbd_box.append (row);
}
content.append (kbd_box);
}
}
} }
public override void on_cc_visibility_change (bool val) { public override void on_cc_visibility_change (bool val) {
#if HAVE_DDC for (int i = 0; i < display_sources.length; i++) {
if (use_ddc) { if (display_sources[i].sysfs != null) {
foreach (var ddc in ddc_clients) {
if (val) { if (val) {
ddc.start (); display_sources[i].sysfs.start ();
} else { } else {
ddc.close (); display_sources[i].sysfs.close ();
} }
} }
return; #if HAVE_DDC
if (display_sources[i].ddc != null) {
if (val) {
display_sources[i].ddc.start ();
} else {
display_sources[i].ddc.close ();
}
} }
#endif #endif
}
for (int i = 0; i < kbd_sources.length; i++) {
if (kbd_sources[i].sysfs != null) {
if (val) { if (val) {
this.client.start (); kbd_sources[i].sysfs.start ();
} else { } else {
this.client.close (); kbd_sources[i].sysfs.close ();
}
}
}
if (val && reveal_btn != null) {
reveal_btn.set_active (false);
} }
} }
} }
......
...@@ -2,7 +2,7 @@ namespace XimperShellNotificationCenter.Widgets { ...@@ -2,7 +2,7 @@ namespace XimperShellNotificationCenter.Widgets {
public struct DdcDisplayInfo { public struct DdcDisplayInfo {
int dispno; int dispno;
string model_name; string display_name;
} }
class DdcUtil { class DdcUtil {
...@@ -51,9 +51,13 @@ namespace XimperShellNotificationCenter.Widgets { ...@@ -51,9 +51,13 @@ namespace XimperShellNotificationCenter.Widgets {
DdcDisplayInfo[] result = {}; DdcDisplayInfo[] result = {};
for (int i = 0; i < dlist.ct; i++) { for (int i = 0; i < dlist.ct; i++) {
string mfg = ((string) dlist.info[i].mfg_id).dup ().strip ();
string model = ((string) dlist.info[i].model_name).dup ().strip ();
string name = mfg.length > 0
? "%s %s".printf (mfg, model) : model;
result += DdcDisplayInfo () { result += DdcDisplayInfo () {
dispno = dlist.info[i].dispno, dispno = dlist.info[i].dispno,
model_name = ((string) dlist.info[i].model_name).dup (), display_name = name,
}; };
} }
return result; return result;
......
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