Hey, listen! Triforce adds a bit of RPG flavor to your coding — XP, levels, and achievements while you work.
I have ADHD, and coding can sometimes 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 actually my first-ever 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.
I made it to gamify my coding workflow — to turn those long, sometimes frustrating coding sessions into something that feels rewarding. Watching the XP bar fill up, unlocking achievements, and seeing my progress in real time gives me that little dopamine boost that helps me stay focused and motivated.
I named it Triforce just because I love The Legend of Zelda — no deep reason beyond that.
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 volt.nvim made the process so much smoother and helped me focus on bringing those ideas to life.
Requirements:
nvzone/volt (UI framework dependency)lazy.nvim{
'gisketch/triforce.nvim',
dependencies = { 'nvzone/volt' },
config = function()
require('triforce').setup({
-- Optional: Add your configuration here
keymap = {
show_profile = '<leader>tp', -- Open profile with <leader>tp
},
})
end,
}
pckr.nvimrequire('pckr').add({
{
'gisketch/triforce.nvim',
requires = { 'nvzone/volt' },
config = function()
require('triforce').setup({
keymap = {
show_profile = '<leader>tp',
},
})
end
}
})
paq-nvimrequire('paq')({
'nvzone/volt',
'gisketch/triforce.nvim',
})
require('triforce').setup({
keymap = {
show_profile = '<leader>tp',
},
})
vim-plugPlug 'nvzone/volt'
Plug 'gisketch/triforce.nvim'
lua << EOF
require('triforce').setup({
keymap = {
show_profile = '<leader>tp',
},
})
EOF
Triforce comes with sensible defaults, but you can customize everything:
require('triforce').setup({
enabled = true, -- Enable/disable the entire plugin
gamification_enabled = true, -- Enable XP, levels, achievements
-- Notification settings
notifications = {
enabled = true, -- Master toggle for all notifications
level_up = true, -- Show level up notifications
achievements = true, -- Show achievement unlock notifications
},
-- Keymap configuration
keymap = {
show_profile = '<leader>tp', -- Set to nil to disable default keymap
},
-- Auto-save interval (in seconds)
auto_save_interval = 300, -- Save stats every 5 minutes
-- Add custom language support
custom_languages = {
gleam = { icon = '✨', name = 'Gleam' },
odin = { icon = '🔷', name = 'Odin' },
-- Add more languages...
},
-- Customize level progression (optional)
level_progression = {
tier_1 = { min_level = 1, max_level = 10, xp_per_level = 300 }, -- Levels 1-10
tier_2 = { min_level = 11, max_level = 20, xp_per_level = 500 }, -- Levels 11-20
tier_3 = { min_level = 21, max_level = math.huge, xp_per_level = 1000 }, -- Levels 21+
},
-- Customize XP rewards (optional)
xp_rewards = {
char = 1, -- XP per character typed
line = 1, -- XP per new line
save = 50, -- XP per file save
},
-- Add filetypes to be excluded
ignore_ft = {},
-- Override heatmap highlight groups (hex colors or existing hl groups)
heat_highlights = {
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 |
|---|---|---|---|
enabled |
boolean |
true |
Enable/disable the plugin |
gamification_enabled |
boolean |
true |
Enable gamification features |
notifications.enabled |
boolean |
true |
Master toggle for notifications |
notifications.level_up |
boolean |
true |
Show level up notifications |
notifications.achievements |
boolean |
true |
Show achievement notifications |
debug |
boolean |
true |
Enable some debugging messages |
auto_save_interval |
number |
300 |
Auto-save interval in seconds |
keymap.show_profile |
string|nil |
nil |
Keymap for opening profile |
custom_languages |
table|nil |
nil |
Custom language definitions |
ignore_ft |
table|nil |
{} |
List of excluded filetypes |
levels |
table|nil |
See below | List of custom levels |
level_progression |
table|nil |
See below | Custom XP requirements per level tier |
xp_rewards |
table|nil |
See below | Custom XP rewards for actions |
achievements |
table |
See below | Custom achievements |
heat_highlights |
table|nil |
Defaults shown above | Override heatmap highlights (hex or links) |
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' } -- NOTE: You can omit your icon for an empty one if you wished
},
})
By default, Triforce uses a simple, easy-to-reach leveling system:
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.
For example, to make it even easier:
require('triforce').setup({
level_progression = {
tier_1 = { min_level = 1, max_level = 15, xp_per_level = 200 }, -- Super easy early levels
tier_2 = { min_level = 16, max_level = 30, xp_per_level = 400 },
tier_3 = { min_level = 31, max_level = math.huge, xp_per_level = 800 },
},
})
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 = 100, -- 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 = 5, -- Moderate XP for lines
save = 25, -- Less emphasis on saves
},
})
| Command | Description |
|---|---|
:Triforce config |
Open floating window showing your setup config |
:Triforce debug languages |
Debug language tracking |
:Triforce profile |
Open the Triforce profile UI |
:Triforce reset |
Reset all stats (useful for testing) |
:Triforce stats |
Display current stats in a notification |
:Triforce stats export |
Export stats to a new Neovim buffer |
:Triforce stats export <json|markdown> <path/to/file> |
Export stats to JSON or Markdown |
:Triforce stats save |
Force save stats immediately |
The profile includes 4 tabs:
H / L or arrow keys)H / L or arrow keys)<Tab>: Cycle forward<S-Tab>: Cycle backwardH / L or ← / →: Navigate achievement/levels pagesq / Esc: Close profileTriforce 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.
[!TIP] The
Achievementtype spec is as follows. DON'T COPY-PASTE DIRECTLY:{ id = 'template_achievement', ---@type string name = '...', ---@type string ---@type fun(stats?: Stats): boolean check = function(stats) return stats.foo > stats.bar -- NOTE: This is just an example end, icon = '...' or nil, ---@type string|nil desc = '...' or nil, ---@type string|nil }
For example:
require('triforce').setup({
achievements = {
{
id = 'first_200',
name = 'On Track',
desc = 'Type 200 Characters',
check = function(stats)
return stats.chars_typed >= 200
end,
},
{
id = 'first_300',
name = 'Newbie',
desc = 'Type 300 Characters',
check = function(stats)
return stats.chars_typed >= 300
end,
},
{
id = 'level_100',
name = 'God-like',
desc = 'Reach level 100',
icon = '',
check = function(stats)
return stats.level >= 100
end,
},
-- ...
},
})
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 = {
enabled = true, -- Keep enabled
level_up = false, -- Disable level up notifications
achievements = true, -- Keep achievement notifications
},
})
If you prefer to set your own keymap:
require('triforce').setup({
keymap = {
show_profile = nil, -- Don't create default keymap
},
})
-- 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 = {
TriforceHeat1 = 'Error',
TriforceHeat2 = 'DiagnosticVirtualTextWarn',
TriforceHeat3 = 'CursorLine',
TriforceHeat4 = '#424242',
},
})
Each key corresponds to a heat level used in the profile activity graph. If you omit a key, the default color for that level is used.
Triforce provides modular statusline components for lualine.nvim, letting you display your coding stats right in your statusline.
| Component | Default Display (uses NerdFont) | Description |
|---|---|---|
level |
Lv.27 ████░░ |
Level + XP progress bar |
achievements |
🏆 12/18 |
Unlocked/total achievements |
streak |
🔥 5 |
Current coding streak (days) |
session_time |
⏰ 2h 34m |
Current session duration |
Add Triforce components to your lualine configuration:
require('lualine').setup({
sections = {
lualine_x = {
-- Add one or more components
require('triforce.lualine').level,
require('triforce.lualine').achievements,
'encoding',
'fileformat',
'filetype',
},
}
})
Use the components() helper to get all components at once:
local triforce = require('triforce.lualine').components()
require('lualine').setup({
sections = {
lualine_x = {
triforce.level,
triforce.achievements,
triforce.streak,
triforce.session_time,
'encoding', 'fileformat', 'filetype'
},
}
})
Each component can be customized independently:
-- Default: prefix + level + bar
function()
return require('triforce.lualine').level()
end
-- Result: Lv.27 ████░░
-- Show percentage instead of bar
function()
return require('triforce.lualine').level({
show = { bar = false, percent = true },
})
end
-- Result: Lv.27 90%
-- Show everything (XP numbers + percentage)
function()
return require('triforce.lualine').level({
show = { bar = true, percent = true, xp = true },
bar = { length = 8 },
})
end
-- Result: Lv.27 ████████ 90% 450/500
-- Customize bar style
function()
return require('triforce.lualine').level({
bar = {
chars = { filled = '●', empty = '○' },
length = 10,
},
})
end
-- Result: Lv.27 ●●●●●●●●●○
-- Custom prefix or no prefix
function()
return require('triforce.lualine').level({
prefix = 'Level ', -- or set to '' for no prefix
})
end
-- Result: Level 27 ████░░
Options:
prefix (string): Text prefix before level number (default: 'Lv.')show (table): Toggles for showing different components:level (boolean): Show level number (default: true)bar (boolean): Show progress bar (default: true)percent (boolean): Show percentage (default: false)xp (boolean): Show XP numbers like 450/500 (default: false)bar (table): Bar properties:length (number): Progress bar length (default: 6)chars (table): { filled = '█', empty = '░' } (default)-- Default
function()
return require('triforce.lualine').achievements()
end
-- Result: 12/18
-- Custom icon or no icon
function()
return require('triforce.lualine').achievements({
icon = '', -- or '' for no icon
})
end
-- Result: 12/18
Options:
icon (string): Icon to display (default: '' - trophy)show_count (boolean): Show unlocked/total count (default: true)-- Default
function()
return require('triforce.lualine').streak()
end
-- Result: 5
-- Different icon
function()
return require('triforce.lualine').streak({ icon = '' })
end
-- Result: 5
Options:
icon (string): Icon to display (default: '' - flame)show_days (boolean): Show day count (default: true)[!NOTE] The streak component returns an empty string when streak is 0, so it won't clutter your statusline.
-- Default (short format)
function()
return require('triforce.lualine').session_time()
end
-- Result: 2h 34m
-- Long format (2:34:12 instead of 2h 34m)
function()
return require('triforce.lualine').session_time({
format = 'long',
})
end
-- Result: 2:34:12
-- Different icon
function()
return require('triforce.lualine').session_time({
icon = '', -- watch icon
})
end
-- Result: 2h 34m
Options:
icon (string): Icon to display (default: '' - clock)show_duration (boolean): Show time duration (default: true)format (string): 'short' (2h 34m) or 'long' (2:34:12) (default: 'short')Set defaults for all components:
-- Configure defaults
require('triforce.lualine').setup({
level = {
prefix = 'Level ',
bar = { length = 8 },
show = { percent = true },
},
achievements = { icon = '' },
streak = { icon = '' },
session_time = { icon = '', format = 'long' },
})
-- Then use components normally
local triforce = require('triforce.lualine').components()
require('lualine').setup({
sections = {
lualine_x = {
function()
return require('triforce.lualine').level()
end,
},
}
})
-- Result: Lv.27 ████░░
local triforce = require('triforce.lualine').components()
require('lualine').setup({
sections = {
lualine_c = { 'filename' },
lualine_x = {
triforce.session_time,
triforce.streak,
triforce.achievements,
triforce.level,
'encoding',
'filetype',
},
}
})
-- Result: 2h 34m 5 12/18 Lv.27 ████░░ ...
require('triforce.lualine').setup({
level = {
prefix = '', -- No prefix, just number
bar = { chars = { filled = '●', empty = '○' }, length = 10 },
show = { percent = true },
},
achievements = {
icon = '', -- medal icon
},
streak = {
icon = '', -- bolt icon
},
})
local triforce = require('triforce.lualine').components()
-- Now all components use your custom config
-- Result: 2h 34m 5 12/18 27 ●●●●●●●●●○ 90%
Stats are saved to ~/.local/share/nvim/triforce_stats.json.
The file is automatically backed up before each save to ~/.local/share/nvim/triforce_stats.json.bak.
{
"xp": 15420,
"level": 12,
"chars_typed": 45230,
"lines_typed": 1240,
"sessions": 42,
"time_coding": 14580,
"achievements": {
"first_100": true,
"level_10": true
},
"chars_by_language": {
"lua": 12000,
"python": 8500
},
"daily_activity": {
"2025-11-07": 145,
"2025-11-08": 203
},
"current_streak": 5,
"longest_streak": 12
}
Have a feature idea? Open an issue on GitHub!
MIT License - see LICENSE for details.
Made with ❤️ for the Neovim community
⭐ Star this repo if you find it useful!