From 70f0ad0ab8f80ab4d3725225b3cdf5cd644c6b4c Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 7 Nov 2023 11:02:51 -0700 Subject: [PATCH] feat: initial implementation A first working, publishable version of colorsaver.nvim. This plugin connects to the ColorScheme event to detect changes to the colorscheme. Any time that changes, it saves the colorscheme's name to a local file. In addition to that, it can watch that local file for changes. This way, when one `nvim` process changes the colorscheme, all other instances will automatically reload the colorscheme. --- .gitignore | 66 ++++++++++ .stylua.toml | 3 + LICENSE | 21 +++ README.md | 158 +++++++++++++++++++++++ lua/colorsaver/autocommands.lua | 103 +++++++++++++++ lua/colorsaver/config.lua | 98 ++++++++++++++ lua/colorsaver/file_watcher.lua | 50 ++++++++ lua/colorsaver/init.lua | 219 ++++++++++++++++++++++++++++++++ lua/colorsaver/logger.lua | 65 ++++++++++ lua/colorsaver/utils.lua | 52 ++++++++ 10 files changed, 835 insertions(+) create mode 100644 .gitignore create mode 100644 .stylua.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lua/colorsaver/autocommands.lua create mode 100644 lua/colorsaver/config.lua create mode 100644 lua/colorsaver/file_watcher.lua create mode 100644 lua/colorsaver/init.lua create mode 100644 lua/colorsaver/logger.lua create mode 100644 lua/colorsaver/utils.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41714f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +#--------------------------------------------------# +# The following was generated with gitignore.nvim: # +#--------------------------------------------------# +# Gitignore for the following technologies: Lua, Vim + +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..2278abd --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,3 @@ +indent_type = "Spaces" +indent_width = 2 +column_width = 100 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2fad05a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2023 Stephen Waits + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b87936c --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +# colorsaver.nvim + +colorsaver.nvim is a Neovim plugin designed to save and load your preferred +colorscheme persistently across sessions. With colorsaver, you can ensure your +aesthetic preferences are maintained automatically. Additionally, it can +automatically load colorscheme changes across all running instances of Neovim, +providing a consistent user experience. + +## Features + +- **Persistent Colorscheme**: Save your selected colorscheme and have it load +automatically on startup. +- **Automatic Updates**: Watch for changes and automatically apply them across +instances of Neovim. + +## Installation + +Install colorsaver using your favorite package manager. + +### [lazy](https://github.com/folke/lazy.nvim) (recommended) + +```lua +{ + "https://git.sr.ht/~swaits/colorsaver.nvim", + lazy = true, + event = "VimEnter", + opts = { + -- your options here + }, +} +``` + +Or, using [LazyVim](https://www.lazyvim.org/), which is built on top of +[lazy](https://github.com/folke/lazy.nvim), one example configuration might +look like this: + +```lua + { + -- tell LazyVim to stop messing with colorschemes + { "catppuccin/nvim", name = "catppuccin", enabled = false }, + { "folke/tokyonight.nvim", enabled = false }, + { + "LazyVim/LazyVim", + opts = { + colorscheme = "default", -- just some random built-in scheme + }, + }, + + + -- load/save our last used colorscheme automatically + { + "https://git.sr.ht/~swaits/colorsaver.nvim", + event = "VimEnter", + opts = {}, + dependencies = { + -- load colorschemes as a dependency of colorsaver + { "AlexvZyl/nordic.nvim" }, + { "EdenEast/nightfox.nvim" }, + { "Shatur/neovim-ayu" }, + { "rebelot/kanagawa.nvim" }, + }, + }, + } + +``` + +### [pckr](https://github.com/lewis6991/pckr.nvim) + +```lua +{ + "https://git.sr.ht/~swaits/colorsaver.nvim", + config = function() + require("colorsaver").setup({ + -- your options here + }) + end +} +``` + +### [vim-plug](https://github.com/junegunn/vim-plug) + +```vim +Plug 'https://git.sr.ht/~swaits/colorsaver.nvim' +lua require("colorsaver").setup({ + -- your options here +}) +``` + +## Configuration + +To configure colorsaver, pass a table with your preferred settings to the +`setup` function. Here is the default configuration: + +```lua +{ + -- log_level: Sets the logging level for the module's output. + -- Acceptable values are "debug", "info", "warn", "error". + log_level = "warn", + + -- debounce_ms: Sets the debounce time in milliseconds for file watching. + -- Accepts any integer greater than or equal to 50. If experiencing issues, + -- consider increasing this value. + debounce_ms = 100, + + -- filename: The name of the file where the colorscheme will be saved. + -- Note that the file is always stored in the "data" directory, which is + -- usually ~/.local/share/nvim/ + filename = "colorsaver", + + -- auto_load: If true, any colorscheme changes from one instance of nvim + -- will be automatically loaded by all other instances of nvim. + auto_load = true, +} +``` + +### File Watching + +If `auto_load` is enabled, colorsaver will watch for changes in the saved +colorscheme file and apply them to all running instances of Neovim. + +### Logging + +colorsaver features an integrated logging system. You can specify the log level +in the configuration options. For troubleshooting, set the `log_level` to +`"debug"` to receive verbose output. + +## Usage + +Once colorsaver is installed and configured, it works automatically. Your +current colorscheme will be saved upon change and reloaded when Neovim starts +or when any Neovim instance changes it (when `auto_load` is `true`). + +## Contributing + +Contributions to colorsaver are welcome! Please feel free to fork the +repository, make your changes, and submit a pull request. + +## MIT License + +Copyright © 2023 [Stephen Waits](https://swaits.com/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lua/colorsaver/autocommands.lua b/lua/colorsaver/autocommands.lua new file mode 100644 index 0000000..bb21de5 --- /dev/null +++ b/lua/colorsaver/autocommands.lua @@ -0,0 +1,103 @@ +-- autocommands.lua +--- AutocommandGroup module for managing groups of autocommands in Neovim. +-- This module encapsulates the creation and management of autocommand groups and their definitions. +local AutocommandGroup = {} +AutocommandGroup.__index = AutocommandGroup + +--- Constructor for creating a new AutocommandGroup. +-- @param name string The name of the autocommand group. +-- @param definitions table A table of autocommand definitions. +-- @return table A new instance of AutocommandGroup. +function AutocommandGroup.new(name, definitions) + -- Validate that 'name' is provided and is a string. + assert(type(name) == "string", "Autocommand group name must be a string") + + -- Create an instance of AutocommandGroup and set its properties. + local self = setmetatable({}, AutocommandGroup) + self.name = name + self.definitions = definitions or {} + -- Create a new autocommand group in Neovim, clearing any existing autocommands in the group. + self.group_id = vim.api.nvim_create_augroup(name, { clear = true }) + -- Set or update the autocommand definitions for the group. + self:set_definitions(self.definitions) + return self +end + +--- Sets or updates the autocommand definitions for the group. +-- @param definitions table The autocommand definitions to set or update. +function AutocommandGroup:set_definitions(definitions) + -- Validate that 'definitions' is a table. + assert(type(definitions) == "table", "Autocommand definitions must be a table") + + -- Clear existing autocommands from the group to prepare for new definitions. + vim.api.nvim_clear_autocmds({ group = self.group_id }) + + -- Iterate over each definition and create an autocommand. + for _, def in ipairs(definitions) do + -- Perform checks to ensure the definition structure is valid. + assert(def.events, "Autocommand definition is missing 'events' key") + assert( + type(def.events) == "string" or type(def.events) == "table", + "'events' must be a string or a table" + ) + assert(def.patterns, "Autocommand definition is missing 'patterns' key") + assert( + type(def.patterns) == "string" or type(def.patterns) == "table", + "'patterns' must be a string or a table" + ) + assert( + def.callback or def.command, + "Autocommand definition must have either 'callback' or 'command'" + ) + + -- Check for 'callback' and 'command' specifics and validate their types. + if def.callback then + assert(type(def.callback) == "function", "'callback' must be a function") + end + + if def.command then + assert(type(def.command) == "string", "'command' must be a string") + end + + -- Validate optional keys if they are present. + if def.once then + assert(type(def.once) == "boolean", "'once' must be a boolean") + end + + if def.nested then + assert(type(def.nested) == "boolean", "'nested' must be a boolean") + end + + -- Create the autocommand in Neovim with the provided definition. + vim.api.nvim_create_autocmd(def.events, { + group = self.group_id, + pattern = def.patterns, + callback = def.callback, + command = def.command, + once = def.once, + nested = def.nested, + }) + end + + -- Update the stored definitions. + self.definitions = definitions +end + +--- Clears the autocommand group, effectively disabling all its autocommands. +function AutocommandGroup:clear() + -- Clear all autocommands associated with the group in Neovim. + vim.api.nvim_clear_autocmds({ group = self.group_id }) +end + +--- Restores the autocommand group using the stored definitions. +function AutocommandGroup:restore() + -- Reapply the stored autocommand definitions. + self:set_definitions(self.definitions) +end + +-- Module table for exporting the constructor. +local M = { + new = AutocommandGroup.new, +} + +return M diff --git a/lua/colorsaver/config.lua b/lua/colorsaver/config.lua new file mode 100644 index 0000000..2284c50 --- /dev/null +++ b/lua/colorsaver/config.lua @@ -0,0 +1,98 @@ +-- config.lua +--- Configuration module for handling user settings and defaults. +-- This module provides validation and fallbacks for user-specified configuration. +local M = {} + +--- Define default configuration values with inline documentation. +-- These defaults are used as a fallback if the user does not provide these settings or if they are invalid. +local defaults = { + --- log_level: Sets the logging level for the module's output. + -- @field log_level Acceptable values are "debug", "info", "warn", "error". + log_level = "warn", + + --- debounce_ms: Sets the debounce time in milliseconds for file watching. + -- @field debounce_ms Accepts any integer greater than or equal to 50. If experiencing issues, consider increasing this value. + debounce_ms = 100, + + --- filename: The name of the file where the colorscheme will be saved. + -- @field filename Note that the file is always stored in the "data" directory, which is usually ~/.local/share/nvim/ + filename = "colorsaver", + + --- auto_load: If true, any colorscheme changes from one instance of nvim will be automatically loaded by all other instances of nvim. + -- @field auto_load + auto_load = true, +} + +--- Validates the logging level. +-- @param level string The log level to validate. +-- @return boolean Whether the log level is valid. +local function is_log_level_valid(level) + local valid_levels = { debug = true, info = true, warn = true, error = true } + return valid_levels[level:lower()] ~= nil +end + +--- Validates the debounce time. +-- @param ms number The debounce time in milliseconds to validate. +-- @return boolean Whether the debounce time is valid. +local function is_debounce_ms_valid(ms) + return type(ms) == "number" and ms >= 50 +end + +--- Validates the filename. +-- @param name string The filename to validate. +-- @return boolean Whether the filename is valid. +local function is_filename_valid(name) + return type(name) == "string" and name ~= "" +end + +--- Validates the auto_load value. +-- @param value boolean The auto_load value to validate. +-- @return boolean Whether the auto_load is valid. +local function is_auto_load_valid(value) + return type(value) == "boolean" +end + +--- Checks for invalid keys in the user configuration and logs a warning for each. +-- @param user_config table The user configuration to check. +local function check_for_invalid_keys(user_config) + for key, _ in pairs(user_config) do + if defaults[key] == nil then + log:warn("Ignoring invalid configuration key '" .. key .. "'.") + user_config[key] = nil + end + end +end + +--- Merges user configuration with defaults and validates the result. +-- @param user_config table The user-specified configuration. +-- @param logger table The logger instance to be used for logging messages. +-- @return table The validated and merged configuration. +function M.setup(user_config, logger) + -- Store the logger after ensuring it's provided. + assert(logger, "Logger instance must be provided for configuration setup.") + log = logger + + -- Check if the provided user_config is a table. + if user_config ~= nil and type(user_config) ~= "table" then + log:error("Provided user_config must be a table") + return + end + + -- Remove invalid keys from the user configuration. + check_for_invalid_keys(user_config or {}) + + -- Deeply merge the defaults with the user configuration, prioritizing user settings. + local config = vim.tbl_deep_extend("force", {}, defaults, user_config or {}) + + -- Validate and apply individual configuration settings. + config.log_level = is_log_level_valid(config.log_level) and config.log_level or defaults.log_level + config.debounce_ms = is_debounce_ms_valid(config.debounce_ms) and config.debounce_ms + or defaults.debounce_ms + config.filename = is_filename_valid(config.filename) and config.filename or defaults.filename + config.auto_load = is_auto_load_valid(config.auto_load) and config.auto_load or defaults.auto_load + + -- Return the validated and potentially corrected configuration. + return config +end + +return M diff --git a/lua/colorsaver/file_watcher.lua b/lua/colorsaver/file_watcher.lua new file mode 100644 index 0000000..3034102 --- /dev/null +++ b/lua/colorsaver/file_watcher.lua @@ -0,0 +1,50 @@ +-- file_watcher.lua +--- FileWatcher module for watching file system events. +-- This module provides the ability to watch for changes in a specified file and execute a callback when changes occur. +local FileWatcher = {} +FileWatcher.__index = FileWatcher + +--- Constructor for the FileWatcher object. +-- @param path string The file system path to watch. +-- @param callback function The callback function to execute when a change is detected. +-- @return table The constructed FileWatcher object. +function FileWatcher.new(path, callback) + -- Validate the input parameters to ensure correct types. + assert(type(path) == "string", "Path must be a string") + assert(type(callback) == "function", "Callback must be a function") + + -- Set up the FileWatcher object. + local self = setmetatable({}, FileWatcher) + self.path = path + self.callback = callback + -- Initialize the file system watcher with libuv. + self.fs_watcher = vim.loop.new_fs_event() + return self +end + +--- Starts watching the file system event. +-- This method activates the file watcher, which will invoke the callback on file changes. +function FileWatcher:start() + -- Ensure the file system watcher starts monitoring for changes. + -- Empty options table and vim.schedule_wrap to ensure the callback is called in the main loop. + self.fs_watcher:start(self.path, {}, vim.schedule_wrap(self.callback)) +end + +--- Stops watching the file system event. +-- This method deactivates the file watcher, preventing the callback from being invoked. +function FileWatcher:stop() + -- If the watcher is active, stop it. + if self.fs_watcher then + self.fs_watcher:stop() + end +end + +--- Restarts watching the file system event. +-- This method stops and then restarts the file watcher. +function FileWatcher:restart() + -- Stop and then start the watcher to refresh the monitoring. + self:stop() + self:start() +end + +return FileWatcher diff --git a/lua/colorsaver/init.lua b/lua/colorsaver/init.lua new file mode 100644 index 0000000..8680461 --- /dev/null +++ b/lua/colorsaver/init.lua @@ -0,0 +1,219 @@ +local M = {} + +-- load modules +local autocommands = require("colorsaver.autocommands") +local config = require("colorsaver.config") +local file_watcher = require("colorsaver.file_watcher") +local logger = require("colorsaver.logger") +local utils = require("colorsaver.utils") + +-- our module local data (and these are just placeholder values, they're overwritten below) +local autocmd_group = nil +local color_file_path = vim.fn.stdpath("data") .. "/colorsaver" +local colorfile_watcher = nil +local log = logger.new("warn", "colorsaver") + +--- save_colorscheme(new_scheme) +-- +-- This function saves a new colorscheme to a file. If no new_scheme is provided, it saves the default colorscheme. +-- +-- Parameters: +-- - new_scheme (string): The name of the new colorscheme to save. +-- +-- Returns: +-- - success (boolean): true if the colorscheme was saved successfully, false otherwise. +-- +-- Raises: +-- - None +-- +-- Example usage: +-- save_colorscheme("my_colorscheme") +-- +local function save_colorscheme(new_scheme) + local colorscheme_name = new_scheme + if not vim.tbl_contains(vim.fn.getcompletion("", "color"), colorscheme_name) then + log:warn("Didn't find " .. colorscheme_name .. " in the list of colorschemes") + return false + end + + local file, err = io.open(color_file_path, "w") + if not file then + log:error("Failed to open " .. color_file_path .. " for writing: " .. err) + return false + end + + local success, write_err = pcall(function() + file:write(colorscheme_name) + end) + + local close_ok, close_err = pcall(function() + file:close() + end) + + if not success then + log:error("Failed to write colorscheme to " .. color_file_path .. ": " .. write_err) + return false + elseif not close_ok then + log:error("Failed to close the file " .. color_file_path .. ": " .. close_err) + return false + end + + return true +end + +--- load_colorscheme() +-- +-- This function loads a colorscheme for the current session in Neovim. It first +-- attempts to read the colorscheme from a specified file path. If the file exists +-- and is not empty, the content of the file is used as the colorscheme. +-- +-- Parameters: +-- None +-- +-- Returns: +-- success (boolean): true if the colorscheme was loaded successfully, false otherwise. +-- +-- Raises: +-- None +-- +-- Example usage: +-- load_colorscheme() +-- +local function load_colorscheme() + local colorscheme = M.default -- Fallback to default colorscheme + + local file, err = io.open(color_file_path, "r") + if not file then + log:warn("Failed to open " .. color_file_path .. " for reading: " .. err) + return false + end + + local content, read_err = file:read("*a") + if read_err then + log:error("Failed to read from " .. color_file_path .. ": " .. read_err) + file:close() + return false + end + + local close_ok, close_err = file:close() + if not close_ok then + log:error("Failed to close " .. color_file_path .. ": " .. close_err) + return false + end + + colorscheme = content ~= "" and content or colorscheme + + if not (colorscheme and colorscheme ~= "") then + log:error("Invalid colorscheme name found in " .. color_file_path .. ".") + return false + end + + -- disable ColorScheme autocmd so we don't get into a bad loop; this is + -- because we're about to call `colorscheme` which will trigger ColorScheme + -- and cause us to write the file again, thus retriggering the + -- load_colorscheme function since the file changed... bad loop + if autocmd_group then + autocmd_group:clear() + end + + log:info("Switching to '" .. colorscheme .. "'") + local ok, cmd_err = pcall(vim.api.nvim_command, "colorscheme " .. colorscheme) + if not ok then + log:error("Failed to apply colorscheme '" .. colorscheme .. "': " .. cmd_err) + return false + end + + -- and re-enable the autocommands + if autocmd_group then + autocmd_group:restore() + end + + return true +end + +--- Callback function for file change events. +-- +-- This function is called when a file change event is triggered. It handles +-- the event by reloading the colorscheme from the updated file. If there is an +-- error, it logs the error message and stops the event. +-- +-- Parameters: +-- - err (string|nil) The error message, if any. If there is no error, this parameter is nil. +-- - filename (string) The name of the file that triggered the event. +-- - events (table) A table containing information about the events that occurred. +-- +-- Returns: +-- None +-- +-- Raises: +-- None +-- +local function on_change(err, filename) + vim.schedule(function() + if err then + -- If there's an error, notify and stop the event + log:error("Error watching file " .. tostring(filename) .. ": " .. tostring(err)) + return + end + + -- Reload the colorscheme from the updated file + load_colorscheme() + end) +end + +--- Sets up the ColorSaver module with the provided options. +-- +-- Parameters: +-- - opts (table): A table containing the options for configuring the ColorSaver module. The following keys are supported: +-- - filename (string): The name of the file where the colorscheme will be saved. +-- - log_level (string): The log level for the ColorSaver module. +-- - debounce_ms (number): The debounce time in milliseconds for saving the colorscheme. +-- - auto_load (boolean): Whether to enable autoloading of the colorscheme file. +-- +-- Returns: +-- None +-- +-- Raises: +-- None +-- +-- This function sets up the ColorSaver module by performing the following steps: +-- 1. Extracts and validates the options provided. +-- 2. Sets up local data, including the file path for saving the colorscheme and a logger instance. +-- 3. Loads the colorscheme at startup. +-- 4. Captures the new colorscheme and saves it on the "ColorScheme" event. +-- 5. Sets up autoloading of the colorscheme file if enabled. +-- +function M.setup(opts) + -- Extract options and validate them + M.config = config.setup(opts, log) + + -- Set up some local data + color_file_path = vim.fn.stdpath("data") .. "/" .. M.config.filename + log = logger.new(M.config.log_level, "colorsaver") + + -- Load the colorscheme at startup. + load_colorscheme() + + -- Capture the new colorscheme and save it on the ColorScheme event. + local debounced_save_colorscheme = utils.debounce(save_colorscheme, M.config.debounce_ms) + autocmd_group = autocommands.new("ColorSaverGroup", { + { + events = { "ColorScheme" }, + patterns = { "*" }, + callback = function(info) + vim.schedule(function() + debounced_save_colorscheme(info.match) -- info.match is the colorscheme parameter (i.e. name) + end) + end, + }, + }) + + -- Setup autoloading if enabled + if M.config.auto_load then + local debounced_on_change = utils.debounce(on_change, M.config.debounce_ms) + colorfile_watcher = file_watcher.new(color_file_path, debounced_on_change) + colorfile_watcher:start() + end +end + +return M diff --git a/lua/colorsaver/logger.lua b/lua/colorsaver/logger.lua new file mode 100644 index 0000000..e662b04 --- /dev/null +++ b/lua/colorsaver/logger.lua @@ -0,0 +1,65 @@ +--- Logger module for neovim that provides a simple logging interface. +-- @module Logger +local Logger = {} +Logger.__index = Logger + +--- Log levels with corresponding neovim's vim.log.levels. +local log_levels = { + debug = vim.log.levels.DEBUG, + info = vim.log.levels.INFO, + warn = vim.log.levels.WARN, + error = vim.log.levels.ERROR, +} + +--- Constructor for Logger object. +-- @param min_level The minimum log level to output messages for. +-- @param prefix A prefix to prepend to all log messages. +-- @return A new Logger instance with specified level and prefix. +function Logger.new(min_level, prefix) + local self = setmetatable({}, Logger) + self.min_level = log_levels[min_level] or log_levels.info -- Fallback to info level if unspecified + self.prefix = prefix or "Logger" -- Default prefix if none provided + return self +end + +--- Sets the minimum log level. +-- Only log messages at this level or higher will be output. +-- @param min_level The minimum log level string ("debug", "info", "warn", "error"). +function Logger:set_level(min_level) + self.min_level = log_levels[min_level] or self.min_level -- Keep current level if the new level is invalid +end + +--- Internal function to process the log message. +-- @param level The log level of the message. +-- @param message The log message. +function Logger:_log(level, message) + if level >= self.min_level then + vim.notify(self.prefix .. ": " .. message, level) + end +end + +--- Logs a message at DEBUG level. +-- @param message The message to log. +function Logger:debug(message) + self:_log(log_levels.debug, "DEBUG: " .. message) +end + +--- Logs a message at INFO level. +-- @param message The message to log. +function Logger:info(message) + self:_log(log_levels.info, "INFO: " .. message) +end + +--- Logs a message at WARN level. +-- @param message The message to log. +function Logger:warn(message) + self:_log(log_levels.warn, "WARN: " .. message) +end + +--- Logs a message at ERROR level. +-- @param message The message to log. +function Logger:error(message) + self:_log(log_levels.error, "ERROR: " .. message) +end + +return Logger diff --git a/lua/colorsaver/utils.lua b/lua/colorsaver/utils.lua new file mode 100644 index 0000000..a88d3dd --- /dev/null +++ b/lua/colorsaver/utils.lua @@ -0,0 +1,52 @@ +--- Utility functions for debouncing and validating colorschemes. +-- @module util +local M = {} + +--- Debounces a function call. This is useful to prevent a function from +-- being called too frequently, delaying its execution until after a certain +-- amount of time has elapsed since the last time it was invoked. +-- @param func function The function to debounce. +-- @param delay number The time delay in milliseconds. +-- @return function A new debounced function. +function M.debounce(func, delay) + assert(type(func) == "function", "Invalid argument 'func': function expected") + assert(type(delay) == "number", "Invalid argument 'delay': number expected") + + local timer = nil + return function(...) + local args = { ... } -- Capture any arguments passed to the function. + + if timer then + timer:stop() -- Stop any existing timer. + timer:close() -- Clean up the timer resources. + end + + -- Create a new timer. + timer = vim.loop.new_timer() + + -- Start the timer with the specified delay. + timer:start( + delay, + 0, + vim.schedule_wrap(function() + func(unpack(args)) -- Call the original function with the captured arguments. + timer:stop() -- Stop the timer. + timer:close() -- Clean up the timer resources. + timer = nil -- Clear the timer variable. + end) + ) + end +end + +--- Validates whether the provided colorscheme name is available in Neovim. +-- This function checks against the list of valid colorschemes that Neovim knows about. +-- @param colorscheme string The colorscheme name to validate. +-- @return boolean True if the colorscheme is valid, false otherwise. +function M.is_colorscheme_valid(colorscheme) + assert(type(colorscheme) == "string", "Invalid argument 'colorscheme': string expected") + + -- Check if the colorscheme exists in the list of completions for "color". + return vim.tbl_contains(vim.fn.getcompletion("", "color"), colorscheme) +end + +return M