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 }