1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,5 @@
local M = {}
M.spring = require("notify.animate.spring")
return M

View File

@ -0,0 +1,56 @@
-- Adapted from https://gist.github.com/Fraktality/1033625223e13c01aa7144abe4aaf54d
-- Explanation found here https://www.ryanjuckett.com/damped-springs/
local pi = math.pi
local exp = math.exp
local sin = math.sin
local cos = math.cos
local sqrt = math.sqrt
---@class SpringState
---@field position number
---@field velocity number | nil
---@param dt number @Step in time
---@param state SpringState
return function(dt, goal, state, frequency, damping)
local angular_freq = frequency * 2 * pi
local cur_vel = state.velocity or 0
local offset = state.position - goal
local decay = exp(-dt * damping * angular_freq)
local new_pos
local new_vel
if damping == 1 then -- critically damped
new_pos = (cur_vel * dt + offset * (angular_freq * dt + 1)) * decay + goal
new_vel = (cur_vel - angular_freq * dt * (offset * angular_freq + cur_vel)) * decay
elseif damping < 1 then -- underdamped
local c = sqrt(1 - damping * damping)
local i = cos(angular_freq * c * dt)
local j = sin(angular_freq * c * dt)
new_pos = (i * offset + j * (cur_vel + damping * angular_freq * offset) / (angular_freq * c))
* decay
+ goal
new_vel = (i * c * cur_vel - j * (cur_vel * damping + angular_freq * offset)) * decay / c
else -- overdamped
local c = sqrt(damping * damping - 1)
local r1 = -angular_freq * (damping - c)
local r2 = -angular_freq * (damping + c)
local co2 = (cur_vel - r1 * offset) / (2 * angular_freq * c)
local co1 = offset - co2
local e1 = co1 * exp(r1 * dt)
local e2 = co2 * exp(r2 * dt)
new_pos = e1 + e2 + goal
new_pos = r1 * e1 + r2 * e2
end
state.position = new_pos
state.velocity = new_vel
end

View File

@ -0,0 +1,41 @@
local M = {}
function M.setup()
vim.cmd([[
hi default link NotifyBackground Normal
hi default NotifyERRORBorder guifg=#8A1F1F
hi default NotifyWARNBorder guifg=#79491D
hi default NotifyINFOBorder guifg=#4F6752
hi default NotifyDEBUGBorder guifg=#8B8B8B
hi default NotifyTRACEBorder guifg=#4F3552
hi default NotifyERRORIcon guifg=#F70067
hi default NotifyWARNIcon guifg=#F79000
hi default NotifyINFOIcon guifg=#A9FF68
hi default NotifyDEBUGIcon guifg=#8B8B8B
hi default NotifyTRACEIcon guifg=#D484FF
hi default NotifyERRORTitle guifg=#F70067
hi default NotifyWARNTitle guifg=#F79000
hi default NotifyINFOTitle guifg=#A9FF68
hi default NotifyDEBUGTitle guifg=#8B8B8B
hi default NotifyTRACETitle guifg=#D484FF
hi default link NotifyERRORBody Normal
hi default link NotifyWARNBody Normal
hi default link NotifyINFOBody Normal
hi default link NotifyDEBUGBody Normal
hi default link NotifyTRACEBody Normal
hi default link NotifyLogTime Comment
hi default link NotifyLogTitle Special
]])
end
M.setup()
vim.cmd([[
augroup NvimNotifyRefreshHighlights
autocmd!
autocmd ColorScheme * lua require('notify.config.highlights').setup()
augroup END
]])
return M

View File

