local log = require("log") ---@class ow.Plugin.Spec ---@field [1] string ---@field name? string ---@field version? string | vim.VersionRange ---@field lazy? boolean ---@field build? string[] | fun(self: ow.Plugin): boolean ---@class ow.Plugin : ow.Plugin.Spec ---@field path string ---@type ow.Plugin[] local plugins = {} ---@param path string ---@return boolean success ---@return string? err local function exec(path) local chunk, load_err = loadfile(path) if not chunk then return false, load_err end local ok, call_err = pcall(chunk) if not ok then return false, call_err end return true end ---@param plugin ow.Plugin local function load(plugin) vim.cmd.packadd(plugin.name) local name = plugin.name:match("[^.]+"):lower() local path = string.format( "%s/lua/plugins/%s.lua", vim.fn.stdpath("config"), name ) if vim.uv.fs_stat(path) then local ok, err = exec(path) if not ok then log.error("Failed to load %s: %s", name, err) end end end ---@param spec string | ow.Plugin.Spec ---@return vim.pack.Spec local function to_pack_spec(spec) if type(spec) == "string" then return { src = spec } end return { src = spec[1], name = spec.name, version = spec.version, data = { lazy = spec.lazy, build = spec.build, }, } end ---@param plugin ow.Plugin ---@return boolean success local function run_build(plugin) if type(plugin.build) == "function" then return plugin.build(plugin) elseif type(plugin.build) == "table" then local ret = vim.system( plugin.build --[[@as table]], { cwd = plugin.path } ):wait() return ret.code == 0 and ret.signal == 0 end log.error("invalid build parameter for %s", plugin.name) return false end ---@class ow.Plugin.PackEvent.Data ---@field active boolean ---@field kind "install" | "update" | "delete" ---@field spec vim.pack.Spec ---@field path string ---@class ow.Plugin.PackEvent : vim.api.keyset.create_autocmd.callback_args ---@field data ow.Plugin.PackEvent.Data ---@param plugin ow.Plugin ---@param events ow.Plugin.PackEvent[] ---@return boolean success local function handle_pack_events(plugin, events) if not plugin.build then return true end for _, ev in ipairs(events) do if ev.data.spec.name == plugin.name and ev.event == "PackChanged" and (ev.data.kind == "install" or ev.data.kind == "update") then vim.cmd.packadd(plugin.name) return run_build(plugin) end end return true end ---@type uv.uv_fs_event_t? local watcher = nil local M = {} function M.get_paths() local paths = {} for _, plugin in ipairs(plugins) do table.insert(paths, plugin.path) end return paths end ---@return string[] function M.get_names() local names = {} for _, plugin in ipairs(plugins) do table.insert(names, plugin.name:match("[^.]+"):lower()) end return names end ---@param name string function M.reload(name) local path = string.format( "%s/lua/plugins/%s.lua", vim.fn.stdpath("config"), name ) if not vim.uv.fs_stat(path) then log.error("No config file found for %s", name) return end local ok, err = exec(path) if ok then log.info("Reloaded %s", name) else log.error("Failed to reload %s: %s", name, err) end end function M.watch() if watcher then return end local plugins_dir = string.format( "%s/lua/plugins", vim.fn.stdpath("config") ) watcher = vim.uv.new_fs_event() if not watcher then log.error("Failed to create fs_event watcher") return end ---@type table local timers = {} watcher:start( plugins_dir, {}, vim.schedule_wrap(function(err, filename) if err then log.error("Watch error: %s", err) return end if not filename or not filename:match("%.lua$") then return end if timers[filename] then timers[filename]:stop() else timers[filename] = vim.uv.new_timer() end timers[filename]:start( 100, 0, vim.schedule_wrap(function() timers[filename]:stop() timers[filename]:close() timers[filename] = nil local path = plugins_dir .. "/" .. filename if not vim.uv.fs_stat(path) then return end local ok, load_err = exec(path) if ok then log.info("Reloaded %s", filename) else log.error( "Failed to reload %s: %s", filename, load_err ) end end) ) end) ) end function M.unwatch() if not watcher then return end watcher:stop() watcher:close() watcher = nil end ---@param specs (string | ow.Plugin.Spec)[] function M.setup(specs) ---@type ow.Plugin.PackEvent[] local events = {} local id = vim.api.nvim_create_autocmd( { "PackChangedPre", "PackChanged" }, { ---@param ev ow.Plugin.PackEvent callback = function(ev) table.insert(events, ev) end } ) vim.pack.add(vim.tbl_map(to_pack_spec, specs), { load = function(data) local d = data.spec.data or {} ---@type ow.Plugin local plugin = { [1] = data.spec.src, name = data.spec.name, version = data.spec.version, lazy = d.lazy or false, build = d.build, path = data.path, } table.insert(plugins, plugin) end }) vim.api.nvim_del_autocmd(id) local lazy_plugins = {} for _, plugin in ipairs(plugins) do if not handle_pack_events(plugin, events) then return end if plugin.lazy then table.insert(lazy_plugins, plugin) else load(plugin) end end vim.api.nvim_create_autocmd("UIEnter", { once = true, callback = vim.schedule_wrap(function() for _, plugin in ipairs(lazy_plugins) do load(plugin) end end) }) end return M