Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
ximper-shell-notification-center
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Ximper Linux
ximper-shell-notification-center
Commits
4ecd858c
Verified
Commit
4ecd858c
authored
Mar 22, 2026
by
Kirill Unitsaev
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
backlight: rewrite as unified brightness widget with auto-detection
parent
441e4f55
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
314 additions
and
157 deletions
+314
-157
configSchema.json
src/configSchema.json
+1
-17
backlight.vala
src/controlCenter/widgets/backlight/backlight.vala
+307
-138
ddcUtil.vala
src/controlCenter/widgets/backlight/ddcUtil.vala
+6
-2
No files found.
src/configSchema.json
View file @
4ecd858c
...
...
@@ -697,29 +697,13 @@
},
"backlight"
:
{
"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
,
"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"
:
{
"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'."
}
}
},
...
...
src/controlCenter/widgets/backlight/backlight.vala
View file @
4ecd858c
...
...
@@ -8,175 +8,344 @@ namespace XimperShellNotificationCenter.Widgets {
}
}
BacklightUtil
client
;
private
struct
BrightnessSource
{
BacklightUtil
?
sysfs
;
#if HAVE_DDC
DdcUtil
[]
ddc_clients
=
{};
bool
use_ddc
=
false
;
DdcUtil
?
ddc
;
#endif
Gtk
.
Box
row
;
Gtk
.
Scale
scale
;
string
icon_name
;
string
tooltip
;
}
BrightnessSource
[]
display_sources
=
{};
BrightnessSource
[]
kbd_sources
=
{};
Gtk
.
Box
?
displays_box
=
null
;
Gtk
.
Box
?
kbd_box
=
null
;
Gtk
.
Separator
?
separator
=
null
;
Gtk
.
Revealer
?
revealer
=
null
;
Gtk
.
ToggleButton
?
reveal_btn
=
null
;
Gtk
.
Image
icon_widget
=
new
Gtk
.
Image
.
from_icon_name
(
"display-brightness-symbolic"
);
Gtk
.
Scale
slider
=
new
Gtk
.
Scale
.
with_range
(
Gtk
.
Orientation
.
HORIZONTAL
,
0
,
100
,
1
);
int
min_brightness
=
0
;
public
Backlight
(
string
suffix
)
{
base
(
suffix
);
set_orientation
(
Gtk
.
Orientation
.
VERTICAL
);
Json
.
Object
?
config
=
get_config
(
this
);
if
(
config
!=
null
)
{
string
device
=
(
get_prop
<
string
>
(
config
,
"device"
)
??
"intel_backlight"
);
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'"
+
min_brightness
=
int
.
max
(
0
,
get_prop
<
int
>
(
config
,
"min"
));
}
detect_sources
();
int
total
=
display_sources
.
length
+
kbd_sources
.
length
;
if
(
total
==
0
)
{
hide
();
return
;
}
build_ui
();
}
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'"
+
ddc
=
null
,
#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
;
icon_name
=
"display-brightness-symbolic"
,
tooltip
=
name
,
};
}
}
catch
(
FileError
e
)
{
debug
(
"No /sys/class/backlight: %s"
,
e
.
message
);
}
#if HAVE_DDC
// Scan DDC displays
var
ddc_displays
=
DdcUtil
.
detect_displays
();
for
(
int
i
=
0
;
i
<
ddc_displays
.
length
;
i
++)
{
var
ddc
=
new
DdcUtil
(
ddc_displays
[
i
].
dispno
);
display_sources
+=
BrightnessSource
()
{
sysfs
=
null
,
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
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
;
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
();
}
}
private
Gtk
.
Box
create_slider_row
(
ref
BrightnessSource
src
,
Gtk
.
SizeGroup
?
sg
,
bool
show_label
)
{
var
outer
=
new
Gtk
.
Box
(
Gtk
.
Orientation
.
VERTICAL
,
0
);
if
(
show_label
)
{
var
name_label
=
new
Gtk
.
Label
(
prettify_source_name
(
src
.
tooltip
));
name_label
.
set_halign
(
Gtk
.
Align
.
START
);
name_label
.
add_css_class
(
"dim-label"
);
name_label
.
add_css_class
(
"caption"
);
outer
.
append
(
name_label
);
}
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
(
Gtk
.
Orientation
.
HORIZONTAL
,
6
);
label_box
.
set_halign
(
Gtk
.
Align
.
CENTER
);
sg
.
add_widget
(
label_box
);
label_box
.
append
(
icon
);
row
.
append
(
label_box
);
}
else
{
row
.
append
(
icon
);
}
row
.
append
(
scl
);
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
)
{
outer
.
set_visible
(
false
);
source_hidden
();
}
else
{
scl
.
set_value
(
percent
);
}
});
scl
.
value_changed
.
connect
(()
=>
{
util
.
set_brightness
.
begin
(
(
float
)
scl
.
get_value
());
scl
.
tooltip_text
=
((
int
)
scl
.
get_value
()).
to_string
();
});
}
#if HAVE_DDC
if
(
use_ddc
)
{
if
(
ddc_clients
.
length
==
1
)
{
// Single DDC display — use the existing icon + slider
icon_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
(
icon_widget
);
append
(
slider
);
}
else
{
// Multiple DDC displays — create a row per display
set_orientation
(
Gtk
.
Orientation
.
VERTICAL
);
var
size_group
=
new
Gtk
.
SizeGroup
(
Gtk
.
SizeGroupMode
.
HORIZONTAL
);
for
(
int
i
=
0
;
i
<
ddc_clients
.
length
;
i
++)
{
var
row
=
new
Gtk
.
Box
(
Gtk
.
Orientation
.
HORIZONTAL
,
0
);
var
label_box
=
new
Gtk
.
Box
(
Gtk
.
Orientation
.
HORIZONTAL
,
6
);
label_box
.
set_halign
(
Gtk
.
Align
.
CENTER
);
size_group
.
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
(
num_label
);
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
.
set_visible
(
false
);
}
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
(
label_box
);
row
.
append
(
scl
);
append
(
row
);
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
);
}
}
return
;
});
scl
.
value_changed
.
connect
(()
=>
{
ddc
.
set_brightness
.
begin
(
(
float
)
scl
.
get_value
());
scl
.
tooltip_text
=
((
int
)
scl
.
get_value
()).
to_string
();
});
}
#endif
this
.
client
.
brightness_change
.
connect
((
percent
)
=>
{
if
(
percent
<
0
)
{
// invalid device path
hide
();
}
else
{
slider
.
set_value
(
percent
);
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
;
}
// Multiple sources — collapsible layout
if
(
need_collapse
)
{
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
{
reveal_btn
.
set_icon_name
(
"ximper-shell-notification-center-up-small-symbolic"
);
remove_css_class
(
"expanded"
);
}
});
// Display sources
if
(
display_sources
.
length
>
0
)
{
displays_box
=
new
Gtk
.
Box
(
Gtk
.
Orientation
.
VERTICAL
,
0
);
var
sg
=
new
Gtk
.
SizeGroup
(
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
);
}
});
slider
.
set_draw_value
(
false
);
slider
.
set_round_digits
(
0
);
slider
.
set_hexpand
(
true
);
slider
.
value_changed
.
connect
(()
=>
{
this
.
client
.
set_brightness
.
begin
((
float
)
slider
.
get_value
());
slider
.
tooltip_text
=
((
int
)
slider
.
get_value
()).
to_string
();
});
append
(
icon_widget
);
append
(
slider
);
// 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
);
}
// Keyboard sources
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
)
{
for
(
int
i
=
0
;
i
<
display_sources
.
length
;
i
++)
{
if
(
display_sources
[
i
].
sysfs
!=
null
)
{
if
(
val
)
{
display_sources
[
i
].
sysfs
.
start
();
}
else
{
display_sources
[
i
].
sysfs
.
close
();
}
}
#if HAVE_DDC
if
(
use_ddc
)
{
foreach
(
var
ddc
in
ddc_clients
)
{
if
(
display_sources
[
i
].
ddc
!=
null
)
{
if
(
val
)
{
ddc
.
start
();
d
isplay_sources
[
i
].
d
dc
.
start
();
}
else
{
ddc
.
close
();
d
isplay_sources
[
i
].
d
dc
.
close
();
}
}
return
;
}
#endif
if
(
val
)
{
this
.
client
.
start
();
}
else
{
this
.
client
.
close
();
}
for
(
int
i
=
0
;
i
<
kbd_sources
.
length
;
i
++)
{
if
(
kbd_sources
[
i
].
sysfs
!=
null
)
{
if
(
val
)
{
kbd_sources
[
i
].
sysfs
.
start
();
}
else
{
kbd_sources
[
i
].
sysfs
.
close
();
}
}
}
if
(
val
&&
reveal_btn
!=
null
)
{
reveal_btn
.
set_active
(
false
);
}
}
}
...
...
src/controlCenter/widgets/backlight/ddcUtil.vala
View file @
4ecd858c
...
...
@@ -2,7 +2,7 @@ namespace XimperShellNotificationCenter.Widgets {
public
struct
DdcDisplayInfo
{
int
dispno
;
string
model
_name
;
string
display
_name
;
}
class
DdcUtil
{
...
...
@@ -51,9 +51,13 @@ namespace XimperShellNotificationCenter.Widgets {
DdcDisplayInfo
[]
result
=
{};
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
()
{
dispno
=
dlist
.
info
[
i
].
dispno
,
model_name
=
((
string
)
dlist
.
info
[
i
].
model_name
).
dup
()
,
display_name
=
name
,
};
}
return
result
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment