File indexing completed on 2024-04-28 16:49:43

0001 /*
0002  *  SPDX-FileCopyrightText: 2014-2016 Sebastian Kügler <sebas@kde.org>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.1-or-later
0005  */
0006 
0007 #include "doctor.h"
0008 #include "mode.h"
0009 #include <dpms.h>
0010 
0011 #include <QCollator>
0012 #include <QCoreApplication>
0013 #include <QDateTime>
0014 #include <QFile>
0015 #include <QGuiApplication>
0016 #include <QJsonArray>
0017 #include <QJsonDocument>
0018 #include <QJsonObject>
0019 #include <QLoggingCategory>
0020 #include <QRect>
0021 #include <QScreen>
0022 #include <QStandardPaths>
0023 
0024 #include <utility>
0025 
0026 #include "../backendmanager_p.h"
0027 #include "../config.h"
0028 #include "../configoperation.h"
0029 #include "../getconfigoperation.h"
0030 #include "../log.h"
0031 #include "../output.h"
0032 #include "../setconfigoperation.h"
0033 
0034 Q_LOGGING_CATEGORY(KSCREEN_DOCTOR, "kscreen.doctor")
0035 
0036 static QTextStream cout(stdout);
0037 static QTextStream cerr(stderr);
0038 
0039 const static QString green = QStringLiteral("\033[01;32m");
0040 const static QString red = QStringLiteral("\033[01;31m");
0041 const static QString yellow = QStringLiteral("\033[01;33m");
0042 const static QString blue = QStringLiteral("\033[01;34m");
0043 const static QString bold = QStringLiteral("\033[01;39m");
0044 const static QString cr = QStringLiteral("\033[0;0m");
0045 
0046 namespace KScreen
0047 {
0048 namespace ConfigSerializer
0049 {
0050 // Exported private symbol in configserializer_p.h in KScreen
0051 extern QJsonObject serializeConfig(const KScreen::ConfigPtr &config);
0052 }
0053 }
0054 
0055 using namespace KScreen;
0056 
0057 Doctor::Doctor(QObject *parent)
0058     : QObject(parent)
0059     , m_config(nullptr)
0060     , m_changed(false)
0061     , m_dpmsClient(nullptr)
0062 {
0063 }
0064 
0065 Doctor::~Doctor()
0066 {
0067 }
0068 
0069 void Doctor::start(QCommandLineParser *parser)
0070 {
0071     m_parser = parser;
0072     if (m_parser->isSet(QStringLiteral("info"))) {
0073         showBackends();
0074     }
0075     if (parser->isSet(QStringLiteral("json")) || parser->isSet(QStringLiteral("outputs")) || !m_outputArgs.isEmpty()) {
0076         KScreen::GetConfigOperation *op = new KScreen::GetConfigOperation();
0077         connect(op, &KScreen::GetConfigOperation::finished, this, [this](KScreen::ConfigOperation *op) {
0078             configReceived(op);
0079         });
0080         return;
0081     }
0082     if (m_parser->isSet(QStringLiteral("dpms"))) {
0083         if (!QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
0084             cerr << "DPMS is only supported on Wayland." << Qt::endl;
0085             // We need to kick the event loop, otherwise .quit() hangs
0086             QTimer::singleShot(0, qApp->quit);
0087             return;
0088         }
0089 
0090         m_dpmsClient = new Dpms(this);
0091         auto screens = qGuiApp->screens();
0092         if (m_parser->isSet(QStringLiteral("dpms-excluded"))) {
0093             const auto excludedConnectors = m_parser->values(QStringLiteral("dpms-excluded"));
0094             auto it = std::remove_if(screens.begin(), screens.end(), [&excludedConnectors](QScreen *screen) {
0095                 return excludedConnectors.contains(screen->name());
0096             });
0097             screens.erase(it, screens.end());
0098         }
0099 
0100         connect(m_dpmsClient, &Dpms::hasPendingChangesChanged, qGuiApp, [](bool hasChanges) {
0101             if (!hasChanges) {
0102                 // We need to hit the event loop, otherwise .quit() hangs
0103                 QTimer::singleShot(0, qApp->quit);
0104             }
0105         });
0106 
0107         const QString dpmsArg = m_parser->value(QStringLiteral("dpms"));
0108         if (dpmsArg == QLatin1String("show")) {
0109         } else {
0110             auto performSwitch = [this, dpmsArg, screens](bool supported) {
0111                 if (!supported) {
0112                     cerr << "DPMS not supported in this system";
0113                     qGuiApp->quit();
0114                     return;
0115                 }
0116 
0117                 if (dpmsArg == QLatin1String("off")) {
0118                     m_dpmsClient->switchMode(KScreen::Dpms::Off, screens);
0119                 } else if (dpmsArg == QLatin1String("on")) {
0120                     m_dpmsClient->switchMode(KScreen::Dpms::On, screens);
0121                 } else {
0122                     cerr << "--dpms argument not understood (" << dpmsArg << ")";
0123                 }
0124             };
0125             if (m_dpmsClient->isSupported()) {
0126                 performSwitch(m_dpmsClient->isSupported());
0127             } else {
0128                 connect(m_dpmsClient, &Dpms::supportedChanged, this, performSwitch);
0129             }
0130         }
0131         return;
0132     }
0133 
0134     if (m_parser->isSet(QStringLiteral("log"))) {
0135         const QString logmsg = m_parser->value(QStringLiteral("log"));
0136         if (!Log::instance()->enabled()) {
0137             qCWarning(KSCREEN_DOCTOR) << "Logging is disabled, unset KSCREEN_LOGGING in your environment.";
0138         } else {
0139             Log::log(logmsg);
0140         }
0141     }
0142     // We need to kick the event loop, otherwise .quit() hangs
0143     QTimer::singleShot(0, qApp->quit);
0144 }
0145 
0146 void Doctor::showBackends() const
0147 {
0148     cout << "Environment: " << Qt::endl;
0149     auto env_kscreen_backend = qEnvironmentVariable("KSCREEN_BACKEND", QStringLiteral("[not set]"));
0150     cout << "  * KSCREEN_BACKEND           : " << env_kscreen_backend << Qt::endl;
0151     auto env_kscreen_backend_inprocess = qEnvironmentVariable("KSCREEN_BACKEND_INPROCESS", QStringLiteral("[not set]"));
0152     cout << "  * KSCREEN_BACKEND_INPROCESS : " << env_kscreen_backend_inprocess << Qt::endl;
0153     auto env_kscreen_logging = qEnvironmentVariable("KSCREEN_LOGGING", QStringLiteral("[not set]"));
0154     cout << "  * KSCREEN_LOGGING           : " << env_kscreen_logging << Qt::endl;
0155 
0156     cout << "Logging to                : " << (Log::instance()->enabled() ? Log::instance()->logFile() : QStringLiteral("[logging disabled]")) << Qt::endl;
0157     const auto backends = BackendManager::instance()->listBackends();
0158     auto preferred = BackendManager::instance()->preferredBackend();
0159     cout << "Preferred KScreen backend : " << green << preferred.fileName() << cr << Qt::endl;
0160     cout << "Available KScreen backends:" << Qt::endl;
0161     for (const QFileInfo &f : backends) {
0162         auto c = blue;
0163         if (preferred == f) {
0164             c = green;
0165         }
0166         cout << "  * " << c << f.fileName() << cr << ": " << f.absoluteFilePath() << Qt::endl;
0167     }
0168     cout << Qt::endl;
0169 }
0170 
0171 void Doctor::setOptionList(const QStringList &outputArgs)
0172 {
0173     m_outputArgs = outputArgs;
0174 }
0175 
0176 OutputPtr Doctor::findOutput(const QString &query)
0177 {
0178     // try as an output name or ID
0179     for (const auto &output : m_config->outputs()) {
0180         if (output->name() == query) {
0181             return output;
0182         }
0183     }
0184     bool ok;
0185     int id = query.toInt(&ok);
0186     if (!ok) {
0187         cerr << "Output with name " << query << " not found." << Qt::endl;
0188         return OutputPtr();
0189     }
0190 
0191     if (m_config->outputs().contains(id)) {
0192         return m_config->outputs()[id];
0193     } else {
0194         cerr << "Output with id " << id << " not found." << Qt::endl;
0195         return OutputPtr();
0196     }
0197 }
0198 
0199 void Doctor::parseOutputArgs()
0200 {
0201     // qCDebug(KSCREEN_DOCTOR) << "POSARGS" << m_positionalArgs;
0202     for (const QString &op : std::as_const(m_outputArgs)) {
0203         auto ops = op.split(QLatin1Char('.'));
0204         if (ops.count() > 2) {
0205             bool ok;
0206             if (ops[0] == QLatin1String("output")) {
0207                 OutputPtr output = findOutput(ops[1]);
0208                 if (!output) {
0209                     qApp->exit(3);
0210                     return;
0211                 }
0212                 int output_id = output->id();
0213 
0214                 const QString subcmd = ops.length() > 2 ? ops[2] : QString();
0215 
0216                 if (ops.count() == 3 && subcmd == QLatin1String("primary")) {
0217                     setPrimary(output);
0218                 } else if (ops.count() == 4 && subcmd == QLatin1String("priority")) {
0219                     uint32_t priority = ops[3].toUInt(&ok);
0220                     if (!ok || priority > 100) {
0221                         qCWarning(KSCREEN_DOCTOR) << "Wrong input: allowed values for priority are from 1 to 100";
0222                         qApp->exit(5);
0223                         return;
0224                     }
0225                     setPriority(output, priority);
0226                 } else if (ops.count() == 3 && subcmd == QLatin1String("enable")) {
0227                     setEnabled(output, true);
0228                 } else if (ops.count() == 3 && subcmd == QLatin1String("disable")) {
0229                     setEnabled(output, false);
0230                 } else if (ops.count() == 4 && subcmd == QLatin1String("mode")) {
0231                     QString mode_id = ops[3];
0232                     // set mode
0233                     if (!setMode(output, mode_id)) {
0234                         qApp->exit(9);
0235                         return;
0236                     }
0237                     qCDebug(KSCREEN_DOCTOR) << "Output" << output_id << "set mode" << mode_id;
0238 
0239                 } else if (ops.count() == 4 && subcmd == QLatin1String("position")) {
0240                     QStringList _pos = ops[3].split(QLatin1Char(','));
0241                     if (_pos.count() != 2) {
0242                         qCWarning(KSCREEN_DOCTOR) << "Invalid position:" << ops[3];
0243                         qApp->exit(5);
0244                         return;
0245                     }
0246                     int x = _pos[0].toInt(&ok);
0247                     int y = _pos[1].toInt(&ok);
0248                     if (!ok) {
0249                         cerr << "Unable to parse position: " << ops[3] << Qt::endl;
0250                         qApp->exit(5);
0251                         return;
0252                     }
0253 
0254                     QPoint p(x, y);
0255                     qCDebug(KSCREEN_DOCTOR) << "Output position" << p;
0256                     setPosition(output, p);
0257 
0258                 } else if ((ops.count() == 4 || ops.count() == 5) && subcmd == QLatin1String("scale")) {
0259                     // be lenient about . vs. comma as separator
0260                     qreal scale = ops[3].replace(QLatin1Char(','), QLatin1Char('.')).toDouble(&ok);
0261                     if (ops.count() == 5) {
0262                         const QString dbl = ops[3] + QStringLiteral(".") + ops[4];
0263                         scale = dbl.toDouble(&ok);
0264                     };
0265                     // set scale
0266                     if (!ok || qFuzzyCompare(scale, 0.0)) {
0267                         qCDebug(KSCREEN_DOCTOR) << "Could not set scale " << scale << " to output " << output_id;
0268                         qApp->exit(9);
0269                         return;
0270                     }
0271                     setScale(output, scale);
0272                 } else if ((ops.count() == 4) && (subcmd == QLatin1String("orientation") || subcmd == QStringLiteral("rotation"))) {
0273                     const QString _rotation = ops[3].toLower();
0274                     bool ok = false;
0275                     const QHash<QString, KScreen::Output::Rotation> rotationMap({{QStringLiteral("none"), KScreen::Output::None},
0276                                                                                  {QStringLiteral("normal"), KScreen::Output::None},
0277                                                                                  {QStringLiteral("left"), KScreen::Output::Left},
0278                                                                                  {QStringLiteral("right"), KScreen::Output::Right},
0279                                                                                  {QStringLiteral("inverted"), KScreen::Output::Inverted}});
0280                     KScreen::Output::Rotation rot = KScreen::Output::None;
0281                     // set orientation
0282                     if (rotationMap.contains(_rotation)) {
0283                         ok = true;
0284                         rot = rotationMap[_rotation];
0285                     }
0286                     if (!ok) {
0287                         qCDebug(KSCREEN_DOCTOR) << "Could not set orientation " << _rotation << " to output " << output_id;
0288                         qApp->exit(9);
0289                         return;
0290                     }
0291                     setRotation(output, rot);
0292                 } else if (ops.count() == 4 && subcmd == QLatin1String("overscan")) {
0293                     const uint32_t overscan = ops[3].toInt();
0294                     if (overscan > 100) {
0295                         qCWarning(KSCREEN_DOCTOR) << "Wrong input: allowed values for overscan are from 0 to 100";
0296                         qApp->exit(9);
0297                         return;
0298                     }
0299                     setOverscan(output, overscan);
0300                 } else if (ops.count() == 4 && subcmd == QLatin1String("vrrpolicy")) {
0301                     const QString _policy = ops[3].toLower();
0302                     KScreen::Output::VrrPolicy policy;
0303                     if (_policy == QStringLiteral("never")) {
0304                         policy = KScreen::Output::VrrPolicy::Never;
0305                     } else if (_policy == QStringLiteral("always")) {
0306                         policy = KScreen::Output::VrrPolicy::Always;
0307                     } else if (_policy == QStringLiteral("automatic")) {
0308                         policy = KScreen::Output::VrrPolicy::Automatic;
0309                     } else {
0310                         qCDebug(KSCREEN_DOCTOR) << "Wrong input: Only allowed values are \"never\", \"always\" and \"automatic\"";
0311                         qApp->exit(9);
0312                         return;
0313                     }
0314                     setVrrPolicy(output, policy);
0315                 } else if (ops.count() == 4 && subcmd == QLatin1String("rgbrange")) {
0316                     const QString _range = ops[3].toLower();
0317                     KScreen::Output::RgbRange range;
0318                     if (_range == QStringLiteral("automatic")) {
0319                         range = KScreen::Output::RgbRange::Automatic;
0320                     } else if (_range == QStringLiteral("full")) {
0321                         range = KScreen::Output::RgbRange::Full;
0322                     } else if (_range == QStringLiteral("limited")) {
0323                         range = KScreen::Output::RgbRange::Limited;
0324                     } else {
0325                         qCDebug(KSCREEN_DOCTOR) << "Wrong input: Only allowed values for rgbrange are \"automatic\", \"full\" and \"limited\"";
0326                         qApp->exit(9);
0327                         return;
0328                     }
0329                     setRgbRange(output, range);
0330                 } else {
0331                     cerr << "Unable to parse arguments: " << op << Qt::endl;
0332                     qApp->exit(2);
0333                     return;
0334                 }
0335             }
0336         }
0337     }
0338 }
0339 
0340 void Doctor::configReceived(KScreen::ConfigOperation *op)
0341 {
0342     m_config = op->config();
0343 
0344     if (!m_config) {
0345         qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
0346         return;
0347     }
0348 
0349     if (m_parser->isSet(QStringLiteral("json"))) {
0350         showJson();
0351         qApp->quit();
0352     }
0353     if (m_parser->isSet(QStringLiteral("outputs"))) {
0354         showOutputs();
0355         qApp->quit();
0356     }
0357 
0358     parseOutputArgs();
0359 
0360     if (m_changed) {
0361         applyConfig();
0362         m_changed = false;
0363     }
0364 }
0365 
0366 void Doctor::showOutputs() const
0367 {
0368     QHash<KScreen::Output::Type, QString> typeString;
0369     typeString[KScreen::Output::Unknown] = QStringLiteral("Unknown");
0370     typeString[KScreen::Output::VGA] = QStringLiteral("VGA");
0371     typeString[KScreen::Output::DVI] = QStringLiteral("DVI");
0372     typeString[KScreen::Output::DVII] = QStringLiteral("DVII");
0373     typeString[KScreen::Output::DVIA] = QStringLiteral("DVIA");
0374     typeString[KScreen::Output::DVID] = QStringLiteral("DVID");
0375     typeString[KScreen::Output::HDMI] = QStringLiteral("HDMI");
0376     typeString[KScreen::Output::Panel] = QStringLiteral("Panel");
0377     typeString[KScreen::Output::TV] = QStringLiteral("TV");
0378     typeString[KScreen::Output::TVComposite] = QStringLiteral("TVComposite");
0379     typeString[KScreen::Output::TVSVideo] = QStringLiteral("TVSVideo");
0380     typeString[KScreen::Output::TVComponent] = QStringLiteral("TVComponent");
0381     typeString[KScreen::Output::TVSCART] = QStringLiteral("TVSCART");
0382     typeString[KScreen::Output::TVC4] = QStringLiteral("TVC4");
0383     typeString[KScreen::Output::DisplayPort] = QStringLiteral("DisplayPort");
0384 
0385     QCollator collator;
0386     collator.setNumericMode(true);
0387 
0388     for (const auto &output : m_config->outputs()) {
0389         cout << green << "Output: " << cr << output->id() << " " << output->name();
0390         cout << " " << (output->isEnabled() ? green + QStringLiteral("enabled") : red + QStringLiteral("disabled")) << cr;
0391         cout << " " << (output->isConnected() ? green + QStringLiteral("connected") : red + QStringLiteral("disconnected")) << cr;
0392         cout << " " << (output->isEnabled() ? green : red) + QStringLiteral("priority ") << output->priority() << cr;
0393         auto _type = typeString[output->type()];
0394         cout << " " << yellow << (_type.isEmpty() ? QStringLiteral("UnmappedOutputType") : _type);
0395         cout << blue << " Modes: " << cr;
0396 
0397         const auto modes = output->modes();
0398         auto modeKeys = modes.keys();
0399         std::sort(modeKeys.begin(), modeKeys.end(), collator);
0400 
0401         for (const auto &key : modeKeys) {
0402             auto mode = *modes.find(key);
0403 
0404             auto name = QStringLiteral("%1x%2@%3")
0405                             .arg(QString::number(mode->size().width()), QString::number(mode->size().height()), QString::number(qRound(mode->refreshRate())));
0406             if (mode == output->currentMode()) {
0407                 name = green + name + QLatin1Char('*') + cr;
0408             }
0409             if (mode == output->preferredMode()) {
0410                 name = name + QLatin1Char('!');
0411             }
0412             cout << mode->id() << ":" << name << " ";
0413         }
0414         const auto g = output->geometry();
0415         cout << yellow << "Geometry: " << cr << g.x() << "," << g.y() << " " << g.width() << "x" << g.height() << " ";
0416         cout << yellow << "Scale: " << cr << output->scale() << " ";
0417         cout << yellow << "Rotation: " << cr << output->rotation() << " ";
0418         cout << yellow << "Overscan: " << cr << output->overscan() << " ";
0419         cout << yellow << "Vrr: ";
0420         if (output->capabilities() & Output::Capability::Vrr) {
0421             switch (output->vrrPolicy()) {
0422             case Output::VrrPolicy::Never:
0423                 cout << cr << "Never ";
0424                 break;
0425             case Output::VrrPolicy::Automatic:
0426                 cout << cr << "Automatic ";
0427                 break;
0428             case Output::VrrPolicy::Always:
0429                 cout << cr << "Always ";
0430             }
0431         } else {
0432             cout << cr << "incapable ";
0433         }
0434         cout << yellow << "RgbRange: ";
0435         if (output->capabilities() & Output::Capability::RgbRange) {
0436             switch (output->rgbRange()) {
0437             case Output::RgbRange::Automatic:
0438                 cout << cr << "Automatic";
0439                 break;
0440             case Output::RgbRange::Full:
0441                 cout << cr << "Full";
0442                 break;
0443             case Output::RgbRange::Limited:
0444                 cout << cr << "Limited";
0445             }
0446         } else {
0447             cout << cr << "unknown";
0448         }
0449         cout << cr << Qt::endl;
0450     }
0451 }
0452 
0453 void Doctor::showJson() const
0454 {
0455     QJsonDocument doc(KScreen::ConfigSerializer::serializeConfig(m_config));
0456     cout << doc.toJson(QJsonDocument::Indented);
0457 }
0458 
0459 void Doctor::setEnabled(OutputPtr output, bool enable)
0460 {
0461     cout << (enable ? "Enabling " : "Disabling ") << "output " << output->id() << Qt::endl;
0462     output->setEnabled(enable);
0463     m_changed = true;
0464 }
0465 
0466 void Doctor::setPosition(OutputPtr output, const QPoint &pos)
0467 {
0468     qCDebug(KSCREEN_DOCTOR) << "Set output position" << pos;
0469     output->setPos(pos);
0470     m_changed = true;
0471 }
0472 
0473 KScreen::ModePtr Doctor::findMode(OutputPtr output, const QString &query)
0474 {
0475     for (const KScreen::ModePtr &mode : output->modes()) {
0476         auto name = QStringLiteral("%1x%2@%3")
0477                         .arg(QString::number(mode->size().width()), QString::number(mode->size().height()), QString::number(qRound(mode->refreshRate())));
0478         if (mode->id() == query || name == query) {
0479             qCDebug(KSCREEN_DOCTOR) << "Taddaaa! Found mode" << mode->id() << name;
0480             return mode;
0481         }
0482     }
0483     cout << "Output mode " << query << " not found." << Qt::endl;
0484     return ModePtr();
0485 }
0486 
0487 bool Doctor::setMode(OutputPtr output, const QString &query)
0488 {
0489     // find mode
0490     const KScreen::ModePtr mode = findMode(output, query);
0491     if (!mode) {
0492         return false;
0493     }
0494     output->setCurrentModeId(mode->id());
0495     m_changed = true;
0496     return true;
0497 }
0498 
0499 void Doctor::setScale(OutputPtr output, qreal scale)
0500 {
0501     output->setScale(scale);
0502     m_changed = true;
0503 }
0504 
0505 void Doctor::setRotation(OutputPtr output, KScreen::Output::Rotation rot)
0506 {
0507     output->setRotation(rot);
0508     m_changed = true;
0509 }
0510 
0511 void Doctor::setOverscan(OutputPtr output, uint32_t overscan)
0512 {
0513     output->setOverscan(overscan);
0514     m_changed = true;
0515 }
0516 
0517 void Doctor::setVrrPolicy(OutputPtr output, KScreen::Output::VrrPolicy policy)
0518 {
0519     output->setVrrPolicy(policy);
0520     m_changed = true;
0521 }
0522 
0523 void Doctor::setRgbRange(OutputPtr output, KScreen::Output::RgbRange rgbRange)
0524 {
0525     output->setRgbRange(rgbRange);
0526     m_changed = true;
0527 }
0528 
0529 void KScreen::Doctor::setPrimary(OutputPtr output)
0530 {
0531     setPriority(output, 1);
0532 }
0533 
0534 void KScreen::Doctor::setPriority(OutputPtr output, uint32_t priority)
0535 {
0536     m_config->setOutputPriority(output, priority);
0537     m_changed = true;
0538 }
0539 
0540 void Doctor::applyConfig()
0541 {
0542     if (!m_changed) {
0543         return;
0544     }
0545     auto setop = new SetConfigOperation(m_config, this);
0546     setop->exec();
0547     qCDebug(KSCREEN_DOCTOR) << "setop exec returned" << m_config;
0548     qApp->exit(0);
0549 }