diff --git a/CHANGELOG.md b/CHANGELOG.md index 38e61eccb..1b576a9e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Minor: Added support for the `{input.text}` placeholder in the **Split** -> **Run a command** hotkey. (#5130) - Minor: Add a new Channel API for experimental plugins feature. (#5141, #5184, #5187) - Minor: Added the ability to change the top-most status of a window regardless of the _Always on top_ setting (right click the notebook). (#5135) +- Minor: Introduce `c2.later()` function to Lua API. (#5154) - Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176) - Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) - Minor: Allow theming of tab live and rerun indicators. (#5188) diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index 8439faa05..9bf6f57c0 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -95,4 +95,5 @@ declare module c2 { : never; function register_callback(type: T, func: CbFunc): void; + function later(callback: () => void, msec: number): void; } diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index ecc38dbaf..2cc56af59 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -185,3 +185,9 @@ function c2.register_callback(type, func) end ---@param ... any Values to log. Should be convertible to a string with `tostring()`. function c2.log(level, ...) end +--- Calls callback around msec milliseconds later. Does not freeze Chatterino. +--- +---@param callback fun() The callback that will be called. +---@param msec number How long to wait. +function c2.later(callback, msec) end + diff --git a/src/controllers/plugins/LuaAPI.cpp b/src/controllers/plugins/LuaAPI.cpp index e087fa111..0e4d16b0a 100644 --- a/src/controllers/plugins/LuaAPI.cpp +++ b/src/controllers/plugins/LuaAPI.cpp @@ -147,6 +147,63 @@ int c2_log(lua_State *L) return 0; } +int c2_later(lua_State *L) +{ + auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + return luaL_error(L, "c2.later: internal error: no plugin?"); + } + if (lua_gettop(L) != 2) + { + return luaL_error( + L, "c2.later expects two arguments (a callback that takes no " + "arguments and returns nothing and a number the time in " + "milliseconds to wait)\n"); + } + int time{}; + if (!lua::pop(L, &time)) + { + return luaL_error(L, "cannot get time (2nd arg of c2.later, " + "expected a number)"); + } + + if (!lua_isfunction(L, lua_gettop(L))) + { + return luaL_error(L, "cannot get callback (1st arg of c2.later, " + "expected a function)"); + } + + auto *timer = new QTimer(); + timer->setInterval(time); + auto id = pl->addTimeout(timer); + auto name = QString("timeout_%1").arg(id); + auto *coro = lua_newthread(L); + + QObject::connect(timer, &QTimer::timeout, [pl, coro, name, timer]() { + timer->deleteLater(); + pl->removeTimeout(timer); + int nres{}; + lua_resume(coro, nullptr, 0, &nres); + + lua_pushnil(coro); + lua_setfield(coro, LUA_REGISTRYINDEX, name.toStdString().c_str()); + if (lua_gettop(coro) != 0) + { + stackDump(coro, + pl->id + + ": timer returned a value, this shouldn't happen " + "and is probably a plugin bug"); + } + }); + stackDump(L, "before setfield"); + lua_setfield(L, LUA_REGISTRYINDEX, name.toStdString().c_str()); + lua_xmove(L, coro, 1); // move function to thread + timer->start(); + + return 0; +} + int g_load(lua_State *L) { # ifdef NDEBUG diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index 762c6d23b..c37cfb7ef 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -86,6 +86,15 @@ int c2_register_callback(lua_State *L); */ int c2_log(lua_State *L); +/** + * Calls callback around msec milliseconds later. Does not freeze Chatterino. + * + * @lua@param callback fun() The callback that will be called. + * @lua@param msec number How long to wait. + * @exposed c2.later + */ +int c2_later(lua_State *L); + // These ones are global int g_load(lua_State *L); int g_print(lua_State *L); diff --git a/src/controllers/plugins/LuaUtilities.cpp b/src/controllers/plugins/LuaUtilities.cpp index b0abb87ff..6866c2cc0 100644 --- a/src/controllers/plugins/LuaUtilities.cpp +++ b/src/controllers/plugins/LuaUtilities.cpp @@ -140,6 +140,18 @@ StackIdx push(lua_State *L, const int &b) return lua_gettop(L); } +bool peek(lua_State *L, int *out, StackIdx idx) +{ + StackGuard guard(L); + if (lua_isnumber(L, idx) == 0) + { + return false; + } + + *out = lua_tointeger(L, idx); + return true; +} + bool peek(lua_State *L, bool *out, StackIdx idx) { StackGuard guard(L); diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index 4dd25ee19..66ca1a9f1 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -66,6 +66,7 @@ StackIdx push(lua_State *L, const bool &b); StackIdx push(lua_State *L, const int &b); // returns OK? +bool peek(lua_State *L, int *out, StackIdx idx = -1); bool peek(lua_State *L, bool *out, StackIdx idx = -1); bool peek(lua_State *L, double *out, StackIdx idx = -1); bool peek(lua_State *L, QString *out, StackIdx idx = -1); diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index 0453c65c1..63c69c388 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -1,6 +1,7 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "controllers/plugins/Plugin.hpp" +# include "common/QLogging.hpp" # include "controllers/commands/CommandController.hpp" # include @@ -167,11 +168,38 @@ std::unordered_set Plugin::listRegisteredCommands() Plugin::~Plugin() { + for (auto *timer : this->activeTimeouts) + { + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->deleteLater(); + } + qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size() + << "timers for plugin" << this->id + << "while destroying the object"; + this->activeTimeouts.clear(); if (this->state_ != nullptr) { lua_close(this->state_); } } +int Plugin::addTimeout(QTimer *timer) +{ + this->activeTimeouts.push_back(timer); + return ++this->lastTimerId; +} + +void Plugin::removeTimeout(QTimer *timer) +{ + for (auto it = this->activeTimeouts.begin(); + it != this->activeTimeouts.end(); ++it) + { + if (*it == timer) + { + this->activeTimeouts.erase(it); + break; + } + } +} } // namespace chatterino #endif diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 7d81609e1..67f9d35ff 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -14,6 +14,7 @@ # include struct lua_State; +class QTimer; namespace chatterino { @@ -126,6 +127,9 @@ public: return this->error_; } + int addTimeout(QTimer *timer); + void removeTimeout(QTimer *timer); + private: QDir loadDirectory_; lua_State *state_; @@ -134,6 +138,8 @@ private: // maps command name -> function name std::unordered_map ownedCommands; + std::vector activeTimeouts; + int lastTimerId = 0; friend class PluginController; }; diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index ad456dbd7..ab45be0f9 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -118,8 +118,7 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta, luaL_Reg{LUA_GNAME, luaopen_base}, // - load - don't allow in release mode - //luaL_Reg{LUA_COLIBNAME, luaopen_coroutine}, - // - needs special support + luaL_Reg{LUA_COLIBNAME, luaopen_coroutine}, luaL_Reg{LUA_TABLIBNAME, luaopen_table}, // luaL_Reg{LUA_IOLIBNAME, luaopen_io}, // - explicit fs access, needs wrapper with permissions, no usage ideas yet @@ -147,6 +146,7 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta, {"register_command", lua::api::c2_register_command}, {"register_callback", lua::api::c2_register_callback}, {"log", lua::api::c2_log}, + {"later", lua::api::c2_later}, {nullptr, nullptr}, }; lua_pushglobaltable(L); @@ -339,6 +339,11 @@ bool PluginController::isPluginEnabled(const QString &id) Plugin *PluginController::getPluginByStatePtr(lua_State *L) { + lua_geti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD); + // Use the main thread for identification, not a coroutine instance + auto *mainL = lua_tothread(L, -1); + lua_pop(L, 1); + L = mainL; for (auto &[name, plugin] : this->plugins_) { if (plugin->state_ == L)