@ -0,0 +1,202 @@
---@tag notify.config
local Config = {}
local util = require("notify.util")
require("notify.config.highlights")
local BUILTIN_RENDERERS = {
DEFAULT = "default",
MINIMAL = "minimal",
}
local BUILTIN_STAGES = {
FADE = "fade",
SLIDE = "slide",
FADE_IN_SLIDE_OUT = "fade_in_slide_out",
STATIC = "static",
}
local default_config = {
level = vim.log.levels.INFO,
timeout = 5000,
max_width = nil,
max_height = nil,
stages = BUILTIN_STAGES.FADE_IN_SLIDE_OUT,
render = BUILTIN_RENDERERS.DEFAULT,
background_colour = "NotifyBackground",
on_open = nil,
on_close = nil,
minimum_width = 50,
fps = 30,
top_down = true,
time_formats = {
notification_history = "%FT%T",
notification = "%T",
},
icons = {
ERROR = "",
WARN = "",
INFO = "",
DEBUG = "",
TRACE = "",
},
}
---@class notify.Config
---@field level string|integer Minimum log level to display. See vim.log.levels.
---@field timeout number Default timeout for notification
---@field max_width number|function Max number of columns for messages
---@field max_height number|function Max number of lines for a message
---@field stages string|function[] Animation stages
---@field background_colour string For stages that change opacity this is treated as the highlight behind the window. Set this to either a highlight group, an RGB hex value e.g. "#000000" or a function returning an RGB code for dynamic values
---@field icons table Icons for each level (upper case names)
---@field time_formats table Time formats for different kind of notifications
---@field on_open function Function called when a new window is opened, use for changing win settings/config
---@field on_close function Function called when a window is closed
---@field render function|string Function to render a notification buffer or a built-in renderer name
---@field minimum_width integer Minimum width for notification windows
---@field fps integer Frames per second for animation stages, higher value means smoother animations but more CPU usage
---@field top_down boolean whether or not to position the notifications at the top or not
local opacity_warned = false
local function validate_highlight(colour_or_group, needs_opacity)
if type(colour_or_group) == "function" then
return colour_or_group
end
if colour_or_group:sub(1, 1) == "#" then
return function()
return colour_or_group
end
end
return function()
local group = vim.api.nvim_get_hl_by_name(colour_or_group, true)
if not group or not group.background then
if needs_opacity and not opacity_warned then
opacity_warned = true
vim.schedule(function()
vim.notify("Highlight group '" .. colour_or_group .. [[' has no background highlight
Please provide an RGB hex value or highlight group with a background value for 'background_colour' option.
This is the colour that will be used for 100% transparency.
```lua
require("notify").setup({
background_colour = "#000000",
})
```
Defaulting to #000000]], "warn", {
title = "nvim-notify",
on_open = function(win)
local buf = vim.api.nvim_win_get_buf(win)
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
end,
})
end)
end
return "#000000"
end
return string.format("#%x", group.background)
end
end
function Config._format_default()
local lines = { "Default values:", ">lua" }
for line in vim.gsplit(vim.inspect(default_config), "\n", true) do
table.insert(lines, " " .. line)
end
table.insert(lines, "<")
return lines
end
function Config.setup(custom_config)
local user_config = vim.tbl_deep_extend("keep", custom_config or {}, default_config)
local config = {}
function config.merged()
return user_config
end
function config.level()
local level = user_config.level
if type(level) == "number" then
return level
end
return vim.log.levels[vim.fn.toupper(level)] or vim.log.levels.INFO
end
function config.fps()
return user_config.fps
end
function config.background_colour()
return tonumber(user_config.background_colour():gsub("#", "0x"), 16)
end
function config.time_formats()
return user_config.time_formats
end
function config.icons()
return user_config.icons
end
function config.stages()
return user_config.stages
end
function config.default_timeout()
return user_config.timeout
end
function config.on_open()
return user_config.on_open
end
function config.top_down()
return user_config.top_down
end
function config.on_close()
return user_config.on_close
end
function config.render()
return user_config.render
end
function config.minimum_width()
return user_config.minimum_width
end
function config.max_width()
return util.is_callable(user_config.max_width) and user_config.max_width()
or user_config.max_width
end
function config.max_height()
return util.is_callable(user_config.max_height) and user_config.max_height()
or user_config.max_height
end
local stages = config.stages()
local needs_opacity =
vim.tbl_contains({ BUILTIN_STAGES.FADE_IN_SLIDE_OUT, BUILTIN_STAGES.FADE }, stages)
if needs_opacity and not vim.opt.termguicolors:get() and vim.fn.has("nvim-0.10") == 0 then
user_config.stages = BUILTIN_STAGES.STATIC
vim.schedule(function()
vim.notify(
"Opacity changes require termguicolors to be set.\nChange to different animation stages or set termguicolors to disable this warning",
"warn",
{ title = "nvim-notify" }
)
end)
end
user_config.background_colour = validate_highlight(user_config.background_colour, needs_opacity)
return config
end
return Config

View File

@ -0,0 +1,199 @@
---@text
--- A fancy, configurable notification manager for NeoVim
local config = require("notify.config")
local instance = require("notify.instance")
---@class notify
local notify = {}
local global_instance, global_config
--- Configure nvim-notify
--- See: ~
--- |notify.Config|
--- |notify-render|
---
---@param user_config notify.Config|nil
---@eval return require('notify.config')._format_default()
function notify.setup(user_config)
global_instance, global_config = notify.instance(user_config)
local has_telescope = (vim.fn.exists("g:loaded_telescope") == 1)
if has_telescope then
require("telescope").load_extension("notify")
end
vim.cmd([[command! Notifications :lua require("notify")._print_history()<CR>]])
end
function notify._config()
return config.setup(global_config)
end
---@class notify.Options
--- Options for an individual notification
---@field title string
---@field icon string
---@field timeout number|boolean Time to show notification in milliseconds, set to false to disable timeout.
---@field on_open function Callback for when window opens, receives window as argument.
---@field on_close function Callback for when window closes, receives window as argument.
---@field keep function Function to keep the notification window open after timeout, should return boolean.
---@field render function|string Function to render a notification buffer.
---@field replace integer|notify.Record Notification record or the record `id` field. Replace an existing notification if still open. All arguments not given are inherited from the replaced notification including message and level.
---@field hide_from_history boolean Hide this notification from the history
---@field animate boolean If false, the window will jump to the timed stage. Intended for use in blocking events (e.g. vim.fn.input)
---@class notify.Events
--- Async events for a notification
---@field open function Resolves when notification is opened
---@field close function Resolved when notification is closed
---@class notify.Record
--- Record of a previously sent notification
---@field id integer
---@field message string[] Lines of the message
---@field level string|integer Log level. See vim.log.levels
---@field title string[] Left and right sections of the title
---@field icon string Icon used for notification
---@field time number Time of message, as returned by `vim.fn.localtime()`
---@field render function Function to render notification buffer
---@class notify.AsyncRecord : notify.Record
---@field events notify.Events
--- Display a notification.
---
--- You can call the module directly rather than using this:
--- >lua
--- require("notify")(message, level, opts)
--- <
---@param message string|string[] Notification message
---@param level string|number Log level. See vim.log.levels
---@param opts notify.Options Notification options
---@return notify.Record
function notify.notify(message, level, opts)
if not global_instance then
notify.setup()
end
return global_instance.notify(message, level, opts)
end
--- Display a notification asynchronously
---
--- This uses plenary's async library, allowing a cleaner interface for
--- open/close events. You must call this function within an async context.
---
--- The `on_close` and `on_open` options are not used.
---
---@param message string|string[] Notification message
---@param level string|number Log level. See vim.log.levels
---@param opts notify.Options Notification options
---@return notify.AsyncRecord
function notify.async(message, level, opts)
if not global_instance then
notify.setup()
end
return global_instance.async(message, level, opts)
end
--- Get records of all previous notifications
---
--- You can use the `:Notifications` command to display a log of previous notifications
---@param opts? notify.HistoryOpts
---@return notify.Record[]
function notify.history(opts)
if not global_instance then
notify.setup()
end
return global_instance.history(opts)
end
---@class notify.HistoryOpts
---@field include_hidden boolean Include notifications hidden from history
--- Dismiss all notification windows currently displayed
---@param opts notify.DismissOpts
function notify.dismiss(opts)
if not global_instance then
notify.setup()
end
return global_instance.dismiss(opts)
end
---@class notify.DismissOpts
---@field pending boolean Clear pending notifications
---@field silent boolean Suppress notification that pending notifications were dismissed.
--- Open a notification in a new buffer
---@param notif_id integer|notify.Record
---@param opts notify.OpenOpts
---@return notify.OpenedBuffer
function notify.open(notif_id, opts)
if not global_instance then
notify.setup()
end
return global_instance.open(notif_id, opts)
end
---@class notify.OpenOpts
---@field buffer integer Use this buffer, instead of creating a new one
---@field max_width integer Render message to this width (used to limit window decoration sizes)
---@class notify.OpenedBuffer
---@field buffer integer Created buffer number
---@field height integer Height of the buffer content including extmarks
---@field width integer width of the buffer content including extmarks
---@field highlights table<string, string> Highlights used for the buffer contents
--- Number of notifications currently waiting to be displayed
---@return integer[]
function notify.pending()
if not global_instance then
notify.setup()
end
return global_instance.pending()
end
function notify._print_history()
if not global_instance then
notify.setup()
end
for _, notif in ipairs(global_instance.history()) do
vim.api.nvim_echo({
{
vim.fn.strftime(notify._config().time_formats().notification_history, notif.time),
"NotifyLogTime",
},
{ " ", "MsgArea" },
{ notif.title[1], "NotifyLogTitle" },
{ #notif.title[1] > 0 and " " or "", "MsgArea" },
{ notif.icon, "Notify" .. notif.level .. "Title" },
{ " ", "MsgArea" },
{ notif.level, "Notify" .. notif.level .. "Title" },
{ " ", "MsgArea" },
{ table.concat(notif.message, "\n"), "MsgArea" },
}, false, {})
end
end
--- Configure an instance of nvim-notify.
--- You can use this to manage a separate instance of nvim-notify with completely different configuration.
--- The returned instance will have the same functions as the notify module.
---@param user_config notify.Config
---@param inherit? boolean Inherit the global configuration, default true
function notify.instance(user_config, inherit)
return instance(user_config, inherit, global_config)
end
setmetatable(notify, {
__call = function(_, m, l, o)
if vim.in_fast_event() or vim.fn.has("vim_starting") == 1 then
vim.schedule(function()
notify.notify(m, l, o)
end)
else
return notify.notify(m, l, o)
end
end,
})
return notify

View File

@ -0,0 +1,169 @@
local stages = require("notify.stages")
local config = require("notify.config")
local Notification = require("notify.service.notification")
local WindowAnimator = require("notify.windows")
local NotificationService = require("notify.service")
local NotificationBuf = require("notify.service.buffer")
local stage_util = require("notify.stages.util")
---@param user_config notify.Config
---@param inherit? boolean Inherit the global configuration, default true
---@param global_config notify.Config
return function(user_config, inherit, global_config)
---@type notify.Notification[]
local notifications = {}
user_config = user_config or {}
if inherit ~= false and global_config then
user_config = vim.tbl_deep_extend("force", global_config, user_config)
end
local instance_config = config.setup(user_config)
local animator_stages = instance_config.stages()
local direction = instance_config.top_down() and stage_util.DIRECTION.TOP_DOWN
or stage_util.DIRECTION.BOTTOM_UP
animator_stages = type(animator_stages) == "string" and stages[animator_stages](direction)
or animator_stages
local animator = WindowAnimator(animator_stages, instance_config)
local service = NotificationService(instance_config, animator)
local instance = {}
local function get_render(render)
if type(render) == "function" then
return render
end
return require("notify.render")[render]
end
function instance.notify(message, level, opts)
opts = opts or {}
if opts.replace then
if type(opts.replace) == "table" then
opts.replace = opts.replace.id
end
local existing = notifications[opts.replace]
if not existing then
vim.notify("Invalid notification to replace", "error", { title = "nvim-notify" })
return
end
local notif_keys = {
"title",
"icon",
"timeout",
"keep",
"on_open",
"on_close",
"render",
"hide_from_history",
"animate",
}
message = message or existing.message
level = level or existing.level
for _, key in ipairs(notif_keys) do
opts[key] = opts[key] or existing[key]
end
end
opts.render = get_render(opts.render or instance_config.render())
local id = #notifications + 1
local notification = Notification(id, message, level, opts, instance_config)
table.insert(notifications, notification)
local level_num = vim.log.levels[notification.level]
if opts.replace then
service:replace(opts.replace, notification)
elseif not level_num or level_num >= instance_config.level() then
service:push(notification)
end
return {
id = id,
}
end
---@param notif_id integer|notify.Record
---@param opts table
function instance.open(notif_id, opts)
opts = opts or {}
if type(notif_id) == "table" then
notif_id = notif_id.id
end
local notif = notifications[notif_id]
if not notif then
vim.notify(
"Invalid notification id: " .. notif_id,
vim.log.levels.WARN,
{ title = "nvim-notify" }
)
return
end
local buf = opts.buffer or vim.api.nvim_create_buf(false, true)
local notif_buf =
NotificationBuf(buf, notif, vim.tbl_extend("keep", opts, { config = instance_config }))
notif_buf:render()
return {
buffer = buf,
height = notif_buf:height(),
width = notif_buf:width(),
highlights = {
body = notif_buf.highlights.body,
border = notif_buf.highlights.border,
title = notif_buf.highlights.title,
icon = notif_buf.highlights.icon,
},
}
end
function instance.async(message, level, opts)
opts = opts or {}
local async = require("plenary.async")
local send_close, wait_close = async.control.channel.oneshot()
opts.on_close = send_close
local send_open, wait_open = async.control.channel.oneshot()
opts.on_open = send_open
async.util.scheduler()
local record = instance.notify(message, level, opts)
return vim.tbl_extend("error", record, {
events = {
open = wait_open,
close = wait_close,
},
})
end
function instance.history(args)
args = args or {}
local records = {}
for _, notif in ipairs(notifications) do
if not notif.hide_from_history or args.include_hidden then
records[#records + 1] = notif:record()
end
end
return records
end
function instance.dismiss(opts)
if service then
service:dismiss(opts or {})
end
end
function instance.pending()
return service and service:pending() or {}
end
setmetatable(instance, {
__call = function(_, m, l, o)
if vim.in_fast_event() then
vim.schedule(function()
instance.notify(m, l, o)
end)
else
return instance.notify(m, l, o)
end
end,
})
return instance, instance_config.merged()
end

View File

@ -0,0 +1,9 @@
local M = {}
local namespace = vim.api.nvim_create_namespace("nvim-notify")
function M.namespace()
return namespace
end
return M

View File

@ -0,0 +1,36 @@
local base = require("notify.render.base")
return function(bufnr, notif, highlights)
local namespace = base.namespace()
local icon = notif.icon
local title = notif.title[1]
local prefix
if type(title) == "string" and #title > 0 then
prefix = string.format("%s | %s:", icon, title)
else
prefix = string.format("%s |", icon)
end
notif.message[1] = string.format("%s %s", prefix, notif.message[1])
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, notif.message)
local icon_length = vim.str_utfindex(icon)
local prefix_length = vim.str_utfindex(prefix)
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
hl_group = highlights.icon,
end_col = icon_length + 1,
priority = 50,
})
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, icon_length + 1, {
hl_group = highlights.title,
end_col = prefix_length + 1,
priority = 50,
})
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, prefix_length + 1, {
hl_group = highlights.body,
end_line = #notif.message,
priority = 50,
})
end

