Skip to content

A Neovim plugin that adds smooth, customizable animations to text operations like yank, paste, search, undo/redo, and more.

License

Notifications You must be signed in to change notification settings

rachartier/tiny-glimmer.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🌟 tiny-glimmer.nvim

Neovim License Stars

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

Table of Contents

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 transition
  • reverse_fade - Reverse fade effect with outBack easing
  • bounce - Bouncing highlight effect
  • left_to_right - Linear left-to-right sweep
  • pulse - Pulsating highlight
  • rainbow - Rainbow color transition
  • custom - 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

tiny-glimmer-demo1.mp4

Yank & Paste Overwrite

tiny-glimmer-demo2.mp4

Search Overwrite

tiny-glimmer-demo3.mp4

Undo/Redo Support

tinyglimmerundo.mp4

Configuration

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
    },
})

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 rainbow animation does not use from_color and to_color options.

Easing Functions

Available easing functions for fade and reverse_fade animations:

  • linear
  • inQuad, outQuad, inOutQuad, outInQuad
  • inCubic, outCubic, inOutCubic, outInCubic
  • inQuart, outQuart, inOutQuart, outInQuart
  • inQuint, outQuint, inOutQuint, outInQuint
  • inSine, outSine, inOutSine, outInSine
  • inExpo, outExpo, inOutExpo, outInExpo
  • inCirc, outCirc, inOutCirc, outInCirc
  • inElastic, outElastic, inOutElastic, outInElastic
  • inBack, outBack, inOutBack, outInBack
  • inBounce, 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

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 animate
  • duration (number, required) - Animation duration in milliseconds
  • from_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 completes
  • loop (boolean, optional) - Whether to loop the animation
  • loop_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 animation
  • opts (table, required) - Same options as create_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,
})

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 overwrite config
  • Verify auto_map = true or 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

License

MIT

About

A Neovim plugin that adds smooth, customizable animations to text operations like yank, paste, search, undo/redo, and more.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published