feat: add REPL for plugins (#6120)
This can be enabled by setting the `.plugins.repl.enabled` setting to true
Close Chatterino and make a backup of your settings.json before attempting any modifications.
```json5
{
"plugins": {
"repl": {
"enabled": true,
},
},
}
```
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
- Minor: Added cached emotes fallback when fetching from a provider fails. (#6125, #6229)
|
- Minor: Added cached emotes fallback when fetching from a provider fails. (#6125, #6229)
|
||||||
- Minor: Add an option for the reduced opacity of message history. (#6121)
|
- Minor: Add an option for the reduced opacity of message history. (#6121)
|
||||||
- Minor: Make paused chat indicator more visible, and fix its zoom behavior. (#6123)
|
- Minor: Make paused chat indicator more visible, and fix its zoom behavior. (#6123)
|
||||||
|
- Minor: Added interactive REPL for plugins. (#6120)
|
||||||
- Minor: Added WebSocket API for plugins. (#6076, #6186)
|
- Minor: Added WebSocket API for plugins. (#6076, #6186)
|
||||||
- Minor: Allow for themes to set transparent values for window background on Linux. (#6137)
|
- Minor: Allow for themes to set transparent values for window background on Linux. (#6137)
|
||||||
- Minor: Popup overlay now only draws an outline when being interacted with. (#6140)
|
- Minor: Popup overlay now only draws an outline when being interacted with. (#6140)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M12 4.5a7.5 7.5 0 11-7.419 6.392c.067-.454-.265-.892-.724-.892a.749.749 0 00-.752.623A9 9 0 106 5.292V4.25a.75.75 0 00-1.5 0v3c0 .414.336.75.75.75h3a.75.75 0 000-1.5H6.9a7.473 7.473 0 015.1-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 274 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="#fff"><path d="M12 4.5a7.5 7.5 0 11-7.419 6.392c.067-.454-.265-.892-.724-.892a.749.749 0 00-.752.623A9 9 0 106 5.292V4.25a.75.75 0 00-1.5 0v3c0 .414.336.75.75.75h3a.75.75 0 000-1.5H6.9a7.473 7.473 0 015.1-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 286 B |
@@ -594,6 +594,8 @@ set(SOURCE_FILES
|
|||||||
widgets/Notebook.hpp
|
widgets/Notebook.hpp
|
||||||
widgets/OverlayWindow.cpp
|
widgets/OverlayWindow.cpp
|
||||||
widgets/OverlayWindow.hpp
|
widgets/OverlayWindow.hpp
|
||||||
|
widgets/PluginRepl.cpp
|
||||||
|
widgets/PluginRepl.hpp
|
||||||
widgets/Scrollbar.cpp
|
widgets/Scrollbar.cpp
|
||||||
widgets/Scrollbar.hpp
|
widgets/Scrollbar.hpp
|
||||||
widgets/TooltipEntryWidget.cpp
|
widgets/TooltipEntryWidget.cpp
|
||||||
|
|||||||
@@ -31,19 +31,6 @@
|
|||||||
namespace {
|
namespace {
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
|
|
||||||
void logHelper(lua_State *L, Plugin *pl, QDebug stream,
|
|
||||||
const sol::variadic_args &args)
|
|
||||||
{
|
|
||||||
stream.noquote();
|
|
||||||
stream << "[" + pl->id + ":" + pl->meta.name + "]";
|
|
||||||
for (const auto &arg : args)
|
|
||||||
{
|
|
||||||
stream << lua::toString(L, arg.stack_index());
|
|
||||||
// Remove this from our stack
|
|
||||||
lua_pop(L, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
||||||
{
|
{
|
||||||
auto base =
|
auto base =
|
||||||
@@ -101,7 +88,7 @@ void c2_log(ThisPluginState L, LogLevel lvl, sol::variadic_args args)
|
|||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
{
|
{
|
||||||
QDebug stream = qdebugStreamForLogLevel(lvl);
|
QDebug stream = qdebugStreamForLogLevel(lvl);
|
||||||
logHelper(L, L.plugin(), stream, args);
|
L.plugin()->log(L.state(), lvl, std::move(stream), args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +248,7 @@ void g_print(ThisPluginState L, sol::variadic_args args)
|
|||||||
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
||||||
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
||||||
.debug());
|
.debug());
|
||||||
logHelper(L, L.plugin(), stream, args);
|
L.plugin()->log(L.state(), LogLevel::Info, std::move(stream), args);
|
||||||
}
|
}
|
||||||
|
|
||||||
void package_loadlib(sol::variadic_args args)
|
void package_loadlib(sol::variadic_args args)
|
||||||
|
|||||||
@@ -218,6 +218,8 @@ std::unordered_set<QString> Plugin::listRegisteredCommands()
|
|||||||
|
|
||||||
Plugin::~Plugin()
|
Plugin::~Plugin()
|
||||||
{
|
{
|
||||||
|
this->onUnloaded();
|
||||||
|
|
||||||
for (auto *timer : this->activeTimeouts)
|
for (auto *timer : this->activeTimeouts)
|
||||||
{
|
{
|
||||||
QObject::disconnect(timer, nullptr, nullptr, nullptr);
|
QObject::disconnect(timer, nullptr, nullptr, nullptr);
|
||||||
@@ -293,6 +295,35 @@ bool Plugin::hasHTTPPermissionFor(const QUrl &url)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Plugin::log(lua_State *L, lua::api::LogLevel level, QDebug stream,
|
||||||
|
const sol::variadic_args &args)
|
||||||
|
{
|
||||||
|
stream.noquote();
|
||||||
|
stream << "[" + this->id + ":" + this->meta.name + "]";
|
||||||
|
QString fullMessage;
|
||||||
|
for (const auto &arg : args)
|
||||||
|
{
|
||||||
|
auto s = lua::toString(L, arg.stack_index());
|
||||||
|
stream << s;
|
||||||
|
|
||||||
|
if (!fullMessage.isEmpty())
|
||||||
|
{
|
||||||
|
fullMessage.append(' ');
|
||||||
|
}
|
||||||
|
fullMessage.append(s);
|
||||||
|
|
||||||
|
// Remove this from our stack
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->onLog(level, fullMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::state_view Plugin::state()
|
||||||
|
{
|
||||||
|
return {this->state_};
|
||||||
|
}
|
||||||
|
|
||||||
bool Plugin::hasNetworkPermission() const
|
bool Plugin::hasNetworkPermission() const
|
||||||
{
|
{
|
||||||
return std::ranges::any_of(this->meta.permissions, [](const auto &p) {
|
return std::ranges::any_of(this->meta.permissions, [](const auto &p) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/PluginPermission.hpp"
|
# include "controllers/plugins/PluginPermission.hpp"
|
||||||
|
|
||||||
|
# include <boost/signals2/signal.hpp>
|
||||||
# include <QDir>
|
# include <QDir>
|
||||||
# include <QString>
|
# include <QString>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
@@ -21,6 +22,10 @@
|
|||||||
struct lua_State;
|
struct lua_State;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
|
|
||||||
|
namespace chatterino::lua::api {
|
||||||
|
enum class LogLevel;
|
||||||
|
} // namespace chatterino::lua::api
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
struct PluginMeta {
|
struct PluginMeta {
|
||||||
@@ -137,12 +142,20 @@ public:
|
|||||||
bool hasHTTPPermissionFor(const QUrl &url);
|
bool hasHTTPPermissionFor(const QUrl &url);
|
||||||
bool hasNetworkPermission() const;
|
bool hasNetworkPermission() const;
|
||||||
|
|
||||||
|
void log(lua_State *L, lua::api::LogLevel level, QDebug stream,
|
||||||
|
const sol::variadic_args &args);
|
||||||
|
|
||||||
|
sol::state_view state();
|
||||||
|
|
||||||
std::map<lua::api::EventType, sol::protected_function> callbacks;
|
std::map<lua::api::EventType, sol::protected_function> callbacks;
|
||||||
|
|
||||||
// In-flight HTTP Requests
|
// In-flight HTTP Requests
|
||||||
// This is a lifetime hack to ensure they get deleted with the plugin. This relies on the Plugin getting deleted on reload!
|
// This is a lifetime hack to ensure they get deleted with the plugin. This relies on the Plugin getting deleted on reload!
|
||||||
std::vector<std::shared_ptr<lua::api::HTTPRequest>> httpRequests;
|
std::vector<std::shared_ptr<lua::api::HTTPRequest>> httpRequests;
|
||||||
|
|
||||||
|
boost::signals2::signal<void()> onUnloaded;
|
||||||
|
boost::signals2::signal<void(lua::api::LogLevel, const QString &)> onLog;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir loadDirectory_;
|
QDir loadDirectory_;
|
||||||
lua_State *state_;
|
lua_State *state_;
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
|||||||
}
|
}
|
||||||
temp->dataDirectory().mkpath(".");
|
temp->dataDirectory().mkpath(".");
|
||||||
|
|
||||||
|
// make sure we capture log messages during load
|
||||||
|
this->onPluginLoaded(temp);
|
||||||
qCDebug(chatterinoLua) << "Running lua file:" << index;
|
qCDebug(chatterinoLua) << "Running lua file:" << index;
|
||||||
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
||||||
if (err != 0)
|
if (err != 0)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# include "controllers/commands/CommandContext.hpp"
|
# include "controllers/commands/CommandContext.hpp"
|
||||||
# include "controllers/plugins/Plugin.hpp"
|
# include "controllers/plugins/Plugin.hpp"
|
||||||
|
|
||||||
|
# include <boost/signals2/signal.hpp>
|
||||||
# include <QDir>
|
# include <QDir>
|
||||||
# include <QFileInfo>
|
# include <QFileInfo>
|
||||||
# include <QJsonArray>
|
# include <QJsonArray>
|
||||||
@@ -63,6 +64,8 @@ public:
|
|||||||
|
|
||||||
WebSocketPool &webSocketPool();
|
WebSocketPool &webSocketPool();
|
||||||
|
|
||||||
|
boost::signals2::signal<void(Plugin *)> onPluginLoaded;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void loadPlugins();
|
void loadPlugins();
|
||||||
void load(const QFileInfo &index, const QDir &pluginDir,
|
void load(const QFileInfo &index, const QDir &pluginDir,
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
|
||||||
# include <QObject>
|
# include <QObject>
|
||||||
|
# include <QStringBuilder>
|
||||||
# include <sol/thread.hpp>
|
# include <sol/thread.hpp>
|
||||||
|
|
||||||
namespace chatterino::lua {
|
namespace chatterino::lua {
|
||||||
@@ -27,9 +29,10 @@ Plugin *ThisPluginState::plugin()
|
|||||||
|
|
||||||
void logError(Plugin *plugin, QStringView context, const QString &msg)
|
void logError(Plugin *plugin, QStringView context, const QString &msg)
|
||||||
{
|
{
|
||||||
|
QString fullMessage = context % u" - " % msg;
|
||||||
qCWarning(chatterinoLua).noquote()
|
qCWarning(chatterinoLua).noquote()
|
||||||
<< "[" + plugin->id + ":" + plugin->meta.name + "]" << context << "-"
|
<< "[" + plugin->id + ":" + plugin->meta.name + "]" << fullMessage;
|
||||||
<< msg;
|
plugin->onLog(api::LogLevel::Warning, fullMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino::lua
|
} // namespace chatterino::lua
|
||||||
|
|||||||
@@ -680,6 +680,15 @@ public:
|
|||||||
|
|
||||||
BoolSetting showSendButton = {"/ui/showSendButton", false};
|
BoolSetting showSendButton = {"/ui/showSendButton", false};
|
||||||
|
|
||||||
|
struct {
|
||||||
|
// this isn't shown in the UI
|
||||||
|
BoolSetting enabled = {"/plugins/repl/enabled", false};
|
||||||
|
// An empty string implies the default monospace font
|
||||||
|
QStringSetting fontFamily = {"/plugins/repl/fontFamily", {}};
|
||||||
|
QStringSetting fontStyle = {"/plugins/repl/fontStyle", "Regular"};
|
||||||
|
IntSetting fontSize = {"/plugins/repl/fontSize", 10};
|
||||||
|
} pluginRepl;
|
||||||
|
|
||||||
// Similarity
|
// Similarity
|
||||||
BoolSetting similarityEnabled = {"/similarity/similarityEnabled", false};
|
BoolSetting similarityEnabled = {"/similarity/similarityEnabled", false};
|
||||||
BoolSetting colorSimilarDisabled = {"/similarity/colorSimilarDisabled",
|
BoolSetting colorSimilarDisabled = {"/similarity/colorSimilarDisabled",
|
||||||
|
|||||||
@@ -0,0 +1,769 @@
|
|||||||
|
#include "widgets/PluginRepl.hpp"
|
||||||
|
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
|
# include "Application.hpp"
|
||||||
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "controllers/plugins/Plugin.hpp"
|
||||||
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
|
# include "singletons/Resources.hpp"
|
||||||
|
# include "singletons/Settings.hpp"
|
||||||
|
# include "singletons/Theme.hpp"
|
||||||
|
# include "widgets/buttons/PixmapButton.hpp"
|
||||||
|
# include "widgets/buttons/SvgButton.hpp"
|
||||||
|
|
||||||
|
# include <QBoxLayout>
|
||||||
|
# include <QFontDatabase>
|
||||||
|
# include <QScrollBar>
|
||||||
|
# include <QSplitter>
|
||||||
|
# include <QTextBlock>
|
||||||
|
# include <QTextEdit>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
class HistoricTextEdit : public QTextEdit
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
HistoricTextEdit(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
QSize sizeHint() const override
|
||||||
|
{
|
||||||
|
return this->minimumSizeHint();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize minimumSizeHint() const override
|
||||||
|
{
|
||||||
|
auto margins = this->contentsMargins();
|
||||||
|
|
||||||
|
auto h = margins.top() + this->fontMetrics().height() +
|
||||||
|
margins.bottom() + 15;
|
||||||
|
return {0, h};
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void onSend(const QString & /* text */);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool event(QEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// If the item wouldn't change, std::nullopt is returned
|
||||||
|
std::optional<QString> nextHistoryItem(qsizetype diff);
|
||||||
|
|
||||||
|
QStringList history;
|
||||||
|
qsizetype historyIdx = 0;
|
||||||
|
QString lastUnfinishedInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
HistoricTextEdit::HistoricTextEdit(QWidget *parent)
|
||||||
|
: QTextEdit(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HistoricTextEdit::event(QEvent *event)
|
||||||
|
{
|
||||||
|
if (event->type() != QEvent::KeyPress)
|
||||||
|
{
|
||||||
|
return QTextEdit::event(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *e = dynamic_cast<QKeyEvent *>(event);
|
||||||
|
if (!e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e->keyCombination() == QKeyCombination(Qt::Key_Up))
|
||||||
|
{
|
||||||
|
e->accept();
|
||||||
|
auto cursor = this->textCursor();
|
||||||
|
if (cursor.movePosition(QTextCursor::Up))
|
||||||
|
{
|
||||||
|
this->setTextCursor(cursor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto next = this->nextHistoryItem(-1);
|
||||||
|
if (next)
|
||||||
|
{
|
||||||
|
this->setPlainText(*std::move(next));
|
||||||
|
this->moveCursor(QTextCursor::End);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e->keyCombination() == QKeyCombination(Qt::Key_Down))
|
||||||
|
{
|
||||||
|
e->accept();
|
||||||
|
auto cursor = this->textCursor();
|
||||||
|
if (cursor.movePosition(QTextCursor::Down))
|
||||||
|
{
|
||||||
|
this->setTextCursor(cursor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto next = this->nextHistoryItem(1);
|
||||||
|
if (next)
|
||||||
|
{
|
||||||
|
this->setPlainText(*std::move(next));
|
||||||
|
this->moveCursor(QTextCursor::End);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e->keyCombination() == QKeyCombination(Qt::Key_Return))
|
||||||
|
{
|
||||||
|
auto text = this->toPlainText().trimmed();
|
||||||
|
if (!text.isEmpty())
|
||||||
|
{
|
||||||
|
e->accept();
|
||||||
|
if (this->history.empty() || this->history.back() != text)
|
||||||
|
{
|
||||||
|
this->history.append(text);
|
||||||
|
}
|
||||||
|
this->historyIdx = this->history.size();
|
||||||
|
this->setPlainText({});
|
||||||
|
this->lastUnfinishedInput = {};
|
||||||
|
this->onSend(text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QTextEdit::event(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QString> HistoricTextEdit::nextHistoryItem(qsizetype diff)
|
||||||
|
{
|
||||||
|
if (this->history.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
bool wasUnfinishedInput = this->historyIdx >= this->history.size();
|
||||||
|
|
||||||
|
auto nextIdx =
|
||||||
|
std::clamp(this->historyIdx + diff, 0LL, this->history.size());
|
||||||
|
if (nextIdx == this->historyIdx)
|
||||||
|
{
|
||||||
|
return {}; // nothing changed
|
||||||
|
}
|
||||||
|
this->historyIdx = nextIdx;
|
||||||
|
|
||||||
|
if (this->historyIdx >= this->history.size())
|
||||||
|
{
|
||||||
|
return this->lastUnfinishedInput;
|
||||||
|
}
|
||||||
|
if (wasUnfinishedInput)
|
||||||
|
{
|
||||||
|
this->lastUnfinishedInput = this->toPlainText();
|
||||||
|
}
|
||||||
|
return this->history[this->historyIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::optional<QString> tryAsStringAndPop(lua_State *L)
|
||||||
|
{
|
||||||
|
auto s = sol::stack::get<sol::optional<std::string_view>>(L);
|
||||||
|
auto qs = s.map([](auto sv) {
|
||||||
|
return QString::fromUtf8(sv.data(), sv.size());
|
||||||
|
});
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return qs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This stringifies a value without recursing.
|
||||||
|
///
|
||||||
|
/// It's similar to luaL_tolstring with a two minor differences:
|
||||||
|
/// - `__tostring` is not executed - this can potentially error because it might
|
||||||
|
/// be called without a `self` argument (e.g. in `c2.Channel`).
|
||||||
|
/// - The address for tables, threads, etc. isn't shown.
|
||||||
|
QString stringifyValue(lua_State *L, int idx)
|
||||||
|
{
|
||||||
|
switch (lua_type(L, idx))
|
||||||
|
{
|
||||||
|
case LUA_TNONE:
|
||||||
|
return u"(none)"_s;
|
||||||
|
case LUA_TNIL:
|
||||||
|
return u"nil"_s;
|
||||||
|
case LUA_TSTRING: {
|
||||||
|
// Safety: We know this is a string
|
||||||
|
auto sv = sol::stack::unqualified_get<std::string_view>(L, idx);
|
||||||
|
return QString::fromUtf8(sv.data(),
|
||||||
|
static_cast<qsizetype>(sv.size()));
|
||||||
|
}
|
||||||
|
case LUA_TNUMBER: {
|
||||||
|
if (lua_isinteger(L, idx) != 0)
|
||||||
|
{
|
||||||
|
auto v = static_cast<LUAI_UACINT>(lua_tointeger(L, idx));
|
||||||
|
return QString::number(v);
|
||||||
|
}
|
||||||
|
auto v = static_cast<LUAI_UACNUMBER>(lua_tonumber(L, idx));
|
||||||
|
return QString::number(v);
|
||||||
|
}
|
||||||
|
case LUA_TBOOLEAN: {
|
||||||
|
if (lua_toboolean(L, idx) != 0)
|
||||||
|
{
|
||||||
|
return u"true"_s;
|
||||||
|
}
|
||||||
|
return u"false"_s;
|
||||||
|
}
|
||||||
|
case LUA_TTHREAD:
|
||||||
|
case LUA_TFUNCTION:
|
||||||
|
case LUA_TUSERDATA:
|
||||||
|
case LUA_TLIGHTUSERDATA:
|
||||||
|
case LUA_TTABLE:
|
||||||
|
default: {
|
||||||
|
int tt = luaL_getmetafield(L, idx, "__name");
|
||||||
|
if (tt != LUA_TNIL)
|
||||||
|
{
|
||||||
|
auto name = tryAsStringAndPop(L);
|
||||||
|
if (name)
|
||||||
|
{
|
||||||
|
return *std::move(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString::fromUtf8(luaL_typename(L, idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString stringifyValue(sol::stack_proxy it)
|
||||||
|
{
|
||||||
|
return stringifyValue(it.lua_state(), it.stack_index());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString stringifyValue(const sol::object &obj)
|
||||||
|
{
|
||||||
|
static_assert(!sol::is_stack_based<std::remove_cvref_t<decltype(obj)>>());
|
||||||
|
// XXX: in many cases the value is already on the stack
|
||||||
|
obj.push();
|
||||||
|
auto s = stringifyValue(obj.lua_state(), -1);
|
||||||
|
obj.pop();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ExpandFlag : uint8_t {
|
||||||
|
None = 0,
|
||||||
|
TryUsertypeStorage = 1 << 0,
|
||||||
|
};
|
||||||
|
using ExpandFlags = FlagsEnum<ExpandFlag>;
|
||||||
|
|
||||||
|
void stringify(sol::stack_proxy it, QString &s, size_t maxItems = 10,
|
||||||
|
ExpandFlags flags = {})
|
||||||
|
{
|
||||||
|
lua::StackGuard g(it.lua_state());
|
||||||
|
|
||||||
|
auto typ = it.get_type();
|
||||||
|
switch (typ)
|
||||||
|
{
|
||||||
|
case sol::type::userdata: {
|
||||||
|
auto meta = lua_getmetatable(it.lua_state(), it.stack_index());
|
||||||
|
if (meta == 0)
|
||||||
|
{
|
||||||
|
s.append(u"userdata");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto table =
|
||||||
|
sol::stack::unqualified_get<sol::table>(it.lua_state());
|
||||||
|
|
||||||
|
s.append(u"userdata{");
|
||||||
|
size_t n = 0;
|
||||||
|
for (const auto &inner : table)
|
||||||
|
{
|
||||||
|
if (n != 0)
|
||||||
|
{
|
||||||
|
s.append(u", ");
|
||||||
|
}
|
||||||
|
s.append(stringifyValue(inner.first));
|
||||||
|
|
||||||
|
n++;
|
||||||
|
if (n >= maxItems)
|
||||||
|
{
|
||||||
|
s.append(u", ...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.append('}');
|
||||||
|
|
||||||
|
lua_pop(it.lua_state(), 1); // metatable
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case sol::type::table: {
|
||||||
|
auto tbl = it.get<sol::optional<sol::table>>();
|
||||||
|
if (!tbl)
|
||||||
|
{
|
||||||
|
s.append(stringifyValue(it));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.append(u"{");
|
||||||
|
size_t n = 0;
|
||||||
|
|
||||||
|
auto meta = lua_getmetatable(it.lua_state(), it.stack_index());
|
||||||
|
if (meta != 0)
|
||||||
|
{
|
||||||
|
s.append(u"(metatable): "_s);
|
||||||
|
stringify({it.lua_state(), -1}, s, maxItems / 2,
|
||||||
|
flags | ExpandFlag::TryUsertypeStorage);
|
||||||
|
n++;
|
||||||
|
lua_pop(it.lua_state(), 1); // metatable
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.has(ExpandFlag::TryUsertypeStorage))
|
||||||
|
{
|
||||||
|
auto uts = sol::u_detail::maybe_get_usertype_storage_base(
|
||||||
|
it.lua_state(), it.stack_index());
|
||||||
|
if (uts)
|
||||||
|
{
|
||||||
|
for (const auto &[key, _] : uts->string_keys)
|
||||||
|
{
|
||||||
|
if (n == 0)
|
||||||
|
{
|
||||||
|
s.append('[');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s.append(u", ["_s);
|
||||||
|
}
|
||||||
|
# if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
|
||||||
|
s.append(QString::fromUtf8(
|
||||||
|
key.data(), static_cast<qsizetype>(key.size())));
|
||||||
|
# else
|
||||||
|
s.append(QUtf8StringView(key));
|
||||||
|
# endif
|
||||||
|
s.append("] = <dyn>");
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &inner : *tbl)
|
||||||
|
{
|
||||||
|
if (n == 0)
|
||||||
|
{
|
||||||
|
s.append('[');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s.append(u", ["_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
s.append(stringifyValue(inner.first));
|
||||||
|
s.append("] = ");
|
||||||
|
s.append(stringifyValue(inner.second));
|
||||||
|
|
||||||
|
n++;
|
||||||
|
if (n >= maxItems)
|
||||||
|
{
|
||||||
|
s.append(u", ..."_s);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.append('}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case sol::type::none:
|
||||||
|
case sol::type::lua_nil:
|
||||||
|
case sol::type::string:
|
||||||
|
case sol::type::number:
|
||||||
|
case sol::type::thread:
|
||||||
|
case sol::type::boolean:
|
||||||
|
case sol::type::function:
|
||||||
|
case sol::type::lightuserdata:
|
||||||
|
case sol::type::poly:
|
||||||
|
default:
|
||||||
|
s.append(stringifyValue(it));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
PluginRepl::PluginRepl(QString id, QWidget *parent)
|
||||||
|
: BaseWindow(
|
||||||
|
{
|
||||||
|
BaseWindow::EnableCustomFrame,
|
||||||
|
BaseWindow::DisableCustomScaling,
|
||||||
|
BaseWindow::DisableLayoutSave,
|
||||||
|
},
|
||||||
|
parent)
|
||||||
|
, id(std::move(id))
|
||||||
|
, isPinned(getSettings()->windowTopMost)
|
||||||
|
{
|
||||||
|
this->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
this->resize(600, 300);
|
||||||
|
|
||||||
|
this->setWindowTitle(this->id + u" - Plugin REPL"_s);
|
||||||
|
|
||||||
|
auto *root = new QVBoxLayout(this->getLayoutContainer());
|
||||||
|
|
||||||
|
// top row
|
||||||
|
{
|
||||||
|
auto *top = new QHBoxLayout;
|
||||||
|
this->ui.clear = new SvgButton(
|
||||||
|
{
|
||||||
|
.dark = u":/buttons/cancel.svg"_s,
|
||||||
|
.light = u":/buttons/cancelDark.svg"_s,
|
||||||
|
},
|
||||||
|
nullptr, {3, 3});
|
||||||
|
this->ui.clear->setScaleIndependentSize({18, 18});
|
||||||
|
this->ui.clear->setToolTip(u"Clear Output"_s);
|
||||||
|
QObject::connect(this->ui.clear, &Button::leftClicked, this, [this] {
|
||||||
|
this->ui.output->clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
this->ui.reload = new SvgButton(
|
||||||
|
{
|
||||||
|
.dark = u":/buttons/reloadLight.svg"_s,
|
||||||
|
.light = u":/buttons/reloadDark.svg"_s,
|
||||||
|
},
|
||||||
|
nullptr, {3, 3});
|
||||||
|
this->ui.reload->setScaleIndependentSize({18, 18});
|
||||||
|
this->ui.reload->setToolTip(u"Reload"_s);
|
||||||
|
QObject::connect(this->ui.reload, &Button::leftClicked, this, [this] {
|
||||||
|
if (!this->plugin)
|
||||||
|
{
|
||||||
|
this->log(lua::api::LogLevel::Critical, "Plugin not loaded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->log({}, u"Reloading..."_s);
|
||||||
|
bool result = getApp()->getPlugins()->reload(this->id);
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
this->log({}, u"Reloaded."_s);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->log(lua::api::LogLevel::Critical, u"Failed to reload."_s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this->ui.pin = new PixmapButton;
|
||||||
|
this->ui.pin->setScaleIndependentSize({18, 18});
|
||||||
|
this->ui.pin->setToolTip(u"Pin Window"_s);
|
||||||
|
QObject::connect(this->ui.pin, &Button::leftClicked, this, [this] {
|
||||||
|
this->isPinned = !this->isPinned;
|
||||||
|
this->updatePinned();
|
||||||
|
});
|
||||||
|
|
||||||
|
top->addStretch(1);
|
||||||
|
top->addWidget(this->ui.clear);
|
||||||
|
top->addWidget(this->ui.reload);
|
||||||
|
top->addWidget(this->ui.pin);
|
||||||
|
|
||||||
|
root->addLayout(top);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *splitter = new QSplitter(Qt::Vertical, this);
|
||||||
|
splitter->setChildrenCollapsible(false);
|
||||||
|
root->addWidget(splitter, 1);
|
||||||
|
|
||||||
|
this->ui.output = new QTextEdit;
|
||||||
|
splitter->addWidget(this->ui.output);
|
||||||
|
this->ui.output->setUndoRedoEnabled(false);
|
||||||
|
this->ui.output->setReadOnly(true);
|
||||||
|
this->ui.output->setAcceptRichText(false);
|
||||||
|
auto *input = new HistoricTextEdit(this);
|
||||||
|
this->ui.input = input;
|
||||||
|
QObject::connect(input, &HistoricTextEdit::onSend, this,
|
||||||
|
&PluginRepl::tryRun);
|
||||||
|
splitter->addWidget(this->ui.input);
|
||||||
|
this->ui.input->setPlaceholderText(u"Type something..."_s);
|
||||||
|
this->ui.input->setAcceptRichText(false);
|
||||||
|
this->ui.input->setFocus();
|
||||||
|
|
||||||
|
this->pluginLoadedConn =
|
||||||
|
getApp()->getPlugins()->onPluginLoaded.connect([this](Plugin *plug) {
|
||||||
|
if (plug->id == this->id)
|
||||||
|
{
|
||||||
|
this->setPlugin(plug);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this->updatePinned();
|
||||||
|
this->themeChangedEvent();
|
||||||
|
this->tryUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginRepl::themeChangedEvent()
|
||||||
|
{
|
||||||
|
this->updateFont();
|
||||||
|
if (getTheme()->isLightTheme())
|
||||||
|
{
|
||||||
|
this->charFormats.self.setForeground(QColor(0x505050));
|
||||||
|
this->charFormats.warning.setForeground(QColor(0x715100));
|
||||||
|
this->blockFormats.warning.setBackground(QColor(0xfffbd6));
|
||||||
|
this->charFormats.error.setForeground(QColor(0xa4000f));
|
||||||
|
this->blockFormats.error.setBackground(QColor(0xfdf2f5));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->charFormats.self.setForeground(QColor(0xa0a0a0));
|
||||||
|
this->charFormats.warning.setForeground(QColor(0xfce2a1));
|
||||||
|
this->blockFormats.warning.setBackground(QColor(0x42381f));
|
||||||
|
this->charFormats.error.setForeground(QColor(0xffb3d2));
|
||||||
|
this->blockFormats.error.setBackground(QColor(0x4b2f36));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->isPinned)
|
||||||
|
{
|
||||||
|
this->ui.pin->setPixmap(getResources().buttons.pinEnabled);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->ui.pin->setPixmap(getTheme()->buttons.pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pal = this->palette();
|
||||||
|
pal.setColor(QPalette::Window,
|
||||||
|
getTheme()->tabs.selected.backgrounds.regular);
|
||||||
|
pal.setColor(QPalette::Base, getTheme()->splits.background);
|
||||||
|
pal.setColor(QPalette::Text, getTheme()->window.text);
|
||||||
|
|
||||||
|
this->ui.output->setPalette(pal);
|
||||||
|
|
||||||
|
pal.setColor(QPalette::Base, getTheme()->splits.input.background);
|
||||||
|
this->ui.input->setPalette(pal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginRepl::tryRun(QString code)
|
||||||
|
{
|
||||||
|
if (!this->plugin)
|
||||||
|
{
|
||||||
|
this->log(lua::api::LogLevel::Critical, "Plugin not loaded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->log({}, u"> "_s + code);
|
||||||
|
|
||||||
|
bool addedReturn = false;
|
||||||
|
size_t maxItems = 10;
|
||||||
|
|
||||||
|
if (code.startsWith('!'))
|
||||||
|
{
|
||||||
|
maxItems = std::numeric_limits<size_t>::max();
|
||||||
|
auto v = QStringView(code).sliced(1).trimmed();
|
||||||
|
auto nChars = v.constData() - code.constData();
|
||||||
|
code.remove(0, nChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!code.startsWith(u"return "))
|
||||||
|
{
|
||||||
|
code.prepend(u"return ");
|
||||||
|
addedReturn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::protected_function_result res;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::string u8code = code.toStdString();
|
||||||
|
std::string chunkName = "<inline code>";
|
||||||
|
auto result =
|
||||||
|
this->plugin->state().load(u8code, chunkName, sol::load_mode::text);
|
||||||
|
|
||||||
|
if (!result.valid() && addedReturn)
|
||||||
|
{
|
||||||
|
u8code = u8code.substr(7); // "return "
|
||||||
|
result = this->plugin->state().load(u8code, chunkName,
|
||||||
|
sol::load_mode::text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.valid())
|
||||||
|
{
|
||||||
|
auto err = result.get<sol::error>();
|
||||||
|
this->log(lua::api::LogLevel::Critical, err.what());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fn = result.get<sol::optional<sol::protected_function>>();
|
||||||
|
if (!fn)
|
||||||
|
{
|
||||||
|
this->log(lua::api::LogLevel::Critical,
|
||||||
|
u"Code didn't result in a callable function."_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::protected_function_result res = (*fn)();
|
||||||
|
this->logResult(res, {
|
||||||
|
.maxItems = maxItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (const sol::error &err)
|
||||||
|
{
|
||||||
|
this->log(lua::api::LogLevel::Critical, err.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginRepl::logResult(const sol::protected_function_result &res,
|
||||||
|
const LogOptions &opts)
|
||||||
|
{
|
||||||
|
if (!res.valid())
|
||||||
|
{
|
||||||
|
sol::error err = res;
|
||||||
|
this->log(lua::api::LogLevel::Critical, err.what());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.return_count() == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString msg;
|
||||||
|
for (auto it : res)
|
||||||
|
{
|
||||||
|
if (!msg.isEmpty())
|
||||||
|
{
|
||||||
|
msg.append(' ');
|
||||||
|
}
|
||||||
|
stringify(it, msg, opts.maxItems);
|
||||||
|
}
|
||||||
|
this->log(lua::api::LogLevel::Info, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginRepl::log(std::optional<lua::api::LogLevel> level,
|
||||||
|
const QString &text)
|
||||||
|
{
|
||||||
|
auto [charFmt, blockFmt] = [&] {
|
||||||
|
if (!level)
|
||||||
|
{
|
||||||
|
return std::tie(this->charFormats.self, this->blockFormats.self);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (*level)
|
||||||
|
{
|
||||||
|
case lua::api::LogLevel::Debug:
|
||||||
|
return std::tie(this->charFormats.debug,
|
||||||
|
this->blockFormats.debug);
|
||||||
|
case lua::api::LogLevel::Warning:
|
||||||
|
return std::tie(this->charFormats.warning,
|
||||||
|
this->blockFormats.warning);
|
||||||
|
case lua::api::LogLevel::Critical:
|
||||||
|
return std::tie(this->charFormats.error,
|
||||||
|
this->blockFormats.error);
|
||||||
|
case lua::api::LogLevel::Info:
|
||||||
|
default:
|
||||||
|
return std::tie(this->charFormats.info,
|
||||||
|
this->blockFormats.info);
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
auto *bar = this->ui.output->verticalScrollBar();
|
||||||
|
bool atBottom = bar != nullptr && bar->value() == bar->maximum();
|
||||||
|
auto cursor = this->ui.output->textCursor();
|
||||||
|
cursor.movePosition(QTextCursor::End);
|
||||||
|
if (this->ui.output->document()->isEmpty())
|
||||||
|
{
|
||||||
|
cursor.setBlockFormat(blockFmt);
|
||||||
|
cursor.setBlockCharFormat(charFmt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cursor.insertBlock(blockFmt, charFmt);
|
||||||
|
}
|
||||||
|
cursor.insertText(text);
|
||||||
|
|
||||||
|
if (atBottom)
|
||||||
|
{
|
||||||
|
bar->setValue(bar->maximum());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginRepl::tryUpdate()
|
||||||
|
{
|
||||||
|
auto it = getApp()->getPlugins()->plugins().find(this->id);
|
||||||
|
if (it == getApp()->getPlugins()->plugins().end())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PluginController::isPluginEnabled(this->id))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->setPlugin(it->second.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginRepl::setPlugin(Plugin *plugin)
|
||||||
|
{
|
||||||
|
this->plugin = plugin;
|
||||||
|
|
||||||
|
if (!plugin)
|
||||||
|
{
|
||||||
|
this->pluginDestroyConn.release();
|
||||||
|
this->pluginLogConn.release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->pluginDestroyConn = this->plugin->onUnloaded.connect([this] {
|
||||||
|
this->setPlugin(nullptr);
|
||||||
|
this->log({}, u"Unloaded."_s);
|
||||||
|
});
|
||||||
|
this->pluginLogConn =
|
||||||
|
this->plugin->onLog.connect([this](auto level, const auto &text) {
|
||||||
|
this->log(level, text);
|
||||||
|
});
|
||||||
|
|
||||||
|
this->log({}, u"Loaded."_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont PluginRepl::currentFont()
|
||||||
|
{
|
||||||
|
auto family = getSettings()->pluginRepl.fontFamily.getValue();
|
||||||
|
auto style = getSettings()->pluginRepl.fontStyle.getValue();
|
||||||
|
auto fontSize = getSettings()->pluginRepl.fontSize.getValue();
|
||||||
|
if (family.isEmpty())
|
||||||
|
{
|
||||||
|
auto font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||||
|
font.setStyleName(style);
|
||||||
|
font.setPointSize(fontSize);
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QFontDatabase::font(family, style, fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginRepl::updateFont()
|
||||||
|
{
|
||||||
|
this->font = PluginRepl::currentFont();
|
||||||
|
this->ui.input->setFont(this->font);
|
||||||
|
this->ui.output->setFont(this->font);
|
||||||
|
|
||||||
|
// Set the tab width to be at least 4 spaces.
|
||||||
|
//
|
||||||
|
// setTabStopDistance:
|
||||||
|
// > Do not set a value less than the horizontalAdvance() of the
|
||||||
|
// > QChar::VisualTabCharacter character, otherwise the tab-character will
|
||||||
|
// > be drawn incompletely.
|
||||||
|
QFontMetricsF metrics(this->font);
|
||||||
|
auto tabCharWidth = metrics.horizontalAdvance(QChar::VisualTabCharacter);
|
||||||
|
auto spaceWidth = metrics.horizontalAdvance(QChar::Space);
|
||||||
|
auto tabDistance = std::max(tabCharWidth, spaceWidth * 4.F);
|
||||||
|
|
||||||
|
this->ui.input->setTabStopDistance(tabDistance);
|
||||||
|
this->ui.output->setTabStopDistance(tabDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginRepl::updatePinned()
|
||||||
|
{
|
||||||
|
this->setTopMost(this->isPinned);
|
||||||
|
if (this->isPinned)
|
||||||
|
{
|
||||||
|
this->ui.pin->setPixmap(getResources().buttons.pinEnabled);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->ui.pin->setPixmap(getTheme()->buttons.pin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
# include "PluginRepl.moc"
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "widgets/BaseWindow.hpp"
|
||||||
|
|
||||||
|
# include <boost/signals2/connection.hpp>
|
||||||
|
# include <QString>
|
||||||
|
# include <QTextBlockFormat>
|
||||||
|
# include <QTextCharFormat>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
|
||||||
|
class QTextEdit;
|
||||||
|
class QTextCharFormat;
|
||||||
|
class QTextBlockFormat;
|
||||||
|
|
||||||
|
namespace chatterino::lua::api {
|
||||||
|
enum class LogLevel;
|
||||||
|
} // namespace chatterino::lua::api
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Plugin;
|
||||||
|
class SvgButton;
|
||||||
|
class PixmapButton;
|
||||||
|
|
||||||
|
class PluginRepl : public BaseWindow
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PluginRepl(QString id, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
static QFont currentFont();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void themeChangedEvent() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct LogOptions {
|
||||||
|
/// Maximum number of items to show in tables.
|
||||||
|
size_t maxItems = 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
void tryRun(QString code);
|
||||||
|
void logResult(const sol::protected_function_result &res,
|
||||||
|
const LogOptions &opts);
|
||||||
|
|
||||||
|
void log(std::optional<lua::api::LogLevel> level, const QString &text);
|
||||||
|
|
||||||
|
void tryUpdate();
|
||||||
|
void setPlugin(Plugin *plugin);
|
||||||
|
|
||||||
|
void updateFont();
|
||||||
|
void updatePinned();
|
||||||
|
|
||||||
|
QString id;
|
||||||
|
Plugin *plugin = nullptr;
|
||||||
|
|
||||||
|
boost::signals2::scoped_connection pluginDestroyConn;
|
||||||
|
boost::signals2::scoped_connection pluginLogConn;
|
||||||
|
boost::signals2::scoped_connection pluginLoadedConn;
|
||||||
|
|
||||||
|
bool isPinned = false;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
QTextEdit *input = nullptr;
|
||||||
|
QTextEdit *output = nullptr;
|
||||||
|
SvgButton *clear = nullptr;
|
||||||
|
SvgButton *reload = nullptr;
|
||||||
|
PixmapButton *pin = nullptr;
|
||||||
|
} ui;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
QTextCharFormat self;
|
||||||
|
QTextCharFormat debug;
|
||||||
|
QTextCharFormat info;
|
||||||
|
QTextCharFormat warning;
|
||||||
|
QTextCharFormat error;
|
||||||
|
} charFormats;
|
||||||
|
struct {
|
||||||
|
QTextBlockFormat self;
|
||||||
|
QTextBlockFormat debug;
|
||||||
|
QTextBlockFormat info;
|
||||||
|
QTextBlockFormat warning;
|
||||||
|
QTextBlockFormat error;
|
||||||
|
} blockFormats;
|
||||||
|
|
||||||
|
QFont font;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -3,12 +3,15 @@
|
|||||||
|
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "common/Args.hpp"
|
# include "common/Args.hpp"
|
||||||
|
# include "controllers/accounts/AccountController.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
# include "singletons/Paths.hpp"
|
# include "singletons/Paths.hpp"
|
||||||
# include "singletons/Settings.hpp"
|
# include "singletons/Settings.hpp"
|
||||||
# include "util/Helpers.hpp"
|
# include "util/Helpers.hpp"
|
||||||
# include "util/LayoutCreator.hpp"
|
# include "util/LayoutCreator.hpp"
|
||||||
# include "util/RemoveScrollAreaBackground.hpp"
|
# include "util/RemoveScrollAreaBackground.hpp"
|
||||||
|
# include "widgets/PluginRepl.hpp"
|
||||||
|
# include "widgets/settingspages/SettingWidget.hpp"
|
||||||
|
|
||||||
# include <QCheckBox>
|
# include <QCheckBox>
|
||||||
# include <QFormLayout>
|
# include <QFormLayout>
|
||||||
@@ -78,6 +81,17 @@ PluginsPage::PluginsPage()
|
|||||||
"enable and disable them.");
|
"enable and disable them.");
|
||||||
groupLayout->addRow(disabledLabel);
|
groupLayout->addRow(disabledLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getSettings()->pluginRepl.enabled)
|
||||||
|
{
|
||||||
|
groupLayout->addRow(SettingWidget::fontButton(
|
||||||
|
"REPL font", getSettings()->pluginRepl.fontFamily,
|
||||||
|
&PluginRepl::currentFont, [](const QFont &font) {
|
||||||
|
getSettings()->pluginRepl.fontFamily = font.family();
|
||||||
|
getSettings()->pluginRepl.fontSize = font.pointSize();
|
||||||
|
getSettings()->pluginRepl.fontStyle = font.styleName();
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this->rebuildContent();
|
this->rebuildContent();
|
||||||
@@ -231,6 +245,16 @@ void PluginsPage::rebuildContent()
|
|||||||
{
|
{
|
||||||
reloadButton->setEnabled(false);
|
reloadButton->setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getSettings()->pluginRepl.enabled)
|
||||||
|
{
|
||||||
|
auto *replButton = new QPushButton("Open REPL", this->dataFrame_);
|
||||||
|
QObject::connect(replButton, &QPushButton::clicked, [id]() {
|
||||||
|
auto *repl = new PluginRepl(id);
|
||||||
|
repl->show();
|
||||||
|
});
|
||||||
|
pluginEntry->addRow(replButton);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <QBoxLayout>
|
#include <QBoxLayout>
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
#include <QFontDialog>
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
@@ -393,6 +394,45 @@ SettingWidget *SettingWidget::lineEdit(const QString &label,
|
|||||||
return widget;
|
return widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingWidget *SettingWidget::fontButton(const QString &label,
|
||||||
|
QStringSetting &familySetting,
|
||||||
|
std::function<QFont()> currentFont,
|
||||||
|
std::function<void(QFont)> onChange)
|
||||||
|
{
|
||||||
|
auto *widget = new SettingWidget(label);
|
||||||
|
|
||||||
|
auto *lbl = new QLabel(label + ":");
|
||||||
|
|
||||||
|
auto *button = new QPushButton(currentFont().family());
|
||||||
|
|
||||||
|
widget->hLayout->addWidget(lbl);
|
||||||
|
widget->hLayout->addStretch(1);
|
||||||
|
widget->hLayout->addWidget(button);
|
||||||
|
|
||||||
|
familySetting.connect(
|
||||||
|
[button, currentFont](const auto &) {
|
||||||
|
button->setText(currentFont().family());
|
||||||
|
},
|
||||||
|
widget->managedConnections);
|
||||||
|
|
||||||
|
QObject::connect(button, &QPushButton::clicked,
|
||||||
|
[widget, currentFont{std::move(currentFont)},
|
||||||
|
onChange{std::move(onChange)}]() {
|
||||||
|
bool ok = false;
|
||||||
|
auto font =
|
||||||
|
QFontDialog::getFont(&ok, currentFont(), widget);
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
onChange(font);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widget->actionWidget = button;
|
||||||
|
widget->label = lbl;
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
SettingWidget *SettingWidget::setTooltip(QString tooltip)
|
SettingWidget *SettingWidget::setTooltip(QString tooltip)
|
||||||
{
|
{
|
||||||
assert(!tooltip.isEmpty());
|
assert(!tooltip.isEmpty());
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace chatterino {
|
|||||||
|
|
||||||
class GeneralPageView;
|
class GeneralPageView;
|
||||||
|
|
||||||
class SettingWidget : QWidget
|
class SettingWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -66,6 +66,11 @@ public:
|
|||||||
QStringSetting &setting,
|
QStringSetting &setting,
|
||||||
const QString &placeholderText = {});
|
const QString &placeholderText = {});
|
||||||
|
|
||||||
|
static SettingWidget *fontButton(const QString &label,
|
||||||
|
QStringSetting &familySetting,
|
||||||
|
std::function<QFont()> currentFont,
|
||||||
|
std::function<void(QFont)> onChange);
|
||||||
|
|
||||||
SettingWidget *setTooltip(QString tooltip);
|
SettingWidget *setTooltip(QString tooltip);
|
||||||
SettingWidget *setDescription(const QString &text);
|
SettingWidget *setDescription(const QString &text);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user