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
3851cbe1
Verified
Commit
3851cbe1
authored
Mar 21, 2026
by
Kirill Unitsaev
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
backlight: add DDC/CI support for external monitors
parent
5a27e306
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
394 additions
and
4 deletions
+394
-4
meson_options.txt
meson_options.txt
+1
-0
configSchema.json
src/configSchema.json
+7
-2
backlight.vala
src/controlCenter/widgets/backlight/backlight.vala
+107
-2
ddcUtil.vala
src/controlCenter/widgets/backlight/ddcUtil.vala
+174
-0
ddcutil.vapi
src/controlCenter/widgets/backlight/ddcutil.vapi
+92
-0
meson.build
src/meson.build
+13
-0
No files found.
meson_options.txt
View file @
3851cbe1
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.')
...
...
src/configSchema.json
View file @
3851cbe1
...
...
@@ -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'."
}
}
},
...
...
src/controlCenter/widgets/backlight/backlight.vala
View file @
3851cbe1
...
...
@@ -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
{
...
...
src/controlCenter/widgets/backlight/ddcUtil.vala
0 → 100644
View file @
3851cbe1
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
;
}
}
}
}
src/controlCenter/widgets/backlight/ddcutil.vapi
0 → 100644
View file @
3851cbe1
[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);
}
src/meson.build
View file @
3851cbe1
...
...
@@ -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',
]
...
...
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