diff --git a/CHANGELOG.md b/CHANGELOG.md index cb753648620..2f3c37f9941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Minor: Improved color selection and display. (#5057) - Minor: Improved Streamlink documentation in the settings dialog. (#5076) - Minor: Normalized the input padding between light & dark themes. (#5095) +- Minor: Add `--activate ` (or `-a`) command line option to activate or add a Twitch channel. (#5111) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) diff --git a/src/common/Args.cpp b/src/common/Args.cpp index 145fe6da46a..eeebbfc201e 100644 --- a/src/common/Args.cpp +++ b/src/common/Args.cpp @@ -17,6 +17,8 @@ namespace { +using namespace chatterino; + template QCommandLineOption hiddenOption(Args... args) { @@ -62,6 +64,29 @@ QStringList extractCommandLine( return args; } +std::optional parseActivateOption(QString input) +{ + auto colon = input.indexOf(u':'); + if (colon >= 0) + { + auto ty = input.left(colon); + if (ty != u"t") + { + qCWarning(chatterinoApp).nospace() + << "Failed to parse active channel (unknown type: " << ty + << ")"; + return std::nullopt; + } + + input = input.mid(colon + 1); + } + + return Args::Channel{ + .provider = ProviderId::Twitch, + .name = input, + }; +} + } // namespace namespace chatterino { @@ -100,6 +125,14 @@ Args::Args(const QApplication &app, const Paths &paths) "If platform isn't specified, default is Twitch.", "t:channel1;t:channel2;..."); + QCommandLineOption activateOption( + {"a", "activate"}, + "Activate the tab with this channel or add one in the main " + "window.\nOnly Twitch is " + "supported at the moment (prefix: 't:').\nIf the platform isn't " + "specified, Twitch is assumed.", + "t:channel"); + parser.addOptions({ {{"V", "version"}, "Displays version information."}, crashRecoveryOption, @@ -110,6 +143,7 @@ Args::Args(const QApplication &app, const Paths &paths) verboseOption, safeModeOption, channelLayout, + activateOption, }); if (!parser.parse(app.arguments())) @@ -163,10 +197,17 @@ Args::Args(const QApplication &app, const Paths &paths) this->safeMode = true; } + if (parser.isSet(activateOption)) + { + this->activateChannel = + parseActivateOption(parser.value(activateOption)); + } + this->currentArguments_ = extractCommandLine(parser, { verboseOption, safeModeOption, channelLayout, + activateOption, }); } diff --git a/src/common/Args.hpp b/src/common/Args.hpp index 60f606f4e7a..b3eeb20ffe1 100644 --- a/src/common/Args.hpp +++ b/src/common/Args.hpp @@ -1,5 +1,6 @@ #pragma once +#include "common/ProviderId.hpp" #include "common/WindowDescriptors.hpp" #include @@ -26,12 +27,18 @@ class Paths; /// -v, --verbose /// -V, --version /// -c, --channels=t:channel1;t:channel2;... +/// -a, --activate=t:channel /// --safe-mode /// /// See documentation on `QGuiApplication` for documentation on Qt arguments like -platform. class Args { public: + struct Channel { + ProviderId provider; + QString name; + }; + Args() = default; Args(const QApplication &app, const Paths &paths); @@ -52,6 +59,7 @@ class Args bool dontSaveSettings{}; bool dontLoadMainWindow{}; std::optional customChannelLayout; + std::optional activateChannel; bool verbose{}; bool safeMode{}; diff --git a/src/common/WindowDescriptors.cpp b/src/common/WindowDescriptors.cpp index 902de665f72..e08069b2cc4 100644 --- a/src/common/WindowDescriptors.cpp +++ b/src/common/WindowDescriptors.cpp @@ -229,4 +229,108 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) return layout; } +void WindowLayout::activateOrAddChannel(ProviderId provider, + const QString &name) +{ + if (provider != ProviderId::Twitch || name.startsWith(u'/') || + name.startsWith(u'$')) + { + qCWarning(chatterinoWindowmanager) + << "Only twitch channels can be set as active"; + return; + } + + auto mainWindow = std::find_if(this->windows_.begin(), this->windows_.end(), + [](const auto &win) { + return win.type_ == WindowType::Main; + }); + + if (mainWindow == this->windows_.end()) + { + this->windows_.emplace_back(WindowDescriptor{ + .type_ = WindowType::Main, + .geometry_ = {-1, -1, -1, -1}, + .tabs_ = + { + TabDescriptor{ + .selected_ = true, + .rootNode_ = SplitNodeDescriptor{{ + .type_ = "twitch", + .channelName_ = name, + }}, + }, + }, + }); + return; + } + + TabDescriptor *bestTab = nullptr; + // The tab score is calculated as follows: + // +2 for every split + // +1 if the desired split has filters + // Thus lower is better and having one split of a channel is preferred over multiple + size_t bestTabScore = std::numeric_limits::max(); + + for (auto &tab : mainWindow->tabs_) + { + tab.selected_ = false; + + if (!tab.rootNode_) + { + continue; + } + + // recursive visitor + struct Visitor { + const QString &spec; + size_t score = 0; + bool hasChannel = false; + + void operator()(const SplitNodeDescriptor &split) + { + this->score += 2; + if (split.channelName_ == this->spec) + { + hasChannel = true; + if (!split.filters_.empty()) + { + this->score += 1; + } + } + } + + void operator()(const ContainerNodeDescriptor &container) + { + for (const auto &item : container.items_) + { + std::visit(*this, item); + } + } + } visitor{name}; + + std::visit(visitor, *tab.rootNode_); + + if (visitor.hasChannel && visitor.score < bestTabScore) + { + bestTab = &tab; + bestTabScore = visitor.score; + } + } + + if (bestTab) + { + bestTab->selected_ = true; + return; + } + + TabDescriptor tab{ + .selected_ = true, + .rootNode_ = SplitNodeDescriptor{{ + .type_ = "twitch", + .channelName_ = name, + }}, + }; + mainWindow->tabs_.emplace_back(tab); +} + } // namespace chatterino diff --git a/src/common/WindowDescriptors.hpp b/src/common/WindowDescriptors.hpp index 0a7a72d1695..b35edf1554e 100644 --- a/src/common/WindowDescriptors.hpp +++ b/src/common/WindowDescriptors.hpp @@ -1,5 +1,7 @@ #pragma once +#include "common/ProviderId.hpp" + #include #include #include @@ -95,12 +97,22 @@ struct WindowDescriptor { class WindowLayout { public: - static WindowLayout loadFromFile(const QString &path); - // A complete window layout has a single emote popup position that is shared among all windows QPoint emotePopupPos_; std::vector windows_; + + /// Selects the split containing the channel specified by @a name for the specified + /// @a provider. Currently, only Twitch is supported as the provider + /// and special channels (such as /mentions) are ignored. + /// + /// Tabs with fewer splits are preferred. + /// Channels without filters are preferred. + /// + /// If no split with the channel exists, a new one is added. + /// If no window exists, a new one is added. + void activateOrAddChannel(ProviderId provider, const QString &name); + static WindowLayout loadFromFile(const QString &path); }; } // namespace chatterino diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 557e2f0a75f..32f7bfa2bb6 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -365,6 +365,12 @@ void WindowManager::initialize(Settings &settings, const Paths &paths) windowLayout = this->loadWindowLayoutFromFile(); } + auto desired = getIApp()->getArgs().activateChannel; + if (desired) + { + windowLayout.activateOrAddChannel(desired->provider, desired->name); + } + this->emotePopupPos_ = windowLayout.emotePopupPos_; this->applyWindowLayout(windowLayout);