addons.lua (8408B)
1 local mp = require 'mp' 2 local msg = require 'mp.msg' 3 local utils = require 'mp.utils' 4 5 local o = require 'modules.options' 6 local g = require 'modules.globals' 7 local fb_utils = require 'modules.utils' 8 local parser_API = require 'modules.apis.parser' 9 10 local API_MAJOR, API_MINOR, API_PATCH = g.API_VERSION:match("(%d+)%.(%d+)%.(%d+)") 11 API_MAJOR, API_MINOR, API_PATCH = tonumber(API_MAJOR), tonumber(API_MINOR), tonumber(API_PATCH) 12 13 ---checks if the given parser has a valid version number 14 ---@param parser Parser|Keybind 15 ---@param id string 16 ---@return boolean? 17 local function check_api_version(parser, id) 18 if parser.version then 19 msg.warn(('%s: use of the `version` field is deprecated - use `api_version` instead'):format(id)) 20 parser.api_version = parser.version 21 end 22 23 local version = parser.api_version 24 if type(version) ~= 'string' then return msg.error(("%s: field `api_version` must be a string, got %s"):format(id, tostring(version))) end 25 26 local major, minor = version:match("(%d+)%.(%d+)") 27 major, minor = tonumber(major), tonumber(minor) 28 29 if not major or not minor then 30 return msg.error(("%s: invalid version number, expected v%d.%d.x, got v%s"):format(id, API_MAJOR, API_MINOR, version)) 31 elseif major ~= API_MAJOR then 32 return msg.error(("%s has wrong major version number, expected v%d.x.x, got, v%s"):format(id, API_MAJOR, version)) 33 elseif minor > API_MINOR then 34 msg.warn(("%s has newer minor version number than API, expected v%d.%d.x, got v%s"):format(id, API_MAJOR, API_MINOR, version)) 35 end 36 return true 37 end 38 39 ---create a unique id for the given parser 40 ---@param parser Parser 41 local function set_parser_id(parser) 42 local name = parser.name 43 if g.parsers[name] then 44 local n = 2 45 name = parser.name.."_"..n 46 while g.parsers[name] do 47 n = n + 1 48 name = parser.name.."_"..n 49 end 50 end 51 52 g.parsers[name] = parser 53 g.parsers[parser] = { id = name } 54 end 55 56 ---runs an addon in a separate environment 57 ---@param path string 58 ---@return unknown 59 local function run_addon(path) 60 local name_sqbr = string.format("[%s]", path:match("/([^/]*)%.lua$")) 61 local addon_environment = fb_utils.redirect_table(_G) 62 addon_environment._G = addon_environment ---@diagnostic disable-line inject-field 63 64 --gives each addon custom debug messages 65 addon_environment.package = fb_utils.redirect_table(addon_environment.package) ---@diagnostic disable-line inject-field 66 addon_environment.package.loaded = fb_utils.redirect_table(addon_environment.package.loaded) 67 local msg_module = { 68 log = function(level, ...) msg.log(level, name_sqbr, ...) end, 69 fatal = function(...) return msg.fatal(name_sqbr, ...) end, 70 error = function(...) return msg.error(name_sqbr, ...) end, 71 warn = function(...) return msg.warn(name_sqbr, ...) end, 72 info = function(...) return msg.info(name_sqbr, ...) end, 73 verbose = function(...) return msg.verbose(name_sqbr, ...) end, 74 debug = function(...) return msg.debug(name_sqbr, ...) end, 75 trace = function(...) return msg.trace(name_sqbr, ...) end, 76 } 77 addon_environment.print = msg_module.info ---@diagnostic disable-line inject-field 78 79 addon_environment.require = function(module) ---@diagnostic disable-line inject-field 80 if module == "mp.msg" then return msg_module end 81 return require(module) 82 end 83 84 ---@type function?, string? 85 local chunk, err 86 if setfenv then ---@diagnostic disable-line deprecated 87 --since I stupidly named a function loadfile I need to specify the global one 88 --I've been using the name too long to want to change it now 89 chunk, err = _G.loadfile(path) 90 if not chunk then return msg.error(err) end 91 setfenv(chunk, addon_environment) ---@diagnostic disable-line deprecated 92 else 93 chunk, err = _G.loadfile(path, "bt", addon_environment) ---@diagnostic disable-line redundant-parameter 94 if not chunk then return msg.error(err) end 95 end 96 97 ---@diagnostic disable-next-line no-unknown 98 local success, result = xpcall(chunk, fb_utils.traceback) 99 return success and result or nil 100 end 101 102 ---Setup an internal or external parser. 103 ---Note that we're somewhat bypassing the type system here as we're converting from a 104 ---ParserConfig object to a Parser object. As such we need to make sure that the 105 ---we're doing everything correctly. A 2.0 release of the addon API could simplify 106 ---this by formally separating ParserConfigs from Parsers and providing an 107 ---API to register parsers. 108 ---@param parser ParserConfig 109 ---@param file string 110 ---@return nil 111 local function setup_parser(parser, file) 112 parser = setmetatable(parser, { __index = parser_API }) --[[@as Parser]] 113 parser.name = parser.name or file:gsub("%-browser%.lua$", ""):gsub("%.lua$", "") 114 115 set_parser_id(parser) 116 if not check_api_version(parser, file) then return msg.error("aborting load of parser", parser:get_id(), "from", file) end 117 118 msg.verbose("imported parser", parser:get_id(), "from", file) 119 120 --sets missing functions 121 if not parser.can_parse then 122 if parser.parse then parser.can_parse = function() return true end 123 else parser.can_parse = function() return false end end 124 end 125 126 if parser.priority == nil then parser.priority = 0 end 127 if type(parser.priority) ~= "number" then return msg.error("parser", parser:get_id(), "needs a numeric priority") end 128 129 table.insert(g.parsers, parser) 130 end 131 132 ---load an external addon 133 ---@param file string 134 ---@param path string 135 ---@return nil 136 local function setup_addon(file, path) 137 if file:sub(-4) ~= ".lua" then return msg.verbose(path, "is not a lua file - aborting addon setup") end 138 139 local addon_parsers = run_addon(path) --[=[@as ParserConfig|ParserConfig[]]=] 140 if addon_parsers and not next(addon_parsers) then return msg.verbose('addon', path, 'returned empry table - special case, ignoring') end 141 if not addon_parsers or type(addon_parsers) ~= "table" then return msg.error("addon", path, "did not return a table") end 142 143 --if the table contains a priority key then we assume it isn't an array of parsers 144 if not addon_parsers[1] then addon_parsers = {addon_parsers} end 145 146 for _, parser in ipairs(addon_parsers --[=[@as ParserConfig[]]=]) do 147 setup_parser(parser, file) 148 end 149 end 150 151 ---loading external addons 152 ---@param directory string 153 ---@return nil 154 local function load_addons(directory) 155 directory = fb_utils.fix_path(directory, true) 156 157 local files = utils.readdir(directory) 158 if not files then return msg.verbose('not loading external addons - could not read', o.addon_directory) end 159 160 for _, file in ipairs(files) do 161 setup_addon(file, directory..file) 162 end 163 end 164 165 local function load_internal_addons() 166 local script_dir = mp.get_script_directory() 167 if not script_dir then return msg.error('script is not being run as a directory script!') end 168 local internal_addon_dir = script_dir..'/modules/addons/' 169 load_addons(internal_addon_dir) 170 end 171 172 local function load_external_addons() 173 local addon_dir = mp.command_native({"expand-path", o.addon_directory..'/'}) --[[@as string|nil]] 174 if not addon_dir then return msg.verbose('not loading external addons - could not resolve', o.addon_directory) end 175 load_addons(addon_dir) 176 end 177 178 ---Orders the addons by priority, sets the parser index values, 179 ---and runs the setup methods of the addons. 180 local function setup_addons() 181 table.sort(g.parsers, function(a, b) return a.priority < b.priority end) 182 183 --we want to store the indexes of the parsers 184 for i = #g.parsers, 1, -1 do g.parsers[ g.parsers[i] ].index = i end 185 186 --we want to run the setup functions for each addon 187 for index, parser in ipairs(g.parsers) do 188 if parser.setup then 189 local success = xpcall(function() parser:setup() end, fb_utils.traceback) 190 if not success then 191 msg.error("parser", parser:get_id(), "threw an error in the setup method - removing from list of parsers") 192 table.remove(g.parsers, index) 193 end 194 end 195 end 196 end 197 198 ---@class addons 199 return { 200 check_api_version = check_api_version, 201 load_internal_addons = load_internal_addons, 202 load_external_addons = load_external_addons, 203 setup_addons = setup_addons, 204 }