View File

@ -0,0 +1,54 @@
local api = vim.api
local base = require("notify.render.base")
return function(bufnr, notif, highlights, config)
local left_icon = notif.icon .. " "
local max_message_width = math.max(math.max(unpack(vim.tbl_map(function(line)
return vim.fn.strchars(line)
end, notif.message))))
local right_title = notif.title[2]
local left_title = notif.title[1]
local title_accum = vim.str_utfindex(left_icon)
+ vim.str_utfindex(right_title)
+ vim.str_utfindex(left_title)
local left_buffer = string.rep(" ", math.max(0, max_message_width - title_accum))
local namespace = base.namespace()
api.nvim_buf_set_lines(bufnr, 0, 1, false, { "", "" })
api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
virt_text = {
{ " " },
{ left_icon, highlights.icon },
{ left_title .. left_buffer, highlights.title },
},
virt_text_win_col = 0,
priority = 10,
})
api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
virt_text = { { " " }, { right_title, highlights.title }, { " " } },
virt_text_pos = "right_align",
priority = 10,
})
api.nvim_buf_set_extmark(bufnr, namespace, 1, 0, {
virt_text = {
{
string.rep(
"",
math.max(vim.str_utfindex(left_buffer) + title_accum + 2, config.minimum_width())
),
highlights.border,
},
},
virt_text_win_col = 0,
priority = 10,
})
api.nvim_buf_set_lines(bufnr, 2, -1, false, notif.message)
api.nvim_buf_set_extmark(bufnr, namespace, 2, 0, {
hl_group = highlights.body,
end_line = 1 + #notif.message,
end_col = #notif.message[#notif.message],
priority = 50, -- Allow treesitter to override
})
end

View File

@ -0,0 +1,38 @@
---@tag notify-render
---@text
--- Notification buffer rendering
---
--- Custom rendering can be provided by both the user config in the setup or on
--- an individual notification using the `render` key.
--- The key can either be the name of a built-in renderer or a custom function.
---
--- Built-in renderers:
--- - `"default"`
--- - `"minimal"`
--- - `"simple"`
--- - `"compact"`
--- - `"wrapped-compact"`
---
--- Custom functions should accept a buffer, a notification record and a highlights table
---
--- >
--- render: fun(buf: integer, notification: notify.Record, highlights: notify.Highlights, config)
--- <
--- You should use the provided highlight groups to take advantage of opacity
--- changes as they will be updated as the notification is animated
---@class notify.Highlights
---@field title string
---@field icon string
---@field border string
---@field body string
local M = {}
setmetatable(M, {
__index = function(_, key)
return require("notify.render." .. key)
end,
})
return M

View File

@ -0,0 +1,14 @@
local api = vim.api
local base = require("notify.render.base")
return function(bufnr, notif, highlights)
local namespace = base.namespace()
api.nvim_buf_set_lines(bufnr, 0, -1, false, notif.message)
api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
hl_group = highlights.icon,
end_line = #notif.message - 1,
end_col = #notif.message[#notif.message],
priority = 50,
})
end

