[!IMPORTANT] The original repository (gisketch/triforce.nvim) is currently unavailable. Since I was the co-maintainer, @gisketch authorized me to mirror it while his account is reinstated.
Read this post for more information on the topic.
Hey, listen! Triforce adds a bit of RPG flavor to your coding — XP, levels and achievements while you work.
I named this plugin Triforce just because I love The Legend of Zelda.
I have ADHD (Attention Deficit Hyperactivity Disorder), which can make coding feel like a grind; it’s hard to stay consistent or even get started some days. That’s part of why I fell in love with Neovim: it’s customizable, expressive, and makes the act of writing code feel fun again.
Triforce is co-maintained by @DrKJeff16, who has ADHD, too.
This is my first Neovim plugin (and the first plugin I’ve ever built in general). I’d always wanted to make something of my own, but I never really knew where to start. Once I got into Neovim’s Lua ecosystem I got completely hooked. I started experimenting, tinkering, breaking things, and slowly, Triforce came to life.
This exists to gamify my coding workflow; to turn those long, sometimes frustrating coding sessions into an experience that actually feels rewarding. Watching the XP bar fill up, unlocking achievements, and seeing my progress in real time gives me that little dopamine boost to help me stay focused and motivated.
The UI is heavily inspired by @siduck’s gorgeous designs and nvzone/typr; their aesthetic sense
and clean interface ideas played a huge role in how this turned out. Building it with nvzone/volt
made the process so much smoother and helped me focus on bringing those ideas to life.
:Triforce items <TAB>Requirements:
{
'DrKJeff16/triforce.nvim',
dependencies = { 'nvzone/volt' },
keys = {
{
'<leader>tp',
function()
require('triforce').show_profile()
end,
},
},
opts = {},
}
vim.pack.add({
{ src = 'https://github.com/nvzone/volt' },
{ src = 'https://github.com/DrKJeff16/triforce.nvim' },
})
require('pckr').add({
{
'DrKJeff16/triforce.nvim',
requires = { 'nvzone/volt' },
config = function()
require('triforce').setup()
end
}
})
require('paq')({
'nvzone/volt',
'DrKJeff16/triforce.nvim',
})
require('triforce').setup()
Plug 'DrKJeff16/triforce.nvim' | Plug 'nvzone/volt'
lua << EOF
require('triforce').setup()
EOF
You can simply call setup() and the defaults will be loaded:
require('triforce').setup()
You can find the defaults in lua/triforce/config.lua:
require('triforce').setup({
-- Enable/disable the entire plugin
enabled = true,
-- Select what icon engine to use to provide with filetype icons:
--
-- - `'builtin'` - Use the built-in icons (default)
-- - `'mini'` - Use `mini.icons`
icon_engine = 'builtin',
-- Enable XP, levels, achievements and optionally items
gamification_enabled = true,
-- Items configuration (EXPERIMENTAL)
items = {
-- Enable items (requires `gamification_enabled`)
enabled = false,
},
-- Backdrop control
backdrop = {
-- Whether to enable the backdrop (i.e. enable background dimming).
-- CREDITS: https://github.com/gisketch/triforce.nvim/issues/50
enabled = true,
-- See `:h 'winblend'`
winblend = 20,
},
-- Custom levels (requires `override_levels`)
levels = {},
-- Don't include the default levels
override_levels = false,
-- Custom achievements
achievements = {},
-- Notification settings
notifications = {
-- Show achievement unlock notifications
achievements = true,
-- Master toggle for all notifications
enabled = true,
-- Show level up notifications
level_up = true,
},
-- Auto-save interval (in seconds)
auto_save_interval = 300, -- Save stats every 5 minutes
-- Add custom language support
custom_languages = {
gleam = { icon = '✨', name = 'Gleam' }, -- `icon` is optional!
odin = { icon = '🔷', name = 'Odin' },
-- Add more languages...
},
-- Customize level progression (optional)
level_progression = {
tier_1 = { min_level = 1, max_level = 10, xp_per_level = 500 },
tier_2 = { min_level = 11, max_level = 20, xp_per_level = 750 },
tier_3 = { min_level = 21, max_level = 30, xp_per_level = 1250 },
tier_4 = { min_level = 31, max_level = 40, xp_per_level = 2500 },
tier_5 = { min_level = 41, max_level = 50, xp_per_level = 3750 },
tier_6 = { min_level = 51, max_level = 75, xp_per_level = 5000 },
tier_7 = { min_level = 76, max_level = 100, xp_per_level = 10000 },
tier_8 = { min_level = 101, max_level = 150, xp_per_level = 12500 },
tier_9 = { min_level = 151, max_level = 225, xp_per_level = 15000 },
tier_10 = { min_level = 226, max_level = math.huge, xp_per_level = 25000 }, -- `max_level` here MUST be `math.huge`!
},
-- Customize XP rewards (optional)
xp_rewards = {
-- XP per character typed
char = 1,
-- XP per new line
line = 1,
-- XP per file save
save = 50,
},
-- Add filetypes to be excluded
ignore_ft = {},
-- Override heatmap highlight groups (hex colors or existing hl groups)
heat_highlights = {
TriforceHeat0 = '#f0f0f0',
TriforceHeat1 = '#f0f0a0',
TriforceHeat2 = '#f0a0a0',
TriforceHeat3 = '#a0a0a0',
TriforceHeat4 = '#707070',
-- Or link to your colorscheme's groups:
-- TriforceHeat1 = 'DiffText',
},
-- Enable some debugging messages
debug = false,
})
| Option | Type | Default | Description |
|---|---|---|---|
achievements |
table|nil |
See below | Custom achievements |
auto_save_interval |
number |
300 |
Auto-save interval in seconds |
backdrop |
table|nil |
{ enabled = true, winblend = 20 } |
List of excluded filetypes |
custom_languages |
table|nil |
nil |
Custom language definitions |
debug |
boolean |
true |
Enable some debugging messages |
enabled |
boolean |
true |
Enable/disable the plugin |
gamification_enabled |
boolean |
true |
Enable gamification features |
heat_highlights |
table|nil |
Defaults shown above | Override heatmap highlights (hex or links) |
ignore_ft |
table|nil |
{} |
Backdrop customization |
items |
table|nil |
{ enabled = false } |
Items configuration |
level_progression |
table|nil |
See below | Custom XP requirements per level tier |
levels |
table|nil |
See below | List of custom levels |
notifications |
table|nil |
{ achievements = true, enabled = true, level_up = true } |
Notifications for different events |
override_levels |
boolean |
false |
Don't include the built-in levels |
xp_rewards |
table|nil |
See below | Custom XP rewards for actions |
By default, Triforce provides a Zelda-themed set of levels with title and icons:
{
[10] = { title = 'Deku Scrub', icon = '🌱' },
[20] = { title = 'Kokiri', icon = '🌳' },
[30] = { title = 'Hylian Soldier', icon = '🗡️' },
[40] = { title = 'Knight', icon = '⚔️' },
[50] = { title = 'Royal Guard', icon = '🛡️' },
[60] = { title = 'Master Swordsman', icon = '⚡' },
[70] = { title = 'Hero of Time', icon = '🔺' },
[80] = { title = 'Sage', icon = '✨' },
[90] = { title = 'Triforce Bearer', icon = '🔱' },
[100] = { title = 'Champion', icon = '👑' },
[120] = { title = 'Divine Beast Pilot', icon = '🦅' },
[150] = { title = 'Ancient Hero', icon = '🏛️' },
[180] = { title = 'Legendary Warrior', icon = '⚜️' },
[200] = { title = 'Goddess Chosen', icon = '🌟' },
[250] = { title = 'Demise Slayer', icon = '💀' },
[300] = { title = 'Eternal Legend', icon = '💫' },
}
You can add custom levels in your config aswell:
require('triforce').setup({
levels = {
{ level = 5, title = 'Newbie', icon = '🌱' },
{ level = 25, title = 'Charmful Coder' } -- The `icon` field is optional, which will become an empty string if unset
},
})
Triforce by default uses a simple, easy-to-reach leveling system.
The default leveling system is as follows:
Example progression:
5 × 300)10 × 300)3,000 + 5 × 500)3,000 + 10 × 500)8,000 + 10 × 1,000)You can customize this by overriding level_progression in your setup.
The defaults are:
require('triforce').setup({
level_progression = {
tier_1 = { min_level = 1, max_level = 10, xp_per_level = 500 },
tier_2 = { min_level = 11, max_level = 20, xp_per_level = 750 },
tier_3 = { min_level = 21, max_level = 30, xp_per_level = 1250 },
tier_4 = { min_level = 31, max_level = 40, xp_per_level = 2500 },
tier_5 = { min_level = 41, max_level = 50, xp_per_level = 3750 },
tier_6 = { min_level = 51, max_level = 75, xp_per_level = 5000 },
tier_7 = { min_level = 76, max_level = 100, xp_per_level = 10000 },
tier_8 = { min_level = 101, max_level = 150, xp_per_level = 12500 },
tier_9 = { min_level = 151, max_level = 225, xp_per_level = 15000 },
tier_10 = { min_level = 226, max_level = math.huge, xp_per_level = 25000 },
},
})
By default, Triforce awards XP for different coding activities:
You can customize these values to match your preferences. For example, if you want to emphasize quality over quantity and reward saves more:
require('triforce').setup({
xp_rewards = {
char = 0.5, -- Less XP for characters
line = 2, -- More XP for new lines
save = 10, -- Reward file saves heavily
},
})
Or if you prefer to focus on typing volume:
require('triforce').setup({
xp_rewards = {
char = 2, -- More XP per character
line = 3, -- Moderate XP for lines
save = 5, -- Less emphasis on saves
},
})
Triforce creates the :Triforce user command for your comfort. Here are the available arguments:
| Command | Description |
|---|---|
:Triforce config |
Open floating window showing your setup config |
:Triforce debug languages |
Debug language tracking |
:Triforce profile |
Toggles the Profile UI. |
:Triforce profile stats|achievements|languages|levels |
Toggles/cycles the Profile UI. If the UI is open and the passed tab is not the same as the current, it will cycle to it |
:Triforce reset |
Reset all stats (useful for testing) |
:Triforce stats |
Display current stats in a notification |
:Triforce stats export |
Export stats to a new buffer |
:Triforce stats export json [</path/to/file> [INDENT]] |
Export stats to JSON format. If a file name is given, the exported data will be saved there, otherwise a new buffer will open with the exported data |
:Triforce stats export markdown [</path/to/file>] |
Export stats to Markdown format. If a file name is given, the exported data will be saved there, otherwise a new buffer will open with the exported data |
:Triforce stats save |
Force-save stats immediately |
The profile UI includes the following tabs:
The following keybindings can be used in the profile UI:
| Key(s) | Description |
|---|---|
<Tab> / <S-Tab> |
Cycle tab (forward or backward, respectively) |
H / L or <Left> / <Right> |
Navigate between items for the achievements/levels tab |
q / <Esc> |
Close profile UI |
H / L or arrow keys):Triforce profile achievements:Triforce profile languagesH / L or arrow keys):Triforce profile levels:Triforce profile statsTriforce includes 18 built-in achievements across 5 categories:
Triforce now allows you to create new achievements with the achievements setup option.
By default it's just an empty table.
The Achievement type spec is shown below:
---@class Stats
---@field xp number
---@field level integer
---@field chars_typed integer
---@field lines_typed integer
---@field sessions integer
---@field time_coding integer
---@field achievements table<string, boolean>
---@field chars_by_language table<string, integer>
---@field daily_activity table<string, integer>
---@field current_streak integer
---@field longest_streak integer
-- DO NOT COPY DIRECTLY
{
---@param stats? Stats
---@return boolean ok
check = function(stats)
return stats.foo > stats.bar -- NOTE: This is just an example
end,
desc = '...' or nil, ---@type string|nil|?
icon = '...' or nil, ---@type string|nil|?
id = 'template_achievement', ---@type string
name = '...', ---@type string
}
Example:
require('triforce').setup({
achievements = {
{
check = function(stats)
return stats.chars_typed >= 200
end,
desc = 'Type 200 Characters',
id = 'first_200',
name = 'On Track',
},
{
check = function(stats)
return stats.chars_typed >= 300
end,
desc = 'Type 300 Characters',
id = 'first_300',
name = 'Newbie',
},
{
check = function(stats)
return stats.level >= 100
end,
desc = 'Reach level 100',
icon = '',
id = 'level_100',
name = 'God-Like',
},
},
})
Triforce supports 50+ programming languages out of the box, but you can add more:
require('triforce').setup({
custom_languages = {
gleam = { icon = '✨', name = 'Gleam' },
zig = { icon = '⚡', name = 'Zig' },
},
})
Turn off all notifications or specific types:
require('triforce').setup({
notifications = {
level_up = false, -- Disable level up notifications
},
})
You can set your own keymap to open Triforce's UI:
-- THIS IS AN EXAMPLE, SET YOUR OWN KEYMAP
vim.keymap.set('n', '<C-s>', require('triforce').show_profile, { desc = 'Show Triforce Stats' })
If your colorscheme uses unconventional highlight groups, point the heatmap to colors that fit your palette.
You can mix hex colors and links to existing highlight groups:
require('triforce').setup({
heat_highlights = {
TriforceHeat0 = nil, -- Will revert back to default
TriforceHeat1 = 'Error',
TriforceHeat2 = 'DiagnosticVirtualTextWarn',
TriforceHeat3 = 'CursorLine',
TriforceHeat4 = '#424242',
},
})
Each key corresponds to a heat level used in the profile activity graph. If a key is omitted then it will revert back to its default value.
Triforce provides modular statusline components for lualine.nvim, letting you display your coding stats right in your statusline.
The following lualine components are available (need a patched font):
| Component | Default Display | Description |
|---|---|---|
achievements |
🏆 12/18 |
Unlocked/total achievements |
currency |
💰 484 |
Current amount of currency |
level |
Lv.27 ████░░ |
Level + XP progress bar |
session_time |
⏰ 2h 34m |
Current session duration |
streak |
🔥 5 |
Current coding streak (days) |
Add Triforce components to your lualine configuration:
require('lualine').setup({
sections = {
lualine_x = { 'triforce' },
}
})
Each component can be customized independently.
Options:
enabled (boolean): Activates this component (default: false)icon (string): Icon to display (default: '' - trophy)show_count (boolean): Show unlocked/total count (default: true)require('lualine').setup({
sections = {
lualine_x = {
{ 'triforce', achievements = { enabled = false, icon = '', show_count = true } },
},
}
})
Options:
enabled (boolean): Activates this component (default: true)prefix (string): The prefix before the amount of currency (default: '💰')require('lualine').setup({
sections = {
lualine_x = {
{ 'triforce', currency = { enabled = true, prefix = '💰' } },
},
}
})
Options:
bar (table): Bar properties:chars (table): Signal both char types (empty and filled) for the bar (default: { empty = '░', filled = '█' })length (number): Progress bar length (default: 6)enabled (boolean): Activates this component (default: true)prefix (string): Text prefix before level number (default: 'Lv.')show (table): Toggles for showing different components:bar (boolean): Show progress bar (default: true)level (boolean): Show level number (default: true)percent (boolean): Show percentage (default: false)xp (boolean): Show XP numbers like 450/500 (default: false)require('lualine').setup({
sections = {
lualine_x = {
{
'triforce',
level = {
bar = { chars = { filled = '█', empty = '░' }, length = 6 },
enabled = true,
prefix = 'Lv.',
show = { bar = true, level = true, percent = false, xp = false },
},
},
},
}
})
Options:
enabled (boolean): Activates this component (default: false)format (string): 'short' (2h 34m) or 'long' (2:34:12) (default: 'short')icon (string): Icon to display (default: '' - clock)show_duration (boolean): Show time duration (default: true)require('lualine').setup({
sections = {
lualine_x = {
{
'triforce',
session_time = { enabled = false, icon = '', show_duration = true, format = 'short' },
},
},
}
})
Options:
enabled (boolean): Activates this component (default: false)icon (string): Icon to display (default: '' - flame)show_days (boolean): Show day count (default: true)The streak component returns an empty string when streak is 0, so it won't clutter your statusline.
require('lualine').setup({
sections = {
lualine_x = {
{
'triforce',
streak = { enabled = false, icon = '', show_days = true },
},
},
}
})
Set defaults for all components:
require('lualine').setup({
sections = {
lualine_x = {
{
'triforce',
achievements = {
enabled = false,
icon = '',
show_count = true,
},
currency = {
enabled = true,
prefix = '💰',
},
level = {
bar = { chars = { empty = '░', filled = '█' }, length = 6 },
enabled = true,
prefix = 'Lv.',
show = { bar = true, level = true, percent = false, xp = false },
},
session_time = {
enabled = false,
icon = '',
show_duration = true,
format = 'short',
},
streak = {
enabled = false,
icon = '',
show_days = true,
},
},
},
}
})
By default, stats are saved to ~/.local/share/nvim/triforce_stats.json. A backup is automatically
created before each save to ~/.local/share/nvim/triforce_stats.json.bak.
You can export your stats with the :Triforce stats export command.
The following is an export output in JSON format:
{
"achievements": {
"first_100": true,
"first_1000": true,
"first_10000": true,
"first_100000": true,
"first_300": true,
"first_session": true,
"level_10": true,
"level_25": true,
"level_5": true,
"level_50": true,
"polyglot_3": true,
"polyglot_5": true,
"sessions_10": true,
"sessions_100": true,
"sessions_50": true,
"time_10h": true,
"time_1h": true
},
"chars_by_language": {
"bash": 3068,
"c": 3389,
"lua": 119157,
"org": 14,
"python": 3189,
"sh": 580
},
"chars_typed": 158384,
"currency": 26066,
"current_streak": 2,
"daily_activity": {
"2026-09-19": 55,
"2026-09-20": 342,
"2026-09-21": 162,
"2026-09-22": 864,
"2026-09-23": 326,
"2026-09-24": 381,
"2026-09-25": 807,
"2026-09-26": 642,
"2026-09-27": 548,
"2026-09-28": 208,
"2026-09-29": 506,
"2026-09-30": 325,
"2026-10-01": 379,
"2026-10-02": 826,
"2026-10-03": 224,
"2026-10-04": 308,
"2026-10-05": 188,
"2026-10-06": 216,
"2026-10-07": 77,
"2026-10-08": 104,
"2026-10-09": 321,
"2026-10-10": 245,
"2026-10-11": 4,
"2026-10-13": 592,
"2026-10-14": 104
},
"db_path": "/home/<YOUR_USERNAME>/.local/share/nvim/triforce_stats.json",
"last_session_start": 1792037375,
"level": 71,
"lines_typed": 8833,
"longest_streak": 23,
"sessions": 1974,
"time_coding": 167848,
"xp": 190536
}
The following user command operations are available:
| Command | Format | Description |
|---|---|---|
:Triforce stats export |
Lua | Raw export of your stats in a new buffer |
:Triforce stats export json [<PATH> [INDENT]] |
JSON | Export your stats to a given JSON file (with optional indent). If no path is given, the stats will be exported in another buffer |
:Triforce stats export markdown [<PATH>] |
Markdown | Export your to a given Markdown file, with custom formatting. If no path is given, the stats will be exported in another buffer |
:Triforce items ...)Have a feature idea? Open an issue on GitHub!
nvzone/volt: Beautiful UI frameworknvzone/typr: Beautiful grid design component inspirationGrzegorzSzczepanek/gamify.nvim: Another cool gamification plugin, inspired the inclusion of achievementsMade with ❤️ for the Neovim community
⭐ Star this repo if you find it useful!