https://github.com/y3owk1n/notifier.nvim
A modern, feature-rich notification system for Neovim that transforms the standard vim.notify experience with beautiful UI, smart grouping, and powerful customization options.
https://github.com/y3owk1n/notifier.nvim
neovim neovim-plugin neovim-plugins notification notifications notifier nvim nvim-plugin nvim-plugins
Last synced: 8 months ago
JSON representation
A modern, feature-rich notification system for Neovim that transforms the standard vim.notify experience with beautiful UI, smart grouping, and powerful customization options.
- Host: GitHub
- URL: https://github.com/y3owk1n/notifier.nvim
- Owner: y3owk1n
- License: mit
- Created: 2025-08-09T16:35:41.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2025-08-09T18:34:28.000Z (8 months ago)
- Last Synced: 2025-08-09T19:27:36.185Z (8 months ago)
- Topics: neovim, neovim-plugin, neovim-plugins, notification, notifications, notifier, nvim, nvim-plugin, nvim-plugins
- Language: Lua
- Homepage:
- Size: 35.2 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 🔔 notifier.nvim
A modern, feature-rich notification system for Neovim that transforms the standard `vim.notify` experience with beautiful UI, smart grouping, and powerful customization options.


## ✨ Features
- 🎯 **Smart Positioning** - Multiple notification groups (corners) with independent management
- 🎨 **Beautiful UI** - Virtual text rendering with syntax highlighting and custom formatting
- ⏱️ **Timeout Management** - Automatic dismissal with configurable timeouts per notification
- 🔄 **ID-based Updates** - Update existing notifications instead of creating duplicates
- 📜 **History Viewer** - Browse all active notifications in a scrollable floating window
- 🎭 **Custom Formatters** - Create your own notification layouts and styling
- ⚡ **High Performance** - Debounced rendering and efficient virtual text handling
- 🎛️ **Fully Configurable** - Every aspect customizable through comprehensive options
## 📦 Installation
### Using [lazy.nvim](https://github.com/folke/lazy.nvim)
```lua
{
"y3owk1n/notifier.nvim",
config = function()
require("notifier").setup({
-- your configuration here
})
end
}
```
Then in your `init.lua`:
```lua
require("notifier").setup()
```
## 🚀 Quick Start
```lua
-- Basic setup with defaults
require("notifier").setup()
-- Now use enhanced vim.notify
vim.notify("Hello, World!")
vim.notify("Warning message", vim.log.levels.WARN)
vim.notify("Error occurred", vim.log.levels.ERROR)
```
## ⚙️ Configuration
### Default Configuration
```lua
require("notifier").setup({
-- Notification timeout in milliseconds
default_timeout = 3000,
-- Border style for floating windows
border = "none", -- "none", "single", "double", "rounded", "solid", "shadow"
-- Padding around notification content
padding = {
top = 0,
right = 0,
bottom = 0,
left = 0
},
-- Default notification group
default_group = "bottom-right",
-- Group positioning configurations
group_configs = {
["bottom-right"] = {
anchor = "SE",
row = vim.o.lines - 2,
col = vim.o.columns,
winblend = 0, -- 0-100 transparency
},
["top-right"] = {
anchor = "NE",
row = 0,
col = vim.o.columns,
winblend = 0,
},
["top-left"] = {
anchor = "NW",
row = 0,
col = 0,
winblend = 0,
},
["bottom-left"] = {
anchor = "SW",
row = vim.o.lines - 2,
col = 0,
winblend = 0,
},
},
-- Icons for different log levels
icons = {
[vim.log.levels.TRACE] = " ",
[vim.log.levels.DEBUG] = " ",
[vim.log.levels.INFO] = " ",
[vim.log.levels.WARN] = " ",
[vim.log.levels.ERROR] = " ",
}
-- Formatters
notif_formatter = U.default_notif_formatter,
notif_history_formatter = U.default_notif_history_formatter,
})
```
### Custom Styling Example
```lua
require("notifier").setup({
default_timeout = 5000,
border = "rounded",
padding = { top = 1, right = 2, bottom = 1, left = 2 },
group_configs = {
["bottom-right"] = {
anchor = "SE",
row = vim.o.lines - 3, -- Leave more space from bottom
col = vim.o.columns - 1,
winblend = 20, -- Semi-transparent
}
},
-- Custom icons
icons = {
[vim.log.levels.ERROR] = "✗ ",
[vim.log.levels.WARN] = "⚠ ",
[vim.log.levels.INFO] = "ℹ ",
[vim.log.levels.DEBUG] = "🐛 ",
[vim.log.levels.TRACE] = "👁 ",
}
})
```
## 📖 Usage Examples
### Basic Notifications
```lua
-- Simple notification
vim.notify("Task completed successfully!")
-- With log level
vim.notify("Configuration reloaded", vim.log.levels.INFO)
-- Multi-line message
vim.notify("Build failed:\n- Syntax error on line 42\n- Missing dependency")
```
### Advanced Options with Data and Inline Formatters
```lua
-- Notification with custom timeout and icon
vim.notify("Long running task started", vim.log.levels.INFO, {
timeout = 10000, -- 10 seconds
icon = "⏳ "
})
-- Target specific group
vim.notify("Debug info", vim.log.levels.DEBUG, {
group_name = "top-left"
})
-- Updateable notification with ID
vim.notify("Downloading... 0%", vim.log.levels.INFO, {
id = "download-progress"
})
-- Update the same notification
vim.notify("Downloading... 50%", vim.log.levels.INFO, {
id = "download-progress" -- Same ID updates existing
})
vim.notify("Download complete!", vim.log.levels.INFO, {
id = "download-progress"
})
-- Inline formatter with custom data - no message needed!
vim.notify("", vim.log.levels.INFO, {
id = "server-status",
_notif_formatter = function(opts)
local data = opts.notif._notif_formatter_data
local status_icon = data.online and "🟢" or "🔴"
local status_text = data.online and "ONLINE" or "OFFLINE"
local status_color = data.online and "String" or "ErrorMsg"
return {
{ display_text = "🖥️ Server ", hl_group = "NotifierInfo", is_virtual = true },
{ display_text = data.name, hl_group = "Identifier", is_virtual = true },
{ display_text = " " .. status_icon .. " ", hl_group = status_color, is_virtual = true },
{ display_text = status_text, hl_group = status_color, is_virtual = true },
data.uptime and { display_text = " (up " .. data.uptime .. ")", hl_group = "Comment", is_virtual = true } or nil,
}
end,
_notif_formatter_data = {
name = "prod-api-01",
online = true,
uptime = "2d 5h"
}
})
-- Update server status with new data
vim.notify("", vim.log.levels.WARN, {
id = "server-status", -- Same ID updates the notification
_notif_formatter_data = {
name = "prod-api-01",
online = false,
uptime = nil
}
})
```
### Custom Highlight Groups
```lua
-- Use custom highlight group
vim.notify("Special message", vim.log.levels.INFO, {
hl_group = "MyCustomHighlight"
})
```
Define your highlight group:
```lua
vim.api.nvim_set_hl(0, "MyCustomHighlight", {
fg = "#ff6b6b",
bold = true
})
```
## 🎨 Custom Formatters
Create your own notification layouts with powerful formatting options:
### Global Custom Formatter
```lua
-- Custom formatter function
local function my_formatter(opts)
local notif = opts.notif
local line = opts.line
local config = opts.config
return {
{ display_text = ">> ", hl_group = "Comment", is_virtual = true },
{ display_text = line, hl_group = notif.hl_group, is_virtual = true },
{ display_text = " <<", hl_group = "Comment", is_virtual = true },
}
end
require("notifier").setup({
notif_formatter = my_formatter
})
```
### Inline Custom Formatters with Data
Pass custom data and formatters for specific notifications:
```lua
-- Progress bar formatter with custom data
vim.notify("", vim.log.levels.INFO, {
id = "progress-bar",
timeout = 10000,
_notif_formatter = function(opts)
local data = opts.notif._notif_formatter_data
local progress = data.progress or 0
local task = data.task or "Processing"
local total_width = 20
local filled = math.floor((progress / 100) * total_width)
local empty = total_width - filled
local bar = "█" .. string.rep("█", filled) .. string.rep("░", empty) .. "█"
local percentage = string.format("%3d%%", progress)
return {
{ display_text = data.icon or "⏳ ", hl_group = "NotifierInfo", is_virtual = true },
{ display_text = task .. ": ", hl_group = "NotifierInfo", is_virtual = true },
{ display_text = bar, hl_group = progress == 100 and "NotifierInfo" or "Comment", is_virtual = true },
{ display_text = " " .. percentage, hl_group = "NotifierInfo", is_virtual = true },
}
end,
_notif_formatter_data = {
progress = 45,
task = "Downloading files",
icon = "📥 "
}
})
-- Update progress (same ID with new data)
vim.notify("", vim.log.levels.INFO, {
id = "progress-bar",
_notif_formatter_data = {
progress = 75,
task = "Downloading files",
icon = "📥 "
}
})
-- Complete
vim.notify("", vim.log.levels.INFO, {
id = "progress-bar",
timeout = 3000,
_notif_formatter_data = {
progress = 100,
task = "Download complete",
icon = "✅ "
}
})
```
### Advanced Data-Driven Formatters
```lua
-- Git status formatter with rich data
vim.notify("", vim.log.levels.INFO, {
id = "git-status",
timeout = 8000,
_notif_formatter = function(opts)
local data = opts.notif._notif_formatter_data
local parts = {}
-- Title
table.insert(parts, { display_text = "🌿 Git Status", hl_group = "NotifierInfo", is_virtual = true })
if data.branch then
table.insert(parts, { display_text = " on ", hl_group = "Comment", is_virtual = true })
table.insert(parts, { display_text = data.branch, hl_group = "String", is_virtual = true })
end
-- Stats with colors
if data.added and data.added > 0 then
table.insert(parts, { display_text = " +" .. data.added, hl_group = "diffAdded", is_virtual = true })
end
if data.modified and data.modified > 0 then
table.insert(parts, { display_text = " ~" .. data.modified, hl_group = "diffChanged", is_virtual = true })
end
if data.deleted and data.deleted > 0 then
table.insert(parts, { display_text = " -" .. data.deleted, hl_group = "diffRemoved", is_virtual = true })
end
return parts
end,
_notif_formatter_data = {
branch = "feature/new-ui",
added = 5,
modified = 3,
deleted = 1
}
})
-- LSP diagnostic summary formatter
vim.notify("", vim.log.levels.WARN, {
id = "lsp-diagnostics",
_notif_formatter = function(opts)
local data = opts.notif._notif_formatter_data
local parts = {
{ display_text = "🔍 Diagnostics: ", hl_group = "NotifierInfo", is_virtual = true }
}
if data.errors > 0 then
table.insert(parts, { display_text = " " .. data.errors, hl_group = "DiagnosticError", is_virtual = true })
end
if data.warnings > 0 then
table.insert(parts, { display_text = " " .. data.warnings, hl_group = "DiagnosticWarn", is_virtual = true })
end
if data.info > 0 then
table.insert(parts, { display_text = "ℹ " .. data.info, hl_group = "DiagnosticInfo", is_virtual = true })
end
if data.hints > 0 then
table.insert(parts, { display_text = " " .. data.hints, hl_group = "DiagnosticHint", is_virtual = true })
end
return parts
end,
_notif_formatter_data = {
errors = 2,
warnings = 5,
info = 3,
hints = 1
}
})
-- Build status with timing information
vim.notify("", vim.log.levels.INFO, {
id = "build-status",
_notif_formatter = function(opts)
local data = opts.notif._notif_formatter_data
local icon = data.status == "success" and "✅" or
data.status == "error" and "❌" or "⏳"
local color = data.status == "success" and "NotifierInfo" or
data.status == "error" and "NotifierError" or "NotifierWarn"
return {
{ display_text = icon .. " Build ", hl_group = color, is_virtual = true },
{ display_text = data.status, hl_group = color, is_virtual = true },
data.duration and { display_text = " (" .. data.duration .. "s)", hl_group = "Comment", is_virtual = true } or nil,
data.target and { display_text = " [" .. data.target .. "]", hl_group = "Identifier", is_virtual = true } or nil,
}
end,
_notif_formatter_data = {
status = "success",
duration = 2.5,
target = "release"
}
})
```
### Real-World Integration Examples
```lua
-- Function to update download progress with rich visualization
local function update_download_progress(filename, current, total)
local progress = math.floor((current / total) * 100)
local speed = current > 0 and string.format("%.1f MB/s", (current / 1024 / 1024)) or "0 MB/s"
vim.notify("", vim.log.levels.INFO, {
id = "download-" .. filename,
timeout = progress == 100 and 3000 or 15000,
_notif_formatter = function(opts)
local data = opts.notif._notif_formatter_data
local bar_width = 25
local filled = math.floor((data.progress / 100) * bar_width)
local bar = string.rep("█", filled) .. string.rep("▒", bar_width - filled)
return {
{ display_text = "📁 ", hl_group = "Directory", is_virtual = true },
{ display_text = data.filename, hl_group = "NotifierInfo", is_virtual = true },
{ display_text = " [", hl_group = "Comment", is_virtual = true },
{ display_text = bar, hl_group = data.progress == 100 and "String" or "Comment", is_virtual = true },
{ display_text = "] ", hl_group = "Comment", is_virtual = true },
{ display_text = data.progress .. "%", hl_group = "Number", is_virtual = true },
{ display_text = " @ " .. data.speed, hl_group = "Comment", is_virtual = true },
}
end,
_notif_formatter_data = {
filename = filename,
progress = progress,
current = current,
total = total,
speed = speed
}
})
end
-- Usage
update_download_progress("large-file.zip", 0, 100) -- 0%
update_download_progress("large-file.zip", 50, 100) -- 50%
update_download_progress("large-file.zip", 100, 100) -- 100%
```
## 🔧 Commands and Functions
### Core Functions
```lua
-- Show notification history
require("notifier").show_history()
-- Dismiss all notifications immediately
require("notifier").dismiss_all()
```
### Keybindings Example
```lua
-- Add to your init.lua
vim.keymap.set("n", "nh", function()
require("notifier").show_history()
end, { desc = "Show notification history" })
vim.keymap.set("n", "nd", function()
require("notifier").dismiss_all()
end, { desc = "Dismiss all notifications" })
```
## 🎯 Notification Groups
Organize notifications by positioning them in different screen areas:
```lua
-- Bottom right (default)
vim.notify("System ready", vim.log.levels.INFO)
-- Top right for less intrusive messages
vim.notify("Background task completed", vim.log.levels.INFO, {
group_name = "top-right"
})
-- Top left for debug information
vim.notify("Variable value: " .. tostring(value), vim.log.levels.DEBUG, {
group_name = "top-left"
})
-- Bottom left for status updates
vim.notify("Syncing files...", vim.log.levels.INFO, {
group_name = "bottom-left",
id = "sync-status"
})
```
## 🎨 Highlight Groups
Customize colors by overriding these highlight groups:
```lua
-- Main notification styling
vim.api.nvim_set_hl(0, "NotifierNormal", { bg = "#1a1a1a", fg = "#ffffff" })
vim.api.nvim_set_hl(0, "NotifierBorder", { fg = "#444444" })
-- Level-specific colors
vim.api.nvim_set_hl(0, "NotifierError", { fg = "#ff6b6b", bold = true })
vim.api.nvim_set_hl(0, "NotifierWarn", { fg = "#feca57", bold = true })
vim.api.nvim_set_hl(0, "NotifierInfo", { fg = "#48cae4" })
vim.api.nvim_set_hl(0, "NotifierDebug", { fg = "#a8a8a8" })
vim.api.nvim_set_hl(0, "NotifierTrace", { fg = "#6c757d" })
-- History window styling
vim.api.nvim_set_hl(0, "NotifierHistoryNormal", { bg = "#0d1117" })
vim.api.nvim_set_hl(0, "NotifierHistoryBorder", { fg = "#30363d" })
vim.api.nvim_set_hl(0, "NotifierHistoryTitle", { fg = "#f0f6fc", bold = true })
```
## 📱 Integration Examples
### LSP Progress Notifications with Rich Data
```lua
-- Enhanced LSP progress with inline formatters
---Setup a progress spinner for LSP.
---@return nil
local function setup_progress_spinner_custom()
local spinner_chars = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
local last_spinner = 0
local spinner_idx = 1
---@type table
local active_timers = {}
vim.lsp.handlers["$/progress"] = function(_, result, ctx)
local client = vim.lsp.get_client_by_id(ctx.client_id)
if not client or type(result.value) ~= "table" then
return
end
local value = result.value
local token = result.token
local is_complete = value.kind == "end"
local has_percentage = value.percentage ~= nil
local function render()
local progress_data = {
percentage = value.percentage or nil,
description = value.title or "Loading workspace",
file_progress = value.message or nil,
}
if is_complete then
progress_data.description = "Done"
progress_data.file_progress = nil
end
local icon
if is_complete then
icon = " "
else
local now = vim.uv.hrtime()
if now - last_spinner > 80e6 then
spinner_idx = (spinner_idx % #spinner_chars) + 1
last_spinner = now
end
icon = spinner_chars[spinner_idx]
end
vim.notify("", vim.log.levels.INFO, {
id = string.format("lsp_progress_%s_%s", client.name, token),
title = client.name,
_notif_formatter = function(opts)
local notif = opts.notif
local _notif_formatter_data = notif._notif_formatter_data
if not _notif_formatter_data then
return {}
end
local separator = { display_text = " " }
local icon_hl = notif.hl_group or opts.log_level_map[notif.level].hl_group
local percent_text = _notif_formatter_data.percentage
and string.format("%3d%%", _notif_formatter_data.percentage)
or nil
local description_text = _notif_formatter_data.description
local file_progress_text = _notif_formatter_data.file_progress or nil
local client_name = client.name
---@type Notifier.FormattedNotifOpts[]
local entries = {}
if icon then
table.insert(entries, { display_text = icon, hl_group = icon_hl })
table.insert(entries, separator)
end
if percent_text then
table.insert(entries, { display_text = percent_text, hl_group = "CmdHistoryIdentifier" })
table.insert(entries, separator)
end
table.insert(entries, { display_text = description_text, hl_group = "Comment" })
if file_progress_text then
table.insert(entries, separator)
table.insert(entries, { display_text = file_progress_text, hl_group = "Removed" })
end
if client_name then
table.insert(entries, separator)
table.insert(entries, { display_text = client_name, hl_group = "ErrorMsg" })
end
return entries
end,
_notif_formatter_data = progress_data,
})
end
render()
if not has_percentage then
if not is_complete then
local timer = active_timers[token]
if not timer or timer:is_closing() then
timer = vim.uv.new_timer()
active_timers[token] = timer
end
if timer then
timer:start(0, 150, function()
vim.schedule(render)
end)
end
else
local timer = active_timers[token]
if timer and not timer:is_closing() then
timer:stop()
timer:close()
active_timers[token] = nil
end
vim.schedule(render)
end
end
end
end
```
### Git Integration with Data Formatting
```lua
-- Git status with rich formatting and data
local function show_git_status(branch, stats)
vim.notify("", vim.log.levels.INFO, {
id = "git-status",
timeout = 8000,
_notif_formatter = function(opts)
local data = opts.notif._notif_formatter_data
local parts = {
{ display_text = "🌿 ", hl_group = "String", is_virtual = true },
{ display_text = data.branch, hl_group = "Identifier", is_virtual = true },
}
if data.stats.ahead > 0 then
table.insert(parts, { display_text = " ↑" .. data.stats.ahead, hl_group = "diffAdded", is_virtual = true })
end
if data.stats.behind > 0 then
table.insert(parts, { display_text = " ↓" .. data.stats.behind, hl_group = "diffRemoved", is_virtual = true })
end
if data.stats.modified > 0 then
table.insert(parts, { display_text = " ~" .. data.stats.modified, hl_group = "diffChanged", is_virtual = true })
end
if data.stats.untracked > 0 then
table.insert(parts, { display_text = " +" .. data.stats.untracked, hl_group = "diffAdded", is_virtual = true })
end
return parts
end,
_notif_formatter_data = {
branch = branch,
stats = stats
}
})
end
-- Usage
show_git_status("main", { ahead = 2, behind = 0, modified = 3, untracked = 1 })
-- Test results with detailed breakdown
vim.notify("", vim.log.levels.INFO, {
id = "test-results",
timeout = 10000,
_notif_formatter = function(opts)
local data = opts.notif._notif_formatter_data
local icon = data.passed == data.total and "✅" or "❌"
local color = data.passed == data.total and "String" or "ErrorMsg"
return {
{ display_text = icon .. " Tests: ", hl_group = color, is_virtual = true },
{ display_text = data.passed .. "/" .. data.total, hl_group = color, is_virtual = true },
{ display_text = " passed", hl_group = "Comment", is_virtual = true },
data.duration and { display_text = " (" .. data.duration .. "ms)", hl_group = "Comment", is_virtual = true } or nil,
data.coverage and { display_text = " " .. data.coverage .. "% coverage", hl_group = "Number", is_virtual = true } or nil,
}
end,
_notif_formatter_data = {
passed = 45,
total = 48,
duration = 2340,
coverage = 87.5
}
})
```
## 📋 Requirements
- Neovim 0.10+
- A terminal that supports Unicode icons (optional, for best experience)
## 🐛 Troubleshooting
### Common Issues
**Notifications not showing:**
- Ensure you've called `require("notifier").setup()`
- Check that your Neovim version is 0.10+
**Icons not displaying:**
- Install a Nerd Font and set it as your terminal font
- Or customize the `icons` config with plain text alternatives
**Performance issues:**
- Reduce `default_timeout` for faster cleanup
- Consider using fewer notification groups
**Window positioning problems:**
- Adjust `row` and `col` values in `group_configs`
- Check your terminal size with `:echo &columns` and `:echo &lines`
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.