Skip to content

Commit

Permalink
feat: add command line argument to select/add tab with a channel (#5111)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerixyz authored Jan 20, 2024
1 parent acee654 commit 7951af6
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <channel>` (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)
Expand Down
41 changes: 41 additions & 0 deletions src/common/Args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

namespace {

using namespace chatterino;

template <class... Args>
QCommandLineOption hiddenOption(Args... args)
{
Expand Down Expand Up @@ -62,6 +64,29 @@ QStringList extractCommandLine(
return args;
}

std::optional<Args::Channel> 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 {
Expand Down Expand Up @@ -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,
Expand All @@ -110,6 +143,7 @@ Args::Args(const QApplication &app, const Paths &paths)
verboseOption,
safeModeOption,
channelLayout,
activateOption,
});

if (!parser.parse(app.arguments()))
Expand Down Expand Up @@ -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,
});
}

Expand Down
8 changes: 8 additions & 0 deletions src/common/Args.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "common/ProviderId.hpp"
#include "common/WindowDescriptors.hpp"

#include <QApplication>
Expand All @@ -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);

Expand All @@ -52,6 +59,7 @@ class Args
bool dontSaveSettings{};
bool dontLoadMainWindow{};
std::optional<WindowLayout> customChannelLayout;
std::optional<Channel> activateChannel;
bool verbose{};
bool safeMode{};

Expand Down
104 changes: 104 additions & 0 deletions src/common/WindowDescriptors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>::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
16 changes: 14 additions & 2 deletions src/common/WindowDescriptors.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "common/ProviderId.hpp"

#include <QJsonObject>
#include <QList>
#include <QRect>
Expand Down Expand Up @@ -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<WindowDescriptor> 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
6 changes: 6 additions & 0 deletions src/singletons/WindowManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 7951af6

Please sign in to comment.