___
/ \
| o=o | fret.nvim
\___/
|
========|========
========|======== Your playing won't
========|======== improve. Your tabs will.
========|========
========|========
========|========
_____|_____
/ | \
/ ( O ) \
| |
| ( O ) |
\ /
\___________/
A guitar tab editor for Neovim. For the tabs you'll write at midnight, forget about, and rediscover six months later wondering who wrote them.
- 6-string tab grid (e B G D A E): all the strings, none of the broken ones
- Beat ruler with time signature support (4/4, 3/4, 12/8, whatever you heard on that one album)
- Subdivisions: quarter, 8th, 16th, 32nd; go ahead, write that run you'll never play up to speed
- Multiple named sections (
[Intro],[Verse],[That part I always mess up]) - Repeat markers (
|:/:|) so you don't have to copy-paste the same riff four times - Song title, subtitle and section order (
A A B A C B A, classic) - Column + cell highlight so you always know where your cursor went
- Press
?for a help popup - no more alt-tabbing to the README mid-riff - Tabs are saved as
.fretJSON files and can be reopened with:FretOpen
Smoke on the Water
Deep Purple
Order: Intro Intro Verse Chorus Verse Chorus Outro
|: [Intro]
4/4 | 1 . 2 . 3 . 4 . |
e | - - - - - - - - |
B | - - - - - - - - |
G | 0 - 3 - 5 - - - |
D | 0 - 3 - 5 - - - |
A | - - - - - - - - |
E | - - - - - - - - |
[Verse]
...
Beat numbers go up as high as you need - 12/8 aligns correctly, no shifting.
Telescope is required.
{
"siatko/fret.nvim",
dependencies = { "nvim-telescope/telescope.nvim" },
config = function()
require("fret").setup({
fret_dir = vim.fn.expand("~/Documents/fret"),
keymaps = {
new_tab = "<leader>gt", -- open new tab editor
find_tab = "<leader>gf", -- telescope picker for saved tabs
},
})
end,
}<leader>gtor:FretNew- enter a title (required), then subtitle, time sig, smallest note- Write your riff with hjkl + digit keys
- Add sections with
a, name them withr, add repeats with[and] - Press
Yto copy to clipboard - the tab is also saved tofret_dir/<title>.fret - Paste into your markdown/notes/wherever - the code block keeps it monospaced
- Press
qto quit (prompts to save if there are unsaved changes) - Come back later with
<leader>gfor:FretOpen- telescope picker or fallback select to reopen any saved tab
Press ? inside the editor to show this as a popup.
Navigation
| Key | Action |
|---|---|
h / l |
Move left / right (slots) |
j / k |
Move down / up (strings) |
<C-k> / <C-j> |
Jump to previous / next section |
Notes
| Key | Action |
|---|---|
0–9 |
Enter fret number (keep typing for two digits, then <Enter>) |
x |
Clear note at cursor |
Measures
| Key | Action |
|---|---|
m |
Add measure |
M |
Delete current measure |
Sections
| Key | Action |
|---|---|
a |
Add section below current |
D |
Delete current section |
r |
Rename section (prompt) |
[ |
Toggle repeat start |: |
] |
Toggle repeat end :| |
Song metadata
| Key | Action |
|---|---|
T |
Edit title |
U |
Edit subtitle |
G |
Edit tuning |
O |
Edit section order |
Other
| Key | Action |
|---|---|
t |
Change time signature |
s |
Change subdivision |
Y |
Copy tab to clipboard and save to disk |
W |
Save tab to disk |
q |
Quit (prompts if unsaved changes) |
? |
Show keybinding help popup |
Five prompts - title is required, the rest are optional (<Enter> to skip):
- Title - required; used as the filename when saving (
my-riff.fret) - Subtitle - e.g.
Van Halen - Tuning - e.g.
Standard,Drop D,Open G - Time signature - pick from presets or type a custom one
- Smallest note - 4th / 8th / 16th / 32nd
Everything can be changed afterwards with T, U, G, O, t, s.
| Has name | Has repeat | Output |
|---|---|---|
| ✓ | [Verse] |
|
| ✓ | start+end | |: [Chorus] :| |
| ✗ | --- |
|
| ✗ | start only | |: |
| Command | Action |
|---|---|
:FretNew |
Open a new tab editor |
:FretOpen [path] |
Pick a saved tab to reopen (or open a path) |
:FretSave |
Save current tab to disk |
:FretCopy |
Copy tab to clipboard and save |
:FretAddMeasure |
Add a measure |
:FretTimeSig |
Change time signature |
:FretSubdiv |
Change subdivision |
:FretAddSection |
Add section below current |
:FretDeleteSection |
Delete current section |
:FretRenameSection |
Rename current section |
:FretTitle |
Edit title |
:FretSubtitle |
Edit subtitle |
:FretTuning |
Edit tuning |
:FretOrder |
Edit section order |
Tabs are saved as JSON in fret_dir (default ~/frets, configurable):
require("fret").setup({
fret_dir = vim.fn.expand("~/Documents/fret"),
})The filename is derived from the title (My Song.fret). Saving happens when you press Y (copy + save) or W (save only). Use :FretOpen or <leader>gf to browse and reopen saved tabs.
These are active everywhere, not just inside the editor:
| Key | Action |
|---|---|
<leader>gt |
Open new tab editor (:FretNew) |
<leader>gf |
Telescope picker for saved tabs |
-
copy_tabgives no feedback when the tab has no title -save_with_conflict_checkreturns silently, so the clipboard is set but the user sees nothing; restore the "tab copied to clipboard" notify for this case -
set_timesigandset_subdivisionusevim.fn.input(blocking, untestable) while every other prompt usesvim.ui.input- makes those two functions inconsistent and impossible to mock in tests -
:FretOpenfallback ininit.luaduplicates thegloblogic already intelescope.lua- extracted intoeditor.list_tab_files()used by both callers -
M.openhas five levels of nested callbacks - hard to follow and brittle to extend; a flat step-function or recursive approach would be easier to maintain -
quit_editorcan leave the user stuck if they try "Save and quit" but have no title and then cancel the rename prompt - now prompts for a title before proceeding
Technique markers (hammer-on h, pull-off p, slide / \) are the biggest missing piece compared to real tabs. They are not standalone notes - they are connections between two notes. That makes them structurally different from what's here: each slot would need to carry a connect_out field (e.g. "h", "p", "/") pointing toward the next slot, and the renderer would have to substitute that character for the space filler it currently puts between the two slots. Until then, you can annotate them by hand after copying with Y.
- Muted/dead notes (
x) - currentlyxclears a note; it could instead write a muted marker so that the rendered tab showsxin the cell rather than- - BPM / tempo - store a tempo field and display it in the header (e.g.
♩ = 120) so exported tabs carry timing intent - Capo - a single header field (
Capo: 2) that renders above the first section; fret numbers stay relative to the capo - Bar numbers - print a small measure counter above the ruler so readers can navigate long tabs without counting bars by hand
- Harmonics - natural harmonics
<12>and artificial harmonics{12}use a different cell format; requires a note-type flag alongside the fret number - Bends -
7b9(bend from fret 7 up to the pitch of 9),br(bend and release), pre-bends; a decorated note rather than a separate slot, but requires storing a target pitch and bend type alongside the fret number - Vibrato (
~) - simpler than bends since it's just a flag on a note with no second pitch; changes how the cell renders but doesn't affect column width or neighbours - Undo/redo - snapshot the song state before every mutation and keep a history stack; removes the risk of accidentally losing work with no recovery
- Copy/paste measures - yank a measure and paste it elsewhere; useful for repeating riffs without re-entering every note
- Transpose - shift all frets in the current section or whole song by ±n semitones; handy when a riff sits better one fret up or when adapting to a capo position
I'm a dad with a full-time job and approximately 45 minutes of free time per week. This plugin exists because I pair programmed it with Claude Code - which turns out to be a pretty good way to ship a Neovim plugin when your other option is waiting until your child moves out.
If you're using this plugin or thinking about contributing - you should know that. The ideas, design decisions, and direction are mine; Claude helped me get them out of my head and into working Lua faster than I could have alone. Issues and PRs are very welcome either way.