- Lua 99.7%
- Shell 0.3%
| .beads | ||
| .github/workflows | ||
| doc | ||
| examples | ||
| lua/tiny-glimmer | ||
| scripts | ||
| tests | ||
| LICENSE | ||
| README.md | ||
| selene.toml | ||
| stylua.toml | ||
🌟 tiny-glimmer.nvim
A Neovim plugin that adds smooth, customizable animations to text operations like yank, paste, search, undo/redo, and more.
Warning
This plugin is still in beta. Breaking changes may occur in future updates.
https://github.com/user-attachments/assets/745cb1e3-9904-4718-9804-ac0a4fee8748
Table of Contents
- Features
- Requirements
- Installation
- Configuration
- Examples
- API
- Library API
- Integrations
- FAQ
- Acknowledgments
Features
Smooth animations for various operations:
- Yank and paste
- Search navigation
- Undo/redo operations
- Custom operations support
Built-in animation styles:
fade- Smooth fade in/out transitionreverse_fade- Reverse fade effect with outBack easingbounce- Bouncing highlight effectleft_to_right- Linear left-to-right sweeppulse- Pulsating highlightrainbow- Rainbow color transitioncustom- Define your own animation logic
Note
Many operations are disabled by default. Enable the animations you want to use in your configuration.
Requirements
- Neovim >= 0.10
Installation
Lazy.nvim
{
"rachartier/tiny-glimmer.nvim",
event = "VeryLazy",
priority = 10, -- Low priority to catch other plugins' keybindings
config = function()
require("tiny-glimmer").setup()
end,
}
Packer.nvim
use {
"rachartier/tiny-glimmer.nvim",
config = function()
require("tiny-glimmer").setup()
end
}
Examples
Some Animations
https://github.com/user-attachments/assets/1bb98834-25d2-4f01-882f-609bec1cbe5c
Yank & Paste Overwrite
https://github.com/user-attachments/assets/1578d19f-f245-4593-a28f-b7e9593cbc68
Search Overwrite
https://github.com/user-attachments/assets/6bc98a8f-8b7e-4b57-958a-74ad5372612f
Undo/Redo Support
https://github.com/user-attachments/assets/5938e28c-2ff3-4e97-8707-67c24e61895c
Configuration
require("tiny-glimmer").setup({
-- Enable/disable the plugin
enabled = true,
-- Disable warnings for debugging highlight issues
disable_warnings = true,
-- Automatically reload highlights when colorscheme changes
-- When enabled, cached highlights will be refreshed on ColorScheme autocmd
autoreload = false,
-- Animation refresh rate in milliseconds
refresh_interval_ms = 8,
-- Timeout in milliseconds to wait after the last edit before processing animations
-- This uses a debouncing approach: the timer restarts on each edit, and only fires
-- when edits stop for this duration. This properly handles multi-location atomic
-- edits from surround plugins and similar tools (default: 50)
text_change_batch_timeout_ms = 50,
-- Automatic keybinding overwrites
overwrite = {
-- Automatically map keys to overwrite operations
-- Set to false if you have custom mappings or prefer manual API calls
auto_map = true,
-- Yank operation animation
yank = {
enabled = true,
default_animation = "fade",
},
-- Search navigation animation
search = {
enabled = false,
default_animation = "pulse",
next_mapping = "n", -- Key for next match
prev_mapping = "N", -- Key for previous match
},
-- Paste operation animation
paste = {
enabled = true,
default_animation = "reverse_fade",
paste_mapping = "p", -- Paste after cursor
Paste_mapping = "P", -- Paste before cursor
},
-- Undo operation animation
undo = {
enabled = false,
default_animation = {
name = "fade",
settings = {
from_color = "DiffDelete",
max_duration = 500,
min_duration = 500,
},
},
undo_mapping = "u",
},
-- Redo operation animation
redo = {
enabled = false,
default_animation = {
name = "fade",
settings = {
from_color = "DiffAdd",
max_duration = 500,
min_duration = 500,
},
},
redo_mapping = "<c-r>",
},
},
-- Third-party plugin integrations
support = {
-- Support for gbprod/substitute.nvim
-- Usage: require("substitute").setup({
-- on_substitute = require("tiny-glimmer.support.substitute").substitute_cb,
-- highlight_substituted_text = { enabled = false },
-- })
substitute = {
enabled = false,
default_animation = "fade",
},
},
-- Special animation presets
presets = {
-- Pulsar-style cursor highlighting on specific events
pulsar = {
enabled = false,
on_events = { "CursorMoved", "CmdlineEnter", "WinEnter" },
default_animation = {
name = "fade",
settings = {
max_duration = 1000,
min_duration = 1000,
from_color = "DiffDelete",
to_color = "Normal",
},
},
},
},
-- Override background color for animations (for transparent backgrounds)
transparency_color = nil,
-- Animation configurations
animations = {
fade = {
max_duration = 400, -- Maximum animation duration in ms
min_duration = 300, -- Minimum animation duration in ms
easing = "outQuad", -- Easing function
chars_for_max_duration = 10, -- Character count for max duration
from_color = "Visual", -- Start color (highlight group or hex)
to_color = "Normal", -- End color (highlight group or hex)
font_style = {}, -- Additional font styling (e.g. { bold = true }, see `:h nvim_set_hl`)
},
reverse_fade = {
max_duration = 380,
min_duration = 300,
easing = "outBack",
chars_for_max_duration = 10,
from_color = "Visual",
to_color = "Normal",
font_style = {},
},
bounce = {
max_duration = 500,
min_duration = 400,
chars_for_max_duration = 20,
oscillation_count = 1, -- Number of bounces
from_color = "Visual",
to_color = "Normal",
font_style = {},
},
left_to_right = {
max_duration = 350,
min_duration = 350,
min_progress = 0.85,
chars_for_max_duration = 25,
lingering_time = 50, -- Time to linger after completion
from_color = "Visual",
to_color = "Normal",
font_style = {},
},
pulse = {
max_duration = 600,
min_duration = 400,
chars_for_max_duration = 15,
pulse_count = 2, -- Number of pulses
intensity = 1.2, -- Pulse intensity
from_color = "Visual",
to_color = "Normal",
font_style = {},
},
rainbow = {
max_duration = 600,
min_duration = 350,
chars_for_max_duration = 20,
-- Note: Rainbow animation does not use from_color/to_color
font_style = {},
},
-- Custom animation example
custom = {
max_duration = 350,
chars_for_max_duration = 40,
color = "#ff0000", -- Custom property
-- Custom effect function
-- @param self table - The effect object with settings
-- @param progress number - Animation progress [0, 1]
-- @return string color - Hex color or highlight group
-- @return number progress - How much of the animation to draw
effect = function(self, progress)
return self.settings.color, progress
end,
},
},
-- Filetypes to disable hijacking/overwrites
hijack_ft_disabled = {
"alpha",
"snacks_dashboard",
},
-- Virtual text display priority
virt_text = {
priority = 2048, -- Higher values appear above other plugins
},
})
Built-in Animation Styles
Each animation can be customized with from_color and to_color options using highlight group names or hex colors:
require("tiny-glimmer").setup({
animations = {
fade = {
from_color = "DiffDelete", -- Highlight group
to_color = "DiffAdd",
},
bounce = {
from_color = "#ff0000", -- Hex color
to_color = "#00ff00",
},
},
})
Warning
The
rainbowanimation does not usefrom_colorandto_coloroptions.
Easing Functions
Available easing functions for fade and reverse_fade animations:
linearinQuad,outQuad,inOutQuad,outInQuadinCubic,outCubic,inOutCubic,outInCubicinQuart,outQuart,inOutQuart,outInQuartinQuint,outQuint,inOutQuint,outInQuintinSine,outSine,inOutSine,outInSineinExpo,outExpo,inOutExpo,outInExpoinCirc,outCirc,inOutCirc,outInCircinElastic,outElastic,inOutElastic,outInElasticinBack,outBack,inOutBack,outInBackinBounce,outBounce,inOutBounce,outInBounce
API
local glimmer = require("tiny-glimmer")
-- Control plugin state
glimmer.enable() -- Enable animations
glimmer.disable() -- Disable animations
glimmer.toggle() -- Toggle animations on/off
-- Change animation highlights dynamically
-- @param animation_name string|string[] - Animation name(s) or "all"
-- @param hl table - Highlight configuration { from_color = "...", to_color = "..." }
glimmer.change_hl("fade", { from_color = "#FF0000", to_color = "#0000FF" })
glimmer.change_hl("all", { from_color = "#FF0000", to_color = "#0000FF" })
glimmer.change_hl({"fade", "pulse"}, { from_color = "#FF0000", to_color = "#0000FF" })
-- Search operations (when overwrite.search.enabled = true)
glimmer.search_next() -- Same as "n"
glimmer.search_prev() -- Same as "N"
glimmer.search_under_cursor() -- Same as "*"
-- Paste operations (when overwrite.paste.enabled = true)
glimmer.paste() -- Same as "p"
glimmer.Paste() -- Same as "P"
-- Undo/redo operations (when undo/redo.enabled = true)
glimmer.undo() -- Undo changes
glimmer.redo() -- Redo changes
-- Refresh highlights after theme change
glimmer.apply() -- Recompute cached highlights for current colorscheme
Commands
:TinyGlimmer enable " Enable animations
:TinyGlimmer disable " Disable animations
:TinyGlimmer fade " Switch to fade animation
:TinyGlimmer reverse_fade " Switch to reverse_fade animation
:TinyGlimmer bounce " Switch to bounce animation
:TinyGlimmer left_to_right " Switch to left_to_right animation
:TinyGlimmer pulse " Switch to pulse animation
:TinyGlimmer rainbow " Switch to rainbow animation
:TinyGlimmer custom " Switch to custom animation
Keybinding examples:
vim.keymap.set("n", "<leader>ge", "<cmd>TinyGlimmer enable<cr>", { desc = "Enable animations" })
vim.keymap.set("n", "<leader>gd", "<cmd>TinyGlimmer disable<cr>", { desc = "Disable animations" })
vim.keymap.set("n", "<leader>gt", "<cmd>TinyGlimmer fade<cr>", { desc = "Switch to fade" })
Library API
The tiny-glimmer.lib module provides a low-level API for creating custom animations programmatically. This is useful for integrating animations into your own plugins or creating custom keybindings.
Quick Start
local glimmer = require("tiny-glimmer.lib")
-- Animate current line with fade effect
vim.keymap.set("n", "<leader>al", function()
glimmer.cursor_line("fade")
end)
-- Animate visual selection
vim.keymap.set("v", "<leader>av", function()
glimmer.visual_selection("pulse")
end)
-- Create custom animation on specific range
vim.keymap.set("n", "<leader>ac", function()
glimmer.create_animation({
range = glimmer.get_line_range(0),
duration = 500,
from_color = "#ff0000",
to_color = "#00ff00",
effect = "fade",
})
end)
Core Functions
create_animation(opts)
Create a simple text animation with full control over parameters.
glimmer.create_animation({
range = {
start_line = 0, -- 0-indexed start line
start_col = 0, -- 0-indexed start column
end_line = 0, -- 0-indexed end line
end_col = 10, -- 0-indexed end column
},
duration = 300, -- Animation duration in ms
from_color = "#ff0000", -- Start color (hex or highlight group)
to_color = "#00ff00", -- End color (hex or highlight group)
effect = "fade", -- Effect type (fade, pulse, bounce, etc.)
easing = "outQuad", -- Easing function (optional)
on_complete = function() -- Callback when done (optional)
print("Animation complete!")
end,
loop = false, -- Whether to loop (optional)
loop_count = 1, -- Number of loops, 0 = infinite (optional)
})
Parameters:
range(AnimationRange, required) - Text range to animateduration(number, required) - Animation duration in millisecondsfrom_color(string, required) - Start color (hex color or highlight group name)to_color(string, required) - End color (hex color or highlight group name)effect(string, optional) - Effect type, defaults to "fade"easing(string, optional) - Easing function, defaults to "linear"on_complete(function, optional) - Callback when animation completesloop(boolean, optional) - Whether to loop the animationloop_count(number, optional) - Number of times to loop (0 = infinite)
create_line_animation(opts)
Create a line-based animation that highlights entire lines (ignores column positions).
glimmer.create_line_animation({
range = glimmer.get_line_range(1),
duration = 400,
from_color = "DiffAdd",
to_color = "Normal",
effect = "pulse",
})
Parameters are the same as create_animation(), but start_col and end_col are ignored.
create_text_animation(opts)
Alias for create_animation() that highlights specific character ranges.
create_named_animation(name, opts)
Create a named animation that can be stopped later using its name.
-- Start an infinite rainbow effect
glimmer.create_named_animation("rainbow_loop", {
range = glimmer.get_line_range(0),
duration = 1000,
from_color = "#ff0000",
to_color = "#00ff00",
effect = "rainbow",
loop = true,
loop_count = 0, -- Infinite
})
-- Stop it later
vim.keymap.set("n", "<leader>x", function()
glimmer.stop_animation("rainbow_loop")
end)
Parameters:
name(string, required) - Unique identifier for this animationopts(table, required) - Same options ascreate_animation()
stop_animation(name)
Stop a named animation.
glimmer.stop_animation("my_animation_name")
create_effect(opts)
Create a custom effect with your own update function.
local effect = glimmer.create_effect({
settings = {
max_duration = 500,
min_duration = 300,
chars_for_max_duration = 10,
custom_color = "#ff00ff",
},
update_fn = function(self, progress)
-- Return color and progress for current frame
-- progress is between 0 and 1
local alpha = math.floor(progress * 255)
local color = string.format("#%02x00ff", alpha)
return color, progress
end,
builder = function(self)
-- Optional: Build initial data
return { initial_state = true }
end,
})
Helper Functions
Convenience functions for common animation patterns.
cursor_line(effect, opts)
Animate the current cursor line.
-- Simple usage
glimmer.cursor_line("pulse")
-- With custom settings
glimmer.cursor_line("fade", {
max_duration = 600,
from_color = "#ff0000",
loop = true,
loop_count = 3,
})
-- With effect configuration
glimmer.cursor_line({
name = "pulse",
settings = {
max_duration = 800,
pulse_count = 3,
}
})
visual_selection(effect, opts)
Animate the current visual selection.
vim.keymap.set("v", "<leader>v", function()
glimmer.visual_selection("bounce", {
max_duration = 500,
})
end)
animate_range(effect, range, opts)
Animate a specific range with an effect.
local range = {
start_line = 5,
start_col = 0,
end_line = 10,
end_col = 20,
}
glimmer.animate_range("fade", range, {
from_color = "DiffDelete",
to_color = "Normal",
})
named_animate_range(name, effect, range, opts)
Create a named animation for a specific range.
glimmer.named_animate_range("highlight_1", "rainbow", glimmer.get_line_range(5), {
loop = true,
loop_count = 0,
})
-- Stop it later
glimmer.stop_animation("highlight_1")
Range Utilities
Functions to get text ranges from various sources.
get_cursor_range()
Get the range of the current cursor position (single character).
local range = glimmer.get_cursor_range()
-- Returns: { start_line = 0, start_col = 5, end_line = 0, end_col = 6 }
get_visual_range()
Get the range of the current visual selection.
-- In visual mode
local range = glimmer.get_visual_range()
if range then
glimmer.animate_range("fade", range)
end
Returns nil if no visual selection exists.
get_line_range(line)
Get the range for a specific line.
-- Get current line (0 or nil)
local current_line = glimmer.get_line_range(0)
-- Get line 5 (1-indexed)
local line_5 = glimmer.get_line_range(5)
Parameters:
line(number) - 1-indexed line number, or 0 for current line
get_yank_range()
Get the range from the last yank operation.
local range = glimmer.get_yank_range()
if range then
glimmer.animate_range("pulse", range)
end
Returns nil if no yank operation has occurred.
Advanced Usage
Looping Animations
-- Loop 3 times
glimmer.create_animation({
range = glimmer.get_line_range(0),
duration = 200,
from_color = "#ff0000",
to_color = "#00ff00",
loop = true,
loop_count = 3,
on_complete = function()
print("Looped 3 times!")
end,
})
-- Infinite loop (must be named to stop)
glimmer.create_named_animation("infinite", {
range = glimmer.get_line_range(0),
duration = 500,
from_color = "Visual",
to_color = "Normal",
effect = "pulse",
loop = true,
loop_count = 0, -- 0 = infinite
})
-- Stop it when done
vim.defer_fn(function()
glimmer.stop_animation("infinite")
end, 5000)
Multiple Animations
-- Animate multiple lines at once
vim.keymap.set("n", "<leader>am", function()
local start_line = vim.api.nvim_win_get_cursor(0)[1]
for i = 0, 4 do
glimmer.create_line_animation({
range = glimmer.get_line_range(start_line + i),
duration = 300 + (i * 50), -- Stagger durations
from_color = "#ff0000",
to_color = "#00ff00",
effect = "fade",
})
end
end)
Custom Autocmd Integration
-- Animate on buffer write
vim.api.nvim_create_autocmd("BufWritePost", {
callback = function()
glimmer.cursor_line("pulse", {
max_duration = 300,
from_color = "DiffAdd",
})
end,
})
-- Animate search results
vim.keymap.set("n", "n", function()
vim.cmd("normal! n")
local pos = vim.api.nvim_win_get_cursor(0)
glimmer.create_animation({
range = glimmer.get_cursor_range(),
duration = 400,
from_color = "IncSearch",
to_color = "Normal",
effect = "pulse",
})
end)
For more examples, see the examples/ directory in the repository.
Integrations
gbprod/substitute.nvim
Add animation support to the substitute plugin:
{
"gbprod/substitute.nvim",
dependencies = { "rachartier/tiny-glimmer.nvim" },
config = function()
require("substitute").setup({
on_substitute = require("tiny-glimmer.support.substitute").substitute_cb,
highlight_substituted_text = {
enabled = false, -- Disable built-in highlight
},
})
end,
}
Then enable it in tiny-glimmer config:
require("tiny-glimmer").setup({
support = {
substitute = {
enabled = true,
default_animation = "fade",
},
},
})
yanky.nvim
Add yanky.nvim to tiny-glimmer dependencies to ensure proper loading order:
{
"rachartier/tiny-glimmer.nvim",
dependencies = { "gbprod/yanky.nvim" },
event = "VeryLazy",
priority = 10,
config = function()
require("tiny-glimmer").setup()
end,
}
FAQ
Why are two animations playing at the same time?
Disable your TextYankPost autocmd that calls vim.highlight.on_yank:
-- Remove or comment out this:
vim.api.nvim_create_autocmd("TextYankPost", {
callback = function()
vim.highlight.on_yank()
end,
})
Search keys not working with Lazy Vim?
When using Lazy Vim with search animations enabled, you may need to add the keys property to your plugin specification to ensure proper key mapping:
{
"rachartier/tiny-glimmer.nvim",
event = "VeryLazy",
priority = 10,
keys = {
"n",
"N",
},
config = function()
require("tiny-glimmer").setup({
overwrite = {
search = {
enabled = true,
},
},
})
end,
}
This tells Lazy Vim to load the plugin when the n or N keys are pressed, ensuring the plugin's key mappings take precedence.
Transparent background issues?
Set the transparency_color option to match your background:
require("tiny-glimmer").setup({
transparency_color = "#000000", -- Your background color
})
How to use custom animations?
Define a custom animation in the animations table:
require("tiny-glimmer").setup({
animations = {
my_custom = {
max_duration = 400,
chars_for_max_duration = 10,
custom_property = "value",
effect = function(self, progress)
-- Your animation logic here
return "#ff0000", progress
end,
},
},
overwrite = {
yank = {
enabled = true,
default_animation = "my_custom",
},
},
})
Animations not working?
Check these common issues:
- Ensure the operation is enabled in
overwriteconfig - Verify
auto_map = trueor set up manual keybindings - Check if the filetype is in
hijack_ft_disabled - Confirm animations are enabled:
:TinyGlimmer enable
How to disable for specific filetypes?
Add them to the hijack_ft_disabled list:
require("tiny-glimmer").setup({
hijack_ft_disabled = {
"alpha",
"dashboard",
"neo-tree",
},
})
Acknowledgments
- EmmanuelOga/easing - Easing function implementations
- tzachar/highlight-undo.nvim - Inspiration for hijack functionality
License
MIT