#include "commandlineparser.h" #include #include #if defined(Q_OS_WIN) #include #endif static bool inRange(int value, int min, int max) { return value >= min && value <= max; } // This method returns key's value from QMap where the key is a QString. // Key matching is case insensitive. template static T mapValue(QMap map, QString key) { for(auto& item : map.toStdMap()) { if (QString::compare(item.first, key, Qt::CaseInsensitive) == 0) { return item.second; } } return T(); } class CommandLineParser : public QCommandLineParser { public: enum MessageType { Info, Error }; void setupCommonOptions() { setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); addHelpOption(); addVersionOption(); } void handleHelpAndVersionOptions() { if (isSet("help")) { showInfo(helpText()); } if (isSet("version")) { showVersion(); } } void handleUnknownOptions() { if (unknownOptionNames().length()) { showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", "))); } } void showMessage(QString message, MessageType type) const { #if defined(Q_OS_WIN) UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND; flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR); QString title = "Moonlight"; MessageBoxW(nullptr, reinterpret_cast(message.utf16()), reinterpret_cast(title.utf16()), flags); #endif message = message.endsWith('\n') ? message : message + '\n'; fputs(qPrintable(message), type == Info ? stdout : stderr); } [[ noreturn ]] void showInfo(QString message) const { showMessage(message, Info); exit(0); } [[ noreturn ]] void showError(QString message) const { showMessage(message + "\n\n" + helpText(), Error); exit(1); } int getIntOption(QString name) const { bool ok; int intValue = value(name).toInt(&ok); if (!ok) { showError(QString("Invalid %1 value: %2").arg(name, value(name))); } return intValue; } bool getToggleOptionValue(QString name, bool defaultValue) const { QRegularExpression re(QString("^(%1|no-%1)$").arg(name)); QStringList options = optionNames().filter(re); if (options.isEmpty()) { return defaultValue; } else { return options.last() == name; } } QString getChoiceOptionValue(QString name) const { if (!m_Choices[name].contains(value(name), Qt::CaseInsensitive)) { showError(QString("Invalid %1 choice: %2").arg(name, value(name))); } return value(name); } QPair getResolutionOptionValue(QString name) const { QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption); auto match = re.match(value(name)); if (!match.hasMatch()) { showError(QString("Invalid %1 format: %2").arg(name, value(name))); } return qMakePair(match.captured(1).toInt(), match.captured(2).toInt()); } void addFlagOption(QString name, QString descriptiveName) { addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName))); } void addToggleOption(QString name, QString descriptiveName) { addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName))); addOption(QCommandLineOption("no-" + name, QString("Do not use %1.").arg(descriptiveName))); } void addValueOption(QString name, QString descriptiveName) { addOption(QCommandLineOption(name, QString("Specify %1 to use.").arg(descriptiveName), name)); } void addChoiceOption(QString name, QString descriptiveName, QStringList choices) { addOption(QCommandLineOption(name, QString("Select %1: %2.").arg(descriptiveName, choices.join('/')), name)); m_Choices[name] = choices; } private: QMap m_Choices; }; GlobalCommandLineParser::GlobalCommandLineParser() { } GlobalCommandLineParser::~GlobalCommandLineParser() { } GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStringList &args) { CommandLineParser parser; parser.setupCommonOptions(); parser.setApplicationDescription( "\n" "Starts Moonlight normally if no arguments are given.\n" "\n" "Available actions:\n" " stream Start streaming an app\n" "\n" "See 'moonlight --help' for help of specific action." ); parser.addPositionalArgument("action", "Action to execute", ""); parser.parse(args); auto posArgs = parser.positionalArguments(); QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower(); if (action == "") { // This method will not return and terminates the process if --version // or --help is specified parser.handleHelpAndVersionOptions(); parser.handleUnknownOptions(); return NormalStartRequested; } else if (action == "stream") { return StreamRequested; } else { parser.showError(QString("Invalid action: %1").arg(action)); } } StreamCommandLineParser::StreamCommandLineParser() { m_WindowModeMap = { {"fullscreen", StreamingPreferences::WM_FULLSCREEN}, {"windowed", StreamingPreferences::WM_WINDOWED}, {"borderless", StreamingPreferences::WM_FULLSCREEN_DESKTOP}, }; m_AudioConfigMap = { {"auto", StreamingPreferences::AC_AUTO}, {"stereo", StreamingPreferences::AC_FORCE_STEREO}, {"surround", StreamingPreferences::AC_FORCE_SURROUND}, }; m_VideoCodecMap = { {"auto", StreamingPreferences::VCC_AUTO}, {"H.264", StreamingPreferences::VCC_FORCE_H264}, {"HEVC", StreamingPreferences::VCC_FORCE_HEVC}, }; m_VideoDecoderMap = { {"auto", StreamingPreferences::VDS_AUTO}, {"software", StreamingPreferences::VDS_FORCE_HARDWARE}, {"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE}, }; } StreamCommandLineParser::~StreamCommandLineParser() { } void StreamCommandLineParser::parse(const QStringList &args, StreamingPreferences *preferences) { CommandLineParser parser; parser.setupCommonOptions(); parser.setApplicationDescription( "\n" "Starts directly streaming a given app." ); parser.addPositionalArgument("stream", "Start stream"); // Add other arguments and options parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", ""); parser.addPositionalArgument("app", "App to stream", "\"\""); parser.addFlagOption("720", "1280x720 resolution"); parser.addFlagOption("1080", "1920x1080 resolution"); parser.addFlagOption("1440", "2560x1440 resolution"); parser.addFlagOption("4K", "3840x2160 resolution"); parser.addValueOption("resolution", "custom x resolution"); parser.addToggleOption("vsync", "V-Sync"); parser.addValueOption("fps", "FPS"); parser.addValueOption("bitrate", "bitrate in Kbps"); parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys()); parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys()); parser.addToggleOption("multi-controller", "multiple controller support"); parser.addToggleOption("mouse-acceleration", "mouse acceleration"); parser.addToggleOption("game-optimization", "game optimizations"); parser.addToggleOption("audio-on-host", "audio on host PC"); parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys()); parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys()); if (!parser.parse(args)) { parser.showError(parser.errorText()); } parser.handleUnknownOptions(); // Resolve display's width and height QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$"); QStringList resoOptions = parser.optionNames().filter(resolutionRexExp); bool displaySet = resoOptions.length(); if (displaySet) { QString name = resoOptions.last(); if (name == "720") { preferences->width = 1280; preferences->height = 720; displaySet = true; } else if (name == "1080") { preferences->width = 1920; preferences->height = 1080; displaySet = true; } else if (name == "1440") { preferences->width = 2560; preferences->height = 1440; displaySet = true; } else if (name == "4K") { preferences->width = 3840; preferences->height = 2160; displaySet = true; } else if (name == "resolution") { auto resolution = parser.getResolutionOptionValue(name); preferences->width = resolution.first; preferences->height = resolution.second; } } // Resolve --fps option if (parser.isSet("fps")) { preferences->fps = parser.getIntOption("fps"); if (!inRange(preferences->fps, 30, 120)) { parser.showError("FPS must be in range: 30 - 120"); } } // Resolve --bitrate option if (parser.isSet("bitrate")) { preferences->bitrateKbps = parser.getIntOption("bitrate"); if (!inRange(preferences->bitrateKbps, 500, 150000)) { parser.showError("Bitrate must be in range: 500 - 150000"); } } else if (displaySet || parser.isSet("fps")) { preferences->bitrateKbps = preferences->getDefaultBitrate( preferences->width, preferences->height, preferences->fps); } // Resolve --display option if (parser.isSet("display-mode")) { preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode")); } // Resolve --vsync and --no-vsync options preferences->enableVsync = parser.getToggleOptionValue("vsync", preferences->enableVsync); // Resolve --audio-config option if (parser.isSet("audio-config")) { preferences->audioConfig = mapValue(m_AudioConfigMap, parser.getChoiceOptionValue("audio-config")); } // Resolve --multi-controller and --no-multi-controller options preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController); // Resolve --mouse-acceleration and --no-mouse-acceleration options preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration); // Resolve --game-optimization and --no-game-optimization options preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations); // Resolve --audio-on-host and --no-audio-on-host options preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost); // Resolve --video-codec option if (parser.isSet("video-codec")) { preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec")); } // Resolve --video-decoder option if (parser.isSet("video-decoder")) { preferences->videoDecoderSelection = mapValue(m_VideoDecoderMap, parser.getChoiceOptionValue("video-decoder")); } // This method will not return and terminates the process if --version or // --help is specified parser.handleHelpAndVersionOptions(); // Verify that both host and app has been provided auto posArgs = parser.positionalArguments(); if (posArgs.length() < 2) { parser.showError("Host not provided"); } m_Host = parser.positionalArguments().at(1); if (posArgs.length() < 3) { parser.showError("App not provided"); } m_AppName = parser.positionalArguments().at(2); } QString StreamCommandLineParser::getHost() const { return m_Host; } QString StreamCommandLineParser::getAppName() const { return m_AppName; }