gesture.nvim and mouse gestures inside Neovim
Mouse gesture plugin for neovim
At a glance
- What is it?
- gesture.nvim is a Neovim plugin that turns mouse drags into command sequences, registering gestures as a list of directional inputs matched against an action, with support for custom matchers that inspect the direction history and the window ids involved. The interesting part is the matcher design, where can_match narrows the candidates before match decides, and the interesting constraint is that the plugin targets Neovim nightly rather than a stable release.
- Who is it for?
- gesture.nvim is a good fit for a Neovim user who wants to reach for a tab switch or a scroll jump without leaving the keyboard flow, because the gesture registration model composes direction sequences declaratively and the can_match and match callbacks let a gesture depend on context such as the direction of the last input and the windows it crosses.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 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 October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the plugin does and what it needs from the editor
gesture.nvim is a mouse gesture plugin for Neovim, and the documentation is explicit that it targets Neovim nightly. That is a meaningful constraint, because the plugin relies on drag-oriented key mappings that have moved around across Neovim versions, and a plugin that only works on nightly is a plugin that will break when the API changes under it.
The setup has three parts, and all three are required. The editor options come first, with vim.o.mouse set to a and vim.o.mousemoveevent set to true, so that Neovim reports mouse movement rather than only clicks. Then the drag events have to be mapped, and the plugin provides two functions for that, draw and finish, called on <LeftDrag> and <LeftRelease> respectively. The third part is registering the gestures themselves.
The mapping looks like this:
vim.o.mouse = "a"
vim.o.mousemoveevent = true
vim.keymap.set("n", "<LeftDrag>", [[<Cmd>lua require("gesture").draw()<CR>]], { silent = true })
vim.keymap.set("n", "<LeftRelease>", [[<Cmd>lua require("gesture").finish()<CR>]], { silent = true })The silent option matters more than it looks. Without it, every drag would echo a message into the command line, which would be noise in a gesture-driven workflow.
The right button variant is documented too, and it needs one extra step. The <RightMouse> mapping is set to <Nop> first, so the right click does not insert a mark or open a menu, and then <RightDrag> and <RightRelease> are mapped to the same draw and finish calls.
So the interaction model is a drag that leaves a visible trace on screen, and a release that resolves the trace into a command. The trace is what makes the gesture legible rather than magic, and the fact that the plugin exposes draw and finish separately means you can decide for yourself what the drag looks like and when the gesture is considered complete.
Registering a gesture from directional inputs
The registration API is where the design becomes clear. A gesture has a name, a list of inputs, and an action, and the inputs are direction constructors rather than raw strings.
A gesture that scrolls to the bottom is an up followed by a down:
local gesture = require("gesture")
gesture.register({
name = "scroll to bottom",
inputs = { gesture.up(), gesture.down() },
action = "normal! G",
})The action is a normal-mode command string here, and that is the simplest case. Tab navigation uses single directions, one gesture for the next tab and one for the previous tab, with the previous-tab gesture using a Lua callable instead of a string, with a comment noting that it can also use callable.
The composition is what makes the model more than a shortcut scheme. A go back gesture is a right followed by a left, which is a natural shape for a gesture and a shape that would be awkward as two separate key bindings.
There is a subtlety in the action string worth pointing out. The scroll to bottom gesture uses normal! G rather than plain G, and the distinction is the difference between a normal-mode command and a normal-mode command that ignores any user mapping. For a gesture that fires on a drag release, going through the normal! prefix means the gesture does the thing you meant rather than the thing you have remapped.
The Lua action form takes a context argument, and that is where the matcher callbacks come in.
The matcher model: can_match, then match
The close gesture traced windows example is the most interesting registration in the documentation, because it shows the two-stage matching design.
The gesture is closed with a match function that inspects the last input and checks that its direction is UP. The can_match function inspects the first input and checks that its direction is RIGHT.
gesture.register({
name = "close gesture traced windows",
match = function(ctx)
local last_input = ctx.inputs[#ctx.inputs]
return last_input and last_input.direction == "UP"
end,
can_match = function(ctx)
local first_input = ctx.inputs[1]
return first_input and first_input.direction == "RIGHT"
end,The split is the design. can_match is a cheap, early filter that asks whether this gesture is even a candidate given where the drag started, and match is the final decision given the whole path. The action then closes windows, sorting ctx.window_ids in descending order and calling nvim_win_close on each one that is still valid, guarded by nvim_win_is_valid.
The window handling is the detail that makes this gesture different from a simple command. The action does not close the window you are in; it closes the set of windows the gesture traced across, in reverse order, and it checks validity first because a window in the list may have been closed by an earlier iteration of the loop.
That is a good illustration of what the context object gives you. ctx.inputs is the full direction history with each element carrying a direction, and ctx.window_ids is the set of windows the drag crossed. A plugin that only offered a key binding could not express a gesture whose effect depends on which windows the pointer travelled over.
The practical cost is that the direction history and the window tracking have to be maintained correctly for this to work, and that is a much larger surface than a single mapping.
Running the tests and the shared workflow
The repository layout is small, and the test setup is the part worth understanding.
There is a Makefile at the root, a lua/ directory for the plugin source, a spec/ directory for the tests, a doc/ directory, and a stylua.toml for formatting. The Makefile itself is three lines of interest:
WORKFLOW_DIR ?= spec/.shared
include $(WORKFLOW_DIR)/neovim-plugin.mk
spec/.shared/neovim-plugin.mk:
git clone https://github.com/notomo/workflow.git --depth 1 spec/.sharedThe plugin does not contain its own test harness rules. It includes a makefile from a separate repository, notomo/workflow, and clones it if it is not already present, using a shallow clone into spec/.shared. The directory is overridable through WORKFLOW_DIR.
That is an interesting choice for a plugin. Rather than vendoring a test workflow, the author shares one across their plugins, so every Neovim plugin in that family is tested the same way. The cost is that running the tests requires network access on a fresh clone, and that a change in the shared workflow repository can change how the plugin's tests behave without a change in the plugin itself.
The stylua.toml file tells you the project uses StyLua for Lua formatting, which is the convention in the Neovim ecosystem, and the fact that formatting is configured separately from the makefile suggests formatting is checked somewhere in the shared workflow rather than locally.
For a user, none of this matters. For a contributor, it is the difference between a self-contained plugin and one that pulls its tooling from a sibling repository on every fresh checkout.
What it is not, and where the limits are
Two limits are worth stating before you install it.
The first is the Neovim nightly requirement. The plugin says outright that it is for Neovim nightly, and there is no compatibility matrix or stable-release fallback in the documentation. The reason is visible in the API surface: the drag mappings and the gesture tracing rely on editor behaviour that has been changing, and a plugin built against that is naturally going to track the tip. For someone who already tracks nightly, that is a non-issue. For someone on a pinned stable release, it means the plugin is not currently an option.
The second is the dependency on mouse events actually arriving. The whole mechanism requires vim.o.mouse to be a, vim.o.mousemoveevent to be true, and the terminal or environment to deliver <LeftDrag> and <LeftRelease> to the editor. That is not automatic. A terminal multiplexer may filter or reinterpret mouse reporting, a remote session over SSH with certain terminals will not deliver drag events at all, and a plain console Linux session often does not support them. On such a setup, the mappings simply never fire and the plugin looks broken rather than misconfigured.
The third point is about scope. gesture.nvim is a gesture recogniser, not a gesture library with a catalogue of ready-made bindings. Every gesture you get is one you register yourself, and the documentation's examples are all specific to a particular workflow, tab navigation and window closing in an editor. There is no built-in set of common gestures and no configuration file of shared bindings.
That is a deliberate division of labour rather than a gap. A plugin that shipped a fixed catalogue would have to guess at your keybindings, and a plugin that only recognises gestures leaves the mapping entirely to you, which is what you want when the actions are things like normal! G or a function that closes a specific set of windows.
The alternative approach to the same problem is to bind the actions to keys and use the mouse only for selection and scrolling, which is the default most people live with. The trade is speed against muscle memory. A gesture is faster once learned and invisible to a screen reader or a shared session, while a key binding is slower but works everywhere and is discoverable from a help file.
Editorial conclusion
gesture.nvim is a good fit for a Neovim user who wants to reach for a tab switch or a scroll jump without leaving the keyboard flow, because the gesture registration model composes direction sequences declaratively and the can_match and match callbacks let a gesture depend on context such as the direction of the last input and the windows it crosses. It is a poor fit if you are on a stable Neovim release rather than nightly, since the plugin states it targets nightly, and a poor fit if your terminal or remote session does not report reliable mouse events, because the whole mechanism depends on mousemoveevent being delivered. Before adopting, test that your setup delivers <LeftDrag> and <LeftRelease> mappings at all, since the plugin needs vim.o.mouse set to a and vim.o.mousemoveevent enabled before any gesture can fire.
Frequently asked questions
What does gesture.nvim do in Neovim?
It registers mouse gestures as lists of directional inputs matched to an action, so that a drag traced across the editor is resolved into a normal-mode command or a Lua function when you release the mouse button.
What setup does gesture.nvim need before any gesture works?
You need vim.o.mouse set to a and vim.o.mousemoveevent set to true, plus keymaps for <LeftDrag> and <LeftRelease> calling require("gesture").draw() and require("gesture").finish(). Without those three pieces no gesture will fire.
What is the difference between match and can_match in gesture.nvim?
can_match is the early filter that checks whether a gesture is a candidate at all, using for example the direction of the first input, while match is the final decision using the whole path, such as the direction of the last input being UP.
Does gesture.nvim work on stable Neovim releases?
The documentation states that gesture.nvim is a mouse gesture plugin for Neovim nightly, and it gives no compatibility table or fallback for stable releases, so it is aimed at users already tracking nightly.
How do the tests for gesture.nvim run?
The Makefile includes a makefile from a separate repository, notomo/workflow, and clones it shallowly into spec/.shared if it is not already there. The WORKFLOW_DIR variable can override where that shared workflow is placed.
What licence is gesture.nvim released under?
MIT, with the LICENSE file at the repository root. The last push to the repository was on 2026-10-17, and the repository has no GitHub releases.
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/notomo-gesture-nvim)Was this useful?