mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 10:49:08 +00:00
feat(notifier): added history to notifier. Closes #14
This commit is contained in:
parent
cc5ee192ca
commit
65d8c8f00b
1 changed files with 146 additions and 34 deletions
|
@ -60,6 +60,14 @@ local M = setmetatable({}, {
|
|||
---@field hl snacks.notifier.hl
|
||||
---@field ns number
|
||||
|
||||
--- ### History
|
||||
---@class snacks.notifier.history
|
||||
---@field filter? snacks.notifier.level|fun(notif: snacks.notifier.Notif): boolean
|
||||
---@field sort? string[] # sort fields, default: {"added"}
|
||||
local history_opts = {
|
||||
sort = { "added" },
|
||||
}
|
||||
|
||||
Snacks.config.style("notification", {
|
||||
border = "rounded",
|
||||
zindex = 100,
|
||||
|
@ -71,6 +79,20 @@ Snacks.config.style("notification", {
|
|||
bo = { filetype = "snacks_notif" },
|
||||
})
|
||||
|
||||
Snacks.config.style("notification.history", {
|
||||
border = "rounded",
|
||||
zindex = 100,
|
||||
width = 0.6,
|
||||
height = 0.6,
|
||||
minimal = false,
|
||||
title = "Notification History",
|
||||
title_pos = "center",
|
||||
ft = "markdown",
|
||||
bo = { filetype = "snacks_notif_history" },
|
||||
wo = { winhighlight = "Normal:SnacksNotifierHistory" },
|
||||
keys = { q = "close" },
|
||||
})
|
||||
|
||||
---@class snacks.notifier.Config
|
||||
---@field keep? fun(notif: snacks.notifier.Notif): boolean # global keep function
|
||||
local defaults = {
|
||||
|
@ -91,10 +113,12 @@ local defaults = {
|
|||
---@type snacks.notifier.style
|
||||
style = "compact",
|
||||
top_down = true, -- place notifications from top to bottom
|
||||
date_format = "%R", -- time format for notifications
|
||||
}
|
||||
|
||||
---@class snacks.notifier.Class
|
||||
---@field queue table<string|number, snacks.notifier.Notif>
|
||||
---@field history table<string|number, snacks.notifier.Notif>
|
||||
---@field sorted? snacks.notifier.Notif[]
|
||||
---@field opts snacks.notifier.Config
|
||||
---@field dirty boolean
|
||||
|
@ -102,6 +126,17 @@ local N = {}
|
|||
|
||||
N.ns = vim.api.nvim_create_namespace("snacks.notifier")
|
||||
|
||||
---@param str string
|
||||
local function cap(str)
|
||||
return str:sub(1, 1):upper() .. str:sub(2):lower()
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param level? snacks.notifier.level
|
||||
local function hl(name, level)
|
||||
return "SnacksNotifier" .. name .. (level and cap(level) or "")
|
||||
end
|
||||
|
||||
---@type table<string, snacks.notifier.render>
|
||||
N.styles = {
|
||||
-- style using border title
|
||||
|
@ -122,6 +157,33 @@ N.styles = {
|
|||
virt_text_pos = "right_align",
|
||||
})
|
||||
end,
|
||||
history = function(buf, notif, ctx)
|
||||
local lines = vim.split(notif.msg, "\n", { plain = true })
|
||||
local prefix = {
|
||||
{ os.date(ctx.notifier.opts.date_format, notif.added), hl("HistoryDateTime") },
|
||||
{ notif.icon, ctx.hl.icon },
|
||||
{ notif.level:upper(), ctx.hl.title },
|
||||
{ notif.title, hl("HistoryTitle") },
|
||||
}
|
||||
prefix = vim.tbl_filter(function(v)
|
||||
return (v[1] or "") ~= ""
|
||||
end, prefix)
|
||||
local prefix_width = 0
|
||||
for i = 1, #prefix do
|
||||
prefix_width = prefix_width + vim.fn.strdisplaywidth(prefix[i * 2 - 1][1]) + 1
|
||||
table.insert(prefix, i * 2, { " " })
|
||||
end
|
||||
local top = vim.api.nvim_buf_line_count(buf)
|
||||
local empty = top == 1 and #vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] == 0
|
||||
top = empty and 0 or top
|
||||
lines[1] = string.rep(" ", prefix_width) .. (lines[1] or "")
|
||||
vim.api.nvim_buf_set_lines(buf, top, -1, false, lines)
|
||||
vim.api.nvim_buf_set_extmark(buf, ctx.ns, top, 0, {
|
||||
virt_text = prefix,
|
||||
virt_text_pos = "overlay",
|
||||
priority = 10,
|
||||
})
|
||||
end,
|
||||
-- similar to the default nvim-notify style
|
||||
fancy = function(buf, notif, ctx)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, 1, false, { "", "" })
|
||||
|
@ -132,7 +194,7 @@ N.styles = {
|
|||
priority = 10,
|
||||
})
|
||||
vim.api.nvim_buf_set_extmark(buf, ctx.ns, 0, 0, {
|
||||
virt_text = { { " " }, { os.date("%X", notif.added), ctx.hl.title }, { " " } },
|
||||
virt_text = { { " " }, { os.date(ctx.notifier.opts.date_format, notif.added), ctx.hl.title }, { " " } },
|
||||
virt_text_pos = "right_align",
|
||||
priority = 10,
|
||||
})
|
||||
|
@ -164,12 +226,6 @@ local function normlevel(level)
|
|||
or "info"
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param level? snacks.notifier.level
|
||||
local function hl(name, level)
|
||||
return "SnacksNotifier" .. name .. (level and (level:sub(1, 1):upper() .. level:sub(2):lower()) or "")
|
||||
end
|
||||
|
||||
local function ts()
|
||||
local ret = assert(vim.uv.clock_gettime("realtime"))
|
||||
return ret.sec + ret.nsec / 1e9
|
||||
|
@ -188,21 +244,26 @@ function N.new(opts)
|
|||
local self = setmetatable({}, { __index = N })
|
||||
self.opts = Snacks.config.get("notifier", defaults, opts)
|
||||
self.queue = {}
|
||||
self.history = {}
|
||||
self:init()
|
||||
self:start()
|
||||
return self
|
||||
end
|
||||
|
||||
function N:init()
|
||||
local links = {} ---@type table<string, string>
|
||||
local links = {
|
||||
[hl("History")] = "Normal",
|
||||
[hl("HistoryTitle")] = "Title",
|
||||
[hl("HistoryDateTime")] = "Special",
|
||||
}
|
||||
for _, level in ipairs(N.level_names) do
|
||||
local cap = level:sub(1, 1):upper() .. level:sub(2):lower()
|
||||
cap = (cap == "Trace" or cap == "Debug") and "Hint" or cap
|
||||
local Level = cap(level)
|
||||
local link = vim.tbl_contains({ "Trace", "Debug" }, Level) and "NonText" or nil
|
||||
links[hl("", level)] = "Normal"
|
||||
links[hl("Icon", level)] = "DiagnosticSign" .. cap
|
||||
links[hl("Border", level)] = "Diagnostic" .. cap
|
||||
links[hl("Title", level)] = "Diagnostic" .. cap
|
||||
links[hl("Footer", level)] = "Diagnostic" .. cap
|
||||
links[hl("Icon", level)] = link or ("DiagnosticSign" .. Level)
|
||||
links[hl("Border", level)] = link or ("Diagnostic" .. Level)
|
||||
links[hl("Title", level)] = link or ("Diagnostic" .. Level)
|
||||
links[hl("Footer", level)] = link or ("Diagnostic" .. Level)
|
||||
end
|
||||
for k, v in pairs(links) do
|
||||
vim.api.nvim_set_hl(0, k, { link = v, default = true })
|
||||
|
@ -235,8 +296,9 @@ end
|
|||
function N:add(opts)
|
||||
local now = ts()
|
||||
local notif = vim.deepcopy(opts) --[[@as snacks.notifier.Notif]]
|
||||
|
||||
notif.msg = notif.msg or ""
|
||||
-- FIXME: normalize title, icon, etc to remove newlines
|
||||
notif.title = notif.title or ""
|
||||
notif.id = notif.id or next_id()
|
||||
notif.level = normlevel(notif.level)
|
||||
notif.icon = notif.icon or self.opts.icons[notif.level]
|
||||
|
@ -253,8 +315,8 @@ function N:add(opts)
|
|||
notif.dirty = true
|
||||
end
|
||||
self.sorted = nil
|
||||
|
||||
self.queue[notif.id] = notif
|
||||
self.history[notif.id] = notif
|
||||
return notif.id
|
||||
end
|
||||
|
||||
|
@ -275,6 +337,38 @@ function N:update()
|
|||
self.sorted = self.sorted or self:sort()
|
||||
end
|
||||
|
||||
---@param opts? snacks.notifier.history
|
||||
---@return snacks.notifier.Notif[]
|
||||
function N:get_history(opts)
|
||||
---@type snacks.notifier.history
|
||||
opts = vim.tbl_deep_extend("force", {}, history_opts, opts or {})
|
||||
local notifs = vim.tbl_values(self.history)
|
||||
local filter = opts.filter
|
||||
if type(filter) == "string" or type(filter) == "number" then
|
||||
local level = normlevel(filter)
|
||||
filter = function(n)
|
||||
return n.level == level
|
||||
end
|
||||
end
|
||||
notifs = filter and vim.tbl_filter(filter, notifs) or notifs
|
||||
return self:sort(notifs, opts.sort)
|
||||
end
|
||||
|
||||
---@param opts? snacks.notifier.history
|
||||
function N:show_history(opts)
|
||||
local win = Snacks.win({ style = "notification.history", enter = true, show = false })
|
||||
local buf = win:open_buf()
|
||||
for _, notif in ipairs(self:get_history(opts)) do
|
||||
N.styles.history(buf, notif, {
|
||||
opts = win.opts,
|
||||
notifier = self,
|
||||
ns = N.ns,
|
||||
hl = self:hl(notif),
|
||||
})
|
||||
end
|
||||
return win:show()
|
||||
end
|
||||
|
||||
---@param id? number|string
|
||||
function N:hide(id)
|
||||
if not id then
|
||||
|
@ -313,19 +407,25 @@ function N:get_render(style)
|
|||
end
|
||||
|
||||
---@param notif snacks.notifier.Notif
|
||||
function N:render(notif)
|
||||
if type(notif.opts) == "function" then
|
||||
notif.opts(notif)
|
||||
end
|
||||
|
||||
function N:hl(notif)
|
||||
---@type snacks.notifier.hl
|
||||
local hls = vim.tbl_extend("force", {
|
||||
return vim.tbl_extend("force", {
|
||||
title = hl("Title", notif.level),
|
||||
icon = hl("Icon", notif.level),
|
||||
border = hl("Border", notif.level),
|
||||
footer = hl("Footer", notif.level),
|
||||
msg = hl("", notif.level),
|
||||
}, notif.hl or {})
|
||||
end
|
||||
|
||||
---@param notif snacks.notifier.Notif
|
||||
function N:render(notif)
|
||||
if type(notif.opts) == "function" then
|
||||
notif.opts(notif)
|
||||
end
|
||||
|
||||
---@type snacks.notifier.hl
|
||||
local notif_hl = self:hl(notif)
|
||||
|
||||
local win = notif.win
|
||||
or Snacks.win({
|
||||
|
@ -337,11 +437,11 @@ function N:render(notif)
|
|||
noautocmd = true,
|
||||
wo = {
|
||||
winhighlight = table.concat({
|
||||
"Normal:" .. hls.msg,
|
||||
"NormalNC:" .. hls.msg,
|
||||
"FloatBorder:" .. hls.border,
|
||||
"FloatTitle:" .. hls.title,
|
||||
"FloatFooter:" .. hls.footer,
|
||||
"Normal:" .. notif_hl.msg,
|
||||
"NormalNC:" .. notif_hl.msg,
|
||||
"FloatBorder:" .. notif_hl.border,
|
||||
"FloatTitle:" .. notif_hl.title,
|
||||
"FloatFooter:" .. notif_hl.footer,
|
||||
}, ","),
|
||||
},
|
||||
keys = {
|
||||
|
@ -362,7 +462,7 @@ function N:render(notif)
|
|||
opts = win.opts,
|
||||
notifier = self,
|
||||
ns = N.ns,
|
||||
hl = hls,
|
||||
hl = notif_hl,
|
||||
})
|
||||
vim.bo[buf].modifiable = false
|
||||
|
||||
|
@ -389,11 +489,13 @@ function N:render(notif)
|
|||
win.opts.height = height
|
||||
end
|
||||
|
||||
function N:sort()
|
||||
---@type snacks.notifier.Notif[]
|
||||
local ret = vim.tbl_values(self.queue)
|
||||
table.sort(ret, function(a, b)
|
||||
for _, key in ipairs(self.opts.sort) do
|
||||
---@param fields? string[]
|
||||
---@param notifs? snacks.notifier.Notif[]
|
||||
function N:sort(notifs, fields)
|
||||
fields = fields or self.opts.sort
|
||||
notifs = notifs or vim.tbl_values(self.queue)
|
||||
table.sort(notifs, function(a, b)
|
||||
for _, key in ipairs(fields) do
|
||||
local function v(n)
|
||||
if key == "level" then
|
||||
return 10 - vim.log.levels[n[key]:upper()]
|
||||
|
@ -407,7 +509,7 @@ function N:sort()
|
|||
end
|
||||
return false
|
||||
end)
|
||||
return ret
|
||||
return notifs
|
||||
end
|
||||
|
||||
function N:layout()
|
||||
|
@ -491,4 +593,14 @@ function M.hide(id)
|
|||
return notifier:hide(id)
|
||||
end
|
||||
|
||||
---@param opts? snacks.notifier.history
|
||||
function M.get_history(opts)
|
||||
return notifier:get_history(opts)
|
||||
end
|
||||
|
||||
---@param opts? snacks.notifier.history
|
||||
function M.show_history(opts)
|
||||
return notifier:show_history(opts)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue