nirish

config files for niri window manager
Log | Files | Refs | README | LICENSE

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 }