---@class snacks.toggle ---@overload fun(... :snacks.toggle.Opts): snacks.toggle.Class local M = setmetatable({}, { __call = function(M, ...) return M.new(...) end, }) M.meta = { desc = "Toggle keymaps integrated with which-key icons / colors", } ---@class snacks.toggle.Config ---@field icon? string|{ enabled: string, disabled: string } ---@field color? string|{ enabled: string, disabled: string } ---@field wk_desc? string|{ enabled: string, disabled: string } ---@field map? fun(mode: string|string[], lhs: string, rhs: string|fun(), opts?: vim.keymap.set.Opts) ---@field which_key? boolean ---@field notify? boolean local defaults = { map = vim.keymap.set, -- keymap.set function to use which_key = true, -- integrate with which-key to show enabled/disabled icons and colors notify = true, -- show a notification when toggling -- icons for enabled/disabled states icon = { enabled = " ", disabled = " ", }, -- colors for enabled/disabled states color = { enabled = "green", disabled = "yellow", }, wk_desc = { enabled = "Disable ", disabled = "Enable ", }, } ---@type table M.toggles = {} ---@class snacks.toggle.Opts: snacks.toggle.Config ---@field id? string ---@field name string ---@field get fun():boolean ---@field set fun(state:boolean) ---@class snacks.toggle.Class ---@field opts snacks.toggle.Opts local Toggle = {} Toggle.__index = Toggle ---@param ... snacks.toggle.Opts function M.new(...) ---@type snacks.toggle.Class local self = setmetatable({}, Toggle) self.opts = Snacks.config.get("toggle", defaults, ...) --[[@as snacks.toggle.Opts]] local id = self.opts.id or self.opts.name:lower():gsub("%W+", "_"):gsub("_+$", ""):gsub("^_+", "") self.opts.id = id M.toggles[id] = self return self end ---@param id string ---@return snacks.toggle.Class? function M.get(id) if not M.toggles[id] and M[id] then M[id]() end return M.toggles[id] end function Toggle:get() local ok, ret = pcall(self.opts.get) if not ok then Snacks.notify.error({ "Failed to get state for `" .. self.opts.name .. "`:\n", ret --[[@as string]], }, { title = self.opts.name, once = true }) return false end return ret end ---@param state boolean function Toggle:set(state) local ok, err = pcall(self.opts.set, state) ---@type boolean, string? if not ok then Snacks.notify.error({ "Failed to set state for `" .. self.opts.name .. "`:\n", err --[[@as string]], }, { title = self.opts.name, once = true }) end end function Toggle:toggle() local state = not self:get() self:set(state) if self.opts.notify then Snacks.notify( (state and "Enabled" or "Disabled") .. " **" .. self.opts.name .. "**", { title = self.opts.name, level = state and vim.log.levels.INFO or vim.log.levels.WARN } ) end end ---@param keys string ---@param opts? vim.keymap.set.Opts | { mode: string|string[]} function Toggle:map(keys, opts) opts = opts or {} local mode = opts.mode or "n" opts.mode = nil opts.desc = opts.desc or ("Toggle " .. self.opts.name) self.opts.map(mode, keys, function() self:toggle() end, opts) if self.opts.which_key then Snacks.util.on_module("which-key", function() self:_wk(keys, mode) end) end return self end function Toggle:_wk(keys, mode) require("which-key").add({ { keys, mode = mode, real = true, icon = function() local key = self:get() and "enabled" or "disabled" return { icon = type(self.opts.icon) == "string" and self.opts.icon or self.opts.icon[key], color = type(self.opts.color) == "string" and self.opts.color or self.opts.color[key], } end, desc = function() local key = self:get() and "enabled" or "disabled" return (type(self.opts.wk_desc) == "string" and self.opts.wk_desc or self.opts.wk_desc[key]) .. self.opts.name end, }, }) end ---@param option string ---@param opts? snacks.toggle.Config | {on?: unknown, off?: unknown, global?: boolean} function M.option(option, opts) opts = opts or {} local on = opts.on == nil and true or opts.on local off = opts.off ~= nil and opts.off or false return M.new({ id = option, name = option, get = function() if opts.global then return vim.opt[option]:get() == on end return vim.opt_local[option]:get() == on end, set = function(state) if opts.global then vim.opt[option] = state and on or off return end vim.opt_local[option] = state and on or off end, }, opts) end ---@param opts? snacks.toggle.Config function M.treesitter(opts) return M.new({ id = "treesitter", name = "Treesitter Highlight", get = function() return vim.b.ts_highlight end, set = function(state) vim.treesitter[state and "start" or "stop"]() end, }, opts) end ---@param opts? snacks.toggle.Config function M.line_number(opts) local number, relativenumber = true, true return M.new({ id = "line_number", name = "Line Numbers", get = function() return vim.opt_local.number:get() or vim.opt_local.relativenumber:get() end, set = function(state) if state then vim.opt_local.number, vim.opt_local.relativenumber = number, relativenumber else number, relativenumber = vim.opt_local.number:get(), vim.opt_local.relativenumber:get() vim.opt_local.number, vim.opt_local.relativenumber = false, false end end, }, opts) end ---@param opts? snacks.toggle.Config function M.inlay_hints(opts) return M.new({ id = "inlay_hints", name = "Inlay Hints", get = function() return vim.lsp.inlay_hint.is_enabled({ bufnr = 0 }) end, set = function(state) vim.lsp.inlay_hint.enable(state, { bufnr = 0 }) end, }, opts) end ---@param opts? snacks.toggle.Config function M.diagnostics(opts) return M.new({ id = "diagnostics", name = "Diagnostics", get = function() local enabled = false if vim.diagnostic.is_enabled then enabled = vim.diagnostic.is_enabled() elseif vim.diagnostic.is_disabled then enabled = not vim.diagnostic.is_disabled() end return enabled end, set = function(state) if vim.fn.has("nvim-0.10") == 0 then if state then pcall(vim.diagnostic.enable) else pcall(vim.diagnostic.disable) end else vim.diagnostic.enable(state) end end, }, opts) end ---@private function M.health() local ok = pcall(require, "which-key") Snacks.health[ok and "ok" or "warn"](("{which-key} is %s"):format(ok and "installed" or "not installed")) end function M.profiler() return M.new({ id = "profiler", name = "Profiler", get = function() return Snacks.profiler.running() end, set = function(state) if state then Snacks.profiler.start() else Snacks.profiler.stop() end end, }) end function M.profiler_highlights() return M.new({ id = "profiler_highlights", name = "Profiler Highlights", get = function() return Snacks.profiler.ui.enabled end, set = function(state) if state then Snacks.profiler.ui.show() else Snacks.profiler.ui.hide() end end, }) end function M.indent() return M.new({ id = "indent", name = "Indent Guides", get = function() return Snacks.indent.enabled end, set = function(state) if state then Snacks.indent.enable() else Snacks.indent.disable() end end, }) end function M.dim() return M.new({ id = "dim", name = "Dimming", get = function() return Snacks.dim.enabled end, set = function(state) if state then Snacks.dim.enable() else Snacks.dim.disable() end end, }) end function M.words() return M.new({ id = "words", name = "LSP Words", get = function() return Snacks.words.enabled end, set = function(state) if state then Snacks.words.enable() else Snacks.words.disable() end end, }) end function M.scroll() return M.new({ id = "scroll", name = "Smooth Scroll", get = function() return Snacks.scroll.enabled end, set = function(state) if state then Snacks.scroll.enable() else Snacks.scroll.disable() end end, }) end function M.zen() return M.new({ id = "zen", name = "Zen Mode", get = function() return Snacks.zen.win and Snacks.zen.win:valid() or false end, set = function(state) if state then Snacks.zen() elseif Snacks.zen.win then Snacks.zen.win:close() end end, }) end function M.zoom() return M.new({ id = "zoom", name = "Zoom Mode", get = function() return Snacks.zen.win and Snacks.zen.win:valid() or false end, set = function(state) if state then Snacks.zen.zoom() elseif Snacks.zen.win then Snacks.zen.win:close() end end, }) end function M.animate() return M.new({ id = "animate", name = "Animations", get = function() return vim.g.snacks_animate ~= false end, set = function(state) vim.g.snacks_animate = state end, }) end return M