Regenerate nvim config
This commit is contained in:
@ -0,0 +1,5 @@
|
||||
local M = {}
|
||||
|
||||
M.spring = require("notify.animate.spring")
|
||||
|
||||
return M
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
199
config/neovim/store/lazy-plugins/nvim-notify/lua/notify/init.lua
Normal file
199
config/neovim/store/lazy-plugins/nvim-notify/lua/notify/init.lua
Normal 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
|
||||
@ -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
|
||||
@ -0,0 +1,9 @@
|
||||
local M = {}
|
||||
|
||||
local namespace = vim.api.nvim_create_namespace("nvim-notify")
|
||||
|
||||
function M.namespace()
|
||||
return namespace
|
||||
end
|
||||
|
||||
return M
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1,118 @@
|
||||
local pickers = require("telescope.pickers")
|
||||
local finders = require("telescope.finders")
|
||||
local conf = require("telescope.config").values
|
||||
local actions = require("telescope.actions")
|
||||
local action_state = require("telescope.actions.state")
|
||||
local previewers = require("telescope.previewers")
|
||||
local entry_display = require("telescope.pickers.entry_display")
|
||||
local notify = require("notify")
|
||||
|
||||
local widths = {
|
||||
time = 8,
|
||||
title = nil,
|
||||
icon = nil,
|
||||
level = nil,
|
||||
message = nil,
|
||||
}
|
||||
|
||||
local displayer = entry_display.create({
|
||||
separator = " ",
|
||||
items = {
|
||||
{ width = widths.time },
|
||||
{ width = widths.title },
|
||||
{ width = widths.icon },
|
||||
{ width = widths.level },
|
||||
{ width = widths.message },
|
||||
},
|
||||
})
|
||||
|
||||
local telescope_notifications = function(opts)
|
||||
local time_format = require("notify")._config().time_formats().notification
|
||||
local notifs = require("notify").history()
|
||||
local reversed = {}
|
||||
for i, notif in ipairs(notifs) do
|
||||
reversed[#notifs - i + 1] = notif
|
||||
end
|
||||
pickers
|
||||
.new(opts, {
|
||||
results_title = "Notifications",
|
||||
prompt_title = "Filter Notifications",
|
||||
finder = finders.new_table({
|
||||
results = reversed,
|
||||
entry_maker = function(notif)
|
||||
return {
|
||||
value = notif,
|
||||
display = function(entry)
|
||||
return displayer({
|
||||
{ vim.fn.strftime(time_format, entry.value.time), "NotifyLogTime" },
|
||||
{ entry.value.title[1], "NotifyLogTitle" },
|
||||
{ entry.value.icon, "Notify" .. entry.value.level .. "Title" },
|
||||
{ entry.value.level, "Notify" .. entry.value.level .. "Title" },
|
||||
{ entry.value.message[1], "Notify" .. entry.value.level .. "Body" },
|
||||
})
|
||||
end,
|
||||
ordinal = notif.title[1] .. " " .. notif.title[2] .. " " .. table.concat(
|
||||
notif.message,
|
||||
" "
|
||||
),
|
||||
}
|
||||
end,
|
||||
}),
|
||||
sorter = conf.generic_sorter(opts),
|
||||
attach_mappings = function(prompt_bufnr, map)
|
||||
actions.select_default:replace(function()
|
||||
actions.close(prompt_bufnr)
|
||||
local selection = action_state.get_selected_entry()
|
||||
if selection == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local notification = selection.value
|
||||
local opened_buffer = notify.open(notification)
|
||||
|
||||
local lines = vim.opt.lines:get()
|
||||
local cols = vim.opt.columns:get()
|
||||
|
||||
local win = vim.api.nvim_open_win(opened_buffer.buffer, true, {
|
||||
relative = "editor",
|
||||
row = (lines - opened_buffer.height) / 2,
|
||||
col = (cols - opened_buffer.width) / 2,
|
||||
height = opened_buffer.height,
|
||||
width = opened_buffer.width,
|
||||
border = "rounded",
|
||||
style = "minimal",
|
||||
})
|
||||
-- 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:"
|
||||
.. opened_buffer.highlights.body
|
||||
.. ",FloatBorder:"
|
||||
.. opened_buffer.highlights.border
|
||||
)
|
||||
vim.fn.setwinvar(win, "&wrap", 0)
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
previewer = previewers.new_buffer_previewer({
|
||||
title = "Message",
|
||||
define_preview = function(self, entry, status)
|
||||
local notification = entry.value
|
||||
local max_width = vim.api.nvim_win_get_config(status.preview_win).width
|
||||
vim.api.nvim_win_set_option(status.preview_win, "wrap", true)
|
||||
notify.open(notification, { buffer = self.state.bufnr, max_width = max_width })
|
||||
end,
|
||||
}),
|
||||
})
|
||||
:find()
|
||||
end
|
||||
|
||||
return require("telescope").register_extension({
|
||||
exports = {
|
||||
notify = telescope_notifications,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user