View File

@ -0,0 +1,44 @@
local api = vim.api
local base = require("notify.render.base")
return function(bufnr, notif, highlights, config)
local max_message_width = math.max(math.max(unpack(vim.tbl_map(function(line)
return vim.fn.strchars(line)
end, notif.message))))
local title = notif.title[1]
local title_accum = vim.str_utfindex(title)
local title_buffer = string.rep(
" ",
(math.max(max_message_width, title_accum, config.minimum_width()) - title_accum) / 2
)
local namespace = base.namespace()
api.nvim_buf_set_lines(bufnr, 0, 1, false, { "", "" })
api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
virt_text = {
{ title_buffer .. title .. title_buffer, highlights.title },
},
virt_text_win_col = 0,
priority = 10,
})
api.nvim_buf_set_extmark(bufnr, namespace, 1, 0, {
virt_text = {
{
string.rep("", math.max(max_message_width, title_accum, config.minimum_width())),
highlights.border,
},
},
virt_text_win_col = 0,
priority = 10,
})
api.nvim_buf_set_lines(bufnr, 2, -1, false, notif.message)
api.nvim_buf_set_extmark(bufnr, namespace, 2, 0, {
hl_group = highlights.body,
end_line = 1 + #notif.message,
end_col = #notif.message[#notif.message],
priority = 50,
})
end

View File

@ -0,0 +1,89 @@
-- alternative compact renderer for nvim-notify.
-- Wraps text and adds some padding (only really to the left, since padding to
-- the right is somehow not display correctly).
-- Modified version of https://github.com/rcarriga/nvim-notify/blob/master/lua/notify/render/compact.lua
--------------------------------------------------------------------------------
---@param line string
---@param width number
---@return string[]
local function split_length(line, width)
local text = {}
local next_line
while true do
if #line == 0 then
return text
end
next_line, line = line:sub(1, width), line:sub(width)
text[#text + 1] = next_line
end
end
---@param lines string[]
---@param max_width number
---@return string[]
local function custom_wrap(lines, max_width)
local wrapped_lines = {}
for _, line in pairs(lines) do
local new_lines = split_length(line, max_width)
for _, nl in ipairs(new_lines) do
nl = nl:gsub("^%s*", " "):gsub("%s*$", " ") -- ensure padding
table.insert(wrapped_lines, nl)
end
end
return wrapped_lines
end
---@param bufnr number
---@param notif object
---@param highlights object
---@param config object plugin config_obj
return function(bufnr, notif, highlights, config)
local namespace = require("notify.render.base").namespace()
local icon = notif.icon
local title = notif.title[1]
local prefix
-- wrap the text & add spacing
local max_width = config.max_width()
if max_width == nil then
max_width = 80
end
local message = custom_wrap(notif.message, max_width)
local default_titles = { "Error", "Warning", "Notify" }
local has_valid_manual_title = type(title) == "string"
and #title > 0
and not vim.tbl_contains(default_titles, title)
if has_valid_manual_title then
-- has title = icon + title as header row
prefix = string.format(" %s %s", icon, title)
table.insert(message, 1, prefix)
else
-- no title = prefix the icon
prefix = string.format(" %s", icon)
message[1] = string.format("%s %s", prefix, message[1])
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, message)
local icon_length = vim.str_utfindex(icon)
local prefix_length = vim.str_utfindex(prefix) + 1
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
hl_group = highlights.icon,
end_col = icon_length + 1,
priority = 50,
})
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, icon_length + 1, {
hl_group = highlights.title,
end_col = prefix_length + 1,
priority = 50,
})
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, prefix_length + 1, {
hl_group = highlights.body,
end_line = #message,
priority = 50,
})
end

View File

@ -0,0 +1,187 @@
local util = require("notify.util")
---@class NotifyBufHighlights
---@field groups table
---@field opacity number
---@field title string
---@field border string
---@field icon string
---@field body string
---@field buffer number
---@field _config table
local NotifyBufHighlights = {}
local function manual_get_hl(name)
local synID = vim.fn.synIDtrans(vim.fn.hlID(name))
local result = {
foreground = tonumber(vim.fn.synIDattr(synID, "fg"):gsub("#", ""), 16),
background = tonumber(vim.fn.synIDattr(synID, "bg"):gsub("#", ""), 16),
}
return result
end
local function get_hl(name)
local definition = vim.api.nvim_get_hl_by_name(name, true)
if definition[true] then
-- https://github.com/neovim/neovim/issues/18024
return manual_get_hl(name)
end
return definition
end
function NotifyBufHighlights:new(level, buffer, config)
local function linked_group(section)
local orig = "Notify" .. level .. section
if vim.fn.hlID(orig) == 0 then
orig = "NotifyINFO" .. section
end
local new = orig .. buffer
vim.api.nvim_set_hl(0, new, { link = orig })
return new, get_hl(new)
end
local title, title_def = linked_group("Title")
local border, border_def = linked_group("Border")
local body, body_def = linked_group("Body")
local icon, icon_def = linked_group("Icon")
local groups = {
[title] = title_def,
[border] = border_def,
[body] = body_def,
[icon] = icon_def,
}
local buf_highlights = {
groups = groups,
opacity = 100,
border = border,
body = body,
title = title,
icon = icon,
buffer = buffer,
_config = config,
}
self.__index = self
setmetatable(buf_highlights, self)
return buf_highlights
end
function NotifyBufHighlights:_redefine_treesitter()
local buf_highlighter = require("vim.treesitter.highlighter").active[self.buffer]
if not buf_highlighter then
return
end
local render_namespace = vim.api.nvim_create_namespace("notify-treesitter-override")
vim.api.nvim_buf_clear_namespace(self.buffer, render_namespace, 0, -1)
local function link(orig)
local new = orig .. self.buffer
if self.groups[new] then
return new
end
vim.api.nvim_set_hl(0, new, { link = orig })
self.groups[new] = get_hl(new)
return new
end
local matches = {}
local i = 0
buf_highlighter.tree:for_each_tree(function(tstree, tree)
if not tstree then
return
end
local root = tstree:root()
local query = buf_highlighter:get_query(tree:lang())
-- Some injected languages may not have highlight queries.
if not query:query() then
return
end
local iter = query:query():iter_captures(root, buf_highlighter.bufnr)
for capture, node, metadata in iter do
-- Wait until we get at least a single capture as we don't know when parsing is complete.
self._treesitter_redefined = true
local hl = query.hl_cache[capture]
if hl then
i = i + 1
local c = query._query.captures[capture] -- name of the capture in the query
if c ~= nil then
local capture_hl
-- Removed in nightly with change of highlight names to @...
-- https://github.com/neovim/neovim/pull/19931
if query._get_hl_from_capture then
local general_hl, is_vim_hl = query:_get_hl_from_capture(capture)
capture_hl = is_vim_hl and general_hl or (tree:lang() .. general_hl)
else
capture_hl = query._query.captures[capture]
if not vim.startswith(capture_hl, "_") then
capture_hl = "@" .. capture_hl .. "." .. tree:lang()
end
end
local start_row, start_col, end_row, end_col = node:range()
local custom_hl = link(capture_hl)
vim.api.nvim_buf_set_extmark(self.buffer, render_namespace, start_row, start_col, {
end_row = end_row,
end_col = end_col,
hl_group = custom_hl,
-- TODO: Not sure how neovim's highlighter doesn't have issues with overriding highlights
-- Three marks on same region always show the second for some reason AFAICT
priority = metadata.priority or i + 200,
conceal = metadata.conceal,
})
end
end
end
end, true)
return matches
end
function NotifyBufHighlights:set_opacity(alpha)
if
not self._treesitter_redefined
and vim.api.nvim_buf_get_option(self.buffer, "filetype") ~= "notify"
then
self:_redefine_treesitter()
end
self.opacity = alpha
local background = self._config.background_colour()
for group, fields in pairs(self.groups) do
local updated_fields = {}
vim.api.nvim_set_hl(0, group, updated_fields)
local hl_string = ""
if fields.foreground then
hl_string = "guifg=#"
.. string.format("%06x", util.blend(fields.foreground, background, alpha / 100))
end
if fields.background then
hl_string = hl_string
.. " guibg=#"
.. string.format("%06x", util.blend(fields.background, background, alpha / 100))
end
if hl_string ~= "" then
-- Can't use nvim_set_hl https://github.com/neovim/neovim/issues/18160
vim.cmd("hi " .. group .. " " .. hl_string)
end
end
end
function NotifyBufHighlights:get_opacity()
return self.opacity
end
---@return NotifyBufHighlights
return function(level, buffer, config)
return NotifyBufHighlights:new(level, buffer, config)
end

