local M = {} -- 1st arg as current theme, 2nd as new theme M.change_theme = function(current_theme, new_theme) if current_theme == nil or new_theme == nil then print "Error: Provide current and new theme name" return false end if current_theme == new_theme then return end local user_config = vim.g.nvchad_user_config local file = vim.fn.stdpath "config" .. "/lua/" .. user_config .. ".lua" -- store in data variable local data = assert(M.file("r", file)) -- escape characters which can be parsed as magic chars current_theme = current_theme:gsub("%p", "%%%0") new_theme = new_theme:gsub("%p", "%%%0") local find = "theme = .?" .. current_theme .. ".?" local replace = 'theme = "' .. new_theme .. '"' local content = string.gsub(data, find, replace) -- see if the find string exists in file if content == data then print("Error: Cannot change default theme with " .. new_theme .. ", edit " .. file .. " manually") return false else assert(M.file("w", file, content)) end end M.clear_cmdline = function() vim.defer_fn(function() vim.cmd "echo" end, 0) end M.close_buffer = function(bufexpr, force) -- This is a modification of a NeoVim plugin from -- Author: ojroques - Olivier Roques -- Src: https://github.com/ojroques/nvim-bufdel -- (Author has okayed copy-paste) -- Options local opts = { next = "cycle", -- how to retrieve the next buffer quit = false, -- exit when last buffer is deleted --TODO make this a chadrc flag/option } -- ---------------- -- Helper functions -- ---------------- -- Switch to buffer 'buf' on each window from list 'windows' local function switch_buffer(windows, buf) local cur_win = vim.fn.winnr() for _, winid in ipairs(windows) do vim.cmd(string.format("%d wincmd w", vim.fn.win_id2win(winid))) vim.cmd(string.format("buffer %d", buf)) end vim.cmd(string.format("%d wincmd w", cur_win)) -- return to original window end -- Select the first buffer with a number greater than given buffer local function get_next_buf(buf) local next = vim.fn.bufnr "#" if opts.next == "alternate" and vim.fn.buflisted(next) == 1 then return next end for i = 0, vim.fn.bufnr "$" - 1 do next = (buf + i) % vim.fn.bufnr "$" + 1 -- will loop back to 1 if vim.fn.buflisted(next) == 1 then return next end end end -- ---------------- -- End helper functions -- ---------------- local buf = vim.fn.bufnr() if vim.fn.buflisted(buf) == 0 then -- exit if buffer number is invalid vim.cmd "close" return end if #vim.fn.getbufinfo { buflisted = 1 } < 2 then if opts.quit then -- exit when there is only one buffer left if force then vim.cmd "qall!" else vim.cmd "confirm qall" end return end local chad_term, type = pcall(function() return vim.api.nvim_buf_get_var(buf, "term_type") end) if chad_term then -- Must be a window type vim.cmd(string.format("setlocal nobl", buf)) vim.cmd "enew" return end -- don't exit and create a new empty buffer vim.cmd "enew" vim.cmd "bp" end local next_buf = get_next_buf(buf) local windows = vim.fn.getbufinfo(buf)[1].windows -- force deletion of terminal buffers to avoid the prompt if force or vim.fn.getbufvar(buf, "&buftype") == "terminal" then local chad_term, type = pcall(function() return vim.api.nvim_buf_get_var(buf, "term_type") end) -- TODO this scope is error prone, make resilient if chad_term then if type == "wind" then -- hide from bufferline vim.cmd(string.format("%d bufdo setlocal nobl", buf)) -- swtich to another buff -- TODO switch to next bufffer, this works too vim.cmd "BufferLineCycleNext" else local cur_win = vim.fn.winnr() -- we can close this window vim.cmd(string.format("%d wincmd c", cur_win)) return end else switch_buffer(windows, next_buf) vim.cmd(string.format("bd! %d", buf)) end else switch_buffer(windows, next_buf) vim.cmd(string.format("silent! confirm bd %d", buf)) end -- revert buffer switches if user has canceled deletion if vim.fn.buflisted(buf) == 1 then switch_buffer(windows, buf) end end -- wrapper to use vim.api.nvim_echo -- table of {string, highlight} -- e.g echo({{"Hello", "Title"}, {"World"}}) M.echo = function(opts) if opts == nil or type(opts) ~= "table" then return end vim.api.nvim_echo(opts, false, {}) end -- 1st arg - r or w -- 2nd arg - file path -- 3rd arg - content if 1st arg is w -- return file data on read, nothing on write M.file = function(mode, filepath, content) local data local fd = assert(vim.loop.fs_open(filepath, mode, 438)) local stat = assert(vim.loop.fs_fstat(fd)) if stat.type ~= "file" then data = false else if mode == "r" then data = assert(vim.loop.fs_read(fd, stat.size, 0)) else assert(vim.loop.fs_write(fd, content, 0)) data = true end end assert(vim.loop.fs_close(fd)) return data end -- hide statusline -- tables fetched from load_config function M.hide_statusline = function(values) local hidden = require("utils").load_config().ui.statusline.hidden local shown = require("utils").load_config().ui.statusline.shown local api = vim.api local buftype = api.nvim_buf_get_option("%", "ft") -- shown table from config has the highest priority if vim.tbl_contains(shown, buftype) then api.nvim_set_option("laststatus", 2) return end if vim.tbl_contains(hidden, buftype) then api.nvim_set_option("laststatus", 0) return else api.nvim_set_option("laststatus", 2) end end -- return a table of available themes M.list_themes = function(return_type) local themes = {} -- folder where theme files are stored local themes_folder = vim.fn.stdpath "config" .. "/lua/themes" -- list all the contents of the folder and filter out files with .lua extension, then append to themes table local fd = vim.loop.fs_scandir(themes_folder) if fd then while true do local name, typ = vim.loop.fs_scandir_next(fd) if name == nil then break end if typ ~= "directory" and string.find(name, ".lua") then -- return the table values as keys if specified if return_type == "keys_as_value" then themes[vim.fn.fnamemodify(name, ":r")] = true else table.insert(themes, vim.fn.fnamemodify(name, ":r")) end end end end return themes end -- Base code: https://gist.github.com/revolucas/184aec7998a6be5d2f61b984fac1d7f7 -- Changes over it: preserving table 1 contents and also update with table b, without duplicating -- 1st arg - base table, 2nd arg - table to merge M.merge_table = function(into, from) -- make sure both are table if type(into) ~= "table" or type(from) ~= "table" then return into end local stack, seen = {}, {} local table1, table2 = into, from while true do for k, v in pairs(table2) do if type(v) == "table" and type(table1[k]) == "table" then table.insert(stack, { table1[k], table2[k] }) else local present = seen[v] or false if not present then if type(k) == "number" then -- add the value to seen table until value is found -- only do when key is number we just want to append to subtables -- todo: maybe improve this for _, value in pairs(table1) do if value == v then present = true break end end seen[v] = true if not present then table1[#table1 + 1] = v end else table1[k] = v end end end end if #stack > 0 then local t = stack[#stack] table1, table2 = t[1], t[2] stack[#stack] = nil else break end end return into end -- load config -- 1st arg = boolean - whether to force reload -- Modifies _G._NVCHAD_CONFIG global variable M.load_config = function(reload) -- only do the stuff below one time, otherwise just return the set config if _G._NVCHAD_CONFIG_CONTENTS ~= nil and not (reload or false) then return _G._NVCHAD_CONFIG_CONTENTS end local default_config = "default_config" local config_name = vim.g.nvchad_user_config or "chadrc" local config_file = vim.fn.stdpath "config" .. "/lua/" .. config_name .. ".lua" -- unload the modules if force reload if reload then package.loaded[default_config or false] = nil package.loaded[config_name or false] = nil end -- don't enclose in pcall, it better break when default config is faulty _G._NVCHAD_CONFIG_CONTENTS = require(default_config) -- user config is not required to run nvchad but a optional -- Make sure the config doesn't break the whole system if user config is not present or in bad state or not a table -- print warning texts if user config file is present -- check if the user config is present if vim.fn.empty(vim.fn.glob(config_file)) < 1 then local present, config = pcall(require, config_name) if present then -- make sure the returned value is table if type(config) == "table" then -- data = require(config_name) _G._NVCHAD_CONFIG_CONTENTS = require("utils").merge_table(_G._NVCHAD_CONFIG_CONTENTS, config) else print("Warning: " .. config_name .. " sourced successfully but did not return a lua table.") end else print("Warning: " .. config_file .. " is present but sourcing failed.") end end return _G._NVCHAD_CONFIG_CONTENTS end -- reload a plugin ( will try to load even if not loaded) -- can take a string or list ( table ) -- return true or false M.reload_plugin = function(plugins) local status = true local function _reload_plugin(plugin) local loaded = package.loaded[plugin] if loaded then package.loaded[plugin] = nil end if not pcall(require, plugin) then print("Error: Cannot load " .. plugin .. " plugin!") status = false end end if type(plugins) == "string" then _reload_plugin(plugins) elseif type(plugins) == "table" then for _, plugin in ipairs(plugins) do _reload_plugin(plugin) end end return status end -- reload themes without restarting vim -- if no theme name given then reload the current theme M.reload_theme = function(theme_name) local reload_plugin = require("utils").reload_plugin -- if theme name is empty or nil, then reload the current theme if theme_name == nil or theme_name == "" then theme_name = vim.g.nvchad_theme end if not pcall(require, "themes/" .. theme_name) then print("No such theme ( " .. theme_name .. " )") return false end vim.g.nvchad_theme = theme_name -- reload the base16 theme local ok, base16 = pcall(require, "base16") if not ok then print "Error: Cannot load base16 plugin!" return false end base16(base16.themes(theme_name), true) if not reload_plugin { "highlights", "plugins.bufferline", "galaxyline", "plugins.statusline", } then print "Error: Not able to reload all plugins." return false end -- yes, this is very hacky, but due to new_async in -- https://github.com/glepnir/galaxyline.nvim/blob/main/lua/galaxyline/provider.lua#L5-L36 -- it doesn't work properly and some statusline stuff dissapears local vcs = require "galaxyline.provider_vcs" local fileinfo = require "galaxyline.provider_fileinfo" local buffer = require "galaxyline.provider_buffer" local extension = require "galaxyline.provider_extensions" local whitespace = require "galaxyline.provider_whitespace" local lspclient = require "galaxyline.provider_lsp" _G.galaxyline_providers = { BufferIcon = buffer.get_buffer_type_icon, BufferNumber = buffer.get_buffer_number, FileTypeName = buffer.get_buffer_filetype, GitBranch = vcs.get_git_branch, DiffAdd = vcs.diff_add, DiffModified = vcs.diff_modified, DiffRemove = vcs.diff_remove, LineColumn = fileinfo.line_column, FileFormat = fileinfo.get_file_format, FileEncode = fileinfo.get_file_encode, FileSize = fileinfo.get_file_size, FileIcon = fileinfo.get_file_icon, FileName = fileinfo.get_current_file_name, SFileName = fileinfo.filename_in_special_buffer, LinePercent = fileinfo.current_line_percent, ScrollBar = extension.scrollbar_instance, VistaPlugin = extension.vista_nearest, WhiteSpace = whitespace.get_item, GetLspClient = lspclient.get_lsp_client, } local diagnostic = require "galaxyline.provider_diagnostic" _G.galaxyline_providers.DiagnosticError = diagnostic.get_diagnostic_error _G.galaxyline_providers.DiagnosticWarn = diagnostic.get_diagnostic_warn _G.galaxyline_providers.DiagnosticHint = diagnostic.get_diagnostic_hint _G.galaxyline_providers.DiagnosticInfo = diagnostic.get_diagnostic_info return true end -- toggle between 2 themes -- argument should be a table with 2 theme names M.toggle_theme = function(themes) local current_theme = vim.g.current_nvchad_theme or vim.g.nvchad_theme for _, name in ipairs(themes) do if name ~= current_theme then if require("utils").reload_theme(name) then -- open a buffer and close it to reload the statusline vim.cmd "new|bwipeout" vim.g.current_nvchad_theme = name if M.change_theme(vim.g.nvchad_theme, name) then vim.g.nvchad_theme = name end end end end end -- update nvchad M.update_nvchad = function() -- in all the comments below, config means user config local config_path = vim.fn.stdpath "config" local config_name = vim.g.nvchad_user_config or "chadrc" local config_file = config_path .. "/lua/" .. config_name .. ".lua" -- generate a random file name local config_file_backup = config_path .. "/" .. config_name .. ".lua.bak." .. math.random() local utils = require "utils" local echo = utils.echo local current_config = utils.load_config() local update_url = current_config.options.update_url or "https://github.com/NvChad/NvChad" local update_branch = current_config.options.update_branch or "main" -- ask the user for confirmation to update because we are going to run git reset --hard echo { { "Url: ", "Title" }, { update_url } } echo { { "Branch: ", "Title" }, { update_branch } } echo { { "\nUpdater will run", "WarningMsg" }, { " git reset --hard " }, { "in config folder, so changes to existing repo files except ", "WarningMsg", }, { config_name }, { " will be lost!\n\nUpdate NvChad ? [y/N]", "WarningMsg" }, } local ans = string.lower(vim.fn.input "-> ") == "y" utils.clear_cmdline() if not ans then echo { { "Update cancelled!", "Title" } } return end -- first try to fetch contents of config, this will make sure it is readable and taking backup of its contents local config_contents = utils.file("r", config_file) -- also make a local backup in ~/.config/nvim, will be removed when config is succesfully restored utils.file("w", config_file_backup, config_contents) -- write original config file with its contents, will make sure charc is writable, this maybe overkill but a little precaution always helps utils.file("w", config_file, config_contents) -- function that will executed when git commands are done local function update_exit(_, code) -- restore config file irrespective of whether git commands were succesfull or not if pcall(function() utils.file("w", config_file, config_contents) end) then -- config restored succesfully, remove backup file that was created if not pcall(os.remove, config_file_backup) then echo { { "Warning: Failed to remove backup chadrc, remove manually.", "WarningMsg" } } echo { { "Path: ", "WarningMsg" }, { config_file_backup } } end else echo { { "Error: Restoring " .. config_name .. " failed.\n", "ErrorMsg" } } echo { { "Backed up " .. config_name .. " path: " .. config_file_backup .. "\n\n", "None" } } end -- close the terminal buffer only if update was success, as in case of error, we need the error message if code == 0 then vim.cmd "bd!" echo { { "NvChad succesfully updated.\n", "String" } } else echo { { "Error: NvChad Update failed.\n", "ErrorMsg" } } end end -- git commands that will executed, reset in case config was modfied -- use --rebase, to not mess up if the local repo is outdated local update_script = table.concat({ "git reset --hard && git pull --set-upstream", update_url, update_branch, "--rebase", }, " ") -- open a new buffer vim.cmd "new" -- finally open the pseudo terminal buffer vim.fn.termopen(update_script, { -- change dir to config path so we don't need to move in script cwd = config_path, on_exit = update_exit, }) end return M