Packages

Elixir wrapper for the ddcutil tool to control monitor settings via DDC/CI on Linux. Allows querying capabilities and modifying brightness, contrast, RGB, input source, and more.

Current section

Files

Jump to
exddcutil lib exddcutil.ex
Raw

lib/exddcutil.ex

defmodule Exddcutil do
@moduledoc """
Elixir wrapper for the `ddcutil` tool to control monitor settings via DDC/CI.
This library provides a high-level interface to query and modify monitor settings
on Linux systems using the DDC/CI protocol through the `ddcutil` command-line tool.
## Prerequisites
- Linux system with `ddcutil` installed
- DDC/CI compatible monitor(s) with DDC/CI enabled in the OSD
## Testing Status
This library has been tested with:
- Dell P2715Q
Other DDC/CI compatible monitors should work, but behavior may vary depending
on manufacturer implementation.
## Basic Usage
# Detect available displays
{:ok, displays} = Exddcutil.detect()
# Get display capabilities
{:ok, caps} = Exddcutil.capabilities(1)
# Set brightness
:ok = Exddcutil.set_brightness(1, 75)
# Get current brightness
{:ok, %{current: 75, max: 100}} = Exddcutil.get_brightness(1)
Most functions take a display index (1-based) as the first argument, as returned
by `detect/1`.
"""
alias Exddcutil.{VCP, Parser, SystemRunner}
@type runner_opt :: {:runner, module()}
@type display_index :: pos_integer()
@type bus_index :: pos_integer()
@type target_opt :: {:display, display_index()} | {:bus, bus_index()}
@type common_opt :: runner_opt | target_opt
# Public API
@doc """
Detects all available displays connected to the system.
Returns a list of maps containing display information including index, bus number,
model, vendor, and serial number.
## Examples
iex> Exddcutil.detect()
{:ok, [
%{index: 1, bus: 5, model: "DELL P2715Q", vendor: "DEL", serial: "ABC123"}
]}
"""
@spec detect([runner_opt()]) :: {:ok, list()} | {:error, any()}
def detect(opts \\ []) do
with {:ok, raw} <- run_ddc(["detect"], opts) do
{:ok, Parser.parse_detect(raw)}
end
end
@doc """
Gets the full capabilities of a display.
Returns a map containing model, vendor, serial, and available features with their values.
## Examples
iex> Exddcutil.capabilities(1)
{:ok, %{
model: "DELL P2715Q",
vendor: "DEL",
features: %{
"Brightness" => %{code: 16, values: %{range: {0, 100}}},
"Input Source" => %{code: 96, values: [%{code: "0x11", name: "HDMI-1"}]}
},
input_sources: [%{code: "0x11", name: "HDMI-1"}]
}}
"""
@spec capabilities(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def capabilities(display_index, opts \\ []) do
with {:ok, raw} <- run_ddc(["capabilities"] ++ addressing_args(display_index, opts), opts) do
{:ok, Parser.parse_capabilities(raw)}
end
end
@doc """
Lists available input sources from display capabilities.
Returns a list of maps with code and name for each input source.
## Examples
iex> Exddcutil.input_sources(1)
{:ok, [
%{code: "0x11", name: "HDMI-1"},
%{code: "0x0f", name: "DisplayPort-1"}
]}
"""
@spec input_sources(display_index(), [common_opt()]) :: {:ok, list()} | {:error, any()}
def input_sources(display_index, opts \\ []) do
case capabilities(display_index, opts) do
{:ok, caps} -> {:ok, Map.get(caps, :input_sources, [])}
{:error, reason} -> {:error, reason}
end
end
@doc """
Lists available input sources with an active flag indicating the current source.
Combines `input_sources/2` and `get_input_source/2` to mark which input is currently active.
## Examples
iex> Exddcutil.get_input_sources(1)
{:ok, [
%{code: "0x11", name: "HDMI-1", active: false},
%{code: "0x0f", name: "DisplayPort-1", active: true}
]}
"""
@spec get_input_sources(display_index(), [common_opt()]) :: {:ok, list()} | {:error, any()}
def get_input_sources(display_index, opts \\ []) do
with {:ok, sources} <- input_sources(display_index, opts),
{:ok, current} <- get_input_source(display_index, opts) do
current_code = current[:current]
sources_with_active =
Enum.map(sources, fn source ->
Map.put(source, :active, source[:code] == current_code)
end)
{:ok, sources_with_active}
end
end
@doc """
Sets the display brightness.
## Parameters
- `display_index` - Display index (1-based)
- `value` - Brightness value (typically 0..100, check capabilities for range)
- `opts` - Optional keyword list (runner, bus)
## Examples
iex> Exddcutil.set_brightness(1, 75)
:ok
"""
@spec set_brightness(display_index(), non_neg_integer(), [common_opt()]) ::
:ok | {:error, any()}
def set_brightness(display_index, value, opts \\ []) do
set_vcp(display_index, VCP.brightness(), value, opts)
end
@doc """
Sets the display contrast.
## Examples
iex> Exddcutil.set_contrast(1, 50)
:ok
"""
@spec set_contrast(display_index(), non_neg_integer(), [common_opt()]) :: :ok | {:error, any()}
def set_contrast(display_index, value, opts \\ []) do
set_vcp(display_index, VCP.contrast(), value, opts)
end
@doc "Sets the red gain/drive."
@spec set_red(display_index(), non_neg_integer(), [common_opt()]) :: :ok | {:error, any()}
def set_red(display_index, value, opts \\ []) do
set_vcp(display_index, VCP.red(), value, opts)
end
@doc "Sets the green gain/drive."
@spec set_green(display_index(), non_neg_integer(), [common_opt()]) :: :ok | {:error, any()}
def set_green(display_index, value, opts \\ []) do
set_vcp(display_index, VCP.green(), value, opts)
end
@doc "Sets the blue gain/drive."
@spec set_blue(display_index(), non_neg_integer(), [common_opt()]) :: :ok | {:error, any()}
def set_blue(display_index, value, opts \\ []) do
set_vcp(display_index, VCP.blue(), value, opts)
end
@doc "Sets the screen orientation. Note: may be read-only on some monitors."
@spec set_screen_orientation(display_index(), term(), [common_opt()]) :: :ok | {:error, any()}
def set_screen_orientation(display_index, orientation, opts \\ []) do
code =
cond do
is_integer(orientation) -> orientation
is_binary(orientation) and String.starts_with?(orientation, "0x") -> orientation
true -> orientation
end
set_vcp(display_index, VCP.screen_orientation(), code, opts)
end
@doc "Sets the display mode."
@spec set_display_mode(display_index(), term(), [common_opt()]) :: :ok | {:error, any()}
def set_display_mode(display_index, mode, opts \\ []) do
code =
cond do
is_integer(mode) -> mode
is_binary(mode) and String.starts_with?(mode, "0x") -> mode
true -> mode
end
set_vcp(display_index, VCP.display_mode(), code, opts)
end
@doc """
Sets the input source.
Accepts either a source name (e.g., "HDMI-1"), hex code (e.g., "0x11"), or integer code.
## Examples
iex> Exddcutil.set_input_source(1, "DisplayPort-1")
:ok
iex> Exddcutil.set_input_source(1, "0x0f")
:ok
"""
@spec set_input_source(display_index(), term(), [common_opt()]) :: :ok | {:error, any()}
def set_input_source(display_index, source, opts \\ []) do
case capabilities(display_index, opts) do
{:ok, caps} ->
code =
cond do
is_integer(source) ->
source
is_binary(source) and String.starts_with?(source, "0x") ->
source
true ->
caps
|> Map.get(:input_sources, [])
|> Enum.find_value(fn %{name: name, code: code} ->
if String.downcase(name) == String.downcase(to_string(source)),
do: code,
else: nil
end)
end
set_vcp(display_index, VCP.input_source(), code, opts)
{:error, reason} ->
{:error, reason}
end
end
@doc """
Sets the color preset.
## Examples
iex> Exddcutil.set_color_preset(1, 0x05)
:ok
iex> Exddcutil.set_color_preset(1, "0x05")
:ok
"""
@spec set_color_preset(display_index(), term(), [common_opt()]) :: :ok | {:error, any()}
def set_color_preset(display_index, value, opts \\ []) do
set_vcp(display_index, VCP.color_preset(), value, opts)
end
@doc """
Sets an arbitrary VCP code to a value.
## Examples
iex> Exddcutil.set_vcp(1, 0x14, 6500) # Color temperature
:ok
"""
@spec set_vcp(display_index(), non_neg_integer() | String.t(), non_neg_integer() | String.t(), [
common_opt()
]) :: :ok | {:error, any()}
def set_vcp(display_index, code, value, opts \\ []) do
args =
["setvcp", VCP.to_code_arg(code), VCP.to_value_arg(value)] ++
addressing_args(display_index, opts)
case run_ddc(args, opts) do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}
end
end
@doc """
Reads an arbitrary VCP code.
Returns a map with code, name, current value, and optionally max value and current label.
## Examples
iex> Exddcutil.get_vcp(1, 0x10)
{:ok, %{code: "0x10", name: "Brightness", current: 75, max: 100}}
"""
@spec get_vcp(display_index(), non_neg_integer() | String.t(), [common_opt()]) ::
{:ok, map()} | {:error, any()}
def get_vcp(display_index, code, opts \\ []) do
with {:ok, raw} <-
run_ddc(
["getvcp", VCP.to_code_arg(code)] ++ addressing_args(display_index, opts),
opts
) do
{:ok, Parser.parse_getvcp(raw)}
end
end
@doc "Gets the current brightness setting."
@spec get_brightness(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_brightness(display_index, opts \\ []) do
get_vcp(display_index, VCP.brightness(), opts)
end
@doc "Gets the current contrast setting."
@spec get_contrast(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_contrast(display_index, opts \\ []) do
get_vcp(display_index, VCP.contrast(), opts)
end
@doc "Gets the current red gain/drive."
@spec get_red(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_red(display_index, opts \\ []) do
get_vcp(display_index, VCP.red(), opts)
end
@doc "Gets the current green gain/drive."
@spec get_green(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_green(display_index, opts \\ []) do
get_vcp(display_index, VCP.green(), opts)
end
@doc "Gets the current blue gain/drive."
@spec get_blue(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_blue(display_index, opts \\ []) do
get_vcp(display_index, VCP.blue(), opts)
end
@doc "Gets the current screen orientation."
@spec get_screen_orientation(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_screen_orientation(display_index, opts \\ []) do
get_vcp(display_index, VCP.screen_orientation(), opts)
end
@doc """
Gets the current input source.
Returns a map with the current source code and label.
## Examples
iex> Exddcutil.get_input_source(1)
{:ok, %{code: "0x60", name: "Input Source", current: "0x0f", current_label: "DisplayPort-1"}}
"""
@spec get_input_source(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_input_source(display_index, opts \\ []) do
get_vcp(display_index, VCP.input_source(), opts)
end
@doc "Gets the current color preset/temperature."
@spec get_color_preset(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_color_preset(display_index, opts \\ []) do
get_vcp(display_index, VCP.color_preset(), opts)
end
@doc "Gets the current display mode."
@spec get_display_mode(display_index(), [common_opt()]) :: {:ok, map()} | {:error, any()}
def get_display_mode(display_index, opts \\ []) do
get_vcp(display_index, VCP.display_mode(), opts)
end
defp addressing_args(display_index, opts) do
[]
|> maybe_put_opt("--display", display_index)
|> maybe_put_opt("--bus", opts[:bus])
end
defp maybe_put_opt(args, _flag, nil), do: args
defp maybe_put_opt(args, flag, value), do: args ++ [flag, to_string(value)]
@spec run_ddc([String.t()], [common_opt()]) :: {:ok, String.t()} | {:error, any()}
def run_ddc(args, opts \\ []) do
runner = opts[:runner] || Application.get_env(:exddcutil, :runner, SystemRunner)
case runner.run(args) do
{:ok, out} -> {:ok, out}
{:error, reason} -> {:error, reason}
end
end
end