diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index e49d300aa..9eebe3bd9 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -18,10 +18,13 @@ # include # include +# include # include # include # include # include +# include +# include # include struct lua_State; @@ -37,6 +40,25 @@ struct SignalCallback; namespace chatterino { +/// A plugin that hasn't been loaded. +/// +/// Most likely, its metadata is invalid. +class UnloadedPlugin +{ +public: + QString id; + PluginMeta meta; + QDir loadDirectory; + + UnloadedPlugin(QString id_, PluginMeta meta_, const QDir &loadDirectory_) + : id(std::move(id_)) + , meta(std::move(meta_)) + , loadDirectory(loadDirectory_) + { + } +}; + +/// A plugin with a valid lua state class Plugin { public: @@ -49,8 +71,12 @@ public: , meta(std::move(meta)) , loadDirectory_(loadDirectory) , state_(state) - , selfRef_(state ? this : nullptr) + , selfRef_(this) { + // The PluginMeta here must be valid, otherwise it should be initialized + // as an UnloadedPlugin + assert(this->meta.isValid()); + assert(this->state_ != nullptr); } ~Plugin(); @@ -148,5 +174,10 @@ private: friend class PluginController; friend class PluginControllerAccess; // this is for tests }; + +using PluginPtr = std::unique_ptr; +using AnyPlugin = std::variant; + } // namespace chatterino + #endif diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index c91f119a7..2da2607ab 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -32,6 +32,7 @@ # include "singletons/Paths.hpp" # include "singletons/Settings.hpp" # include "singletons/WindowManager.hpp" +# include "util/Variant.hpp" # include "widgets/splits/SplitContainer.hpp" # include "widgets/Window.hpp" @@ -131,9 +132,10 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir) { qCWarning(chatterinoLua) << "- " << why; } - auto plugin = std::make_unique(pluginDir.dirName(), nullptr, - meta, pluginDir); - this->plugins_.insert({pluginDir.dirName(), std::move(plugin)}); + + this->plugins_.insert( + {pluginDir.dirName(), + UnloadedPlugin{pluginDir.dirName(), meta, pluginDir}}); return false; } this->load(index, pluginDir, meta); @@ -357,11 +359,26 @@ bool PluginController::reload(const QString &id) return false; } - for (const auto &[cmd, _] : it->second->ownedCommands) + if (const auto *oPlugin = std::get_if(&it->second)) { - getApp()->getCommands()->unregisterPluginCommand(cmd); + const auto &plugin = *oPlugin; + + for (const auto &[cmd, _] : plugin->ownedCommands) + { + getApp()->getCommands()->unregisterPluginCommand(cmd); + } } - QDir loadDir = it->second->loadDirectory_; + + const auto loadDir = std::visit(variant::Overloaded{ + [&](const PluginPtr &plugin) { + return plugin->loadDirectory_; + }, + [&](const UnloadedPlugin &plugin) { + return plugin.loadDirectory; + }, + }, + it->second); + // Since Plugin owns the state, it will clean up everything related to it this->plugins_.erase(id); this->queueChangeNotification(); @@ -372,8 +389,15 @@ bool PluginController::reload(const QString &id) QString PluginController::tryExecPluginCommand(const QString &commandName, const CommandContext &ctx) { - for (auto &[name, plugin] : this->plugins_) + for (const auto &[name, anyPlugin] : this->allPlugins()) { + const auto *oPl = std::get_if(&anyPlugin); + if (oPl == nullptr) + { + continue; + } + const auto &plugin = *oPl; + if (auto it = plugin->ownedCommands.find(commandName); it != plugin->ownedCommands.end()) { @@ -421,8 +445,15 @@ Plugin *PluginController::getPluginByStatePtr(lua_State *L) auto *mainL = lua_tothread(L, -1); lua_pop(L, 1); L = mainL; - for (auto &[name, plugin] : this->plugins_) + for (const auto &[name, anyPlugin] : this->allPlugins()) { + const auto *oPl = std::get_if(&anyPlugin); + if (oPl == nullptr) + { + continue; + } + const auto &plugin = *oPl; + if (plugin->state_ == L) { return plugin.get(); @@ -431,8 +462,19 @@ Plugin *PluginController::getPluginByStatePtr(lua_State *L) return nullptr; } -const std::map> &PluginController::plugins() - const +void PluginController::forEachPlugin( + FunctionRef &)> cb) const +{ + for (const auto &[_, anyPlugin] : this->allPlugins()) + { + if (const auto *plugin = std::get_if(&anyPlugin)) + { + cb(*plugin); + } + } +} + +const std::map &PluginController::allPlugins() const { return this->plugins_; } @@ -443,8 +485,16 @@ std::pair PluginController::updateCustomCompletions( { QStringList results; - for (const auto &[name, pl] : this->plugins()) + for (const auto &[name, anyPlugin] : this->allPlugins()) { + const auto *oPl = std::get_if(&anyPlugin); + if (oPl == nullptr) + { + continue; + } + + const auto &pl = *oPl; + if (!pl->error().isNull() || pl->state_ == nullptr) { continue; diff --git a/src/controllers/plugins/PluginController.hpp b/src/controllers/plugins/PluginController.hpp index 0bb268938..4533526a7 100644 --- a/src/controllers/plugins/PluginController.hpp +++ b/src/controllers/plugins/PluginController.hpp @@ -9,6 +9,7 @@ # include "common/websockets/WebSocketPool.hpp" # include "controllers/commands/CommandContext.hpp" # include "controllers/plugins/Plugin.hpp" +# include "util/FunctionRef.hpp" # include # include @@ -45,8 +46,11 @@ public: // This is required to be public because of c functions Plugin *getPluginByStatePtr(lua_State *L); - // TODO: make a function that iterates plugins that aren't errored/enabled - const std::map> &plugins() const; + /// Run `cb` on every loaded plugin, including those with load errors + void forEachPlugin( + FunctionRef &)>) const; + + const std::map &allPlugins() const; /** * @brief Reload plugin given by id @@ -86,7 +90,7 @@ private: void queueChangeNotification(); - std::map> plugins_; + std::map plugins_; WebSocketPool webSocketPool_; std::vector< diff --git a/src/widgets/PluginRepl.cpp b/src/widgets/PluginRepl.cpp index 6eac4b03d..cb83e4c69 100644 --- a/src/widgets/PluginRepl.cpp +++ b/src/widgets/PluginRepl.cpp @@ -665,16 +665,24 @@ void PluginRepl::log(std::optional level, void PluginRepl::tryUpdate() { - auto it = getApp()->getPlugins()->plugins().find(this->id); - if (it == getApp()->getPlugins()->plugins().end()) + auto it = getApp()->getPlugins()->allPlugins().find(this->id); + if (it == getApp()->getPlugins()->allPlugins().end()) { return; } + + const auto *oPl = std::get_if(&it->second); + if (oPl == nullptr) + { + return; + } + const auto &pl = *oPl; + if (!PluginController::isPluginEnabled(this->id)) { return; } - this->setPlugin(it->second.get()); + this->setPlugin(pl.get()); } void PluginRepl::setPlugin(Plugin *plugin) diff --git a/src/widgets/settingspages/PluginsPage.cpp b/src/widgets/settingspages/PluginsPage.cpp index b65bcabde..194793576 100644 --- a/src/widgets/settingspages/PluginsPage.cpp +++ b/src/widgets/settingspages/PluginsPage.cpp @@ -8,12 +8,14 @@ # include "Application.hpp" # include "common/Args.hpp" # include "controllers/accounts/AccountController.hpp" +# include "controllers/plugins/Plugin.hpp" # include "controllers/plugins/PluginController.hpp" # include "singletons/Paths.hpp" # include "singletons/Settings.hpp" # include "util/Helpers.hpp" # include "util/LayoutCreator.hpp" # include "util/RemoveScrollAreaBackground.hpp" +# include "util/Variant.hpp" # include "widgets/PluginRepl.hpp" # include "widgets/settingspages/SettingWidget.hpp" @@ -121,54 +123,53 @@ void PluginsPage::rebuildContent() this->scrollAreaWidget_.append(this->dataFrame_); auto layout = frame.setLayoutType(); layout->setParent(this->dataFrame_); - for (const auto &[id, plugin] : getApp()->getPlugins()->plugins()) + for (const auto &[id, plugin] : getApp()->getPlugins()->allPlugins()) { + const auto &meta = std::visit(variant::Overloaded{ + [&](const PluginPtr &plugin) { + return plugin->meta; + }, + [&](const UnloadedPlugin &plugin) { + return plugin.meta; + }, + }, + plugin); + auto groupHeaderText = QString("%1 (%2, from %3)") - .arg(plugin->meta.name, - QString::fromStdString(plugin->meta.version.to_string()), - id); + .arg(meta.name, + QString::fromStdString(meta.version.to_string()), id); auto groupBox = layout.emplace(groupHeaderText); groupBox->setParent(this->dataFrame_); + auto pluginEntry = groupBox.setLayoutType(); pluginEntry->setParent(groupBox.getElement()); - if (!plugin->meta.isValid()) + if (!meta.isValid()) { QString errors = "
    "; - for (const auto &err : plugin->meta.errors) + for (const auto &err : meta.errors) { errors += "
  • " + err.toHtmlEscaped() + "
  • "; } errors += "
