Your music, inside Neovim.
gtm.nvim is the small piece of GTM that belongs in your editor: transport controls, a statusline, and reactive theming that tints your whole editor with the colours of whatever is playing. Under a thousand lines, no dependencies, no windows.
Anything with an interface — browsing, queues, search, downloads — lives in gtm's own TUI. This plugin does not reimplement it.
You need gtmd running. Grab it from the GTM repo, start it, then:
{ "prjctimg/gtm.nvim", opts = {} }That's it. The statusline shows a play/pause icon and a source marker, and the editor tints itself with whatever is playing.
There are no default keymaps. Pick the ones you want:
{
"prjctimg/gtm.nvim",
opts = {
keymaps = {
play_pause = "<leader>gp",
next = "<leader>gn",
prev = "<leader>gN",
volume_up = "<leader>g>",
volume_down = "<leader>g<",
stop = "<leader>gS",
},
},
}The statusline is a list of items you turn on. By default only the source marker and the state icon are shown; everything else is opt-in, so nothing is squeezed in without asking.
{
"prjctimg/gtm.nvim",
opts = {
statusline = {
source = true, -- ♫ Spotify ◉ radio ☁ podcast ❋ chart ▤
state = true, -- ▶ ⏸ ⏹
artist = true,
title = true,
album = false,
progress = true, -- ━━━─ 20 columns
time = false, -- [01:23/04:56]
volume = false, -- vol:80%
repeat = false, -- [R]
shuffle = false, -- [S]
},
},
}Items render in the order above. An item with nothing to show is skipped, so the empty state is just ⏹ rather than a row of zeroes.
statusline.source shows a marker for where the audio came from, using Nerd Font codepoints from Material Design Icons:
| Source | Glyph | Codepoint |
|---|---|---|
| spotify | md-spotify |
U+F04C7 |
| radio | md-radio |
U+F0439 |
| podcast | md-podcast |
U+F0994 |
| local | md-folder_music |
U+F1359 |
| chart | md-chart_line |
U+F012A |
A source your gtmd doesn't name renders nothing rather than guessing.
No Nerd Font? Those are private-use codepoints and will show as empty boxes. Paste the plain-Unicode set instead — it needs no patched font:
icons = { spotify = "◉", radio = "☁", podcast = "❋", local = "♫", chart = "▤" },You can override just the ones you like; the rest keep their defaults.
lualine:
require("lualine").setup({
sections = { lualine_x = { require("gtm").lualine_component() } },
})or any statusline:
vim.o.statusline = "%{%v:lua.require'gtm'.statusline()%}"The component hides itself while the daemon is unreachable. The clock advances locally between daemon events, so the progress bar does not stutter waiting on the socket.
On by default. Your editor takes on the colours of the album you're listening to: every surface gets a wash of the dominant colour and the syntax highlights shift to the palette, so scrolling from one track to the next feels like the room changed with it.
The daemon does the colour work, not the plugin. gtmd reads the cover art, extracts a palette, resolves it into a full set of roles — surfaces, foregrounds, accents, selection, syntax, semantics — and pushes them over the pulse socket. The plugin maps roles onto highlight groups and does no arithmetic at all. That is what keeps gtm.nvim, the gtm TUI, and any future editor plugin in agreement: one derivation, one answer.
A few things worth knowing:
- It keeps the tint while paused, and only restores your theme when playback really stops — no flicker on every pause.
- Error, warning, diff, spell and
Todocolours never move. They're how you spot problems, so they stay readable no matter what the artwork is doing. - Swapping colorscheme mid-song re-picks light or dark and re-tints, instead of dropping the theme.
:Gtm theme resetputs everything back exactly as it was.- A track with no cover art never tints. That is the daemon's call to make, not the plugin's.
- Turn it off with
theme = { reactive = false }.
It needs a gtmd that sends role sets. Against an older daemon the editor stays untinted — deliberately, since a second derivation is how two clients end up rendering the same track differently.
The plugin derives nothing. If your gtmd predates role sets, :checkhealth gtm says so and the editor simply stays untinted rather than guessing colours the TUI would render differently.
| Command | What it does |
|---|---|
:Gtm play |
Play / pause |
:Gtm next / :Gtm prev |
Skip tracks |
:Gtm stop |
Stop |
:Gtm volup / :Gtm voldown |
Volume ±5% |
:Gtm mute |
Mute / unmute |
:Gtm seek {secs} |
Jump in the track |
:Gtm shuffle |
Toggle shuffle |
:Gtm repeat |
Cycle repeat: off → one → all |
:Gtm repeat {off|one|all} |
Set repeat mode |
:Gtm status |
What is playing right now |
:Gtm theme |
Show the active palette |
:Gtm theme reset |
Clear the tint |
The Lua API mirrors the commands, so require("gtm").next() and friends work if you'd rather bind them yourself.
Two Unix sockets, both handled asynchronously through vim.uv:
gtmd.sock— newline-delimited JSON, request in, response out, correlated by a client-chosenid.gtmd.pulse— MessagePack frames the daemon pushes when things change.
Nothing polls for the things that matter: the plugin claims the event stream at startup, so a track change reaches you the moment it happens. A slow get_status_lite resync just keeps the clock honest.
Neovim ──► gtmd.sock "what's playing?" ──► a JSON reply
◄── gtmd.pulse "this track started" ── pushed, unprompted
- Neovim 0.11+
gtmdrunning (GTM)- For reactive theming, gtmd needs to be able to read cover art — see the GTM docs
No plugin dependencies, and none optional.
:checkhealth gtmReports the Neovim version, whether gtmd is installed, whether both sockets are live, how stale the status snapshot is, and whether the palette is flowing. If nothing is happening, start here.
See CONTRIBUTING.md. Bugs and feedback welcome; PRs are restricted for now.
Copyright (C) 2026 - present prjctimg prjctimg@outlook.com
Released under the GPL-3.0 license — same terms as GTM. In short: you may use, study, modify and redistribute it, and any distributed work must stay under the same license.