View File

@ -0,0 +1,159 @@
local api = vim.api
local NotifyBufHighlights = require("notify.service.buffer.highlights")
---@class NotificationBuf
---@field highlights NotifyBufHighlights
---@field _config table
---@field _notif notify.Notification
---@field _state "open" | "closed"
---@field _buffer number
---@field _height number
---@field _width number
---@field _max_width number | nil
local NotificationBuf = {}
local BufState = {
OPEN = "open",
CLOSED = "close",
}
function NotificationBuf:new(kwargs)
local notif_buf = {
_config = kwargs.config,
_max_width = kwargs.max_width,
_buffer = kwargs.buffer,
_state = BufState.CLOSED,
_width = 0,
_height = 0,
}
setmetatable(notif_buf, self)
self.__index = self
notif_buf:set_notification(kwargs.notif)
return notif_buf
end
function NotificationBuf:set_notification(notif)
self._notif = notif
self:_create_highlights()
end
function NotificationBuf:_create_highlights()
local existing_opacity = self.highlights and self.highlights.opacity or 100
self.highlights = NotifyBufHighlights(self._notif.level, self._buffer, self._config)
if existing_opacity < 100 then
self.highlights:set_opacity(existing_opacity)
end
end
function NotificationBuf:open(win)
if self._state ~= BufState.CLOSED then
return
end
self._state = BufState.OPEN
local record = self._notif:record()
if self._notif.on_open then
self._notif.on_open(win, record)
end
if self._config.on_open() then
self._config.on_open()(win, record)
end
end
function NotificationBuf:should_animate()
return self._notif.animate
end
function NotificationBuf:close(win)
if self._state ~= BufState.OPEN then
return
end
self._state = BufState.CLOSED
vim.schedule(function()
if self._notif.on_close then
self._notif.on_close(win)
end
if self._config.on_close() then
self._config.on_close()(win)
end
pcall(api.nvim_buf_delete, self._buffer, { force = true })
end)
end
function NotificationBuf:height()
return self._height
end
function NotificationBuf:width()
return self._width
end
function NotificationBuf:should_stay()
if self._notif.keep then
return self._notif.keep()
end
return false
end
function NotificationBuf:render()
local notif = self._notif
local buf = self._buffer
local render_namespace = require("notify.render.base").namespace()
api.nvim_buf_set_option(buf, "filetype", "notify")
api.nvim_buf_set_option(buf, "modifiable", true)
api.nvim_buf_clear_namespace(buf, render_namespace, 0, -1)
notif.render(buf, notif, self.highlights, self._config)
api.nvim_buf_set_option(buf, "modifiable", false)
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
local width = self._config.minimum_width()
for _, line in pairs(lines) do
width = math.max(width, vim.str_utfindex(line))
end
local success, extmarks =
pcall(api.nvim_buf_get_extmarks, buf, render_namespace, 0, #lines, { details = true })
if not success then
extmarks = {}
end
local virt_texts = {}
for _, mark in ipairs(extmarks) do
local details = mark[4]
for _, virt_text in ipairs(details.virt_text or {}) do
virt_texts[mark[2]] = (virt_texts[mark[2]] or "") .. virt_text[1]
end
end
for _, text in pairs(virt_texts) do
width = math.max(width, vim.str_utfindex(text))
end
self._width = width
self._height = #lines
end
function NotificationBuf:timeout()
return self._notif.timeout
end
function NotificationBuf:buffer()
return self._buffer
end
function NotificationBuf:is_valid()
return self._buffer and vim.api.nvim_buf_is_valid(self._buffer)
end
function NotificationBuf:level()
return self._notif.level
end
---@param buf number
---@param notification notify.Notification;q
---@return NotificationBuf
return function(buf, notification, opts)
return NotificationBuf:new(
vim.tbl_extend("keep", { buffer = buf, notif = notification }, opts or {})
)
end

View File

@ -0,0 +1,116 @@
local util = require("notify.util")
local NotificationBuf = require("notify.service.buffer")
---@class NotificationService
---@field private _running boolean
---@field private _pending FIFOQueue
---@field private _animator WindowAnimator
---@field private _buffers table<integer, NotificationBuf>
---@field private _fps integer
local NotificationService = {}
---@class notify.ServiceConfig
---@field fps integer
---@param config notify.ServiceConfig
function NotificationService:new(config, animator)
local service = {
_config = config,
_fps = config.fps(),
_animator = animator,
_pending = util.FIFOQueue(),
_running = false,
_buffers = {},
}
self.__index = self
setmetatable(service, self)
return service
end
function NotificationService:_run()
self._running = true
local succees, updated =
pcall(self._animator.render, self._animator, self._pending, 1 / self._fps)
if not succees then
print("Error running notification service: " .. updated)
self._running = false
return
end
if not updated then
self._running = false
return
end
vim.defer_fn(function()
self:_run()
end, 1000 / self._fps)
end
---@param notif notify.Notification;q
---@return integer
function NotificationService:push(notif)
local buf = vim.api.nvim_create_buf(false, true)
local notif_buf = NotificationBuf(buf, notif, { config = self._config })
notif_buf:render()
self._buffers[notif.id] = notif_buf
self._pending:push(notif_buf)
if not self._running then
self:_run()
else
-- Forces a render during blocking events
-- https://github.com/rcarriga/nvim-notify/issues/5
pcall(self._animator.render, self._animator, self._pending, 1 / self._fps)
end
vim.cmd("redraw")
return buf
end
function NotificationService:replace(id, notif)
local existing = self._buffers[id]
if not (existing and existing:is_valid()) then
vim.notify("No matching notification found to replace")
return
end
existing:set_notification(notif)
self._buffers[id] = nil
self._buffers[notif.id] = existing
pcall(existing.render, existing)
local win = vim.fn.bufwinid(existing:buffer())
if win ~= -1 then
-- Highlights can change name if level changed so we have to re-link
-- vim.wo does not behave like setlocal, thus we use setwinvar to set a
-- local option. Otherwise our changes would affect subsequently opened
-- windows.
-- see e.g. neovim#14595
vim.fn.setwinvar(
win,
"&winhl",
"Normal:" .. existing.highlights.body .. ",FloatBorder:" .. existing.highlights.border
)
self._animator:on_refresh(win)
end
end
function NotificationService:dismiss(opts)
local notif_wins = vim.tbl_keys(self._animator.win_stages)
for _, win in pairs(notif_wins) do
pcall(vim.api.nvim_win_close, win, true)
end
if opts.pending then
local cleared = 0
while self._pending:pop() do
cleared = cleared + 1
end
if not opts.silent then
vim.notify("Cleared " .. cleared .. " pending notifications")
end
end
end
function NotificationService:pending()
return self._pending:length()
end
---@return NotificationService
return function(config, animator)
return NotificationService:new(config, animator)
end

View File

@ -0,0 +1,78 @@
---@class notify.Notification
---@field id integer
---@field level string
---@field message string[]
---@field timeout number | nil
---@field title string[]
---@field icon string
---@field time number
---@field width number
---@field animate boolean
---@field hide_from_history boolean
---@field keep fun(): boolean
---@field on_open fun(win: number, record: notify.Record) | nil
---@field on_close fun(win: number, record: notify.Record) | nil
---@field render fun(buf: integer, notification: notify.Notification, highlights: table<string, string>)
local Notification = {}
local level_maps = vim.tbl_extend("keep", {}, vim.log.levels)
for k, v in pairs(vim.log.levels) do
level_maps[v] = k
end
function Notification:new(id, message, level, opts, config)
if type(level) == "number" then
level = level_maps[level]
end
if type(message) == "string" then
message = vim.split(message, "\n")
end
level = vim.fn.toupper(level or "info")
local time = vim.fn.localtime()
local title = opts.title or ""
if type(title) == "string" then
title = { title, vim.fn.strftime(config.time_formats().notification, time) }
end
vim.validate({
message = { message, "table" },
level = { level, "string" },
title = { title, "table" },
})
local notif = {
id = id,
message = message,
title = title,
icon = opts.icon or config.icons()[level] or config.icons().INFO,
time = time,
timeout = opts.timeout,
level = level,
keep = opts.keep,
on_open = opts.on_open,
on_close = opts.on_close,
animate = opts.animate ~= false,
render = opts.render,
hide_from_history = opts.hide_from_history,
}
self.__index = self
setmetatable(notif, self)
return notif
end
function Notification:record()
return {
id = self.id,
message = self.message,
level = self.level,
time = self.time,
title = self.title,
icon = self.icon,
render = self.render,
}
end
---@param message string | string[]
---@param level string | number
---@param opts notify.Options
return function(id, message, level, opts, config)
return Notification:new(id, message, level, opts, config)
end

View File

@ -0,0 +1,48 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = state.message.width,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
opacity = 0,
}
end,
function()
return {
opacity = { 100 },
col = { vim.opt.columns:get() },
}
end,
function()
return {
col = { vim.opt.columns:get() },
time = true,
}
end,
function()
return {
opacity = {
0,
frequency = 2,
complete = function(cur_opacity)
return cur_opacity <= 4
end,
},
col = { vim.opt.columns:get() },
}
end,
}
end