"; - auto *warningLabel = new QLabel( - "There were errors while loading metadata for this plugin:" + - errors, - this->dataFrame_); + auto *warningLabel = new QLabel("There were errors while loading " + "metadata for this plugin:" + + errors, + this->dataFrame_); warningLabel->setTextFormat(Qt::RichText); warningLabel->setStyleSheet("color: #f00"); pluginEntry->addRow(warningLabel); } - if (!plugin->error().isNull()) - { - auto *errorLabel = - new QLabel("There was an error while loading this plugin: " + - plugin->error(), - this->dataFrame_); - errorLabel->setStyleSheet("color: #f00"); - errorLabel->setWordWrap(true); - pluginEntry->addRow(errorLabel); - } - auto *description = - new QLabel(plugin->meta.description, this->dataFrame_); + auto *description = new QLabel(meta.description, this->dataFrame_); description->setWordWrap(true); description->setStyleSheet("color: #bbb"); pluginEntry->addRow(description); QString authorsTxt; - for (const auto &author : plugin->meta.authors) + for (const auto &author : meta.authors) { if (!authorsTxt.isEmpty()) { @@ -180,32 +181,49 @@ void PluginsPage::rebuildContent() pluginEntry->addRow("Authors", new QLabel(authorsTxt, this->dataFrame_)); - if (!plugin->meta.homepage.isEmpty()) + if (!meta.homepage.isEmpty()) { - auto *homepage = new QLabel(formatRichLink(plugin->meta.homepage), - this->dataFrame_); + auto *homepage = + new QLabel(formatRichLink(meta.homepage), this->dataFrame_); homepage->setOpenExternalLinks(true); pluginEntry->addRow("Homepage", homepage); } pluginEntry->addRow("License", - new QLabel(plugin->meta.license, this->dataFrame_)); + new QLabel(meta.license, this->dataFrame_)); - QString commandsTxt; - for (const auto &cmdName : plugin->listRegisteredCommands()) + if (const auto *oValidPlugin = std::get_if(&plugin)) { - if (!commandsTxt.isEmpty()) + const auto &validPlugin = *oValidPlugin; + + if (!validPlugin->error().isNull()) { - commandsTxt += ", "; + auto *errorLabel = new QLabel( + "There was an error while loading this plugin: " + + validPlugin->error(), + this->dataFrame_); + errorLabel->setStyleSheet("color: #f00"); + errorLabel->setWordWrap(true); + pluginEntry->addRow(errorLabel); } - commandsTxt += cmdName; + QString commandsTxt; + for (const auto &cmdName : validPlugin->listRegisteredCommands()) + { + if (!commandsTxt.isEmpty()) + { + commandsTxt += ", "; + } + + commandsTxt += cmdName; + } + pluginEntry->addRow("Commands", + new QLabel(commandsTxt, this->dataFrame_)); } - pluginEntry->addRow("Commands", - new QLabel(commandsTxt, this->dataFrame_)); - if (!plugin->meta.permissions.empty()) + + if (!meta.permissions.empty()) { QString perms = "
    "; - for (const auto &perm : plugin->meta.permissions) + for (const auto &perm : meta.permissions) { perms += "
  • " + perm.toHtml() + "
  • "; } @@ -217,30 +235,34 @@ void PluginsPage::rebuildContent() pluginEntry->addRow(lbl); } - if (plugin->meta.isValid()) + QString toggleTxt = "Enable"; + if (PluginController::isPluginEnabled(id)) { - QString toggleTxt = "Enable"; - if (PluginController::isPluginEnabled(id)) - { - toggleTxt = "Disable"; - } + toggleTxt = "Disable"; + } - auto *toggleButton = new QPushButton(toggleTxt, this->dataFrame_); - QObject::connect( - toggleButton, &QPushButton::pressed, [name = id, this]() { - QStringList val = getSettings()->enabledPlugins; - if (PluginController::isPluginEnabled(name)) - { - val.removeAll(name); - } - else - { - val.push_back(name); - } - getSettings()->enabledPlugins.setValue(val); - getApp()->getPlugins()->reload(name); - }); - pluginEntry->addRow(toggleButton); + auto *toggleButton = new QPushButton(toggleTxt, this->dataFrame_); + QObject::connect(toggleButton, &QPushButton::pressed, [name = id]() { + QStringList val = getSettings()->enabledPlugins; + if (PluginController::isPluginEnabled(name)) + { + val.removeAll(name); + } + else + { + val.push_back(name); + } + getSettings()->enabledPlugins.setValue(val); + getApp()->getPlugins()->reload(name); + }); + pluginEntry->addRow(toggleButton); + if (meta.isValid()) + { + toggleButton->setEnabled(true); + } + else + { + toggleButton->setEnabled(false); } auto *reloadButton = new QPushButton("Reload", this->dataFrame_); diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp index 057494589..cb6b9bdc9 100644 --- a/tests/src/Plugins.cpp +++ b/tests/src/Plugins.cpp @@ -206,7 +206,7 @@ public: getApp()->getPlugins()->openLibrariesFor(plugin); } - static std::map> &plugins() + static std::map &plugins() { return getApp()->getPlugins()->plugins_; }