smear-cursor.nvim repaints text cells so a plain terminal cursor can glide
🌠 Neovim plugin to animate the cursor with a smear effect in all terminals
At a glance
- What is it?
- A Lua plugin for Neovim that animates the cursor in terminals limited to text, using stiffness, damping, particles and a timed redraw loop. Useful on a bare terminal, redundant under Neovide or Kitty, and expensive on a slow link.
- Who is it for?
- smear-cursor.nvim earns its place on a text-only terminal, where nothing else will animate the cursor, and the price is redraws you control with time_interval. Leave it out under Neovide or Kitty, which draw their own cursor, and on a high-latency link, where the 7ms fallback makes the redraw problem worse.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 34 days ago.
- What is it written in?
- Mainly Lua, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The trail that Neovide and Kitty already draw
A text terminal shows a Neovim cursor by writing one character cell and moving the terminal cursor around it. Nothing in that model has room for the animation Neovide renders, so a jump from one end of a line to the other is a single discrete jump. smear-cursor.nvim fills that gap by repainting text cells along the path between the old position and the new one, on a fixed tick, using nothing but characters.
That boundary is worth reading carefully, because the repository description promises a smear effect in all terminals while the README narrows the claim to terminals and GUIs without graphical capabilities. The README names Neovide, which animates its cursor on its own, and Kitty, which has graphical capabilities, as the places this plugin is not aimed at. On those hosts the trail duplicates work the terminal already does.
So the audience is narrow: people on plain terminals, over ssh, or in lightweight GUI clients, who want the motion of a frontend and accept redraws as the cost. The README also points smooth scrolling elsewhere, at karb94/neoscroll.nvim, which keeps this plugin's scope on the cursor alone.
Three package managers, three different activation calls
The floor is Neovim 0.10.2, and the three documented managers do not activate the plugin the same way. Read the snippet for your manager before assuming the others apply.
With the built-in package manager, vim.pack.add fetches the repository, and you write the setup call yourself in ~/.config/nvim/init.lua:
vim.pack.add({
"https://github.com/sphamba/smear-cursor.nvim",
})
require('smear_cursor').setup({
-- Your options
})With lazy.nvim the same options move into the opts key of a spec in ~/.config/nvim/lua/plugins/smear_cursor.lua, and the manager passes them through for you:
return {
"sphamba/smear-cursor.nvim",
opts = {},
}The vim-plug path is the odd one out, because it never calls setup. The snippet loads the plugin with Plug and then flips a boolean field on the module:
call plug#begin()
Plug 'sphamba/smear-cursor.nvim'
call plug#end()
lua require('smear_cursor').enabled = trueNothing in that example shows how a vim-plug user reaches any of the options described below. The path is documented as far as turning the effect on, and no further.
Five booleans decide whether a trail appears at all
Five keys in the default table decide where the plugin draws anything. This is the lazy.nvim spec with its documented defaults:
return {
"sphamba/smear-cursor.nvim",
opts = {
-- Smear cursor when switching buffers or windows.
smear_between_buffers = true,
-- Smear cursor when moving within line or to neighbor lines.
-- Use `min_horizontal_distance_smear` and `min_vertical_distance_smear` for finer control
smear_between_neighbor_lines = true,
-- Draw the smear in buffer space instead of screen space when scrolling
scroll_buffer_space = true,
-- Set to `true` if your font supports legacy computing symbols (block unicode symbols).
-- Smears and particles will look a lot less blocky.
legacy_computing_symbols_support = false,
-- Smear cursor in insert mode.
-- See also `vertical_bar_cursor_insert_mode` and `distance_stop_animating_vertical_bar`.
smear_insert_mode = true,
},
}Two of them define the trigger. smear_between_buffers covers jumps that leave the current buffer or window, and smear_between_neighbor_lines covers movement inside a line and one line over. Both are on by default, and both point at a pair of distance keys, min_horizontal_distance_smear and min_vertical_distance_smear, that decide when a short move is worth animating at all.
scroll_buffer_space is the one with a visible consequence. With it on, the trail is drawn in buffer coordinates while you scroll, so it belongs to the text and moves with it instead of staying pinned to the screen.
Insert mode is a separate switch because a bar cursor is not a block cursor, and the plugin ships vertical_bar_cursor_insert_mode and distance_stop_animating_vertical_bar for that case.
Stiffness, damping and the 17ms draw tick
The motion is a small simulation with two knobs. Stiffness decides how hard the trail is pulled toward the target, damping decides how much of that motion survives into the next tick, and each has an insert-mode twin. The documented faster configuration retunes six values and adds a seventh:
opts = { -- Default Range
stiffness = 0.8, -- 0.6 [0, 1]
trailing_stiffness = 0.6, -- 0.45 [0, 1]
stiffness_insert_mode = 0.7, -- 0.5 [0, 1]
trailing_stiffness_insert_mode = 0.7, -- 0.5 [0, 1]
damping = 0.95, -- 0.85 [0, 1]
damping_insert_mode = 0.95, -- 0.9 [0, 1]
distance_stop_animating = 0.5, -- 0.1 > 0
},The two damping defaults, 0.85 and 0.9, are described as the bounciness control: dropping them to 0.65 makes the trail overshoot the target, which is the elastic look rather than the tight one. distance_stop_animating is the single value outside the 0 to 1 range, it must stay above zero, and it names the distance at which the plugin stops animating; raising it from 0.1 to 0.5 is what makes the faster example look quicker.
Frame rate is a separate control from physics. Draws happen every 17ms by default, and the fix for a low framerate is a shorter interval rather than different numbers:
opts = {
time_interval = 7, -- milliseconds
},A 7ms tick means more than twice as many full redraws of the affected window every second, and nothing in the README puts a price on that over a remote session.
Cursor colour that some terminals quietly override
One key paints the trail. cursor_color defaults to the Cursor GUI color, and it accepts a hex code, a highlight group name, or the string "none", which matches the text colour at the target cursor position instead of drawing a visible stripe over your code.
The README warns that some terminals override the cursor colour Neovim asks for, and when that happens the smear and the real cursor stop matching. The documented fix is manual: read the colour your terminal actually uses and put that value in the config.
opts = {
-- Smear cursor color. Defaults to Cursor GUI color if not set.
-- Set to "none" to match the text color at the target cursor position.
-- Can be a hex color code, or a highlight group name.
cursor_color = "#d3cdc3",
},Sharpness is a font question. legacy_computing_symbols_support is false by default because fonts carrying block unicode symbols are rare, and Cascadia Code is the one example named. Switching it on makes smears and particles less blocky, since the renderer can pick from a wider set of block characters. With the flag off the trail is still drawn, using the characters a normal terminal font already has, and the README says you can live with that.
Particles, the fire hazard block and two comments that disagree
The particle system is where decoration turns into load. Each particle is given its own initial velocity, its own damping, a gravity, a spread angle, a per-second cap, a per-length cap, a minimum distance between emissions and a maximum lifetime. None of this is part of the defaults. It is switched on by a named example the README presents with a warning that some combinations make the cursor flicker:
opts = {
cursor_color = "#ff4000",
particles_enabled = true,
stiffness = 0.5,
trailing_stiffness = 0.2,
trailing_exponent = 5,
damping = 0.6,
gradient_exponent = 0,
gamma = 1,
never_draw_over_target = true, -- if you want to actually see under the cursor
hide_target_hack = true, -- same
},The rest of that example is the cost written as numbers, and it is the block most people copy without reading to the end:
particle_spread = 1,
particles_per_second = 500,
particles_per_length = 50,
particle_max_lifetime = 800,
particle_max_initial_velocity = 20,
particle_velocity_from_cursor = 0.5,
particle_damping = 0.15,
particle_gravity = -50,
min_distance_emit_particles = 0,Five hundred particles a second, up to fifty per unit of cursor travel, each alive for 800ms and falling at a gravity of -50, with emission allowed at every position because min_distance_emit_particles is zero.
The last two lines of the first block also contradict each other, and anyone pasting that snippet inherits the problem. never_draw_over_target carries the comment about wanting to see under the cursor, which matches its name: keep the trail from painting over the destination. The line below it, hide_target_hack, is annotated "same", yet its name describes hiding the target, which is the opposite of the stated goal. The two names cannot both describe the intent written in that comment, and no file in the repository settles which flag the annotation was written about.
Option docs generated from config.lua and checked against it
The option list is not maintained as prose by hand. Defaults live in lua/smear_cursor/config.lua and colours in lua/smear_cursor/color.lua, and a script under scripts turns them into generated documentation. The Makefile exposes both halves:
luadoc:
python3 scripts/generate_config_luadoc.py
luadoc-check:
python3 scripts/generate_config_luadoc.py --checkThe second target is the one that matters to a reader deciding whether to trust the options. Running the generator with --check fails when the generated text has drifted from config.lua, so an option cannot be renamed or retuned in one file and left stale in the other. That is a stricter arrangement than most plugins bother with, and it is the reason the README can safely send readers to two files instead of a long table.
Around that sits an ordinary Lua toolchain: stylua.toml for formatting, .pre-commit-config.yaml with hooks that the install target registers for both commit and commit-msg, a tests directory, and a doc directory the tree shows but the README never describes. Code lands under GPL-3.0, and the last push was on 2026-09-19.
Wrong for two hosts and for a slow ssh link
Three cases put it out of scope, and the project states the first two itself. Neovide animates its cursor natively, and Kitty has graphical capabilities of its own, so on either host this plugin adds redraws where the terminal already draws the motion.
A high-latency session is the third. Every redraw is traffic over that link, the default tick is 17ms, and the documented fix for a choppy trail is to shorten the interval to 7ms, which raises the number of draws instead of lowering it. A user who wants smooth scrolling is also looking in the wrong place, because the README assigns that job to karb94/neoscroll.nvim.
What stays thin is operational. There is no documented way to reload options at runtime, no rollback recipe for a configuration that made the cursor unreadable, and no platform list beyond the Neovim 0.10.2 floor and the assumption that the terminal renders text cells. The one runtime control the README documents is the toggle, :SmearCursorToggle, with :lua require("smear_cursor").toggle() as its scripted equivalent.
Editorial conclusion
smear-cursor.nvim earns its place on a text-only terminal, where nothing else will animate the cursor, and the price is redraws you control with time_interval. Leave it out under Neovide or Kitty, which draw their own cursor, and on a high-latency link, where the 7ms fallback makes the redraw problem worse. Particles stay off by default, so leave them there until the trail alone looks right. Before adopting, read lua/smear_cursor/config.lua for the option you actually need, then confirm your terminal does not override the Cursor GUI color that cursor_color copies.
Frequently asked questions
How do I install smear-cursor.nvim with lazy.nvim?
Put the repository name in a plugin spec and the options in the opts key, and the manager passes them to the plugin for you. With vim.pack the same setup call is written by hand after vim.pack.add fetches the repository.
What is the minimum Neovim version for smear-cursor.nvim?
Neovim 0.10.2 is the stated minimum requirement. The README gives no guidance for older versions, so an older Neovim has to be upgraded before the plugin applies.
How do I make the smear cursor color match my terminal cursor?
Set cursor_color to a hex code or a highlight group name; it defaults to the Cursor GUI color. Some terminals override the cursor color Neovim sets, so that value may have to be entered by hand, and the string none matches the text color at the target cursor position.
Does smear-cursor.nvim need a special font to look smooth?
No. legacy_computing_symbols_support defaults to false because fonts with block unicode symbols are rare, and Cascadia Code is named as one that has them. Enabling the flag makes smears and particles less blocky, but the plugin is usable without such a font.
How do I turn the smear cursor on and off without removing the plugin?
Run the :SmearCursorToggle command, or call :lua require("smear_cursor").toggle(). Both are given as ways to switch the effect after the plugin is enabled in the configuration.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sphamba-smear-cursor-nvim)Was this useful?