[plasma/kscreen] /: kscreenctl: New utility
Vlad Zahorodnii <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit c200b0f3ad1766507aa8050e83e3c75a99b65f43 by Vlad Zahorodnii.
Committed on 24/07/2026 at 06:49.
Pushed by vladz into branch 'master'.
kscreenctl: New utility
kscreenctl provides a more conventional command line interface in
comparison to kscreen-doctor, i.e.
kscreenctl commmand
or
kscreenctl OUTPUT command
For example,
kscreenctl identify
or
kscreenctl active-output calibrate-hdr
One of the goals was to make sure that the help message automatically
adjusts to new commands. kscreen-doctor gained a bunch of new commands
but they are poorly documented. kscreenctl solves it by storing all
commands and options in special sections that are inspected at runtime
to build help messages.
Unfortunately, shell completion still requires manual adjusting. There
are some ideas how to generate shell completion automatically, but it's
on backburner because it's all quite convoluted.
Shell completion is provided only for zsh and fish.
In order to add a new command, you will need to add a cpp file whose
name matches the command name. Then add an OUTPUT_COMMAND or a COMMAND
macro to register the function that will be called when the user types
that command. You may also need to update shell completion files.
The purpose of kscreenctl is to replace kscreen-doctor.
On the technical level, the most important bits are
`__attribute__((used, retain, section("COMMAND"), aligned(sizeof(void *))))`
and
extern const Command __start_COMMAND[];
extern const Command __stop_COMMAND[];
return std::span(__start_COMMAND, __stop_COMMAND);
The __attribute__(section("COMMAND")) is used to register supported
commands. The Command structures are stored in the COMMAND section,
which can be accessed using __start_COMMAND and __stop_COMMAND. We then
walk through all available commands to find the matching command for
the specified CLI arguments.
The main advantage of the __attribute__(section("COMMAND")) approach is
that it allows to add new commands with very little boilerplate code.
M +2 -1 CMakeLists.txt
A +102 -0 kscreenctl/CMakeLists.txt
A +156 -0 kscreenctl/command.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +278 -0 kscreenctl/command.h [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/identify.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +91 -0 kscreenctl/command/list-outputs.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +31 -0 kscreenctl/command/off.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +31 -0 kscreenctl/command/on.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +34 -0 kscreenctl/command/output/above.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +94 -0 kscreenctl/command/output/add-custom-mode.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +34 -0 kscreenctl/command/output/below.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +22 -0 kscreenctl/command/output/calibrate-hdr.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +48 -0 kscreenctl/command/output/cmd.h [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +19 -0 kscreenctl/command/output/get-name.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +34 -0 kscreenctl/command/output/left-of.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +27 -0 kscreenctl/command/output/list-custom-modes.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +18 -0 kscreenctl/command/output/list-modes.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +38 -0 kscreenctl/command/output/remove-custom-mode.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +34 -0 kscreenctl/command/output/right-of.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +35 -0 kscreenctl/command/output/self.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/output/set-abm.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +27 -0 kscreenctl/command/output/set-auto-brightness.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +39 -0 kscreenctl/command/output/set-auto-rotate.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +34 -0 kscreenctl/command/output/set-brightness.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +37 -0 kscreenctl/command/output/set-color-power-tradeoff.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +27 -0 kscreenctl/command/output/set-color-profile-source.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +27 -0 kscreenctl/command/output/set-ddc-ci-allowed.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/output/set-dimming.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +37 -0 kscreenctl/command/output/set-edr-policy.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +30 -0 kscreenctl/command/output/set-enabled.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +28 -0 kscreenctl/command/output/set-hdr-and-wcg.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/output/set-hdr-color-profile-source.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +21 -0 kscreenctl/command/output/set-hdr-icc-profile.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +27 -0 kscreenctl/command/output/set-hdr.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +24 -0 kscreenctl/command/output/set-icc-profile.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +37 -0 kscreenctl/command/output/set-max-average-brightness-override.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +37 -0 kscreenctl/command/output/set-max-peak-brightness-override.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +37 -0 kscreenctl/command/output/set-maxbpc.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +37 -0 kscreenctl/command/output/set-min-brightness-override.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +34 -0 kscreenctl/command/output/set-mode.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/output/set-overscan.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +33 -0 kscreenctl/command/output/set-position.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +15 -0 kscreenctl/command/output/set-primary.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/output/set-priority.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +37 -0 kscreenctl/command/output/set-replica-of.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +39 -0 kscreenctl/command/output/set-rgb-range.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +56 -0 kscreenctl/command/output/set-rotation.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +46 -0 kscreenctl/command/output/set-scale.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/output/set-sdr-brightness.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/output/set-sdr-gamut.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +32 -0 kscreenctl/command/output/set-sharpness.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +39 -0 kscreenctl/command/output/set-vrr-policy.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +27 -0 kscreenctl/command/output/set-wcg.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +15 -0 kscreenctl/command/output/toggle-auto-brightness.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +15 -0 kscreenctl/command/output/toggle-enabled.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +20 -0 kscreenctl/command/output/toggle-hdr-and-wcg.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +15 -0 kscreenctl/command/output/toggle-hdr.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +15 -0 kscreenctl/command/output/toggle-wcg.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +411 -0 kscreenctl/main.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +113 -0 kscreenctl/shell-completion/fish/kscreenctl.fish
A +109 -0 kscreenctl/shell-completion/zsh/_kscreenctl
A +841 -0 kscreenctl/util.cpp [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
A +69 -0 kscreenctl/util.h [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
https://invent.kde.org/plasma/kscreen/-/commit/c200b0f3ad1766507aa8050e83e3c75a99b65f43
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 02a51859..243079a0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,7 +11,7 @@ set(QT_MIN_VERSION "6.10.0")
set(KF6_MIN_VERSION "6.26.0")
set(KDE_COMPILERSETTINGS_LEVEL "5.82")
-set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE)
@@ -88,6 +88,7 @@ add_subdirectory(plasmoid)
add_subdirectory(tests)
add_subdirectory(console)
add_subdirectory(hdrcalibrator)
+add_subdirectory(kscreenctl)
# add clang-format target for all our real source files
file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h)
diff --git a/kscreenctl/CMakeLists.txt b/kscreenctl/CMakeLists.txt
new file mode 100644
index 00000000..cdc2df22
--- /dev/null
+++ b/kscreenctl/CMakeLists.txt
@@ -0,0 +1,102 @@
+# SPDX-FileCopyrightText: Vlad Zahorodnii <[email protected]>
+# SPDX-License-Identifier: BSD-3-Clause
+
+add_compile_options(-Wno-missing-designated-field-initializers)
+add_compile_options(-Wno-missing-field-initializers)
+
+function(add_verb target)
+ foreach(_cmd IN LISTS ARGN)
+ get_filename_component(_verb "${_cmd}" NAME_WLE)
+
+ get_filename_component(_object "${_cmd}" DIRECTORY)
+ if(_object STREQUAL "command")
+ set(_object "")
+ else()
+ string(REGEX REPLACE "^command/" "" _object "${_object}")
+ string(TOUPPER "${_object}" _object)
+ endif()
+
+ set(_compile_options "-DVERB_NAME=\\\"${_verb}\\\"")
+ if(_object)
+ set(_compile_options "${_compile_options} -DOBJECT_NAME=\\\"${_object}\\\"")
+ endif()
+
+ set_source_files_properties(${_cmd} PROPERTIES COMPILE_FLAGS "${_compile_options}")
+ target_sources(${target} PRIVATE ${_cmd})
+ endforeach()
+endfunction()
+
+add_executable(kscreenctl
+ command.cpp
+ main.cpp
+ util.cpp
+)
+
+add_verb(kscreenctl
+ command/identify.cpp
+ command/list-outputs.cpp
+ command/off.cpp
+ command/on.cpp
+ command/output/above.cpp
+ command/output/add-custom-mode.cpp
+ command/output/below.cpp
+ command/output/calibrate-hdr.cpp
+ command/output/get-name.cpp
+ command/output/left-of.cpp
+ command/output/list-custom-modes.cpp
+ command/output/list-modes.cpp
+ command/output/remove-custom-mode.cpp
+ command/output/right-of.cpp
+ command/output/self.cpp
+ command/output/set-abm.cpp
+ command/output/set-auto-brightness.cpp
+ command/output/set-auto-rotate.cpp
+ command/output/set-brightness.cpp
+ command/output/set-color-power-tradeoff.cpp
+ command/output/set-color-profile-source.cpp
+ command/output/set-ddc-ci-allowed.cpp
+ command/output/set-dimming.cpp
+ command/output/set-edr-policy.cpp
+ command/output/set-enabled.cpp
+ command/output/set-hdr-and-wcg.cpp
+ command/output/set-hdr-color-profile-source.cpp
+ command/output/set-hdr-icc-profile.cpp
+ command/output/set-hdr.cpp
+ command/output/set-icc-profile.cpp
+ command/output/set-max-average-brightness-override.cpp
+ command/output/set-max-peak-brightness-override.cpp
+ command/output/set-maxbpc.cpp
+ command/output/set-min-brightness-override.cpp
+ command/output/set-mode.cpp
+ command/output/set-overscan.cpp
+ command/output/set-position.cpp
+ command/output/set-primary.cpp
+ command/output/set-priority.cpp
+ command/output/set-replica-of.cpp
+ command/output/set-rgb-range.cpp
+ command/output/set-rotation.cpp
+ command/output/set-scale.cpp
+ command/output/set-sdr-brightness.cpp
+ command/output/set-sdr-gamut.cpp
+ command/output/set-sharpness.cpp
+ command/output/set-vrr-policy.cpp
+ command/output/set-wcg.cpp
+ command/output/toggle-auto-brightness.cpp
+ command/output/toggle-enabled.cpp
+ command/output/toggle-hdr-and-wcg.cpp
+ command/output/toggle-hdr.cpp
+ command/output/toggle-wcg.cpp
+)
+
+target_link_libraries(kscreenctl
+ PRIVATE
+ Qt6::Core
+ Qt6::DBus
+ KF6::Screen
+ KF6::ScreenDpms
+)
+
+install(TARGETS kscreenctl ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+
+install(FILES shell-completion/fish/kscreenctl.fish DESTINATION ${KDE_INSTALL_DATADIR}/fish/vendor_completions.d/)
+install(FILES shell-completion/zsh/_kscreenctl DESTINATION ${KDE_INSTALL_ZSHAUTOCOMPLETEDIR})
diff --git a/kscreenctl/command.cpp b/kscreenctl/command.cpp
new file mode 100644
index 00000000..38fef0b2
--- /dev/null
+++ b/kscreenctl/command.cpp
@@ -0,0 +1,156 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command.h"
+
+std::span<const Command> Command::all()
+{
+ extern const Command __start_COMMAND[];
+ extern const Command __stop_COMMAND[];
+ return std::span(__start_COMMAND, __stop_COMMAND);
+}
+
+std::span<const CommandExample> CommandExample::all()
+{
+ extern const CommandExample __start_COMMAND_EXAMPLE[];
+ extern const CommandExample __stop_COMMAND_EXAMPLE[];
+ return std::span(__start_COMMAND_EXAMPLE, __stop_COMMAND_EXAMPLE);
+}
+
+std::vector<const CommandExample *> CommandExample::all(const Command *command)
+{
+ std::vector<const CommandExample *> examples;
+
+ for (const CommandExample &example : all()) {
+ if (bool(example.object) != bool(command->object) ||
+ (command->object && strcmp(example.object, command->object) != 0)) {
+ continue;
+ }
+
+ if (strcmp(example.command, command->name) == 0) {
+ examples.push_back(&example);
+ }
+ }
+
+ return examples;
+}
+
+std::span<const CommandOption> CommandOption::all()
+{
+ extern const CommandOption __start_COMMAND_OPTION[];
+ extern const CommandOption __stop_COMMAND_OPTION[];
+ return std::span(__start_COMMAND_OPTION, __stop_COMMAND_OPTION);
+}
+
+std::vector<const CommandOption *> CommandOption::all(const Command *command)
+{
+ std::vector<const CommandOption *> options;
+
+ for (const CommandOption &option : all()) {
+ if (bool(option.object) != bool(command->object) ||
+ (command->object && strcmp(option.object, command->object) != 0)) {
+ continue;
+ }
+
+ if (strcmp(option.command, command->name) == 0) {
+ options.push_back(&option);
+ }
+ }
+
+ return options;
+}
+
+bool CommandOption::process(const Command *command, const QStringList &arguments)
+{
+ const auto options = CommandOption::all(command);
+ if (options.empty()) {
+ return arguments.isEmpty();
+ }
+
+ QStringList positionalArguments;
+
+ for (auto it = arguments.begin(); it != arguments.end(); ++it) {
+ if (!it->startsWith("--")) {
+ positionalArguments.append(*it);
+ continue;
+ }
+
+ const CommandOption *option = nullptr;
+
+ const QString optionName = it->mid(2);
+ for (const CommandOption *candidate : options) {
+ if (optionName == candidate->name) {
+ option = candidate;
+ break;
+ }
+ }
+
+ if (!option) {
+ std::println(std::cerr, "Unknown argument: {}", it->toStdString());
+ return false;
+ }
+
+ if (option->format) {
+ QString value;
+
+ const int equalsIndex = it->indexOf('=');
+ if (equalsIndex == -1) {
+ ++it;
+ if (it == arguments.end()) {
+ std::println(std::cerr, "Missing value for option {}.", option->name);
+ return false;
+ }
+
+ if (it->startsWith("--")) {
+ std::println(std::cerr, "Missing value for option {}.", option->name);
+ return false;
+ }
+
+ value = *it;
+ } else {
+ value = it->mid(equalsIndex + 1).trimmed();
+ if (value.isEmpty()) {
+ std::println(std::cerr, "Missing value for option {}.", option->name);
+ return false;
+ }
+ }
+
+ QString *context = static_cast<QString *>(option->variable);
+ *context = value;
+ } else {
+ bool *context = static_cast<bool *>(option->variable);
+ *context = true;
+ }
+ }
+
+ const CommandOption *positionalOption = nullptr;
+ for (const CommandOption *option : options) {
+ if (option->positional) {
+ positionalOption = option;
+ break;
+ }
+ }
+
+ if (positionalOption) {
+ const QByteArrayList parts = QByteArray(positionalOption->format).split(' ');
+ if (parts.size() != positionalArguments.size()) {
+ if (positionalArguments.size() < parts.size()) {
+ std::println(std::cerr, "Too few positional arguments. Expected: {}.", positionalOption->format);
+ } else {
+ std::println(std::cerr, "Too many positional arguments. Expected: {}.", positionalOption->format);
+ }
+ return false;
+ }
+
+ QStringList *context = static_cast<QStringList *>(positionalOption->variable);
+ *context = positionalArguments;
+ } else if (!positionalArguments.isEmpty()) {
+ std::println(std::cerr, "Unexpected positional argument: {}", positionalArguments[0].toStdString());
+ return false;
+ }
+
+ return true;
+}
diff --git a/kscreenctl/command.h b/kscreenctl/command.h
new file mode 100644
index 00000000..b32220f3
--- /dev/null
+++ b/kscreenctl/command.h
@@ -0,0 +1,278 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#pragma once
+
+#include <QStringList>
+
+#include <iostream>
+#include <print>
+#include <vector>
+
+using namespace Qt::StringLiterals;
+
+/**
+ * The Command type represents an operation that can be performed. There are two types of commands:
+ *
+ * - standalone commands, e.g. list-outputs;
+ * - and commands that have an object/output associated with them, which they operate on, for
+ * example, OUTPUT set-scale.
+ *
+ * All available commands are stored in the COMMAND section, which is then traversed when processing
+ * command line arguments or displaying a help message.
+ */
+struct Command
+{
+ /**
+ * List all available commands.
+ */
+ static std::span<const Command> all();
+
+ /**
+ * The associated object. If it's a standalone command, the object will be @c null.
+ */
+ const char *object;
+
+ /**
+ * The command name.
+ */
+ const char *name;
+
+ /**
+ * Short command description.
+ */
+ const char *summary;
+
+ /**
+ * Long command description.
+ */
+ const char *description;
+
+ /**
+ * Command group. It can be used to group similar commands in help output.
+ */
+ const char *topic;
+
+ int (*dispatch)();
+ int (*dispatchObject)(const QString &object);
+};
+
+#define COMMAND_ATTRIBUTE __attribute__((used, retain, section("COMMAND"), aligned(sizeof(void *))))
+
+#define COMMAND(dispatch_func, summary_text) \
+ COMMAND_ATTRIBUTE \
+ static const Command command = { \
+ .name = VERB_NAME, \
+ .summary = summary_text, \
+ .dispatch = dispatch_func, \
+ };
+
+#define COMMAND_WITH_DESCRIPTION(dispatch_func, summary_text, description_text) \
+ COMMAND_ATTRIBUTE \
+ static const Command command = { \
+ .name = VERB_NAME, \
+ .summary = summary_text, \
+ .description = description_text, \
+ .dispatch = dispatch_func, \
+ };
+
+#define OBJECT_COMMAND(dispatch_func, summary_text) \
+ COMMAND_ATTRIBUTE \
+ static const Command command = { \
+ .object = OBJECT_NAME, \
+ .name = VERB_NAME, \
+ .summary = summary_text, \
+ .dispatchObject = dispatch_func, \
+ };
+
+#define OBJECT_COMMAND_WITH_TOPIC(dispatch_func, summary_text, topic_text) \
+ COMMAND_ATTRIBUTE \
+ static const Command command = { \
+ .object = OBJECT_NAME, \
+ .name = VERB_NAME, \
+ .summary = summary_text, \
+ .topic = topic_text, \
+ .dispatchObject = dispatch_func, \
+ };
+
+#define OBJECT_COMMAND_WITH_TOPIC_AND_DESCRIPTION(dispatch_func, summary_text, description_text, topic_text) \
+ COMMAND_ATTRIBUTE \
+ static const Command command = { \
+ .object = OBJECT_NAME, \
+ .name = VERB_NAME, \
+ .summary = summary_text, \
+ .description = description_text, \
+ .topic = topic_text, \
+ .dispatchObject = dispatch_func, \
+ };
+
+/**
+ * The CommandExample type represents a usage example of a command.
+ */
+struct CommandExample
+{
+ /**
+ * List all available command examples.
+ */
+ static std::span<const CommandExample> all();
+
+ /**
+ * List command examples for the specified @a command.
+ */
+ static std::vector<const CommandExample *> all(const Command *command);
+
+ /**
+ * The object that the command is associated with. Can be null.
+ */
+ const char *object;
+
+ /**
+ * The command name.
+ */
+ const char *command;
+
+ /**
+ * Short example description.
+ */
+ const char *summary;
+
+ /**
+ * Example usage.
+ */
+ const char *example;
+};
+
+// The CONCATENATE_RAW helper exists to force macro expansion. Without it, ## will literally join
+// __COUNTER__ with the preceeding token, e.g. foo__COUNTER__ instead of foo0.
+#define CONCATENATE_RAW(x, y) x##y
+#define CONCATENATE(x, y) CONCATENATE_RAW(x, y)
+
+#define COMMAND_EXAMPLE_ATTRIBUTE __attribute__((used, retain, section("COMMAND_EXAMPLE"), aligned(sizeof(void *))))
+
+#define COMMAND_EXAMPLE(summary_text, example_text) \
+ COMMAND_EXAMPLE_ATTRIBUTE \
+ static const CommandExample CONCATENATE(commandExample_, __COUNTER__) = { \
+ .command = VERB_NAME, \
+ .summary = summary_text, \
+ .example = example_text, \
+ };
+
+#define OBJECT_COMMAND_EXAMPLE(summary_text, example_text) \
+ COMMAND_EXAMPLE_ATTRIBUTE \
+ static const CommandExample CONCATENATE(commandExample_, __COUNTER__) = { \
+ .object = OBJECT_NAME, \
+ .command = VERB_NAME, \
+ .summary = summary_text, \
+ .example = example_text, \
+ };
+
+/**
+ * The CommandOption type represents an option that a Command accepts. There are several command types:
+ *
+ * - simple toggle switches. They have the "--option" format;
+ * - value options. They have the "--option value" or "--option=value" format;
+ * - positional arguments: these are options that don't start with "--". For example, they can be
+ * used to specify the position in the "set-position X Y" command.
+ *
+ */
+struct CommandOption
+{
+ /**
+ * List all registered command options.
+ */
+ static std::span<const CommandOption> all();
+
+ /**
+ * List all options for the specified @a command.
+ */
+ static std::vector<const CommandOption *> all(const Command *command);
+
+ /**
+ * Process the command line @a arguments for the specified @a command. It returns @c true
+ * if the command line arguments have been processed successfully, otherwise returns @c false.
+ */
+ static bool process(const Command *command, const QStringList &arguments);
+
+ /**
+ * The object that the command is associated with it. Can be @c null.
+ */
+ const char *object;
+
+ /**
+ * The command name.
+ */
+ const char *command;
+
+ /**
+ * The option name.
+ */
+ const char *name;
+
+ /**
+ * The value format.
+ *
+ * For example, if there is a "set-position" that takes two values x and y, then the format will
+ * be "X Y". The format is used to display the help message and validate command line arguments.
+ *
+ * The format can be @c null for simple toggle options.
+ */
+ const char *format;
+
+ /**
+ * Short command option description.
+ */
+ const char *summary;
+
+ /**
+ * Where the value will be stored.
+ */
+ void *variable = nullptr;
+
+ /**
+ * Whether this is a positional option or a --regular option.
+ */
+ bool positional = false;
+};
+
+#define COMMAND_OPTION_ATTRIBUTE __attribute__((used, retain, section("COMMAND_OPTION"), aligned(sizeof(void *))))
+
+#define COMMAND_TOGGLE_OPTION(option_name, option_variable, option_summary) \
+ COMMAND_OPTION_ATTRIBUTE \
+ static const CommandOption CONCATENATE(commandOption_, __COUNTER__) = { \
+ .command = VERB_NAME, \
+ .name = option_name, \
+ .summary = option_summary, \
+ .variable = option_variable, \
+ };
+
+#define COMMAND_POSITIONAL_OPTION(option_format, option_variable) \
+ COMMAND_OPTION_ATTRIBUTE \
+ static const CommandOption CONCATENATE(commandOption_, __COUNTER__) = { \
+ .command = VERB_NAME, \
+ .format = option_format, \
+ .variable = option_variable, \
+ .positional = true, \
+ };
+
+#define OBJECT_COMMAND_TOGGLE_OPTION(option_name, option_variable, option_summary) \
+ COMMAND_OPTION_ATTRIBUTE \
+ static const CommandOption CONCATENATE(commandOption_, __COUNTER__) = { \
+ .object = OBJECT_NAME, \
+ .command = VERB_NAME, \
+ .name = option_name, \
+ .summary = option_summary, \
+ .variable = option_variable, \
+ };
+
+#define OBJECT_COMMAND_POSITIONAL_OPTION(option_format, option_variable) \
+ COMMAND_OPTION_ATTRIBUTE \
+ static const CommandOption CONCATENATE(commandOption_, __COUNTER__) = { \
+ .object = OBJECT_NAME, \
+ .command = VERB_NAME, \
+ .format = option_format, \
+ .variable = option_variable, \
+ .positional = true, \
+ };
diff --git a/kscreenctl/command/identify.cpp b/kscreenctl/command/identify.cpp
new file mode 100644
index 00000000..c69c02fb
--- /dev/null
+++ b/kscreenctl/command/identify.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command.h"
+
+#include <QDBusConnection>
+#include <QDBusMessage>
+#include <QDBusReply>
+
+static int run()
+{
+ const QDBusReply<void> reply = QDBusConnection::sessionBus().call(
+ QDBusMessage::createMethodCall(u"org.kde.KWin"_s,
+ u"/org/kde/KWin/Effect/OutputLocator1"_s,
+ u"org.kde.KWin.Effect.OutputLocator1"_s,
+ u"show"_s)
+ );
+
+ if (!reply.isValid()) {
+ std::println(std::cerr, "org.kde.KWin.Effect.OutputLocator1.show failed: {}", reply.error().message().toStdString());
+ return 1;
+ }
+
+ return 0;
+}
+
+COMMAND_WITH_DESCRIPTION(run,
+ "Identify available outputs",
+ "This command will show a popup on every screen with detailed output information. It can be used to determine the connector names to be used with other commands.")
diff --git a/kscreenctl/command/list-outputs.cpp b/kscreenctl/command/list-outputs.cpp
new file mode 100644
index 00000000..00d2a3ac
--- /dev/null
+++ b/kscreenctl/command/list-outputs.cpp
@@ -0,0 +1,91 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command.h"
+#include "util.h"
+
+#include <KScreen/Config>
+#include <KScreen/GetConfigOperation>
+
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+static bool enabled = false;
+COMMAND_TOGGLE_OPTION("enabled",
+ &enabled,
+ "Only list enabled outputs")
+
+static bool disabled = false;
+COMMAND_TOGGLE_OPTION("disabled",
+ &disabled,
+ "Only list disabled outputs")
+
+static bool names = false;
+COMMAND_TOGGLE_OPTION("names",
+ &names,
+ "Only display output names")
+
+static bool json = false;
+COMMAND_TOGGLE_OPTION("json",
+ &json,
+ "Display output information as JSON")
+
+static int run()
+{
+ auto getConfigOperation = new KScreen::GetConfigOperation();
+ getConfigOperation->exec();
+
+ KScreen::ConfigPtr config = getConfigOperation->config();
+ if (!config) {
+ return 1;
+ }
+
+ auto outputs = config->connectedOutputs();
+ if (enabled) {
+ outputs.removeIf([](const auto &output) {
+ return !(*output)->isEnabled();
+ });
+ } else if (disabled) {
+ outputs.removeIf([](const auto &output) {
+ return (*output)->isEnabled();
+ });
+ }
+
+ if (names) {
+ if (json) {
+ QJsonArray array;
+ for (const auto &output : outputs) {
+ array.append(output->name());
+ }
+ std::println(std::cout, "{}", QJsonDocument(array).toJson().toStdString());
+ } else {
+ for (const auto &output : outputs) {
+ std::println(std::cout, "{}", output->name().toStdString());
+ }
+ }
+ } else {
+ if (json) {
+ QJsonArray array;
+ for (const auto &output : outputs) {
+ array.append(outputAsJson(output));
+ }
+ std::println(std::cout, "{}", QJsonDocument(array).toJson().toStdString());
+ } else {
+ for (const auto &output : outputs) {
+ showOutputAsText(output);
+ }
+ }
+ }
+
+ return 0;
+}
+
+COMMAND(run, "List all connected outputs")
+
+COMMAND_EXAMPLE("List all available outputs", "")
+
+COMMAND_EXAMPLE("List all enabled outputs", "--enabled")
diff --git a/kscreenctl/command/off.cpp b/kscreenctl/command/off.cpp
new file mode 100644
index 00000000..efe9fafc
--- /dev/null
+++ b/kscreenctl/command/off.cpp
@@ -0,0 +1,31 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command.h"
+
+#include <KScreenDpms/Dpms>
+
+#include <QGuiApplication>
+#include <QTimer>
+
+static int run()
+{
+ auto dpms = new KScreen::Dpms();
+ QObject::connect(dpms, &KScreen::Dpms::hasPendingChangesChanged, qGuiApp, [](bool hasChanges) {
+ if (!hasChanges) {
+ // We need to hit the event loop, otherwise .quit() hangs
+ QTimer::singleShot(0, qApp->quit);
+ }
+ });
+
+ dpms->switchMode(KScreen::Dpms::Off, qGuiApp->screens());
+
+ return qGuiApp->exec();
+}
+
+COMMAND(run, "Turn all outputs off")
+
+COMMAND_EXAMPLE("Turn all outputs off", "")
diff --git a/kscreenctl/command/on.cpp b/kscreenctl/command/on.cpp
new file mode 100644
index 00000000..aef6440b
--- /dev/null
+++ b/kscreenctl/command/on.cpp
@@ -0,0 +1,31 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command.h"
+
+#include <KScreenDpms/Dpms>
+
+#include <QGuiApplication>
+#include <QTimer>
+
+static int run()
+{
+ auto dpms = new KScreen::Dpms();
+ QObject::connect(dpms, &KScreen::Dpms::hasPendingChangesChanged, qGuiApp, [](bool hasChanges) {
+ if (!hasChanges) {
+ // We need to hit the event loop, otherwise .quit() hangs
+ QTimer::singleShot(0, qApp->quit);
+ }
+ });
+
+ dpms->switchMode(KScreen::Dpms::On, qGuiApp->screens());
+
+ return qGuiApp->exec();
+}
+
+COMMAND(run, "Turn all outputs on")
+
+COMMAND_EXAMPLE("Turn all outputs on", "")
diff --git a/kscreenctl/command/output/above.cpp b/kscreenctl/command/output/above.cpp
new file mode 100644
index 00000000..4ee5d3af
--- /dev/null
+++ b/kscreenctl/command/output/above.cpp
@@ -0,0 +1,34 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "OUTPUT",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const KScreen::OutputPtr other = outputByQuery(config, arguments[0]);
+ if (!other) {
+ std::println(std::cerr, "Unknown output: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (output->id() == other->id()) {
+ std::println(std::cerr, "The reference output cannot be the same as the moved output");
+ return 1;
+ }
+
+ output->setPos(QPoint(other->pos().x(), other->pos().y() - output->explicitLogicalSizeInt().height()));
+ fixupOutputPositions(config);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Place an output above another output", "Positioning")
diff --git a/kscreenctl/command/output/add-custom-mode.cpp b/kscreenctl/command/output/add-custom-mode.cpp
new file mode 100644
index 00000000..3d145a27
--- /dev/null
+++ b/kscreenctl/command/output/add-custom-mode.cpp
@@ -0,0 +1,94 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static bool reducedBlanking = false;
+OUTPUT_COMMAND_TOGGLE_OPTION(
+ "reduced-blanking",
+ &reducedBlanking,
+ "Add a custom mode with reduced blanking"
+)
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "MODE",
+ &arguments
+)
+
+static std::optional<KScreen::ModeInfo> tryBasicModeline()
+{
+ if (arguments.isEmpty()) {
+ return std::nullopt;
+ }
+
+ const QString modeline = arguments[0];
+
+ const int xIndex = modeline.indexOf('x');
+ if (xIndex == -1) {
+ return std::nullopt;
+ }
+
+ const int atIndex = modeline.indexOf('@', xIndex);
+ if (atIndex == -1) {
+ return std::nullopt;
+ }
+
+ const std::optional<int> width = parseInt(modeline.left(xIndex));
+ if (!width) {
+ return std::nullopt;
+ }
+
+ const std::optional<int> height = parseInt(modeline.mid(xIndex + 1, atIndex - xIndex - 1));
+ if (!height) {
+ return std::nullopt;
+ }
+
+ QString refreshRatePart = modeline.mid(atIndex + 1);
+ if (refreshRatePart.endsWith("Hz")) {
+ refreshRatePart.chop(2);
+ }
+
+ const std::optional<double> refreshRate = parseDouble(refreshRatePart);
+ if (!refreshRate) {
+ return std::nullopt;
+ }
+
+ KScreen::ModeInfo::Flags flags = KScreen::ModeInfo::Flag::Custom;
+ if (reducedBlanking) {
+ flags |= KScreen::ModeInfo::Flag::ReducedBlanking;
+ }
+
+ return KScreen::ModeInfo {
+ .size = QSize(*width, *height),
+ .refreshRate = float(*refreshRate),
+ .flags = flags,
+ };
+}
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto modeInfo = tryBasicModeline();
+ if (!modeInfo) {
+ std::println(std::cerr, "Invalid custom mode: {}", arguments.join(' ').toStdString());
+ return 1;
+ }
+
+ auto modes = output->customModes();
+ if (modes.contains(*modeInfo)) {
+ return 0;
+ }
+
+ modes.append(*modeInfo);
+ output->setCustomModes(modes);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Add a custom mode", "Modes")
+
+OUTPUT_COMMAND_EXAMPLE("Add a custom mode with reduced blanking",
+ "active-output add-custom-mode --reduced-blanking [email protected]")
diff --git a/kscreenctl/command/output/below.cpp b/kscreenctl/command/output/below.cpp
new file mode 100644
index 00000000..9a2aeef0
--- /dev/null
+++ b/kscreenctl/command/output/below.cpp
@@ -0,0 +1,34 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "OUTPUT",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const KScreen::OutputPtr other = outputByQuery(config, arguments[0]);
+ if (!other) {
+ std::println(std::cerr, "Unknown output: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (output->id() == other->id()) {
+ std::println(std::cerr, "The reference output cannot be the same as the moved output");
+ return 1;
+ }
+
+ output->setPos(QPoint(other->pos().x(), other->pos().y() + other->explicitLogicalSizeInt().height()));
+ fixupOutputPositions(config);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Place an output below another output", "Positioning")
diff --git a/kscreenctl/command/output/calibrate-hdr.cpp b/kscreenctl/command/output/calibrate-hdr.cpp
new file mode 100644
index 00000000..f5753613
--- /dev/null
+++ b/kscreenctl/command/output/calibrate-hdr.cpp
@@ -0,0 +1,22 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+#include <QProcess>
+
+static int run(KScreen::OutputPtr output)
+{
+ if (!output->isHdrEnabled()) {
+ std::println(std::cerr, "HDR is not active on output {}", output->name().toStdString());
+ return 1;
+ }
+
+ const bool ok = QProcess::startDetached(u"hdrcalibrator"_s, {output->name()});
+ return ok ? 0 : 1;
+}
+
+OUTPUT_COMMAND(run, "Calibrate HDR", "Color Management")
diff --git a/kscreenctl/command/output/cmd.h b/kscreenctl/command/output/cmd.h
new file mode 100644
index 00000000..8d3c629b
--- /dev/null
+++ b/kscreenctl/command/output/cmd.h
@@ -0,0 +1,48 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#pragma once
+
+#include "command.h"
+#include "util.h"
+
+#include <KScreen/Config>
+#include <KScreen/GetConfigOperation>
+#include <KScreen/Mode>
+
+#define OUTPUT_TRAMPOLINE(func) \
+ [](const QString &object) -> int { \
+ auto getConfigOperation = new KScreen::GetConfigOperation(); \
+ getConfigOperation->exec(); \
+ \
+ KScreen::ConfigPtr config = getConfigOperation->config(); \
+ if (!config) { \
+ return 1; \
+ } \
+ \
+ const KScreen::OutputPtr output = outputByQuery(getConfigOperation->config(), object); \
+ if (!output) { \
+ std::println(std::cerr, "Unknown output: {}", object.toStdString()); \
+ return 1; \
+ } \
+ \
+ return wrap(func, output, config); \
+ }
+
+#define OUTPUT_COMMAND(dispatch_func, summary_text, topic_text) \
+ OBJECT_COMMAND_WITH_TOPIC(OUTPUT_TRAMPOLINE(dispatch_func), summary_text, topic_text)
+
+#define OUTPUT_COMMAND_WITH_DESCRIPTION(dispatch_func, summary_text, description_text, topic_text) \
+ OBJECT_COMMAND_WITH_TOPIC_AND_DESCRIPTION(OUTPUT_TRAMPOLINE(dispatch_func), summary_text, description_text, topic_text)
+
+#define OUTPUT_COMMAND_EXAMPLE(summary_text, example_text) \
+ OBJECT_COMMAND_EXAMPLE(summary_text, example_text)
+
+#define OUTPUT_COMMAND_TOGGLE_OPTION(option_name, option_variable, option_summary) \
+ OBJECT_COMMAND_TOGGLE_OPTION(option_name, option_variable, option_summary)
+
+#define OUTPUT_COMMAND_POSITIONAL_OPTION(option_format, option_variable) \
+ OBJECT_COMMAND_POSITIONAL_OPTION(option_format, option_variable)
diff --git a/kscreenctl/command/output/get-name.cpp b/kscreenctl/command/output/get-name.cpp
new file mode 100644
index 00000000..6f82f715
--- /dev/null
+++ b/kscreenctl/command/output/get-name.cpp
@@ -0,0 +1,19 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output)
+{
+ std::println(std::cout, "{}", output->name().toStdString());
+
+ return 0;
+}
+
+OUTPUT_COMMAND(run, "Get connector name", "Basic")
+
+OUTPUT_COMMAND_EXAMPLE("Get the connector name of the active output",
+ "active-output get-name")
diff --git a/kscreenctl/command/output/left-of.cpp b/kscreenctl/command/output/left-of.cpp
new file mode 100644
index 00000000..167b24c8
--- /dev/null
+++ b/kscreenctl/command/output/left-of.cpp
@@ -0,0 +1,34 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "OUTPUT",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const KScreen::OutputPtr other = outputByQuery(config, arguments[0]);
+ if (!other) {
+ std::println(std::cerr, "Unknown output: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (output->id() == other->id()) {
+ std::println(std::cerr, "The reference output cannot be the same as the moved output");
+ return 1;
+ }
+
+ output->setPos(QPoint(other->pos().x() - output->explicitLogicalSizeInt().width(), other->pos().y()));
+ fixupOutputPositions(config);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Place this output to the left of another", "Positioning")
diff --git a/kscreenctl/command/output/list-custom-modes.cpp b/kscreenctl/command/output/list-custom-modes.cpp
new file mode 100644
index 00000000..fe5f0ddd
--- /dev/null
+++ b/kscreenctl/command/output/list-custom-modes.cpp
@@ -0,0 +1,27 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output)
+{
+ const auto customModes = output->customModes();
+ for (int i = 0; i < customModes.size(); ++i) {
+ const auto &mode = customModes[i];
+ std::print(std::cerr, "{}: {}x{}@{:.2f}Hz", i, mode.size.width(), mode.size.height(), mode.refreshRate);
+ if (mode.flags & KScreen::ModeInfo::Flag::ReducedBlanking) {
+ std::print(std::cerr, " +reduced-blanking");
+ }
+ std::print(std::cerr, "\n");
+ }
+
+ return 0;
+}
+
+OUTPUT_COMMAND(run, "List custom output modes", "Modes")
+
+OUTPUT_COMMAND_EXAMPLE("List custom modes",
+ "active-output list-custom-modes")
diff --git a/kscreenctl/command/output/list-modes.cpp b/kscreenctl/command/output/list-modes.cpp
new file mode 100644
index 00000000..8deb6588
--- /dev/null
+++ b/kscreenctl/command/output/list-modes.cpp
@@ -0,0 +1,18 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output)
+{
+ for (const auto modes = output->modes(); const auto &mode : modes) {
+ std::println(std::cout, "{}", printableMode(output, mode).toStdString());
+ }
+
+ return 0;
+}
+
+OUTPUT_COMMAND(run, "List available output modes", "Modes")
diff --git a/kscreenctl/command/output/remove-custom-mode.cpp b/kscreenctl/command/output/remove-custom-mode.cpp
new file mode 100644
index 00000000..179a8cd4
--- /dev/null
+++ b/kscreenctl/command/output/remove-custom-mode.cpp
@@ -0,0 +1,38 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "MODE",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto index = parseInt(arguments[0]);
+ if (!index) {
+ std::println(std::cerr, "Invalid mode index: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ auto modes = output->customModes();
+ if (index < 0 || index >= modes.size()) {
+ std::println(std::cerr, "Invalid mode index: {}", *index);
+ return 1;
+ }
+
+ modes.removeAt(*index);
+ output->setCustomModes(modes);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Remove a custom mode. MODE is the index of the mode in list-custom-modes", "Modes")
+
+OUTPUT_COMMAND_EXAMPLE("Remove the first custom mode. Note that the command takes the index of the custom mode in `list-custom-modes`",
+ "active-output remove-custom-mode 0")
diff --git a/kscreenctl/command/output/right-of.cpp b/kscreenctl/command/output/right-of.cpp
new file mode 100644
index 00000000..4ca1c95c
--- /dev/null
+++ b/kscreenctl/command/output/right-of.cpp
@@ -0,0 +1,34 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "OUTPUT",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const KScreen::OutputPtr other = outputByQuery(config, arguments[0]);
+ if (!other) {
+ std::println(std::cerr, "Unknown output: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (output->id() == other->id()) {
+ std::println(std::cerr, "The reference output cannot be the same as the moved output");
+ return 1;
+ }
+
+ output->setPos(QPoint(other->pos().x() + other->explicitLogicalSizeInt().width(), other->pos().y()));
+ fixupOutputPositions(config);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Place this output to the right of another output", "Positioning")
diff --git a/kscreenctl/command/output/self.cpp b/kscreenctl/command/output/self.cpp
new file mode 100644
index 00000000..e38a5906
--- /dev/null
+++ b/kscreenctl/command/output/self.cpp
@@ -0,0 +1,35 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static bool json = false;
+OUTPUT_COMMAND_TOGGLE_OPTION("json",
+ &json,
+ "Show output information as JSON")
+
+static int run(KScreen::OutputPtr output)
+{
+ if (json) {
+ showOutputAsJson(output);
+ } else {
+ showOutputAsText(output);
+ }
+
+ return 0;
+}
+
+OUTPUT_COMMAND_WITH_DESCRIPTION(
+ run,
+ "Query or change output settings",
+ "The OUTPUT specifies the connector name, e.g. DP-1, or one of the special identifiers - \"active-output\" or \"primary-output\".",
+ "Basic")
+
+OUTPUT_COMMAND_EXAMPLE("Show information about the current output",
+ "active-output")
+
+OUTPUT_COMMAND_EXAMPLE("Show information about the current output as JSON",
+ "active-output --json")
diff --git a/kscreenctl/command/output/set-abm.cpp b/kscreenctl/command/output/set-abm.cpp
new file mode 100644
index 00000000..91d879eb
--- /dev/null
+++ b/kscreenctl/command/output/set-abm.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "LEVEL",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto level = parseInt(arguments[0]);
+ if (!level) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (level < 0 || level > 4) {
+ std::println(std::cerr, "Allowed values for abm level are 0, 1, 2, 3, 4");
+ return 1;
+ }
+
+ output->setAbmLevel(*level);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Change the adaptive backlight management level (possible values: 0-4)", "Misc")
diff --git a/kscreenctl/command/output/set-auto-brightness.cpp b/kscreenctl/command/output/set-auto-brightness.cpp
new file mode 100644
index 00000000..ea91f4b1
--- /dev/null
+++ b/kscreenctl/command/output/set-auto-brightness.cpp
@@ -0,0 +1,27 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BOOL",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto enabled = parseBool(arguments[0]);
+ if (!enabled.has_value()) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setAutomaticBrightness(*enabled);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Turn on or turn off automatic brightness adjustment", "Brightness")
diff --git a/kscreenctl/command/output/set-auto-rotate.cpp b/kscreenctl/command/output/set-auto-rotate.cpp
new file mode 100644
index 00000000..0385b4b9
--- /dev/null
+++ b/kscreenctl/command/output/set-auto-rotate.cpp
@@ -0,0 +1,39 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "POLICY",
+ &arguments
+)
+
+static std::optional<KScreen::Output::AutoRotatePolicy> parseAutoRotatePolicy(const QString &input)
+{
+ if (input == "never") {
+ return KScreen::Output::AutoRotatePolicy::Never;
+ } else if (input == "in-tablet-mode") {
+ return KScreen::Output::AutoRotatePolicy::InTabletMode;
+ } else if (input == "always") {
+ return KScreen::Output::AutoRotatePolicy::Always;
+ }
+ return std::nullopt;
+}
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto policy = parseAutoRotatePolicy(arguments[0]);
+ if (!policy) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setAutoRotatePolicy(*policy);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Turn on or turn off auto rotation", "Misc")
diff --git a/kscreenctl/command/output/set-brightness.cpp b/kscreenctl/command/output/set-brightness.cpp
new file mode 100644
index 00000000..158b6356
--- /dev/null
+++ b/kscreenctl/command/output/set-brightness.cpp
@@ -0,0 +1,34 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BRIGHTNESS",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto brightness = parsePercents(arguments[0]);
+ if (!brightness) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (brightness < 0 || brightness > 1) {
+ std::println(std::cerr, "Allowed range for brightness is 0% to 100%");
+ return 1;
+ }
+
+ output->setBrightness(*brightness);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set screen brightness (possible values: 0%-100%)", "Brightness")
+
+OUTPUT_COMMAND_EXAMPLE("Change the brightness of the current output to 50%", "active-output set-brightness 50%")
diff --git a/kscreenctl/command/output/set-color-power-tradeoff.cpp b/kscreenctl/command/output/set-color-power-tradeoff.cpp
new file mode 100644
index 00000000..1d819ee8
--- /dev/null
+++ b/kscreenctl/command/output/set-color-power-tradeoff.cpp
@@ -0,0 +1,37 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "TRADEOFF",
+ &arguments
+)
+
+static std::optional<KScreen::Output::ColorPowerTradeoff> parseColorPowerTradeoff(const QString &input)
+{
+ if (input == "prefer-accuracy") {
+ return KScreen::Output::ColorPowerTradeoff::PreferAccuracy;
+ } else if (input == "prefer-efficiency") {
+ return KScreen::Output::ColorPowerTradeoff::PreferEfficiency;
+ }
+ return std::nullopt;
+}
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto tradeoff = parseColorPowerTradeoff(arguments[0]);
+ if (!tradeoff) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setColorPowerPreference(*tradeoff);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set color power tradeoff (prefer-accuracy or prefer-efficiency)", "Color Management")
diff --git a/kscreenctl/command/output/set-color-profile-source.cpp b/kscreenctl/command/output/set-color-profile-source.cpp
new file mode 100644
index 00000000..29d52356
--- /dev/null
+++ b/kscreenctl/command/output/set-color-profile-source.cpp
@@ -0,0 +1,27 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "SOURCE",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto source = parseColorProfileSource(arguments[0]);
+ if (!source) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setColorProfileSource(*source);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set color profile source (possible values: EDID, ICC, or sRGB)", "Color Management")
diff --git a/kscreenctl/command/output/set-ddc-ci-allowed.cpp b/kscreenctl/command/output/set-ddc-ci-allowed.cpp
new file mode 100644
index 00000000..39b5ad9f
--- /dev/null
+++ b/kscreenctl/command/output/set-ddc-ci-allowed.cpp
@@ -0,0 +1,27 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BOOL",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto allowed = parseBool(arguments[0]);
+ if (!allowed.has_value()) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setDdcCiAllowed(*allowed);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Allow or disallow DDCI CI to be used to control the output", "Misc")
diff --git a/kscreenctl/command/output/set-dimming.cpp b/kscreenctl/command/output/set-dimming.cpp
new file mode 100644
index 00000000..eb36482f
--- /dev/null
+++ b/kscreenctl/command/output/set-dimming.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "DIMMING",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto dimming = parsePercents(arguments[0]);
+ if (!dimming) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (dimming < 0 || dimming > 1) {
+ std::println(std::cerr, "Allowed range for dimming is 0% to 100%");
+ return 1;
+ }
+
+ output->setDimming(*dimming);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the dimming level (possible values: 0%-100%)", "Brightness")
diff --git a/kscreenctl/command/output/set-edr-policy.cpp b/kscreenctl/command/output/set-edr-policy.cpp
new file mode 100644
index 00000000..2ca71d37
--- /dev/null
+++ b/kscreenctl/command/output/set-edr-policy.cpp
@@ -0,0 +1,37 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "POLICY",
+ &arguments
+)
+
+static std::optional<KScreen::Output::EdrPolicy> parseEdrPolicy(const QString &input)
+{
+ if (input == "never") {
+ return KScreen::Output::EdrPolicy::Never;
+ } else if (input == "always") {
+ return KScreen::Output::EdrPolicy::Always;
+ }
+ return std::nullopt;
+}
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto policy = parseEdrPolicy(arguments[0]);
+ if (!policy) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setEdrPolicy(*policy);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the extended dynamic range policy (possible values: never or always)", "Color Management")
diff --git a/kscreenctl/command/output/set-enabled.cpp b/kscreenctl/command/output/set-enabled.cpp
new file mode 100644
index 00000000..25560340
--- /dev/null
+++ b/kscreenctl/command/output/set-enabled.cpp
@@ -0,0 +1,30 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BOOL",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto enabled = parseBool(arguments[0]);
+ if (!enabled.has_value()) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setEnabled(*enabled);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Enable or disable the output", "Basic")
+
+OUTPUT_COMMAND_EXAMPLE("Disable the current output",
+ "active-output set-enabled false")
diff --git a/kscreenctl/command/output/set-hdr-and-wcg.cpp b/kscreenctl/command/output/set-hdr-and-wcg.cpp
new file mode 100644
index 00000000..08715bb4
--- /dev/null
+++ b/kscreenctl/command/output/set-hdr-and-wcg.cpp
@@ -0,0 +1,28 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BOOL",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto enabled = parseBool(arguments[0]);
+ if (!enabled.has_value()) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setHdrEnabled(*enabled);
+ output->setWcgEnabled(*enabled);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Turn on or turn off both HDR and wide color gamut", "Color Management")
diff --git a/kscreenctl/command/output/set-hdr-color-profile-source.cpp b/kscreenctl/command/output/set-hdr-color-profile-source.cpp
new file mode 100644
index 00000000..61e7b05b
--- /dev/null
+++ b/kscreenctl/command/output/set-hdr-color-profile-source.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "SOURCE",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto source = parseColorProfileSource(arguments[0]);
+ if (!source) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (source == KScreen::Output::ColorProfileSource::sRGB) {
+ std::println(std::cerr, "sRGB color profile source with the HDR mode is unsupported");
+ return 1;
+ }
+
+ output->setColorProfileSource(*source);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set color profile source (possible values: EDID, or ICC)", "Color Management")
diff --git a/kscreenctl/command/output/set-hdr-icc-profile.cpp b/kscreenctl/command/output/set-hdr-icc-profile.cpp
new file mode 100644
index 00000000..84f589a5
--- /dev/null
+++ b/kscreenctl/command/output/set-hdr-icc-profile.cpp
@@ -0,0 +1,21 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "PATH",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ output->setHdrIccProfilePath(arguments[0]);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the ICC profile path for the HDR mode", "Color Management")
diff --git a/kscreenctl/command/output/set-hdr.cpp b/kscreenctl/command/output/set-hdr.cpp
new file mode 100644
index 00000000..897cebce
--- /dev/null
+++ b/kscreenctl/command/output/set-hdr.cpp
@@ -0,0 +1,27 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BOOL",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto hdrEnabled = parseBool(arguments[0]);
+ if (!hdrEnabled.has_value()) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setHdrEnabled(*hdrEnabled);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Turn on or turn off the HDR mode", "Color Management")
diff --git a/kscreenctl/command/output/set-icc-profile.cpp b/kscreenctl/command/output/set-icc-profile.cpp
new file mode 100644
index 00000000..7c6b3f2e
--- /dev/null
+++ b/kscreenctl/command/output/set-icc-profile.cpp
@@ -0,0 +1,24 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "PATH",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ output->setIccProfilePath(arguments[0]);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the ICC profile path", "Color Management")
+
+OUTPUT_COMMAND_EXAMPLE("Assign an ICC profile to the current output. The profile will only be used in the SDR mode. For HDR, you want to use the `set-hdr-icc-profile` command",
+ "active-output set-icc-profile ~/path/to/profile.icc")
diff --git a/kscreenctl/command/output/set-max-average-brightness-override.cpp b/kscreenctl/command/output/set-max-average-brightness-override.cpp
new file mode 100644
index 00000000..c9d76997
--- /dev/null
+++ b/kscreenctl/command/output/set-max-average-brightness-override.cpp
@@ -0,0 +1,37 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BRIGHTNESS",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ if (arguments[0] == "none") {
+ output->setMaxAverageBrightnessOverride(std::nullopt);
+ } else {
+ const auto brightness = parseInt(arguments[0]);
+ if (!brightness) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (brightness < 0 || brightness > 10000) {
+ std::println(std::cerr, "Allowed range for priority is 0 to 10000");
+ return 1;
+ }
+
+ output->setMaxAverageBrightnessOverride(*brightness / 10000.0);
+ }
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Override the max average brightness (possible values: none or 0-10000)", "Color Management")
diff --git a/kscreenctl/command/output/set-max-peak-brightness-override.cpp b/kscreenctl/command/output/set-max-peak-brightness-override.cpp
new file mode 100644
index 00000000..e5fbc30d
--- /dev/null
+++ b/kscreenctl/command/output/set-max-peak-brightness-override.cpp
@@ -0,0 +1,37 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BRIGHTNESS",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ if (arguments[0] == "none") {
+ output->setMaxPeakBrightnessOverride(std::nullopt);
+ } else {
+ const auto brightness = parseInt(arguments[0]);
+ if (!brightness) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (brightness < 0 || brightness > 10000) {
+ std::println(std::cerr, "Allowed range for priority is 0 to 10000");
+ return 1;
+ }
+
+ output->setMaxPeakBrightnessOverride(*brightness);
+ }
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Override the max peak brightness (possible values: none or 0-10000)", "Color Management")
diff --git a/kscreenctl/command/output/set-maxbpc.cpp b/kscreenctl/command/output/set-maxbpc.cpp
new file mode 100644
index 00000000..4fba3c98
--- /dev/null
+++ b/kscreenctl/command/output/set-maxbpc.cpp
@@ -0,0 +1,37 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BPC",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ if (arguments[0] == "automatic") {
+ output->setMaxBitsPerColor(0);
+ } else {
+ const auto maxbpc = parseInt(arguments[0]);
+ if (!maxbpc) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (maxbpc < 6 || maxbpc > 16 || *maxbpc % 2 != 0) {
+ std::println(std::cerr, "Only whole numbers between 6 and 16 are allowed");
+ return 1;
+ }
+
+ output->setMaxBitsPerColor(*maxbpc);
+ }
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the maximum bits per color (possible values: 6, 8, 10, 12, 14, and 16)", "Misc")
diff --git a/kscreenctl/command/output/set-min-brightness-override.cpp b/kscreenctl/command/output/set-min-brightness-override.cpp
new file mode 100644
index 00000000..6c75e7b3
--- /dev/null
+++ b/kscreenctl/command/output/set-min-brightness-override.cpp
@@ -0,0 +1,37 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BRIGHTNESS",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ if (arguments[0] == "none") {
+ output->setMinBrightnessOverride(std::nullopt);
+ } else {
+ const auto brightness = parseInt(arguments[0]);
+ if (!brightness) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (brightness < 0 || brightness > 10000) {
+ std::println(std::cerr, "Allowed range for priority is 0 to 10000");
+ return 1;
+ }
+
+ output->setMinBrightnessOverride(*brightness);
+ }
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Override the minimum brightness (possible values: none or 0-10000)", "Color Management")
diff --git a/kscreenctl/command/output/set-mode.cpp b/kscreenctl/command/output/set-mode.cpp
new file mode 100644
index 00000000..3698bc77
--- /dev/null
+++ b/kscreenctl/command/output/set-mode.cpp
@@ -0,0 +1,34 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "MODE",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto mode = modeByQuery(output, arguments[0]);
+ if (!mode) {
+ std::println(std::cerr, "Unknown mode: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ const QSize previousSize = output->explicitLogicalSizeInt();
+ output->setCurrentModeId(mode->id());
+ output->setSize(mode->size());
+ output->setExplicitLogicalSize(config->logicalSizeForOutputInt(*output));
+ snapOutputAfterResize(output, previousSize, config);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the current mode", "Modes")
+
+OUTPUT_COMMAND_EXAMPLE("Set the 1920x1080@60 mode as the current mode. Note that you can also address modes by their IDs in `list-modes` or without the refresh rate, e.g. 1920x1080", "DP-1 set-mode 1920x1080@60")
diff --git a/kscreenctl/command/output/set-overscan.cpp b/kscreenctl/command/output/set-overscan.cpp
new file mode 100644
index 00000000..3011d0b0
--- /dev/null
+++ b/kscreenctl/command/output/set-overscan.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "OVERSCAN",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto overscan = parseInt(arguments[0]);
+ if (!overscan) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (overscan < 0 || overscan > 100) {
+ std::println(std::cerr, "Allowed range for overscan is 0 to 100");
+ return 1;
+ }
+
+ output->setOverscan(*overscan);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the overscan (possible values: 0-100)", "Misc")
diff --git a/kscreenctl/command/output/set-position.cpp b/kscreenctl/command/output/set-position.cpp
new file mode 100644
index 00000000..fcef1be7
--- /dev/null
+++ b/kscreenctl/command/output/set-position.cpp
@@ -0,0 +1,33 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "X Y",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto x = parseDouble(arguments[0]);
+ if (!x) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ const auto y = parseDouble(arguments[1]);
+ if (!y) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[1].toStdString());
+ return 1;
+ }
+
+ output->setPos(QPoint(*x, *y));
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the position", "Positioning")
diff --git a/kscreenctl/command/output/set-primary.cpp b/kscreenctl/command/output/set-primary.cpp
new file mode 100644
index 00000000..8919a2e5
--- /dev/null
+++ b/kscreenctl/command/output/set-primary.cpp
@@ -0,0 +1,15 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ config->setOutputPriority(output, 1);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Make the output primary", "Basic")
diff --git a/kscreenctl/command/output/set-priority.cpp b/kscreenctl/command/output/set-priority.cpp
new file mode 100644
index 00000000..7577f730
--- /dev/null
+++ b/kscreenctl/command/output/set-priority.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "PRIORITY",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto priority = parseInt(arguments[0]);
+ if (!priority) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (priority < 1 || priority > 100) {
+ std::println(std::cerr, "Allowed range for priority is 1 to 100");
+ return 1;
+ }
+
+ config->setOutputPriority(output, *priority);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the output priority (possible values: 1-100)", "Basic")
diff --git a/kscreenctl/command/output/set-replica-of.cpp b/kscreenctl/command/output/set-replica-of.cpp
new file mode 100644
index 00000000..1a126cd3
--- /dev/null
+++ b/kscreenctl/command/output/set-replica-of.cpp
@@ -0,0 +1,37 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "OUTPUT",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ if (arguments[0] == "none") {
+ output->setReplicationSource(0);
+ } else {
+ const KScreen::OutputPtr replicationSource = outputByQuery(config, arguments[0]);
+ if (!replicationSource) {
+ std::println(std::cerr, "Unknown replication source: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (output->id() == replicationSource->id()) {
+ std::println(std::cerr, "{} cannot mirror itself", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setReplicationSource(replicationSource->id());
+ }
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Mirror or unmirror another output (possible values: none or an OUTPUT)", "Misc")
diff --git a/kscreenctl/command/output/set-rgb-range.cpp b/kscreenctl/command/output/set-rgb-range.cpp
new file mode 100644
index 00000000..c57ce86d
--- /dev/null
+++ b/kscreenctl/command/output/set-rgb-range.cpp
@@ -0,0 +1,39 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "RANGE",
+ &arguments
+)
+
+static std::optional<KScreen::Output::RgbRange> parseRgbRange(const QString &input)
+{
+ if (input == "automatic") {
+ return KScreen::Output::RgbRange::Automatic;
+ } else if (input == "full") {
+ return KScreen::Output::RgbRange::Full;
+ } else if (input == "limited") {
+ return KScreen::Output::RgbRange::Limited;
+ }
+ return std::nullopt;
+}
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto range = parseRgbRange(arguments[0]);
+ if (!range) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setRgbRange(*range);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the RGB range (possible values: full, limited, or automatic)", "Color Management")
diff --git a/kscreenctl/command/output/set-rotation.cpp b/kscreenctl/command/output/set-rotation.cpp
new file mode 100644
index 00000000..15f4d2aa
--- /dev/null
+++ b/kscreenctl/command/output/set-rotation.cpp
@@ -0,0 +1,56 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "ROTATION",
+ &arguments
+)
+
+static std::optional<KScreen::Output::Rotation> parseRotation(const QString &value)
+{
+ static const QHash<QString, KScreen::Output::Rotation> rotationMap {
+ { u"none"_s, KScreen::Output::None },
+ { u"normal"_s, KScreen::Output::None },
+ { u"left"_s, KScreen::Output::Left },
+ { u"right"_s, KScreen::Output::Right },
+ { u"inverted"_s, KScreen::Output::Inverted },
+ { u"flipped"_s, KScreen::Output::Flipped },
+ { u"flipped90"_s, KScreen::Output::Flipped90 },
+ { u"flipped180"_s, KScreen::Output::Flipped180 },
+ { u"flipped270"_s, KScreen::Output::Flipped270 },
+ { u"0"_s, KScreen::Output::None },
+ { u"90"_s, KScreen::Output::Left },
+ { u"180"_s, KScreen::Output::Inverted },
+ { u"270"_s, KScreen::Output::Right },
+ };
+
+ if (const auto it = rotationMap.find(value); it != rotationMap.end()) {
+ return *it;
+ }
+
+ return std::nullopt;
+}
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto rotation = parseRotation(arguments[0]);
+ if (!rotation) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ const QSize previousSize = output->explicitLogicalSizeInt();
+ output->setRotation(*rotation);
+ output->setExplicitLogicalSize(config->logicalSizeForOutputInt(*output));
+ snapOutputAfterResize(output, previousSize, config);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the output transform. Outputs are rotated counter-clockwise (possible values: none, normal, 0, 90, 180, 270, flipped, flipped90, flipped180, or flipped270)", "Basic")
diff --git a/kscreenctl/command/output/set-scale.cpp b/kscreenctl/command/output/set-scale.cpp
new file mode 100644
index 00000000..ed193b15
--- /dev/null
+++ b/kscreenctl/command/output/set-scale.cpp
@@ -0,0 +1,46 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "SCALE",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto scale = parsePercents(arguments[0]);
+ if (!scale) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ const double minScale = 0.5;
+ if (scale < minScale) {
+ std::println(std::cerr, "The scale factor should be at least {}%", std::round(minScale * 100));
+ return 1;
+ }
+
+ const double maxScale = 10.0;
+ if (scale > maxScale) {
+ std::println(std::cerr, "The scale factor cannot be greater than {}%", std::round(maxScale * 100));
+ return 1;
+ }
+
+ const QSize previousSize = output->explicitLogicalSizeInt();
+ output->setScale(*scale);
+ output->setExplicitLogicalSize(config->logicalSizeForOutputInt(*output));
+ snapOutputAfterResize(output, previousSize, config);
+
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the output scale", "Basic")
+
+OUTPUT_COMMAND_EXAMPLE("Change the scale factor of output DP-1 to 200%",
+ "DP-1 set-scale 200%")
diff --git a/kscreenctl/command/output/set-sdr-brightness.cpp b/kscreenctl/command/output/set-sdr-brightness.cpp
new file mode 100644
index 00000000..f25a36f6
--- /dev/null
+++ b/kscreenctl/command/output/set-sdr-brightness.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BRIGHTNESS",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto brightness = parseInt(arguments[0]);
+ if (!brightness) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (brightness < 50 || brightness > 10000) {
+ std::println(std::cerr, "Allowed range for sdr-brightness is 50 to 10000");
+ return 1;
+ }
+
+ output->setSdrBrightness(*brightness);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the SDR content brightness in the HDR mode (possible values: 0-10000)", "Color Management")
diff --git a/kscreenctl/command/output/set-sdr-gamut.cpp b/kscreenctl/command/output/set-sdr-gamut.cpp
new file mode 100644
index 00000000..9ea56aab
--- /dev/null
+++ b/kscreenctl/command/output/set-sdr-gamut.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "WIDENESS",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto sdrGamutWideness = parseInt(arguments[0]);
+ if (!sdrGamutWideness) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (sdrGamutWideness < 0 || sdrGamutWideness > 100) {
+ std::println(std::cerr, "Allowed range for sdr-gamut is 0 to 100");
+ return 1;
+ }
+
+ output->setSdrGamutWideness(*sdrGamutWideness / 100.0);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the SDR gamut wideness (possible values: 0-100)", "Color Management")
diff --git a/kscreenctl/command/output/set-sharpness.cpp b/kscreenctl/command/output/set-sharpness.cpp
new file mode 100644
index 00000000..91594094
--- /dev/null
+++ b/kscreenctl/command/output/set-sharpness.cpp
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "SHARPNESS",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto sharpness = parseInt(arguments[0]);
+ if (!sharpness) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ if (sharpness < 0 || sharpness > 100) {
+ std::println(std::cerr, "Allowed range for sharpness is 0 to 100");
+ return 1;
+ }
+
+ output->setSharpness(*sharpness / 100.0);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the sharpness (possible values: 0-100)", "Misc")
diff --git a/kscreenctl/command/output/set-vrr-policy.cpp b/kscreenctl/command/output/set-vrr-policy.cpp
new file mode 100644
index 00000000..ca644b01
--- /dev/null
+++ b/kscreenctl/command/output/set-vrr-policy.cpp
@@ -0,0 +1,39 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "POLICY",
+ &arguments
+)
+
+static std::optional<KScreen::Output::VrrPolicy> parseVrrPolicy(const QString &input)
+{
+ if (input == "never") {
+ return KScreen::Output::VrrPolicy::Never;
+ } else if (input == "always") {
+ return KScreen::Output::VrrPolicy::Always;
+ } else if (input == "automatic") {
+ return KScreen::Output::VrrPolicy::Automatic;
+ }
+ return std::nullopt;
+}
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto policy = parseVrrPolicy(arguments[0]);
+ if (!policy) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setVrrPolicy(*policy);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Set the VRR policy (possible values: never, always, and automatic)", "Misc")
diff --git a/kscreenctl/command/output/set-wcg.cpp b/kscreenctl/command/output/set-wcg.cpp
new file mode 100644
index 00000000..9792d58a
--- /dev/null
+++ b/kscreenctl/command/output/set-wcg.cpp
@@ -0,0 +1,27 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static QStringList arguments;
+OUTPUT_COMMAND_POSITIONAL_OPTION(
+ "BOOL",
+ &arguments
+)
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto wcgEnabled = parseBool(arguments[0]);
+ if (!wcgEnabled.has_value()) {
+ std::println(std::cerr, "Invalid argument: {}", arguments[0].toStdString());
+ return 1;
+ }
+
+ output->setWcgEnabled(*wcgEnabled);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Turn on or turn off wide color gamut", "Color Management")
diff --git a/kscreenctl/command/output/toggle-auto-brightness.cpp b/kscreenctl/command/output/toggle-auto-brightness.cpp
new file mode 100644
index 00000000..53062050
--- /dev/null
+++ b/kscreenctl/command/output/toggle-auto-brightness.cpp
@@ -0,0 +1,15 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ output->setAutomaticBrightness(!output->automaticBrightness());
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Toggle automatic brightness adjustment", "Brightness")
diff --git a/kscreenctl/command/output/toggle-enabled.cpp b/kscreenctl/command/output/toggle-enabled.cpp
new file mode 100644
index 00000000..75dcc2ce
--- /dev/null
+++ b/kscreenctl/command/output/toggle-enabled.cpp
@@ -0,0 +1,15 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ output->setEnabled(!output->isEnabled());
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Enable or disable the output", "Basic")
diff --git a/kscreenctl/command/output/toggle-hdr-and-wcg.cpp b/kscreenctl/command/output/toggle-hdr-and-wcg.cpp
new file mode 100644
index 00000000..1068c2be
--- /dev/null
+++ b/kscreenctl/command/output/toggle-hdr-and-wcg.cpp
@@ -0,0 +1,20 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ const auto enabled = output->isHdrEnabled() || output->isWcgEnabled();
+ output->setHdrEnabled(!enabled);
+ output->setWcgEnabled(!enabled);
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Toggle both HDR and wide color gamut", "Color Management")
+
+OUTPUT_COMMAND_EXAMPLE("Toggle both HDR and wide color gamut on the current output",
+ "active-output toggle-hdr-and-wcg")
diff --git a/kscreenctl/command/output/toggle-hdr.cpp b/kscreenctl/command/output/toggle-hdr.cpp
new file mode 100644
index 00000000..a47ca6ed
--- /dev/null
+++ b/kscreenctl/command/output/toggle-hdr.cpp
@@ -0,0 +1,15 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ output->setHdrEnabled(!output->isHdrEnabled());
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Toggle HDR", "Color Management")
diff --git a/kscreenctl/command/output/toggle-wcg.cpp b/kscreenctl/command/output/toggle-wcg.cpp
new file mode 100644
index 00000000..b64cccc8
--- /dev/null
+++ b/kscreenctl/command/output/toggle-wcg.cpp
@@ -0,0 +1,15 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "command/output/cmd.h"
+
+static int run(KScreen::OutputPtr output, KScreen::ConfigPtr config)
+{
+ output->setWcgEnabled(!output->isWcgEnabled());
+ return applyConfig(config);
+}
+
+OUTPUT_COMMAND(run, "Toggle wide color gamut", "Color Management")
diff --git a/kscreenctl/main.cpp b/kscreenctl/main.cpp
new file mode 100644
index 00000000..ee7281fd
--- /dev/null
+++ b/kscreenctl/main.cpp
@@ -0,0 +1,411 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include <QGuiApplication>
+
+#include "command.h"
+
+class HelpStreambuf : public std::streambuf
+{
+public:
+ explicit HelpStreambuf(const std::ostream &system)
+ : m_system(system.rdbuf())
+ {
+ }
+
+ void setMinLeft(int_type left)
+ {
+ m_minLeft = left;
+ m_overshot = m_minLeft && m_cursor && m_cursor > m_minLeft;
+ }
+
+ void setMaxRight(int_type right)
+ {
+ m_maxRight = right;
+ }
+
+private:
+ int_type overflow(int_type character) override
+ {
+ if (traits_type::eq_int_type(traits_type::eof(), character)) {
+ return traits_type::not_eof(character);
+ }
+
+ if (character == '\n') {
+ m_buffer += character;
+ const int_type result = m_system->sputn(m_buffer.data(), m_buffer.size());
+
+ m_buffer.clear();
+ m_cursor = 0;
+ return result;
+ }
+
+ if (m_overshot) {
+ m_buffer += '\n';
+ m_system->sputn(m_buffer.data(), m_buffer.size());
+
+ m_buffer.clear();
+ m_cursor = 0;
+ m_overshot = false;
+ }
+
+ if (m_cursor < m_minLeft) {
+ const int_type n = m_minLeft - m_cursor;
+ m_buffer.append(n, ' ');
+ m_cursor += n;
+ }
+
+ m_buffer += character;
+
+ // Avoid bumping the cursor for UTF-8 continuation bytes.
+ m_cursor += (character & 0xc0) != 0x80;
+
+ if (m_maxRight && m_cursor >= m_maxRight) {
+ if (std::isspace(m_buffer.back())) {
+ int_type lastChar = -1;
+ for (size_t i = 0; i < m_buffer.size(); ++i) {
+ if (!std::isspace(m_buffer[i])) {
+ lastChar = i;
+ }
+ }
+
+ if (lastChar != -1) {
+ const int_type newlineIndex = lastChar + 1;
+ m_buffer[newlineIndex] = '\n';
+ m_system->sputn(m_buffer.data(), newlineIndex + 1);
+ }
+
+ m_buffer.clear();
+ m_cursor = 0;
+ }
+ }
+
+ return character;
+ }
+
+ std::streambuf *m_system;
+ std::basic_string<char_type> m_buffer;
+ int_type m_minLeft = 0;
+ int_type m_maxRight = 0;
+ int_type m_cursor = 0;
+ bool m_overshot = false;
+};
+
+class HelpStream : public std::ostream
+{
+public:
+ HelpStream(const std::ostream &stream)
+ : std::ostream(&m_buffer)
+ , m_buffer(stream)
+ {
+ }
+
+ void setMinLeft(int left)
+ {
+ m_buffer.setMinLeft(left);
+ }
+
+ void setMaxRight(int right)
+ {
+ m_buffer.setMaxRight(right);
+ }
+
+private:
+ HelpStreambuf m_buffer;
+};
+
+static void showHelp()
+{
+ const std::string applicationName = QCoreApplication::applicationName().toStdString();
+ HelpStream help(std::cerr);
+
+ std::println(help, "Usage: {} COMMAND …\n", applicationName);
+ std::println(help, "Utility to change the output configuration.\n");
+
+ std::println(help, "Commands:");
+
+ const size_t padding = 30;
+ const size_t maxWidth = 80;
+ const size_t indent = 3;
+
+ for (const Command &command : Command::all()) {
+ if (command.object && command.name != "self"_ba) {
+ continue;
+ }
+
+ help.setMinLeft(indent);
+ help.setMaxRight(0);
+ if (command.object) {
+ std::print(help, "{}", command.object);
+ } else {
+ std::print(help, "{}", command.name);
+ }
+
+ help.setMinLeft(indent + padding);
+ help.setMaxRight(maxWidth);
+ std::print(help, "{}\n", command.summary);
+ }
+
+ std::println(help);
+
+ for (const Command &command : Command::all()) {
+ if (command.object) {
+ continue;
+ }
+
+ std::vector<const CommandOption *> options = CommandOption::all(&command);
+
+ const CommandOption *positional = nullptr;
+ for (const CommandOption *option : options) {
+ if (option->positional) {
+ positional = option;
+ break;
+ }
+ }
+
+ help.setMinLeft(0);
+ help.setMaxRight(0);
+ std::println(help, "\x1b[1m{}: {}\x1b[22m", command.name, command.summary);
+
+ help.setMinLeft(indent);
+ help.setMaxRight(0);
+ std::print(help, "Usage: {} {}", applicationName, command.name);
+ if (!options.empty()) {
+ std::print(help, " [OPTIONS…]");
+ }
+ if (positional) {
+ const QByteArrayList parts = QByteArray::fromRawData(positional->format, strlen(positional->format)).split(' ');
+ for (const QByteArray &part : parts) {
+ std::print(help, " {}…", part.data());
+ }
+ }
+ std::print(help, "\n\n");
+
+ if (command.description) {
+ help.setMinLeft(indent);
+ help.setMaxRight(maxWidth);
+ std::println(help, "{}\n", command.description);
+ }
+
+ const auto examples = CommandExample::all(&command);
+ if (!examples.empty()) {
+ help.setMinLeft(0);
+ help.setMaxRight(0);
+ std::println(help, "\x1b[4mExamples:\x1b[24m\n");
+
+ for (const CommandExample *example : examples) {
+ help.setMinLeft(indent);
+ help.setMaxRight(maxWidth);
+ std::println(help, "{}\n", example->summary);
+
+ help.setMinLeft(2 * indent);
+ help.setMaxRight(0);
+ std::println(help, "{} {} {}\n", applicationName, example->command, example->example);
+ }
+ }
+
+ if (!options.empty()) {
+ help.setMinLeft(0);
+ help.setMaxRight(0);
+ std::println(help, "\x1b[4mOptions:\x1b[24m");
+
+ for (const CommandOption *option : std::as_const(options)) {
+ help.setMinLeft(indent);
+ help.setMaxRight(0);
+ if (!option->format) {
+ std::print(help, "--{}", option->name);
+ } else {
+ std::print(help, "--{}={}…", option->name, option->format);
+ }
+
+ help.setMinLeft(indent + padding);
+ help.setMaxRight(maxWidth);
+ std::print(help, "{}\n", option->summary);
+ }
+
+ std::println(help);
+ }
+ }
+
+ std::vector<const Command *> objectCommands;
+ const Command *selfCommand = nullptr;
+ for (const Command &command : Command::all()) {
+ if (command.object) {
+ objectCommands.push_back(&command);
+
+ if (command.name == "self"_ba) {
+ selfCommand = &command;
+ }
+ }
+ }
+
+ help.setMinLeft(0);
+ help.setMaxRight(0);
+ std::println(help, "\x1b[1m{}: {}\x1b[22m", selfCommand->object, selfCommand->summary);
+
+ help.setMinLeft(indent);
+ help.setMaxRight(0);
+ std::println(help, "Usage: {} {} COMMAND… [OPTIONS…]\n", applicationName, selfCommand->object);
+
+ if (selfCommand->description) {
+ help.setMinLeft(indent);
+ help.setMaxRight(maxWidth);
+ std::println(help, "{}\n", selfCommand->description);
+ }
+
+ QList<const CommandOption *> options;
+ for (const CommandOption &option : CommandOption::all()) {
+ if (!option.object || option.positional) {
+ continue;
+ }
+ options.append(&option);
+ }
+
+ if (!options.empty()) {
+ help.setMinLeft(0);
+ help.setMaxRight(0);
+ std::println(help, "\x1b[4mOptions:\x1b[24m");
+
+ for (const CommandOption *option : std::as_const(options)) {
+ help.setMinLeft(indent);
+ help.setMaxRight(0);
+ if (!option->format) {
+ std::print(help, "--{}", option->name);
+ } else {
+ std::print(help, "--{}={}…", option->name, option->format);
+ }
+
+ help.setMinLeft(indent + padding);
+ help.setMaxRight(maxWidth);
+ std::print(help, "{}\n", option->summary);
+ }
+
+ std::println(help);
+ }
+
+ QMultiMap<QString, const Command *> topics;
+ QMultiMap<QString, const CommandExample *> topicExamples;
+ for (const Command *command : objectCommands) {
+ if (command->name != "self"_ba) {
+ topics.insert(command->topic, command);
+ }
+
+ const auto examples = CommandExample::all(command);
+ for (const CommandExample *example : examples) {
+ topicExamples.insert(command->topic, example);
+ }
+ }
+
+ const QList<QString> sortedTopics = topics.uniqueKeys();
+ for (const QString &topic : sortedTopics) {
+ QList<const Command *> commands = topics.values(topic);
+ std::sort(commands.begin(), commands.end(), [](const auto &a, const auto &b) {
+ return strcmp(a->name, b->name) < 0;
+ });
+
+ QList<const CommandExample *> commandExamples = topicExamples.values(topic);
+ std::sort(commandExamples.begin(), commandExamples.end(), [](const auto &a, const auto &b) {
+ return strcmp(a->command, b->command) < 0;
+ });
+
+ help.setMinLeft(0);
+ help.setMaxRight(0);
+ std::println(help, "\x1b[4m{} Commands:\x1b[24m", topic.toStdString());
+ for (const Command *command : commands) {
+ const auto options = CommandOption::all(command);
+
+ const CommandOption *positional = nullptr;
+ for (const CommandOption *option : options) {
+ if (option->positional) {
+ positional = option;
+ break;
+ }
+ }
+
+ help.setMinLeft(indent);
+ help.setMaxRight(0);
+ std::print(help, "{}", command->name);
+ if (positional) {
+ const QByteArrayList parts = QByteArray::fromRawData(positional->format, strlen(positional->format)).split(' ');
+ for (const QByteArray &part : parts) {
+ std::print(help, " {}…", part.data());
+ }
+ }
+
+ help.setMinLeft(indent + padding);
+ help.setMaxRight(maxWidth);
+ std::print(help, "{}\n", command->summary);
+ }
+
+ std::println(help);
+
+ if (!commandExamples.isEmpty()) {
+ help.setMinLeft(0);
+ help.setMaxRight(0);
+ std::println(help, "\x1b[4m{} Command Examples:\x1b[24m\n", topic.toStdString());
+
+ for (const CommandExample *example : commandExamples) {
+ help.setMinLeft(indent);
+ help.setMaxRight(maxWidth);
+ std::println(help, "{}\n", example->summary);
+
+ help.setMinLeft(2 * indent);
+ help.setMaxRight(0);
+ std::println(help, "{} {}\n", applicationName, example->example);
+ }
+ }
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+
+ const QStringList arguments = app.arguments().sliced(1);
+ if (!arguments.isEmpty()) {
+ if (arguments[0] == "help" || arguments.contains("--help")) {
+ showHelp();
+ return 0;
+ }
+
+ for (const Command &command : Command::all()) {
+ if (command.name == arguments[0]) {
+ if (command.dispatch) {
+ if (!CommandOption::process(&command, arguments.sliced(1))) {
+ return 1;
+ }
+ return command.dispatch();
+ }
+ }
+ }
+
+ if (arguments.size() >= 2) {
+ const QString object = arguments[0];
+ const QString verb = arguments[1];
+
+ for (const Command &command : Command::all()) {
+ if (command.dispatchObject && command.name == verb) {
+ if (!CommandOption::process(&command, arguments.sliced(2))) {
+ return 1;
+ }
+ return command.dispatchObject(object);
+ }
+ }
+ }
+
+ for (const Command &command : Command::all()) {
+ if (command.dispatchObject && command.name == "self"_ba) {
+ if (!CommandOption::process(&command, arguments.sliced(1))) {
+ return 1;
+ }
+ return command.dispatchObject(arguments[0]);
+ }
+ }
+ }
+
+ showHelp();
+ return 1;
+}
diff --git a/kscreenctl/shell-completion/fish/kscreenctl.fish b/kscreenctl/shell-completion/fish/kscreenctl.fish
new file mode 100644
index 00000000..c89d4210
--- /dev/null
+++ b/kscreenctl/shell-completion/fish/kscreenctl.fish
@@ -0,0 +1,113 @@
+# SPDX-FileCopyrightText: none
+# SPDX-License-Identifier: CC0-1.0
+
+# Disable file completions.
+complete -c kscreenctl -f
+
+# Suggest available outputs.
+function __fish_kde_outputs
+ echo active-output
+ echo primary-output
+ kscreenctl list-outputs --names
+end
+
+function __fish_kde_enabled_outputs
+ echo none
+ echo active-output
+ echo primary-output
+ kscreenctl list-outputs --names --enabled
+end
+
+complete -c kscreenctl -n "__fish_is_nth_token 1" -a "(__fish_kde_outputs)" -d "Output"
+
+# Toplevel commands.
+complete -c kscreenctl -n "__fish_is_nth_token 1" -a help -d "Show help information"
+complete -c kscreenctl -n "__fish_is_nth_token 1" -a list-outputs -d "List available outputs"
+complete -c kscreenctl -n "__fish_is_nth_token 1" -a on -d "Turn on all outputs"
+complete -c kscreenctl -n "__fish_is_nth_token 1" -a off -d "Turn off all outputs"
+complete -c kscreenctl -n "__fish_is_nth_token 1" -a identify -d "Identify outputs"
+
+# list-outputs
+complete -c kscreenctl -n "__fish_seen_subcommand_from list-outputs" -l names -d "Only display connector names"
+complete -c kscreenctl -n "__fish_seen_subcommand_from list-outputs" -l enabled -d "Only list enabled outputs"
+complete -c kscreenctl -n "__fish_seen_subcommand_from list-outputs" -l disabled -d "Only list disabled outputs"
+complete -c kscreenctl -n "__fish_seen_subcommand_from list-outputs" -l json -d "Show output information as JSON"
+
+# output/active-output
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a get-name -d "Get connector name"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-scale -d "Set scale"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-position -d "Set position"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a left-of -d "Place this output to the left of another output"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a right-of -d "Place this output to the right of another output"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a above -d "Place this output above another output"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a below -d "Plasma this output below another output"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-enabled -d "Enable or disable the output"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a toggle-enabled -d "Toggle the enabled state"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-rotation -d "Change output transform"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-dimming -d "Set dimming, in percents"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-brightness -d "Change brightness, in percents"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-sdr-brightness -d "Change SDR brightness, in nits"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-sdr-gamut -d "Change SDR gamut wideness, in percents"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a add-custom-mode -d "Add custom mode"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a remove-custom-mode -d "Remove custom mode"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a list-custom-modes -d "List custom modes"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-hdr -d "Enable or disable HDR"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a toggle-hdr -d "Toggle HDR"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-wcg -d "Enable or disable wide color gamut"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a toggle-wcg -d "Toggle wide color gamut"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-hdr-and-wcg -d "Enable or disable both HDR and wide color gamut"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a toggle-hdr-and-wcg -d "Toggle both HDR and wide color gamut"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-auto-brightness -d "Enable or disable automatic brightness"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a toggle-auto-brightness -d "Toggle automatic brightness"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-replica-of -d "Mirror or unmirror another output"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-icc-profile -d "Set the ICC profile path"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-hdr-icc-profile -d "Set the ICC profile path for the HDR mode"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-color-profile-source -d "Set the color profile source"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-hdr-color-profile-source -d "Set the color profile source for the HDR mode"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-color-power-tradeoff -d "Set the color power tradeoff"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-sharpness -d "Change sharpness, in percents"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-overscan -d "Change overscan, in percents"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-priority -d "Change priority"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-primary -d "Make the output primary"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-vrr-policy -d "Change VRR policy"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-rgb-range -d "Change RGB range"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-edr-policy -d "Change Extended Dynamic Range policy"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-auto-rotate -d "Change auto rotate policy"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-min-brightness-override -d "Change the minimum brightness override"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-max-peak-brightness-override -d "Change the maximum peak brightness override"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-max-average-brightness-override -d "Change the maximum average brightness override"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-ddc-ci-allowed -d "Allow or disallow DDCI CI to be used with this output"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-maxbpc -d "Change maximum bits per color"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-abm -d "Change the adaptive backlight management level"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a list-modes -d "List available modes"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a set-mode -d "Set current mode"
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -a calibrate-hdr -d "Calibrate HDR"
+
+complete -c kscreenctl -n "__fish_is_nth_token 2 && __fish_seen_subcommand_from (__fish_kde_outputs)" -l json -d "Show output information as JSON"
+
+complete -c kscreenctl -n "__fish_seen_subcommand_from add-custom-mode" -l reduced-blanking -d "Add a custom mode with reduced blanking"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-scale" -a "100% 125% 150% 175% 200% 225% 250% 275% 300%"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-enabled" -a "on off"
+complete -c kscreenctl -n "__fish_seen_subcommand_from left-of" -a "(__fish_kde_enabled_outputs)"
+complete -c kscreenctl -n "__fish_seen_subcommand_from right-of" -a "(__fish_kde_enabled_outputs)"
+complete -c kscreenctl -n "__fish_seen_subcommand_from above" -a "(__fish_kde_enabled_outputs)"
+complete -c kscreenctl -n "__fish_seen_subcommand_from below" -a "(__fish_kde_enabled_outputs)"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-rotation" -a "none normal left right inverted flipped 0 90 180 270 flipped90 flipped180 flipped270"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-hdr" -a "on off"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-wcg" -a "on off"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-hdr-and-wcg" -a "on off"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-auto-brightness" -a "on off"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-replica-of" -a "(__fish_kde_enabled_outputs)"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-icc-profile" -F
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-hdr-icc-profile" -F
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-color-profile-source" -a "sRGB ICC EDID"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-hdr-color-profile-source" -a "ICC EDID"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-color-power-tradeoff" -a "prefer-efficiency prefer-accuracy"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-vrr-policy" -a "never always automatic"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-rgb-range" -a "limited full automatic"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-edr-policy" -a "never always"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-auto-rotate" -a "never in-tablet-mode always"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-ddc-ci-allowed" -a "on off"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-maxbpc" -a "automatic 6 8 10 12 14 16"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-abm" -a "0 1 2 3 4"
+complete -c kscreenctl -n "__fish_seen_subcommand_from set-mode" -a "(kscreenctl (__fish_nth_token 1) list-modes)"
diff --git a/kscreenctl/shell-completion/zsh/_kscreenctl b/kscreenctl/shell-completion/zsh/_kscreenctl
new file mode 100644
index 00000000..1d9e5274
--- /dev/null
+++ b/kscreenctl/shell-completion/zsh/_kscreenctl
@@ -0,0 +1,109 @@
+#compdef kscreenctl
+
+# SPDX-FileCopyrightText: none
+# SPDX-License-Identifier: CC0-1.0
+
+_kscreenctl-connected-outputs() {
+ echo active-output
+ echo primary-output
+ kscreenctl list-outputs --names
+}
+
+_kscreenctl-enabled-outputs() {
+ local -a values=("${(@f)$(kscreenctl list-outputs --names --enabled)}")
+ values+=(active-output primary-output)
+ _describe -t outputs 'output' values
+}
+
+_kscreenctl-output-modes() {
+ local -a values=("${(@f)$(kscreenctl $1 list-modes)}")
+ _describe -t modes 'mode' values
+}
+
+_kscreenctl-list-outputs() {
+ _arguments \
+ '--disabled[Only list disabled outputs]' \
+ '--enabled[Only list enabled outputs]' \
+ '--names[Only display connector names]' \
+ '--json[Show output information as JSON]'
+}
+
+_kscreenctl-output() {
+ local curcontext="$curcontext" state line
+ local output="$1"
+
+ _arguments -C \
[suppressed due to size limit]
+ '*::arg:->args'
+
+ case $line[1] in
+ get-name) ;;
+ set-scale) _alternative 'scale::(100% 125% 150% 175% 200% 225% 250% 275% 300%)' ;;
+ set-position) _arguments '1:x:_numbers' '2:y:_numbers' ;;
+ left-of) _kscreenctl-enabled-outputs ;;
+ right-of) _kscreenctl-enabled-outputs ;;
+ above) _kscreenctl-enabled-outputs ;;
+ below) _kscreenctl-enabled-outputs ;;
+ set-enabled) _alternative 'enabled::(on off)' ;;
+ toggle-enabled) ;;
+ set-rotation) _alternative 'rotation::(none normal left right inverted flipped 0 90 180 270 flipped90 flipped180 flipped270)' ;;
+ set-dimming) _numbers -t 'dimming' -u percents -l 0 -m 100 ;;
+ set-brightness) _numbers -t 'brightness' -u percents -l 0 -m 100 ;;
+ set-sdr-brightness) _numbers -t 'sdr-brightness' -u nits -l 100 -m 10000 ;;
+ set-sdr-gamut) _numbers -t 'wideness' -u percents -l 0 -m 100 ;;
+ add-custom-mode) _arguments '--reduced-blanking[Add a custom mode with reduced blanking]' ;;
+ remove-custom-mode) ;;
+ list-custom-modes) ;;
+ set-hdr) _alternative 'hdr::(on off)' ;;
+ toggle-hdr) ;;
+ set-wcg) _alternative 'wcg::(on off)' ;;
+ toggle-wcg) ;;
+ set-hdr-and-wcg) _alternative 'hdr-and-wcg::(on off)' ;;
+ toggle-hdr-and-wcg) ;;
+ set-auto-brightness) _alternative 'auto-brightness::(on off)' ;;
+ toggle-auto-brightness) ;;
+ set-replica-of) _kscreenctl-enabled-outputs ;;
+ set-icc-profile) _files ;;
+ set-hdr-icc-profile) _files ;;
+ set-color-profile-source) _alternative 'source::(sRGB ICC EDID)' ;;
+ set-hdr-color-profile-source) _alternative 'source::(ICC EDID)' ;;
+ set-color-power-tradeoff) _alternative 'tradeoff::(prefer-efficiency prefer-accuracy)' ;;
+ set-sharpness) _numbers -t 'sharpness' -l 0 -m 100 ;;
+ set-overscan) _numbers -t 'overscan' -l 0 -m 100 ;;
+ set-priority) _numbers -t 'priority' -l 0 -m 100 ;;
+ set-primary) ;;
+ set-vrr-policy) _alternative 'vrr-policy::(never always automatic)' ;;
+ set-rgb-range) _alternative 'rgb-range::(limited full automatic)' ;;
+ set-edr-policy) _alternative 'edr-policy::(never always)' ;;
+ set-auto-rotate) _alternative 'auto-rotate::(never in-tablet-mode always)' ;;
+ set-min-brightness-override) ;;
+ set-max-peak-brightness-override) ;;
+ set-max-average-brightness-override) ;;
+ set-ddc-ci-allowed) _alternative 'allowed::(true false)' ;;
+ set-maxbpc) _alternative 'bpc::(automatic 6 8 10 12 14 16)' ;;
+ set-abm) _numbers -t 'abm' -l 0 -m 4 ;;
+ list-modes) ;;
+ set-mode) _kscreenctl-output-modes "$output" ;;
+ calibrate-hdr) ;;
+ esac
+}
+
+_kscreenctl() {
+ local curcontext="$curcontext" state line
+
+ _arguments -C \
+ '1:command:(identify list-outputs on off $(_kscreenctl-connected-outputs))' \
+ '*::arg:->args'
+
+ if [[ -n $line[1] ]]; then
+ case $line[1] in
+ identify) ;;
+ list-outputs) _kscreenctl-list-outputs ;;
+ on) ;;
+ off) ;;
+ *) _kscreenctl-output "$line[1]" ;;
+ esac
+ fi
+}
+
+_kscreenctl "$@"
diff --git a/kscreenctl/util.cpp b/kscreenctl/util.cpp
new file mode 100644
index 00000000..715bdf75
--- /dev/null
+++ b/kscreenctl/util.cpp
@@ -0,0 +1,841 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#include "util.h"
+
+#include <KScreen/SetConfigOperation>
+
+#include <QCollator>
+#include <QDBusConnection>
+#include <QDBusMessage>
+#include <QDBusReply>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonValue>
+#include <QRect>
+
+#include <iostream>
+
+using namespace Qt::StringLiterals;
+
+const static QString green = u"\033[01;32m"_s;
+const static QString red = u"\033[01;31m"_s;
+const static QString yellow = u"\033[01;33m"_s;
+const static QString blue = u"\033[01;34m"_s;
+const static QString bold = u"\033[01;39m"_s;
+const static QString cr = u"\033[0;0m"_s;
+
+KScreen::OutputPtr outputByQuery(const KScreen::ConfigPtr &config, const QString &query)
+{
+ if (query == "primary-output") {
+ return config->primaryOutput();
+ }
+
+ QString name = query;
+ if (name == "active-output") {
+ const QDBusReply<QString> reply = QDBusConnection::sessionBus().call(
+ QDBusMessage::createMethodCall(u"org.kde.KWin"_s,
+ u"/KWin"_s,
+ u"org.kde.KWin"_s,
+ u"activeOutputName"_s)
+ );
+ if (!reply.isValid()) {
+ std::println(std::cerr, "Failed to determine active output: {}", reply.error().message().toStdString());
+ return nullptr;
+ }
+
+ name = reply.value();
+ }
+
+ const auto outputs = config->outputs();
+ for (const auto &output : outputs) {
+ if (output->name() == name || output->uuid() == name) {
+ return output;
+ }
+ }
+
+ bool isInteger;
+ const int id = name.toInt(&isInteger);
+ if (!isInteger) {
+ return nullptr;
+ }
+
+ for (const auto &output : outputs) {
+ if (output->id() == id) {
+ return output;
+ }
+ }
+
+ return nullptr;
+}
+
+KScreen::ModePtr modeByQuery(const KScreen::OutputPtr &output, const QString &query)
+{
+ const auto availableModes = output->modes();
+
+ for (const auto &mode : availableModes) {
+ if (mode->id() == query) {
+ return mode;
+ }
+ }
+
+ for (const auto &mode : availableModes) {
+ if (printableMode(output, mode) == query) {
+ return mode;
+ }
+ }
+
+ const int xIndex = query.indexOf('x');
+ if (xIndex == -1) {
+ return nullptr;
+ }
+
+ const int atIndex = query.indexOf('@', xIndex);
+
+ // Try to match the mode by widthxheight.
+ if (atIndex == -1) {
+ const std::optional<int> width = parseInt(query.left(xIndex));
+ if (!width) {
+ return nullptr;
+ }
+
+ const std::optional<int> height = parseInt(query.mid(xIndex + 1));
+ if (!height) {
+ return nullptr;
+ }
+
+ KScreen::ModePtr mode;
+ for (const auto &candidate : availableModes) {
+ if (candidate->size() != QSize(*width, *height)) {
+ continue;
+ }
+ if (!mode || candidate->refreshRate() > mode->refreshRate()) {
+ mode = candidate;
+ }
+ }
+
+ return mode;
+ }
+
+ // Try to match the mode by widthxheight@refreshRate.
+ const std::optional<int> width = parseInt(query.left(xIndex));
+ if (!width) {
+ return nullptr;
+ }
+
+ const std::optional<int> height = parseInt(query.mid(xIndex + 1, atIndex - xIndex - 1));
+ if (!height) {
+ return nullptr;
+ }
+
+ QString refreshRatePart = query.mid(atIndex + 1);
+ if (refreshRatePart.endsWith("Hz")) {
+ refreshRatePart.chop(2);
+ }
+
+ const std::optional<double> refreshRate = parseDouble(refreshRatePart);
+ if (!refreshRate) {
+ return nullptr;
+ }
+
+ KScreen::ModePtr mode;
+ for (const auto &candidate : availableModes) {
+ if (candidate->size() != QSize(*width, *height)) {
+ continue;
+ }
+ if (std::abs(candidate->refreshRate() - *refreshRate) < 0.01) {
+ mode = candidate;
+ }
+ }
+
+ return mode;
+}
+
+QString printableRotation(KScreen::Output::Rotation rotation)
+{
+ static const QHash<KScreen::Output::Rotation, QString> rotationMap {
+ { KScreen::Output::None, u"none"_s },
+ { KScreen::Output::None, u"normal"_s },
+ { KScreen::Output::Left, u"90"_s },
+ { KScreen::Output::Right, u"270"_s },
+ { KScreen::Output::Inverted, u"180"_s },
+ { KScreen::Output::Flipped, u"flipped"_s },
+ { KScreen::Output::Flipped90, u"flipped90"_s },
+ { KScreen::Output::Flipped180, u"flipped180"_s },
+ { KScreen::Output::Flipped270, u"flipped270"_s },
+ };
+
+ return rotationMap.value(rotation);
+}
+
+std::optional<KScreen::Output::ColorProfileSource> parseColorProfileSource(QStringView input)
+{
+ if (input == "EDID") {
+ return KScreen::Output::ColorProfileSource::EDID;
+ } else if (input == "ICC") {
+ return KScreen::Output::ColorProfileSource::ICC;
+ } else if (input == "sRGB") {
+ return KScreen::Output::ColorProfileSource::sRGB;
+ }
+ return std::nullopt;
+}
+
+std::optional<bool> parseBool(QStringView value)
+{
+ if (value == "on" || value == "true") {
+ return true;
+ } else if (value == "off" || value == "false") {
+ return false;
+ }
+
+ return std::nullopt;
+}
+
+std::optional<int> parseInt(QStringView input)
+{
+ bool ok;
+ const int number = input.toInt(&ok);
+ if (ok) {
+ return number;
+ } else {
+ return std::nullopt;
+ }
+}
+
+std::optional<double> parseDouble(QStringView value)
+{
+ bool ok;
+ const double number = value.toDouble(&ok);
+ if (ok) {
+ return number;
+ } else {
+ return std::nullopt;
+ }
+}
+
+std::optional<double> parsePercents(QStringView input)
+{
+ if (input.endsWith('%')) {
+ input.chop(1);
+ }
+
+ return parseDouble(input).transform([](double value) {
+ return value / 100;
+ });
+}
+
+QString printableColorProfileSource(KScreen::Output::ColorProfileSource source)
+{
+ switch (source) {
+ case KScreen::Output::ColorProfileSource::EDID:
+ return u"EDID"_s;
+ case KScreen::Output::ColorProfileSource::ICC:
+ return u"ICC"_s;
+ case KScreen::Output::ColorProfileSource::sRGB:
+ return u"sRGB"_s;
+ }
+
+ Q_UNREACHABLE_RETURN(u"sRGB"_s);
+}
+
+QString printableColorPowerTradeoff(KScreen::Output::ColorPowerTradeoff source)
+{
+ switch (source) {
+ case KScreen::Output::ColorPowerTradeoff::PreferAccuracy:
+ return u"prefer-accuracy"_s;
+ case KScreen::Output::ColorPowerTradeoff::PreferEfficiency:
+ return u"prefer-efficiency"_s;
+ }
+
+ Q_UNREACHABLE_RETURN(u"prefer-efficiency"_s);
+}
+
+QString printableVrrPolicy(KScreen::Output::VrrPolicy source)
+{
+ switch (source) {
+ case KScreen::Output::VrrPolicy::Never:
+ return u"never"_s;
+ case KScreen::Output::VrrPolicy::Always:
+ return u"always"_s;
+ case KScreen::Output::VrrPolicy::Automatic:
+ return u"automatic"_s;
+ }
+
+ Q_UNREACHABLE_RETURN(u"automatic"_s);
+}
+
+QString printableRgbRange(KScreen::Output::RgbRange source)
+{
+ switch (source) {
+ case KScreen::Output::RgbRange::Automatic:
+ return u"automatic"_s;
+ case KScreen::Output::RgbRange::Full:
+ return u"full"_s;
+ case KScreen::Output::RgbRange::Limited:
+ return u"limited"_s;
+ }
+
+ Q_UNREACHABLE_RETURN(u"automatic"_s);
+}
+
+QString printableEdrPolicy(KScreen::Output::EdrPolicy source)
+{
+ switch (source) {
+ case KScreen::Output::EdrPolicy::Never:
+ return u"never"_s;
+ case KScreen::Output::EdrPolicy::Always:
+ return u"always"_s;
+ }
+
+ Q_UNREACHABLE_RETURN(u"never"_s);
+}
+
+QString printableAutoRotatePolicy(KScreen::Output::AutoRotatePolicy source)
+{
+ switch (source) {
+ case KScreen::Output::AutoRotatePolicy::Never:
+ return u"never"_s;
+ case KScreen::Output::AutoRotatePolicy::InTabletMode:
+ return u"in-tablet-mode"_s;
+ case KScreen::Output::AutoRotatePolicy::Always:
+ return u"always"_s;
+ }
+
+ Q_UNREACHABLE_RETURN(u"in-tablet-mode"_s);
+}
+
+QString printableMode(const KScreen::OutputPtr &output, const KScreen::ModePtr &mode)
+{
+ QString name = u"%1:%2x%3@%4"_s
+ .arg(mode->id(),
+ QString::number(mode->size().width()),
+ QString::number(mode->size().height()),
+ QString::number(mode->refreshRate(), 'f', 2));
+
+ if (mode == output->currentMode()) {
+ name += '*'_L1;
+ }
+ if (mode == output->preferredMode()) {
+ name += '!'_L1;
+ }
+
+ return name;
+}
+
+int applyConfig(KScreen::ConfigPtr config)
+{
+ auto operation = new KScreen::SetConfigOperation(config);
+ if (operation->exec()) {
+ return 0;
+ } else {
+ std::println(std::cerr, "Failed to apply output configuration: {}", operation->errorString().toStdString());
+ return 1;
+ }
+}
+
+void fixupOutputPositions(KScreen::ConfigPtr config)
+{
+ const auto outputs = config->connectedOutputs();
+ if (outputs.isEmpty()) {
+ return;
+ }
+
+ std::optional<QPoint> topLeft;
+ for (const auto &output : outputs) {
+ if (!output->isEnabled()) {
+ continue;
+ }
+
+ if (!topLeft) {
+ topLeft = output->pos();
+ } else {
+ topLeft = QPoint(std::min(topLeft->x(), output->pos().x()),
+ std::min(topLeft->y(), output->pos().y()));
+ }
+ }
+
+ if (!topLeft) {
+ return;
+ }
+
+ for (const auto &output : outputs) {
+ if (output->isEnabled()) {
+ output->setPos(output->pos() - *topLeft);
+ }
+ }
+}
+
+void snapOutputAfterResize(KScreen::OutputPtr resizedOutput, const QSize &oldSize, KScreen::ConfigPtr config)
+{
+ const QPoint oldCenter(resizedOutput->pos().x() + oldSize.width() / 2,
+ resizedOutput->pos().y() + oldSize.height() / 2);
+
+ const QPoint offset(resizedOutput->explicitLogicalSizeInt().width() - oldSize.width(),
+ resizedOutput->explicitLogicalSizeInt().height() - oldSize.height());
+
+ const auto outputs = config->connectedOutputs();
+ for (const KScreen::OutputPtr &output : outputs) {
+ if (!output->isEnabled()) {
+ continue;
+ }
+
+ if (output->id() == resizedOutput->id()) {
+ continue;
+ }
+
+ QPoint translation;
+ if (output->pos().x() >= oldCenter.x()) {
+ translation.setX(offset.x());
+ }
+ if (output->pos().y() >= oldCenter.y()) {
+ translation.setY(offset.y());
+ }
+
+ if (!translation.isNull()) {
+ output->setPos(output->pos() + translation);
+ }
+ }
+
+ fixupOutputPositions(config);
+}
+
+void showOutputAsText(const KScreen::OutputPtr &output)
+{
+ QHash<KScreen::Output::Type, QString> typeString;
+ typeString[KScreen::Output::Unknown] = u"Unknown"_s;
+ typeString[KScreen::Output::VGA] = u"VGA"_s;
+ typeString[KScreen::Output::DVI] = u"DVI"_s;
+ typeString[KScreen::Output::DVII] = u"DVII"_s;
+ typeString[KScreen::Output::DVIA] = u"DVIA"_s;
+ typeString[KScreen::Output::DVID] = u"DVID"_s;
+ typeString[KScreen::Output::HDMI] = u"HDMI"_s;
+ typeString[KScreen::Output::Panel] = u"Panel"_s;
+ typeString[KScreen::Output::TV] = u"TV"_s;
+ typeString[KScreen::Output::TVComposite] = u"TVComposite"_s;
+ typeString[KScreen::Output::TVSVideo] = u"TVSVideo"_s;
+ typeString[KScreen::Output::TVComponent] = u"TVComponent"_s;
+ typeString[KScreen::Output::TVSCART] = u"TVSCART"_s;
+ typeString[KScreen::Output::TVC4] = u"TVC4"_s;
+ typeString[KScreen::Output::DisplayPort] = u"DisplayPort"_s;
+
+ QCollator collator;
+ collator.setNumericMode(true);
+
+ static QTextStream cout(stdout);
+ static QTextStream cerr(stderr);
+
+ cout << green << "Output: " << cr << output->id() << " " << output->name() << " " << output->uuid() << Qt::endl;
+ cout << "\t" << (output->isEnabled() ? green + u"enabled"_s : red + u"disabled"_s) << cr << Qt::endl;
+ cout << "\t" << (output->isConnected() ? green + u"connected"_s : red + u"disconnected"_s) << cr << Qt::endl;
+ cout << "\t" << (output->isEnabled() ? green : red) + u"priority "_s << output->priority() << cr << Qt::endl;
+ auto _type = typeString[output->type()];
+ cout << "\t" << yellow << (_type.isEmpty() ? u"UnmappedOutputType"_s : _type) << cr << Qt::endl;
+ cout << "\t" << yellow << "replication source:" << cr << output->replicationSource() << Qt::endl;
+ cout << "\t" << blue << "Modes: " << cr;
+
+ const auto modes = output->modes();
+ auto modeKeys = modes.keys();
+ std::sort(modeKeys.begin(), modeKeys.end(), collator);
+
+ for (const auto &key : modeKeys) {
+ auto mode = *modes.find(key);
+
+ auto name = u"%1x%2@%3"_s
+ .arg(QString::number(mode->size().width()), QString::number(mode->size().height()), QString::number(mode->refreshRate(), 'f', 2));
+ if (mode == output->currentMode()) {
+ name = green + name + '*'_L1 + cr;
+ }
+ if (mode == output->preferredMode()) {
+ name = name + '!'_L1;
+ }
+ cout << " " << mode->id() << ":" << name << " ";
+ }
+ cout << Qt::endl;
+
+ cout << yellow << "\tCustom modes:" << cr;
+ const auto customModes = output->customModes();
+ if (customModes.empty()) {
+ cout << " None" << Qt::endl;
+ } else {
+ cout << Qt::endl;
+ for (const auto &[info, index] : std::views::zip(customModes, std::views::iota(0))) {
+ cout << "\t\t" << index << ": ";
+ cout << std::format("{}x{}@{:.2f}", info.size.width(), info.size.height(), info.refreshRate).c_str();
+ if (info.flags & KScreen::ModeInfo::Flag::ReducedBlanking) {
+ cout << " (reduced blanking)";
+ }
+ cout << Qt::endl;
+ }
+ }
+
+ const auto g = output->geometry();
+ cout << yellow << "\tGeometry: " << cr << g.x() << "," << g.y() << " " << g.width() << "x" << g.height() << Qt::endl;
+ cout << yellow << "\tScale: " << cr << output->scale() << Qt::endl;
+ cout << yellow << "\tRotation: " << cr << output->rotation() << Qt::endl;
+ cout << yellow << "\tOverscan: " << cr << output->overscan() << Qt::endl;
+ cout << yellow << "\tVrr: ";
+ if (output->capabilities() & KScreen::Output::Capability::Vrr) {
+ switch (output->vrrPolicy()) {
+ case KScreen::Output::VrrPolicy::Never:
+ cout << cr << "Never" << Qt::endl;
+ break;
+ case KScreen::Output::VrrPolicy::Automatic:
+ cout << cr << "Automatic" << Qt::endl;
+ break;
+ case KScreen::Output::VrrPolicy::Always:
+ cout << cr << "Always" << Qt::endl;
+ }
+ } else {
+ cout << cr << "incapable" << Qt::endl;
+ }
+ cout << yellow << "\tRgbRange: ";
+ if (output->capabilities() & KScreen::Output::Capability::RgbRange) {
+ switch (output->rgbRange()) {
+ case KScreen::Output::RgbRange::Automatic:
+ cout << cr << "Automatic" << Qt::endl;
+ break;
+ case KScreen::Output::RgbRange::Full:
+ cout << cr << "Full" << Qt::endl;
+ break;
+ case KScreen::Output::RgbRange::Limited:
+ cout << cr << "Limited" << Qt::endl;
+ }
+ } else {
+ cout << cr << "unknown" << Qt::endl;
+ }
+ cout << yellow << "\tHDR: ";
+ if (output->capabilities() & KScreen::Output::Capability::HighDynamicRange) {
+ if (output->isHdrEnabled()) {
+ cout << cr << "enabled" << Qt::endl;
+ cout << yellow << "\t\tSDR brightness: " << cr << output->sdrBrightness() << " nits" << Qt::endl;
+ cout << yellow << "\t\tSDR gamut wideness: " << cr << std::round(output->sdrGamutWideness() * 100) << "%" << Qt::endl;
+ if (output->maxPeakBrightness() == 0) {
+ cout << yellow << "\t\tPeak brightness: " << cr << "unknown";
+ } else {
+ cout << yellow << "\t\tPeak brightness: " << cr << output->maxPeakBrightness() << " nits";
+ }
+ if (const auto used = output->maxPeakBrightnessOverride()) {
+ cout << yellow << ", overridden with: " << cr << *used << " nits";
+ }
+ cout << Qt::endl;
+ if (output->maxAverageBrightness() == 0) {
+ cout << yellow << "\t\tMax average brightness: " << cr << "unknown";
+ } else {
+ cout << yellow << "\t\tMax average brightness: " << cr << output->maxAverageBrightness() << " nits";
+ }
+ if (const auto used = output->maxAverageBrightnessOverride()) {
+ cout << yellow << ", overridden with: " << cr << *used << " nits";
+ }
+ cout << Qt::endl;
+ cout << yellow << "\t\tMin brightness: " << cr << output->minBrightness() << " nits";
+ if (const auto used = output->minBrightnessOverride()) {
+ cout << yellow << ", overridden with: " << cr << (*used) / 10'000.0 << " nits";
+ }
+ cout << Qt::endl;
+
+ cout << yellow << "\t\tHDR color profile source: ";
+ if (output->capabilities() & KScreen::Output::Capability::HdrIccProfile) {
+ cout << cr;
+ switch (output->hdrColorProfileSource()) {
+ case KScreen::Output::ColorProfileSource::sRGB:
+ cout << "sRGB";
+ break;
+ case KScreen::Output::ColorProfileSource::ICC:
+ cout << "ICC";
+ break;
+ case KScreen::Output::ColorProfileSource::EDID:
+ cout << "EDID";
+ break;
+ }
+ cout << Qt::endl;
+
+ cout << yellow << "\t\tHDR ICC profile: ";
+ if (!output->hdrIccProfilePath().isEmpty()) {
+ cout << cr << output->hdrIccProfilePath() << Qt::endl;
+ } else {
+ cout << cr << "none" << Qt::endl;
+ }
+ } else {
+ cout << cr << "incapable" << Qt::endl;
+ }
+ } else {
+ cout << cr << "disabled" << Qt::endl;
+ }
+ } else {
+ cout << cr << "incapable" << Qt::endl;
+ }
+ cout << yellow << "\tWide Color Gamut: ";
+ if (output->capabilities() & KScreen::Output::Capability::WideColorGamut) {
+ if (output->isWcgEnabled()) {
+ cout << cr << "enabled" << Qt::endl;
+ } else {
+ cout << cr << "disabled" << Qt::endl;
+ }
+ } else {
+ cout << cr << "incapable" << Qt::endl;
+ }
+ cout << yellow << "\tICC profile: ";
+ if (output->capabilities() & KScreen::Output::Capability::IccProfile) {
+ if (!output->iccProfilePath().isEmpty()) {
+ cout << cr << output->iccProfilePath() << Qt::endl;
+ } else {
+ cout << cr << "none" << Qt::endl;
+ }
+ } else {
+ cout << cr << "incapable" << Qt::endl;
+ }
+ cout << yellow << "\tColor profile source: ";
+ if (output->capabilities() & (KScreen::Output::Capabilities(KScreen::Output::Capability::IccProfile) | KScreen::Output::Capability::BuiltInColorProfile)) {
+ cout << cr;
+ switch (output->colorProfileSource()) {
+ case KScreen::Output::ColorProfileSource::sRGB:
+ cout << "sRGB";
+ break;
+ case KScreen::Output::ColorProfileSource::ICC:
+ cout << "ICC";
+ break;
+ case KScreen::Output::ColorProfileSource::EDID:
+ cout << "EDID";
+ break;
+ }
+ cout << Qt::endl << yellow << "\tColor power preference: " << cr;
+ switch (output->colorPowerPreference()) {
+ case KScreen::Output::ColorPowerTradeoff::PreferEfficiency:
+ cout << "prefer efficiency and performance";
+ break;
+ case KScreen::Output::ColorPowerTradeoff::PreferAccuracy:
+ cout << "prefer accuracy";
+ break;
+ }
+ cout << Qt::endl;
+ } else {
+ cout << cr << "incapable" << Qt::endl;
+ }
+ cout << yellow << "\tBrightness control: ";
+ if (output->capabilities() & KScreen::Output::Capability::BrightnessControl) {
+ cout << cr << "supported, set to " << std::round(output->brightness() * 100) << "%"
+ << " and dimming to " << std::round(output->dimming() * 100) << "%" << Qt::endl;
+ } else {
+ cout << cr << "unsupported" << Qt::endl;
+ }
+ if (output->capabilities() & KScreen::Output::Capability::DdcCi) {
+ cout << yellow << "\tDDC/CI: ";
+ cout << cr << (output->ddcCiAllowed() ? "allowed" : "disallowed") << Qt::endl;
+ }
+ cout << yellow << "\tColor resolution: ";
+ if (output->capabilities() & KScreen::Output::Capability::MaxBitsPerColor) {
+ cout << cr;
+ if (output->maxBitsPerColor() != 0) {
+ cout << output->maxBitsPerColor() << " bits per color";
+ } else {
+ uint32_t autoBpc = 0;
+ if (output->colorPowerPreference() == KScreen::Output::ColorPowerTradeoff::PreferEfficiency) {
+ autoBpc = 10;
+ } else {
+ autoBpc = 16;
+ }
+ if (output->automaticMaxBitsPerColorLimit()) {
+ autoBpc = std::min(autoBpc, output->automaticMaxBitsPerColorLimit());
+ }
+ cout << "automatic (" << autoBpc << ")";
+ }
+ cout << ", range: [" << output->bitsPerColorRange().min << "; " << output->bitsPerColorRange().max << "] bits per color" << Qt::endl;
+ } else {
+ cout << "unknown" << Qt::endl;
+ }
+ cout << yellow << "\tAllow EDR: ";
+ if (output->capabilities() & KScreen::Output::Capability::ExtendedDynamicRange) {
+ cout << cr;
+ switch (output->edrPolicy()) {
+ case KScreen::Output::EdrPolicy::Never:
+ cout << "never";
+ break;
+ case KScreen::Output::EdrPolicy::Always:
+ cout << "always";
+ break;
+ }
+ cout << Qt::endl;
+ } else {
+ cout << cr << "unsupported" << Qt::endl;
+ }
+ cout << yellow << "\tSharpness control: ";
+ if (output->capabilities() & KScreen::Output::Capability::SharpnessControl) {
+ cout << cr << "supported, set to " << std::round(output->sharpness() * 100) << "%"
+ << Qt::endl;
+ } else {
+ cout << cr << "unsupported" << Qt::endl;
+ }
+ cout << yellow << "\tAutomatic brightness: ";
+ if (output->capabilities() & KScreen::Output::Capability::AutomaticBrightness) {
+ cout << cr << "supported, " << (output->automaticBrightness() ? "enabled" : "disabled") << Qt::endl;
+ } else {
+ cout << cr << "unsupported" << Qt::endl;
+ }
+ cout << yellow << "\tAuto Rotate Policy: ";
+ if (output->capabilities() & KScreen::Output::Capability::AutoRotation) {
+ switch (output->autoRotatePolicy()) {
+ case KScreen::Output::AutoRotatePolicy::Never:
+ cout << cr << "never" << Qt::endl;
+ break;
+ case KScreen::Output::AutoRotatePolicy::InTabletMode:
+ cout << cr << "inTabletMode" << Qt::endl;
+ break;
+ case KScreen::Output::AutoRotatePolicy::Always:
+ cout << cr << "always" << Qt::endl;
+ }
+ } else {
+ cout << cr << "incapable" << Qt::endl;
+ }
+
+ cout << yellow << "\tAdaptive backlight modulation: ";
+ if (output->capabilities() & KScreen::Output::Capability::AbmLevel) {
+ cout << cr << "supported, set to " << output->abmLevel() << Qt::endl;
+ } else {
+ cout << cr << "unsupported" << Qt::endl;
+ }
+}
+
+QJsonObject outputAsJson(const KScreen::OutputPtr &output)
+{
+ const auto jsonMode = [&output](const KScreen::ModePtr &mode) -> QJsonValue {
+ return QJsonObject {
+ { u"id"_s, mode->id(), },
+ {
+ u"resolution"_s, QJsonObject {
+ { u"width"_s, mode->size().width() },
+ { u"height"_s, mode->size().height() },
+ }
+ },
+ { u"refreshRate"_s, mode->refreshRate() },
+ { u"current"_s, output->currentMode() == mode },
+ { u"preferred"_s, output->preferredMode() == mode },
+ };
+ };
+
+ QJsonObject status = {
+ { u"name"_s, output->name() },
+ { u"uuid"_s, output->uuid() },
+ { u"enabled"_s, output->isEnabled() },
+ { u"connected"_s, output->isConnected() },
+ { u"scale"_s, output->scale() },
+ {
+ u"position"_s, QJsonObject {
+ { u"x"_s, output->pos().x() },
+ { u"y"_s, output->pos().y() },
+ }
+ },
+ { u"rotation"_s, printableRotation(output->rotation()) },
+ { u"dimming"_s, output->dimming() },
+ { u"brightness"_s, output->brightness() },
+ { u"sdrBrightness"_s, int(output->sdrBrightness()) },
+ { u"sdrGamut"_s, output->sdrGamutWideness() },
+ { u"hdr"_s, output->isHdrEnabled() },
+ { u"wcg"_s, output->isWcgEnabled() },
+ { u"iccProfilePath"_s, output->iccProfilePath() },
+ { u"hdrIccProfilePath"_s, output->hdrIccProfilePath() },
+ { u"colorProfileSource"_s, printableColorProfileSource(output->colorProfileSource()) },
+ { u"hdrColorProfileSource"_s, printableColorProfileSource(output->hdrColorProfileSource()) },
+ { u"colorPowerTradeoff"_s, printableColorPowerTradeoff(output->colorPowerPreference()) },
+ { u"autoBrightness"_s, output->automaticBrightness() },
+ { u"replicaOf"_s, output->replicationSource() },
+ { u"sharpness"_s, std::round(100 * output->sharpness()) },
+ { u"overscan"_s, int(output->overscan()) },
+ { u"priority"_s, int(output->priority()) },
+ { u"primary"_s, output->priority() == 1 },
+ { u"vrrPolicy"_s, printableVrrPolicy(output->vrrPolicy()) },
+ { u"rgbRange"_s, printableRgbRange(output->rgbRange()) },
+ { u"autoRotate"_s, printableAutoRotatePolicy(output->autoRotatePolicy()) },
+ { u"ddcCiAllowed"_s, output->ddcCiAllowed() },
+ { u"maxbpc"_s, int(output->maxBitsPerColor()) },
+ { u"abm"_s, int(output->abmLevel()) },
+ { u"mode"_s, jsonMode(output->currentMode()) },
+ {
+ u"modes"_s,
+ [&output, &jsonMode]() -> QJsonValue {
+ QJsonArray ret;
+ for (const auto modes = output->modes(); const auto &mode : modes) {
+ ret.append(jsonMode(mode));
+ }
+ return ret;
+ }()
+ },
+ {
+ u"customModes"_s,
+ [&output]() -> QJsonValue {
+ QJsonArray ret;
+ for (const auto modes = output->customModes(); const auto &mode : modes) {
+ ret.append(QJsonObject {
+ {
+ u"resolution"_s, QJsonObject {
+ { u"width"_s, mode.size.width() },
+ { u"height"_s, mode.size.height() },
+ }
+ },
+ { u"refreshRate"_s, mode.refreshRate },
+ });
+ }
+ return ret;
+ }()
+ },
+ { u"edrPolicy"_s, printableEdrPolicy(output->edrPolicy()) },
+ {
+ u"capabilities"_s,
+ [&output]() -> QJsonValue {
+ const struct {
+ KScreen::Output::Capability capability;
+ QString name;
+ } mappings[] = {
+ { KScreen::Output::Capability::Overscan, u"overscan"_s },
+ { KScreen::Output::Capability::Vrr, u"vrr"_s },
+ { KScreen::Output::Capability::RgbRange, u"rgb-range"_s },
+ { KScreen::Output::Capability::HighDynamicRange, u"hdr"_s },
+ { KScreen::Output::Capability::WideColorGamut, u"wcg"_s },
+ { KScreen::Output::Capability::AutoRotation, u"auto-rotate"_s },
+ { KScreen::Output::Capability::IccProfile, u"icc"_s },
+ { KScreen::Output::Capability::BrightnessControl, u"brightness"_s },
+ { KScreen::Output::Capability::BuiltInColorProfile, u"builtin-color-profile"_s },
+ { KScreen::Output::Capability::DdcCi, u"ddc-ci"_s },
+ { KScreen::Output::Capability::MaxBitsPerColor, u"maxbpc"_s },
+ { KScreen::Output::Capability::ExtendedDynamicRange, u"edr"_s },
+ { KScreen::Output::Capability::SharpnessControl, u"sharpness"_s },
+ { KScreen::Output::Capability::CustomModes, u"custom-modes"_s },
+ { KScreen::Output::Capability::AutomaticBrightness, u"auto-brightness"_s },
+ { KScreen::Output::Capability::HdrIccProfile, u"hdr-icc"_s },
+ { KScreen::Output::Capability::AbmLevel, u"abm"_s },
+ };
+
+ const auto capabilities = output->capabilities();
+ QJsonArray ret;
+ for (const auto &mapping : mappings) {
+ if (capabilities & mapping.capability) {
+ ret.append(mapping.name);
+ }
+ }
+
+ return ret;
+ }()
+ }
+ };
+
+ if (const auto value = output->minBrightnessOverride()) {
+ status.insert(u"minBrightnessOverride"_s, *value);
+ }
+
+ if (const auto value = output->maxPeakBrightnessOverride()) {
+ status.insert(u"maxPeakBrightnessOverride"_s, *value);
+ }
+
+ if (const auto value = output->maxAverageBrightnessOverride()) {
+ status.insert(u"maxAverageBrightnessOverride"_s, *value);
+ }
+
+ return status;
+}
+
+void showOutputAsJson(const KScreen::OutputPtr &output)
+{
+ std::print(std::cout, "{}", QJsonDocument(outputAsJson(output)).toJson().toStdString());
+}
diff --git a/kscreenctl/util.h b/kscreenctl/util.h
new file mode 100644
index 00000000..1b769088
--- /dev/null
+++ b/kscreenctl/util.h
@@ -0,0 +1,69 @@
+/*
+ SPDX-FileCopyrightText: 2026 Vlad Zahorodnii <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+*/
+
+#pragma once
+
+#include <KScreen/Config>
+#include <KScreen/Mode>
+
+template <typename Func>
+struct FunctionPointer
+{
+};
+
+template <typename Ret, typename... Args>
+struct FunctionPointer<Ret (*)(Args...)>
+{
+ enum { ArgCount = sizeof...(Args) };
+};
+
+/**
+ * Returns the first Indices values in the specified @a tuple.
+ */
+template <typename Tuple, std::size_t... Indices>
+auto left(Tuple &&tuple, std::index_sequence<Indices...>)
+{
+ return std::forward_as_tuple(std::get<Indices>(tuple)...);
+}
+
+/**
+ * Calls the specified @a func with the given @a args. The arguments are applied partially. That
+ * is, if the specified function takes no arguments, it will be called without any args; if the
+ * function takes one argument, then the first value in the @a args will be passed, and so on.
+ */
+template <typename Func, typename... Args>
+auto wrap(Func func, Args... args)
+{
+ constexpr size_t argumentCount = FunctionPointer<Func>::ArgCount;
+ return std::apply(func, left(std::forward_as_tuple(args...),
+ std::make_index_sequence<argumentCount>()));
+}
+
+void fixupOutputPositions(KScreen::ConfigPtr config);
+void snapOutputAfterResize(KScreen::OutputPtr resizedOutput, const QSize &oldSize, KScreen::ConfigPtr config);
+
+int applyConfig(KScreen::ConfigPtr config);
+KScreen::OutputPtr outputByQuery(const KScreen::ConfigPtr &config, const QString &query);
+KScreen::ModePtr modeByQuery(const KScreen::OutputPtr &output, const QString &query);
+
+std::optional<KScreen::Output::ColorProfileSource> parseColorProfileSource(QStringView input);
+std::optional<bool> parseBool(QStringView value);
+std::optional<int> parseInt(QStringView input);
+std::optional<double> parseDouble(QStringView value);
+std::optional<double> parsePercents(QStringView input);
+
+QString printableAutoRotatePolicy(KScreen::Output::AutoRotatePolicy source);
+QString printableMode(const KScreen::OutputPtr &output, const KScreen::ModePtr &mode);
+QString printableEdrPolicy(KScreen::Output::EdrPolicy source);
+QString printableRgbRange(KScreen::Output::RgbRange source);
+QString printableVrrPolicy(KScreen::Output::VrrPolicy source);
+QString printableColorPowerTradeoff(KScreen::Output::ColorPowerTradeoff source);
+QString printableColorProfileSource(KScreen::Output::ColorProfileSource source);
+QString printableRotation(KScreen::Output::Rotation rotation);
+
+void showOutputAsText(const KScreen::OutputPtr &output);
+QJsonObject outputAsJson(const KScreen::OutputPtr &output);
+void showOutputAsJson(const KScreen::OutputPtr &output);