View File

@ -0,0 +1,77 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = state.message.width,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
opacity = 0,
}
end,
function(state, win)
return {
opacity = { 100 },
col = { vim.opt.columns:get() },
row = {
stages_util.slot_after_previous(win, state.open_windows, direction),
frequency = 3,
complete = function()
return true
end,
},
}
end,
function(state, win)
return {
col = { vim.opt.columns:get() },
time = true,
row = {
stages_util.slot_after_previous(win, state.open_windows, direction),
frequency = 3,
complete = function()
return true
end,
},
}
end,
function(state, win)
return {
width = {
1,
frequency = 2.5,
damping = 0.9,
complete = function(cur_width)
return cur_width < 3
end,
},
opacity = {
0,
frequency = 2,
complete = function(cur_opacity)
return cur_opacity <= 4
end,
},
col = { vim.opt.columns:get() },
row = {
stages_util.slot_after_previous(win, state.open_windows, direction),
frequency = 3,
complete = function()
return true
end,
},
}
end,
}
end

View File

@ -0,0 +1,20 @@
local M = {}
---@class MessageState
---@field width number
---@field height number
---@alias InitStage fun(open_windows: number[], message_state: MessageState): table | nil
---@alias AnimationStage fun(win: number, message_state: MessageState): table
---@alias Stage InitStage | AnimationStage
---@alias Stages Stage[]
setmetatable(M, {
---@return Stages
__index = function(_, key)
return require("notify.stages." .. key)
end,
})
return M

View File

@ -0,0 +1,30 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = state.message.width,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
}
end,
function(state, win)
return {
col = vim.opt.columns:get(),
time = true,
row = stages_util.slot_after_previous(win, state.open_windows, direction),
}
end,
}
end

View File

@ -0,0 +1,48 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = 1,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
}
end,
function(state)
return {
width = { state.message.width, frequency = 2 },
col = { vim.opt.columns:get() },
}
end,
function()
return {
col = { vim.opt.columns:get() },
time = true,
}
end,
function()
return {
width = {
1,
frequency = 2.5,
damping = 0.9,
complete = function(cur_width)
return cur_width < 2
end,
},
col = { vim.opt.columns:get() },
}
end,
}
end

View File

@ -0,0 +1,29 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = state.message.width,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
}
end,
function()
return {
col = vim.opt.columns:get(),
time = true,
}
end,
}
end

View File

