feat(ipc): adjust brightness ipc to follow a similar syntax as volume

This commit is contained in:
Lysec
2026-04-17 13:19:52 +02:00
parent e7623b7bda
commit c0afdce0e5
5 changed files with 113 additions and 99 deletions
+7 -5
View File
@@ -760,21 +760,23 @@ Notes:
IPC:
```sh
noctalia msg set-brightness current 65
noctalia msg set-brightness 65 # current display
noctalia msg set-brightness DP-1 0.65
noctalia msg set-brightness all 40%
noctalia msg set-brightness * 40% # set brightness for all displays
noctalia msg raise-brightness current
noctalia msg raise-brightness # current display, default 5% step
noctalia msg raise-brightness DP-1 10
noctalia msg lower-brightness all 5%
noctalia msg lower-brightness * 5% # lower brightness for all displays
```
- Targets: `current`, `all`, or a specific display id such as `eDP-1` / `DP-1`.
- Targets: `current`, `all`/`*`, a display id (`eDP-1`, `DP-1`), or a monitor selector token using the same matching rules as monitor overrides.
- `current` resolves from the active/focused output when possible, then falls back to the last interactive output.
- Values and steps accept either:
- normalized values in the `0.0``1.0` range, such as `0.65` or `0.05`
- percentage-style values, such as `65`, `65%`, `5`, `5%`, or `12.5%`
- Rule of thumb: if you include a decimal and the value is `<= 1.0`, it is treated as normalized; otherwise it is treated as a percentage.
- `raise-brightness` / `lower-brightness` accept optional `[target] [step]`. With no arguments, they target `current` and use a `5%` step.
- `set-brightness` accepts `<value>` (current display) or `<target> <value>`.
---
+1 -1
View File
@@ -208,4 +208,4 @@ gdbus call --session --dest dev.noctalia.Debug --object-path /dev/noctalia/Debug
Noctalia reads `$XDG_CONFIG_HOME/noctalia/config.toml` or `~/.config/noctalia/config.toml`.
If no config file exists, it falls back to built-in defaults in code.
See [CONFIG.md](CONFIG.md) for the full configuration reference.
See [CONFIG.md](CONFIG.md) for the full configuration reference, including shell IPC command examples.
+25 -25
View File
@@ -218,34 +218,34 @@ namespace {
return {};
}
bool matchesOutput(const std::string& match, const WaylandOutput& output) {
// Exact connector name match
if (!output.connectorName.empty() && match == output.connectorName) {
return true;
}
// Word-boundary substring match on description.
// A bare substring search would let "DP-1" match "eDP-1" inside descriptions
// like "BOE 0x0BCA eDP-1", so require the token to be surrounded by whitespace
// or string boundaries.
if (!output.description.empty()) {
std::size_t pos = 0;
while ((pos = output.description.find(match, pos)) != std::string::npos) {
const bool startOk = (pos == 0 || std::isspace(static_cast<unsigned char>(output.description[pos - 1])) != 0);
const bool endOk = (pos + match.size() == output.description.size() ||
std::isspace(static_cast<unsigned char>(output.description[pos + match.size()])) != 0);
if (startOk && endOk) {
return true;
}
++pos;
}
}
return false;
}
constexpr Logger kLog("config");
} // namespace
bool outputMatchesSelector(const std::string& match, const WaylandOutput& output) {
// Exact connector name match.
if (!output.connectorName.empty() && match == output.connectorName) {
return true;
}
// Word-boundary substring match on description.
// A bare substring search would let "DP-1" match "eDP-1" inside descriptions
// like "BOE 0x0BCA eDP-1", so require the token to be surrounded by whitespace
// or string boundaries.
if (!output.description.empty()) {
std::size_t pos = 0;
while ((pos = output.description.find(match, pos)) != std::string::npos) {
const bool startOk = (pos == 0 || std::isspace(static_cast<unsigned char>(output.description[pos - 1])) != 0);
const bool endOk = (pos + match.size() == output.description.size() ||
std::isspace(static_cast<unsigned char>(output.description[pos + match.size()])) != 0);
if (startOk && endOk) {
return true;
}
++pos;
}
}
return false;
}
// ── Lifecycle ────────────────────────────────────────────────────────────────
ConfigService::WallpaperBatch::WallpaperBatch(ConfigService& config) : m_config(config) {
@@ -615,7 +615,7 @@ BarConfig ConfigService::resolveForOutput(const BarConfig& base, const WaylandOu
BarConfig resolved = base;
for (const auto& ovr : base.monitorOverrides) {
if (!matchesOutput(ovr.match, output)) {
if (!outputMatchesSelector(ovr.match, output)) {
continue;
}
+4
View File
@@ -133,6 +133,10 @@ struct WidgetConfig {
// Theme role or `#` hex for `[widget.*] color` and other user color strings (same rules as `capsule_fill`).
[[nodiscard]] ThemeColor themeColorFromConfigString(const std::string& raw);
// Shared output selector matching used by monitor-scoped config and IPC selectors.
// Matches connector name exactly, or a word-boundary token within output description.
[[nodiscard]] bool outputMatchesSelector(const std::string& match, const WaylandOutput& output);
enum class WallpaperFillMode : std::uint8_t {
Center = 0,
Crop = 1,
+76 -68
View File
@@ -180,25 +180,6 @@ namespace {
return nullptr;
}
bool matchesOutput(std::string_view match, const WaylandOutput& output) {
if (!output.connectorName.empty() && match == output.connectorName) {
return true;
}
if (!output.description.empty()) {
std::size_t pos = 0;
while ((pos = output.description.find(match, pos)) != std::string::npos) {
const bool startOk = (pos == 0 || std::isspace(static_cast<unsigned char>(output.description[pos - 1])) != 0);
const bool endOk = (pos + match.size() == output.description.size() ||
std::isspace(static_cast<unsigned char>(output.description[pos + match.size()])) != 0);
if (startOk && endOk) {
return true;
}
++pos;
}
}
return false;
}
BrightnessBackendPreference backendPreferenceForOutput(const BrightnessConfig& config, const WaylandOutput* output) {
if (output == nullptr) {
return BrightnessBackendPreference::Auto;
@@ -206,7 +187,7 @@ namespace {
for (const auto& override : config.monitorOverrides) {
const std::string match = !override.match.empty() ? override.match : std::string{};
if (match.empty() || !matchesOutput(match, *output)) {
if (match.empty() || !outputMatchesSelector(match, *output)) {
continue;
}
if (override.backend.has_value()) {
@@ -1327,14 +1308,13 @@ void BrightnessService::registerIpc(IpcService& ipc, std::function<void()> onBat
return false;
}
if (token == "all") {
for (const auto& display : displays()) {
ids.push_back(display.id);
auto appendUnique = [&ids](const std::string& id) {
if (std::find(ids.begin(), ids.end(), id) == ids.end()) {
ids.push_back(id);
}
return !ids.empty();
}
};
if (token == "current") {
if (token.empty() || token == "current") {
wl_output* output = m_impl->wayland.activeToplevelOutput();
if (output == nullptr) {
output = m_impl->wayland.preferredPanelOutput();
@@ -1348,12 +1328,32 @@ void BrightnessService::registerIpc(IpcService& ipc, std::function<void()> onBat
error = "error: current output has no brightness control\n";
return false;
}
ids.push_back(display->id);
appendUnique(display->id);
return true;
}
if (token == "all" || token == "*") {
for (const auto& display : displays()) {
appendUnique(display.id);
}
return !ids.empty();
}
if (const auto* display = findDisplay(std::string(token)); display != nullptr) {
ids.push_back(display->id);
appendUnique(display->id);
return true;
}
for (const auto& output : m_impl->wayland.outputs()) {
if (!outputMatchesSelector(std::string(token), output)) {
continue;
}
if (const auto* display = findByOutput(output.output); display != nullptr) {
appendUnique(display->id);
}
}
if (!ids.empty()) {
return true;
}
@@ -1390,59 +1390,67 @@ void BrightnessService::registerIpc(IpcService& ipc, std::function<void()> onBat
"set-brightness",
[this, applyToTargets](const std::string& args) -> std::string {
const auto parts = noctalia::ipc::splitWords(args);
if (parts.size() != 2) {
return "error: set-brightness requires <current|all|display-id> <value>\n";
if (parts.empty() || parts.size() > 2) {
return "error: set-brightness requires <value> or <target> <value>\n";
}
const auto amount = noctalia::ipc::parseNormalizedOrPercent(parts[1]);
std::string target = "current";
std::string valueToken = parts[0];
if (parts.size() == 2) {
target = parts[0];
valueToken = parts[1];
}
const auto amount = noctalia::ipc::parseNormalizedOrPercent(valueToken);
if (!amount.has_value()) {
return "error: invalid brightness value (use percent like 65 or 65%, or normalized like 0.65)\n";
}
return applyToTargets(parts[0],
return applyToTargets(target,
[this, amount](const BrightnessDisplay& display) { setBrightness(display.id, *amount); });
},
"set-brightness <current|all|display-id> <value>", "Set brightness for one or more displays");
"set-brightness <value> | set-brightness <current|*|all|monitor-selector> <value>",
"Set brightness (defaults to current display)");
ipc.registerHandler(
"raise-brightness",
[this, applyToTargets](const std::string& args) -> std::string {
const auto parts = noctalia::ipc::splitWords(args);
if (parts.empty() || parts.size() > 2) {
return "error: raise-brightness requires <current|all|display-id> [step]\n";
}
auto registerDeltaHandler = [this, &ipc, applyToTargets](const std::string& command, float direction,
std::string usage, std::string description) {
ipc.registerHandler(
command,
[this, applyToTargets, command, direction](const std::string& args) -> std::string {
const auto parts = noctalia::ipc::splitWords(args);
if (parts.size() > 2) {
return "error: " + command + " accepts at most [target] [step]\n";
}
const auto step = parts.size() == 2 ? noctalia::ipc::parseNormalizedOrPercent(parts[1])
: std::optional<float>(kDefaultBrightnessStep);
if (!step.has_value()) {
return "error: invalid brightness step (use percent like 5 or 5%, or normalized like 0.05)\n";
}
std::string target = "current";
std::optional<float> step = kDefaultBrightnessStep;
if (parts.size() == 1) {
const auto maybeStep = noctalia::ipc::parseNormalizedOrPercent(parts[0]);
if (maybeStep.has_value()) {
step = maybeStep;
} else {
target = parts[0];
}
} else if (parts.size() == 2) {
target = parts[0];
step = noctalia::ipc::parseNormalizedOrPercent(parts[1]);
}
return applyToTargets(parts[0], [this, step](const BrightnessDisplay& display) {
setBrightness(display.id, display.brightness + *step);
});
},
"raise-brightness <current|all|display-id> [step]", "Increase brightness for one or more displays");
if (!step.has_value()) {
return "error: invalid brightness step (use percent like 5 or 5%, or normalized like 0.05)\n";
}
ipc.registerHandler(
"lower-brightness",
[this, applyToTargets](const std::string& args) -> std::string {
const auto parts = noctalia::ipc::splitWords(args);
if (parts.empty() || parts.size() > 2) {
return "error: lower-brightness requires <current|all|display-id> [step]\n";
}
return applyToTargets(target, [this, step, direction](const BrightnessDisplay& display) {
setBrightness(display.id, display.brightness + direction * *step);
});
},
std::move(usage), std::move(description));
};
const auto step = parts.size() == 2 ? noctalia::ipc::parseNormalizedOrPercent(parts[1])
: std::optional<float>(kDefaultBrightnessStep);
if (!step.has_value()) {
return "error: invalid brightness step (use percent like 5 or 5%, or normalized like 0.05)\n";
}
return applyToTargets(parts[0], [this, step](const BrightnessDisplay& display) {
setBrightness(display.id, display.brightness - *step);
});
},
"lower-brightness <current|all|display-id> [step]", "Decrease brightness for one or more displays");
registerDeltaHandler("raise-brightness", 1.0f, "raise-brightness [current|*|all|monitor-selector] [step]",
"Increase brightness (defaults to current display)");
registerDeltaHandler("lower-brightness", -1.0f, "lower-brightness [current|*|all|monitor-selector] [step]",
"Decrease brightness (defaults to current display)");
}
void BrightnessService::setChangeCallback(ChangeCallback callback) { m_impl->changeCallback = std::move(callback); }