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.
tinyglimmerundo.mp4
- Features
- Requirements
- Installation
- Configuration
- Examples
- API
- Library API
- Integrations
- FAQ
- Acknowledgments
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.
- Neovim >= 0.10
{
"rachartier/tiny-glimmer.nvim",
event = "VeryLazy",
priority = 10, -- Low priority to catch other plugins' keybindings
config = function()
require("tiny-glimmer").setup()
end,
}use {
"rachartier/tiny-glimmer.nvim",
config = function()
require("tiny-glimmer").setup()
end
}tiny-glimmer-demo1.mp4
tiny-glimmer-demo2.mp4
tiny-glimmer-demo3.mp4
tinyglimmerundo.mp4
require("tiny-glimmer").setup({
-- Enable/disable the plugin
enabled = true,
-- Disable warnings for debugging highlight issues
disable_warnings = true,
-- Animation refresh rate in milliseconds
refresh_interval_ms = 8,
-- 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)
},
reverse_fade = {
max_duration = 380,
min_duration = 300,
easing = "outBack",
chars_for_max_duration = 10,
from_color = "Visual",
to_color = "Normal",
},
bounce = {
max_duration = 500,
min_duration = 400,
chars_for_max_duration = 20,
oscillation_count = 1, -- Number of bounces
from_color = "Visual",
to_color = "Normal",
},
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",
},
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",
},
rainbow = {
max_duration = 600,
min_duration = 350,
chars_for_max_duration = 20,
-- Note: Rainbow animation does not use from_color/to_color
},
-- 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
},
})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 rainbow animation does not use from_color and to_color options.
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
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: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 animationKeybinding 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" })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.
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)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 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.
Alias for create_animation() that highlights specific character ranges.
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 a named animation.
glimmer.stop_animation("my_animation_name")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,
})Convenience functions for common animation patterns.
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,
}
})Animate the current visual selection.
vim.keymap.set("v", "<leader>v", function()
glimmer.visual_selection("bounce", {
max_duration = 500,
})
end)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",
})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")Functions to get text ranges from various sources.
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 the range of the current visual selection.
-- In visual mode
local range = glimmer.get_visual_range()
if range then
glimmer.animate_range("fade", range)
endReturns nil if no visual selection exists.
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 the range from the last yank operation.
local range = glimmer.get_yank_range()
if range then
glimmer.animate_range("pulse", range)
endReturns nil if no yank operation has occurred.
-- 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)-- 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)-- 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.
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",
},
},
})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,
}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,
})Set the transparency_color option to match your background:
require("tiny-glimmer").setup({
transparency_color = "#000000", -- Your background color
})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",
},
},
})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
Add them to the hijack_ft_disabled list:
require("tiny-glimmer").setup({
hijack_ft_disabled = {
"alpha",
"dashboard",
"neo-tree",
},
})- EmmanuelOga/easing - Easing function implementations
- tzachar/highlight-undo.nvim - Inspiration for hijack functionality
MIT