@ -0,0 +1,197 @@
local max, min = math.max, math.min
local util = require("notify.util")
local M = {}
M.DIRECTION = {
TOP_DOWN = "top_down",
BOTTOM_UP = "bottom_up",
LEFT_RIGHT = "left_right",
RIGHT_LEFT = "right_left",
}
local function is_increasing(direction)
return (direction == M.DIRECTION.TOP_DOWN or direction == M.DIRECTION.LEFT_RIGHT)
end
local function moves_vertically(direction)
return (direction == M.DIRECTION.TOP_DOWN or direction == M.DIRECTION.BOTTOM_UP)
end
function M.slot_name(direction)
if moves_vertically(direction) then
return "height"
end
return "width"
end
local function less(a, b)
return a < b
end
local function greater(a, b)
return a > b
end
local function overlaps(a, b)
return a.min <= b.max and b.min <= a.max
end
local move_slot = function(direction, slot, delta)
if is_increasing(direction) then
return slot + delta
end
return slot - delta
end
local function slot_key(direction)
return moves_vertically(direction) and "row" or "col"
end
local function space_key(direction)
return moves_vertically(direction) and "height" or "width"
end
-- TODO: Use direction to check border lists
local function border_padding(direction, win_conf)
if not win_conf.border or win_conf.border == "none" then
return 0
end
return 2
end
---@param windows number[]
---@param direction string
---@return { max: integer, min: integer}[]
local function window_intervals(windows, direction, cmp)
local win_intervals = {}
for _, w in ipairs(windows) do
local exists, existing_conf = util.get_win_config(w)
if exists then
local border_space = border_padding(direction, existing_conf)
win_intervals[#win_intervals + 1] = {
min = existing_conf[slot_key(direction)],
max = existing_conf[slot_key(direction)]
+ existing_conf[space_key(direction)]
+ border_space
- 1,
}
end
end
table.sort(win_intervals, function(a, b)
return cmp(a.min, b.min)
end)
return win_intervals
end
function M.get_slot_range(direction)
local top = vim.opt.tabline:get() == "" and 0 or 1
local bottom = vim.opt.lines:get()
- (vim.opt.cmdheight:get() + (vim.opt.laststatus:get() > 0 and 1 or 0))
local left = 1
local right = vim.opt.columns:get()
if M.DIRECTION.TOP_DOWN == direction then
return top, bottom
elseif M.DIRECTION.BOTTOM_UP == direction then
return bottom, top
elseif M.DIRECTION.LEFT_RIGHT == direction then
return left, right
elseif M.DIRECTION.RIGHT_LEFT == direction then
return right, left
end
error(string.format("Invalid direction: %s", direction))
end
---@param existing_wins number[] Windows to avoid overlapping
---@param required_space number Window height or width including borders
---@param direction string Direction to stack windows, one of M.DIRECTION
---@return number | nil Slot to place window at or nil if no slot available
function M.available_slot(existing_wins, required_space, direction)
local increasing = is_increasing(direction)
local cmp = increasing and less or greater
local first_slot, last_slot = M.get_slot_range(direction)
local function create_interval(start_slot)
local end_slot = move_slot(direction, start_slot, required_space - 1)
return { min = min(start_slot, end_slot), max = max(start_slot, end_slot) }
end
local interval = create_interval(first_slot)
local intervals = window_intervals(existing_wins, direction, cmp)
for _, next_interval in ipairs(intervals) do
if overlaps(next_interval, interval) then
interval = create_interval(
move_slot(direction, increasing and next_interval.max or next_interval.min, 1)
)
end
end
if #intervals > 0 and not cmp(is_increasing and interval.max or interval.min, last_slot) then
return nil
end
return interval.min
end
---Gets the next slot available for the given window while maintaining its position using the given list.
---@param win number
---@param open_windows number[]
---@param direction string
function M.slot_after_previous(win, open_windows, direction)
local key = slot_key(direction)
local cmp = is_increasing(direction) and less or greater
local exists, cur_win_conf = util.get_win_config(win)
if not exists then
return 0
end
local cur_slot = cur_win_conf[key]
local win_confs = {}
for _, w in ipairs(open_windows) do
local success, conf = util.get_win_config(w)
if success then
win_confs[w] = conf
end
end
local preceding_wins = vim.tbl_filter(function(open_win)
return win_confs[open_win] and cmp(win_confs[open_win][key], cur_slot)
end, open_windows)
if #preceding_wins == 0 then
local start = M.get_slot_range(direction)
if is_increasing(direction) then
return start
end
return move_slot(
direction,
start,
cur_win_conf[space_key(direction)] + border_padding(direction, cur_win_conf)
)
end
table.sort(preceding_wins, function(a, b)
return cmp(win_confs[a][key], win_confs[b][key])
end)
local last_win = preceding_wins[#preceding_wins]
local last_win_conf = win_confs[last_win]
if is_increasing(direction) then
return move_slot(
direction,
last_win_conf[key],
last_win_conf[space_key(direction)] + border_padding(direction, last_win_conf)
)
else
return move_slot(
direction,
last_win_conf[key],
cur_win_conf[space_key(direction)] + border_padding(direction, cur_win_conf)
)
end
end
return M

View File

@ -0,0 +1,120 @@
local M = {}
local min, max, floor = math.min, math.max, math.floor
local rshift, lshift, band, bor = bit.rshift, bit.lshift, bit.band, bit.bor
function M.is_callable(obj)
return type(obj) == "function" or (type(obj) == "table" and obj.__call)
end
function M.lazy_require(require_path)
return setmetatable({}, {
__call = function(_, ...)
return require(require_path)(...)
end,
__index = function(_, key)
return require(require_path)[key]
end,
__newindex = function(_, key, value)
require(require_path)[key] = value
end,
})
end
function M.pop(tbl, key, default)
local val = default
if tbl[key] then
val = tbl[key]
tbl[key] = nil
end
return val
end
function M.blend(fg_hex, bg_hex, alpha)
local segment = 0xFF0000
local result = 0
for i = 2, 0, -1 do
local blended = alpha * rshift(band(fg_hex, segment), i * 8)
+ (1 - alpha) * rshift(band(bg_hex, segment), i * 8)
result = bor(lshift(result, 8), floor((min(max(blended, 0), 255)) + 0.5))
segment = rshift(segment, 8)
end
return result
end
function M.round(num, decimals)
if decimals then
return tonumber(string.format("%." .. decimals .. "f", num))
end
return math.floor(num + 0.5)
end
function M.partial(func, ...)
local args = { ... }
return function(...)
local final = {}
vim.list_extend(final, args)
vim.list_extend(final, { ... })
return func(unpack(final))
end
end
function M.get_win_config(win)
local success, conf = pcall(vim.api.nvim_win_get_config, win)
if not success or not conf.row then
return false, conf
end
if type(conf.row) == "table" then
conf.row = conf.row[false]
end
if type(conf.col) == "table" then
conf.col = conf.col[false]
end
return success, conf
end
function M.open_win(notif_buf, enter, opts)
local win = vim.api.nvim_open_win(notif_buf:buffer(), enter, opts)
-- vim.wo does not behave like setlocal, thus we use setwinvar to set local
-- only options. Otherwise our changes would affect subsequently opened
-- windows.
-- see e.g. neovim#14595
vim.fn.setwinvar(
win,
"&winhl",
"Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
)
vim.fn.setwinvar(win, "&wrap", 0)
return win
end
M.FIFOQueue = require("notify.util.queue")
function M.rgb_to_numbers(s)
local colours = {}
for a in string.gmatch(s, "[A-Fa-f0-9][A-Fa-f0-9]") do
colours[#colours + 1] = tonumber(a, 16)
end
return colours
end
function M.numbers_to_rgb(colours)
local colour = "#"
for _, num in pairs(colours) do
colour = colour .. string.format("%X", num)
end
return colour
end
function M.highlight(name, fields)
local fields_string = ""
for field, value in pairs(fields) do
fields_string = fields_string .. " " .. field .. "=" .. value
end
if fields_string ~= "" then
vim.cmd("hi " .. name .. fields_string)
end
end
return M

View File

@ -0,0 +1,54 @@
---@class FIFOQueue
local FIFOQueue = {}
function FIFOQueue:pop()
if self:is_empty() then
return nil
end
local r = self[self.pop_from]
self[self.pop_from] = nil
self.pop_from = self.pop_from - 1
return r
end
function FIFOQueue:peek()
return self[self.pop_from]
end
function FIFOQueue:push(val)
self[self.push_to] = val
self.push_to = self.push_to - 1
end
function FIFOQueue:is_empty()
return self:length() == 0
end
function FIFOQueue:length()
return self.pop_from - self.push_to
end
function FIFOQueue:iter()
local i = self.pop_from + 1
return function()
if i > self.push_to + 1 then
i = i - 1
return self[i]
end
end
end
function FIFOQueue:new()
local queue = {
pop_from = 1,
push_to = 1,
}
self.__index = self
setmetatable(queue, self)
return queue
end
---@return FIFOQueue
return function()
return FIFOQueue:new()
end

View File

@ -0,0 +1,326 @@
local api = vim.api
local animate = require("notify.animate")
local util = require("notify.util")
local round = util.round
local max = math.max
---@class WindowAnimator
---@field config table
---@field win_states table<number, table<string, SpringState>>
---@field win_stages table<number, integer>
---@field notif_bufs table<number, NotificationBuf>
---@field timers table
---@field stages table
local WindowAnimator = {}
function WindowAnimator:new(stages, config)
local animator = {
config = config,
win_stages = {},
win_states = {},
notif_bufs = {},
timers = {},
stages = stages,
}
self.__index = self
setmetatable(animator, self)
return animator
end
function WindowAnimator:render(queue, time)
self:push_pending(queue)
if vim.tbl_isempty(self.win_stages) then
return false
end
local open_windows = vim.tbl_keys(self.win_stages)
for win, _ in pairs(self.win_stages) do
self:_update_window(time, win, open_windows)
end
return true
end
function WindowAnimator:push_pending(queue)
if queue:is_empty() then
return
end
while not queue:is_empty() do
---@type NotificationBuf
local notif_buf = queue:peek()
if not notif_buf:is_valid() then
queue:pop()
else
local windows = vim.tbl_keys(self.win_stages)
local win_opts = self.stages[1]({
message = self:_get_dimensions(notif_buf),
open_windows = windows,
})
if not win_opts then
return
end
local opacity = util.pop(win_opts, "opacity")
if opacity then
notif_buf.highlights:set_opacity(opacity)
end
win_opts.noautocmd = true
local win = util.open_win(notif_buf, false, win_opts)
vim.fn.setwinvar(
win,
"&winhl",
"Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
)
self.win_stages[win] = 2
self.win_states[win] = {}
self.notif_bufs[win] = notif_buf
queue:pop()
notif_buf:open(win)
end
end
end
function WindowAnimator:_advance_win_stage(win)
local cur_stage = self.win_stages[win]
if not cur_stage then
return
end
if cur_stage < #self.stages then
if api.nvim_get_current_win() == win then
return
end
self.win_stages[win] = cur_stage + 1
return
end
self.win_stages[win] = nil
local function close()
if api.nvim_get_current_win() == win then
return vim.defer_fn(close, 1000)
end
self:_remove_win(win)
end
close()
end
function WindowAnimator:_remove_win(win)
pcall(api.nvim_win_close, win, true)
self.win_stages[win] = nil
self.win_states[win] = nil
local notif_buf = self.notif_bufs[win]
self.notif_bufs[win] = nil
notif_buf:close(win)
end
function WindowAnimator:on_refresh(win)
local notif_buf = self.notif_bufs[win]
if not notif_buf then
return
end
if self.timers[win] then
self.timers[win]:set_repeat(notif_buf:timeout() or self.config.default_timeout())
self.timers[win]:again()
end
end
function WindowAnimator:_start_timer(win)
local buf_time = self.notif_bufs[win]:timeout() == nil and self.config.default_timeout()
or self.notif_bufs[win]:timeout()
if buf_time ~= false then
if buf_time == true then
buf_time = nil
end
local timer = vim.loop.new_timer()
self.timers[win] = timer
timer:start(
buf_time,
buf_time,
vim.schedule_wrap(function()
timer:stop()
self.timers[win] = nil
local notif_buf = self.notif_bufs[win]
if notif_buf and notif_buf:should_stay() then
return
end
self:_advance_win_stage(win)
end)
)
end
end
function WindowAnimator:_update_window(time, win, open_windows)
local stage = self.win_stages[win]
local notif_buf = self.notif_bufs[win]
local win_goals = self:_get_win_goals(win, stage, open_windows)
if not win_goals then
self:_remove_win(win)
end
-- If we don't animate, then we move to all goals instantly.
-- Can't just jump to the end, because we need to the intermediate changes
while
not notif_buf:should_animate()
and win_goals.time == nil
and self.win_stages[win] < #self.stages
do
for field, goal in pairs(win_goals) do
if type(goal) == "table" then
win_goals[field] = goal[1]
end
end
self:_advance_win_state(win, win_goals, time)
self:_advance_win_stage(win)
stage = self.win_stages[win]
win_goals = self:_get_win_goals(win, stage, open_windows)
end
if win_goals.time and not self.timers[win] then
self:_start_timer(win)
end
self:_advance_win_state(win, win_goals, time)
if self:_is_complete(win, win_goals) and not win_goals.time then
self:_advance_win_stage(win)
end
end
function WindowAnimator:_is_complete(win, goals)
local complete = true
local win_state = self.win_states[win]
if not win_state then
return true
end
for field, goal in pairs(goals) do
if field ~= "time" then
if type(goal) == "table" then
if goal.complete then
complete = goal.complete(win_state[field].position)
else
complete = goal[1] == round(win_state[field].position, 2)
end
end
if not complete then
break
end
end
end
return complete
end
function WindowAnimator:_advance_win_state(win, goals, time)
local win_state = self.win_states[win]
local win_configs = {}
local function win_conf(win_)
if win_configs[win_] then
return win_configs[win_]
end
local exists, conf = util.get_win_config(win_)
if not exists then
self:_remove_win(win_)
return
end
win_configs[win_] = conf
return conf
end
for field, goal in pairs(goals) do
if field ~= "time" then
local goal_type = type(goal)
-- Handle spring goal
if goal_type == "table" and goal[1] then
if not win_state[field] then
if field == "opacity" then
win_state[field] = { position = self.notif_bufs[win].highlights:get_opacity() }
else
local conf = win_conf(win)
if not conf then
return true
end
win_state[field] = { position = conf[field] }
end
end
animate.spring(time, goal[1], win_state[field], goal.frequency or 1, goal.damping or 1)
--- Directly move goal
elseif goal_type ~= "table" then
win_state[field] = { position = goal }
else
error("nvim-notify: Invalid stage goal: " .. vim.inspect(goal))
end
end
end
return self:_apply_win_state(win, win_state)
end
function WindowAnimator:_get_win_goals(win, win_stage, open_windows)
local notif_buf = self.notif_bufs[win]
local win_goals = self.stages[win_stage]({
buffer = notif_buf:buffer(),
message = self:_get_dimensions(notif_buf),
open_windows = open_windows,
}, win)
return win_goals
end
function WindowAnimator:_get_dimensions(notif_buf)
return {
height = math.min(self.config.max_height() or 1000, notif_buf:height()),
width = math.min(self.config.max_width() or 1000, notif_buf:width()),
}
end
function WindowAnimator:_apply_win_state(win, win_state)
local win_updated = false
if win_state.opacity then
win_updated = true
local notif_buf = self.notif_bufs[win]
if notif_buf:is_valid() then
notif_buf.highlights:set_opacity(win_state.opacity.position)
vim.fn.setwinvar(
win,
"&winhl",
"Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
)
end
end
local exists, conf = util.get_win_config(win)
local new_conf = {}
if not exists then
self:_remove_win(win)
else
local function set_field(field, min, round_to)
if not win_state[field] then
return
end
local new_value = max(round(win_state[field].position, round_to), min)
if new_value == conf[field] then
return
end
win_updated = true
new_conf[field] = new_value
end
set_field("row", 0, 1)
set_field("col", 0, 1)
set_field("width", 1)
set_field("height", 1)
if win_updated then
if new_conf.row or new_conf.col then
new_conf.relative = conf.relative
new_conf.row = new_conf.row or conf.row
new_conf.col = new_conf.col or conf.col
end
api.nvim_win_set_config(win, new_conf)
end
end
return win_updated
end
---@return WindowAnimator
return function(stages, config)
return WindowAnimator:new(stages, config)
end