Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9de23b6
feat: cache rendered object until ttl expires
nqrk Jan 8, 2026
17389e7
feat: refactor notification view renderer
nqrk Jan 11, 2026
8513789
feat: add treesitter highlight to view renderer
nqrk Jan 17, 2026
9af680d
feat: add renderer highlight config options
nqrk Jan 17, 2026
937cae4
feat: merge markdown with markdown_inline
nqrk Jan 17, 2026
2d5ae61
fix: clean up dead code, lsp warning etc
nqrk Jan 17, 2026
75940b6
fix: prevent extra_line to reset on each iteration
nqrk Jan 18, 2026
c4e6363
fix: use window max_width option before fallback
nqrk Jan 19, 2026
59f59e0
feat: add renderer test file
nqrk Jan 19, 2026
2a6266f
feat: add unicode characters support
nqrk Jan 22, 2026
73582e4
fix: lines overflow when max_width > window width
nqrk Jan 22, 2026
369bec0
feat: tokenize tab as space (see window.tabstop)
nqrk Jan 22, 2026
c8cf929
feat: return unified message object from view render
nqrk Jan 31, 2026
d47755e
feat: add left-aligned text support
nqrk Jan 31, 2026
475fc7a
feat: add per-message text alignment to notify
nqrk Jan 31, 2026
55e04ee
fix: cache window max size instead of editor size
nqrk Jan 31, 2026
e3404d0
feat: add per-message text highlight to notify
nqrk Jan 31, 2026
991af7b
fix: test loop stuck when g.colors_name is nil
nqrk Feb 1, 2026
d167c14
fix: cache rendered object logic
nqrk Feb 6, 2026
835521a
feat: skip duplicates in history if update_hook != false
nqrk Feb 6, 2026
4181aeb
perf: improve highlighter logic
nqrk Feb 7, 2026
2aa7cf5
perf: improve items rendering logic
nqrk Feb 8, 2026
98ebfaf
refactor: buf_set_extmark virtual text position for nvim < 0.11
nqrk Feb 9, 2026
097fcea
perf: compute nvim version check only once
nqrk Feb 9, 2026
589e775
fix: clean up integration loader and unused code
nqrk Feb 10, 2026
799ec3e
perf: minor improvement to window set_lines
nqrk Feb 10, 2026
9e157f7
refactor: polling timer (start_polling)
nqrk Feb 11, 2026
3b95c46
perf(poller): uses libuv to handle time based events
nqrk Feb 12, 2026
b6e61c0
perf(model.tick): reduces memory allocation
nqrk Feb 12, 2026
a066a13
perf(guard): reduces memory allocation
nqrk Feb 13, 2026
361b607
fix: direct cache access instead of function
nqrk Feb 14, 2026
51196aa
perf: cache editor columns (updated on resize)
nqrk Feb 14, 2026
3726cee
fix: respects window relative positioning
nqrk Feb 15, 2026
8922f84
perf: prevent redundant render operations
nqrk Feb 15, 2026
b68b284
fix: out of bounds index when checking history duplicate
nqrk Feb 17, 2026
28615aa
refactor(model): move cache cleanup and state update calls
nqrk Feb 17, 2026
ea64680
refactor(poller): handle errors via callback
nqrk Feb 18, 2026
7904ea0
feat: add reset to fidget command line
nqrk Feb 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lua/fidget/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,13 @@ SC.subcommands = {
{ name = "group_key", type = GroupKey, desc = "group to clear" },
},
},
reset = {
desc = "Reset notification subsystem state",
func = function()
require("fidget.notification").reset()
end,
args = {}
},
history = {
desc = "Show notifications history",
func = function(args)
Expand Down
100 changes: 73 additions & 27 deletions lua/fidget/notification.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---@mod fidget.notification Notification subsystem
local notification = {}
notification.model = require("fidget.notification.model")
notification.window = require("fidget.notification.window")
notification.view = require("fidget.notification.view")
local poll = require("fidget.poll")
local logger = require("fidget.logger")
local notification = {}
notification.model = require("fidget.notification.model")
notification.window = require("fidget.notification.window")
notification.view = require("fidget.notification.view")
local poll = require("fidget.poll")
local logger = require("fidget.logger")

--- Used to determine the identity of notification items and groups.
---@alias Key any
Expand All @@ -21,6 +21,8 @@ local logger = require("fidget.logger")
---@field key Key|nil Replace existing notification item of the same key
---@field group Key|nil Group that this notification item belongs to
---@field annote string|nil Optional single-line title that accompanies the message
---@field position string|nil Optional text position inside the window
---@field lang string|nil Optional tree-sitter highlight language to use
---@field hidden boolean|nil Whether this item should be shown
---@field ttl number|nil How long after a notification item should exist; pass 0 to use default value
---@field update_only boolean|nil If true, don't create new notification items
Expand Down Expand Up @@ -76,6 +78,8 @@ local logger = require("fidget.logger")
---@field content_key Key What to deduplicate items by (do not deduplicate if `nil`)
---@field message string Displayed message for the item
---@field annote string|nil Optional title that accompanies the message
---@field position string|nil Optional text position inside the window
---@field lang string|nil Optional tree-sitter highlight language to use
---@field style string Style used to render the annote/title, if any
---@field hidden boolean Whether this item should be shown
---@field expires_at number What time this item should be removed; math.huge means never
Expand All @@ -86,11 +90,11 @@ local logger = require("fidget.logger")
--- A notification element in the notifications history.
---
---@class HistoryItem : Item
---@field removed boolean Whether this item is deleted
---@field group_key Key Key of the group this item belongs to
---@field group_name string|nil Title of the group this item belongs to
---@field group_icon string|nil Icon of the group this item belongs to
---@field last_updated number What time this item was last updated, in seconds since Jan 1, 1970
---@field removed boolean Whether this item is deleted
---@field group_key Key Key of the group this item belongs to
---@field group_name string|false|nil Title of the group this item belongs to
---@field group_icon string|false|nil Icon of the group this item belongs to
---@field last_updated number What time this item was last updated, in seconds since Jan 1, 1970

--- Filter options when querying for notifications history.
---
Expand All @@ -104,15 +108,30 @@ local logger = require("fidget.logger")
---@field include_active boolean|nil Include items that have not been removed (default: true)

--- The "model" (abstract state) of notifications.
---@type State
local state = {
---@class State
local state = {
groups = {},
render = false,
view_suppressed = false,
removed = {},
removed_cap = 128,
removed_first = 1,
}

--- Must be called when the groups table changes
function state:update()
if not self.render then
self.render = true
end
end

--- Serves as a lock to prevent unnecessary rendering
function state:wait()
if self.render then
self.render = false
end
end

--- Default notification configuration.
---
--- Exposed publicly because it might be useful for users to integrate for when
Expand Down Expand Up @@ -156,8 +175,8 @@ notification.default_config = {
--- Sets a |fidget.notification.Item|'s `content_key`, for deduplication.
---
--- This default implementation sets an item's `content_key` to its `message`,
--- appended with its `annote` (or a null byte if it has no `annote`), a rough
--- "hash" of its contents. You can write your own `update_hook` that "hashes"
--- appended with its `position` and `annote` (or a null byte if it has no `annote`),
--- a rough "hash" of its contents. You can write your own `update_hook` that "hashes"
--- the message differently, e.g., only considering the `message`, or taking the
--- `data` or style fields into account.
---
Expand All @@ -181,13 +200,19 @@ notification.default_config = {
---
---@param item Item
function notification.set_content_key(item)
item.content_key = item.message .. " " .. (item.annote and item.annote or string.char(0))
item.content_key = string.format(
"%s-%s-%s%s",
item.message,
item.lang and item.lang or "",
item.position and item.position or "",
item.annote and item.annote or string.char(0)
)
end

---@options notification [[
---@protected
--- Notification options
notification.options = {
notification.options = {
--- How frequently to update and render notifications
---
--- Measured in Hertz (frames per second).
Expand All @@ -206,6 +231,12 @@ notification.options = {
---@type 0|1|2|3|4|5
filter = vim.log.levels.INFO,

-- Whether to show errors that occur while rendering notifications
-- When false, errors are logged instead of shown to the user
---
---@type boolean
show_errors = false,

--- Number of removed messages to retain in history
---
--- Set to 0 to keep around history indefinitely (until cleared).
Expand Down Expand Up @@ -317,9 +348,7 @@ end
---
---@return boolean closed_successfully Whether the window closed successfully.
function notification.close()
return notification.window.guard(function()
notification.window.close()
end)
return notification.window.guard(notification.window.close)
end

--- Clear active notifications.
Expand Down Expand Up @@ -355,35 +384,52 @@ function notification.reset()
notification.clear()
notification.clear_history()
notification.poller:reset_error() -- Clear error if previously encountered one
notification.poller:release() -- Release timer resources
end

---@private
local _guard = notification.window.guard

--- The poller for the notification subsystem.
---@protected
notification.poller = poll.Poller {
name = "notification",
poll = function(self)
notification.model.tick(self:now(), state)

-- TODO: if not modified, don't re-render
local lines, width = notification.view.render(self:now(), state.groups)
local message = notification.view.render(self:now(), state)

if #lines > 0 then
if message and #message.lines > 0 and state.render then
if state.view_suppressed then
return true
end

notification.window.guard(function()
notification.window.set_lines(lines, width)
end)
_guard(notification.window.set_lines, message)

state:wait()

return true
else
if state.view_suppressed then
return false
else
if not state.render then
return true
end
end

-- If we could not close the window, keep polling, i.e., keep trying to close the window.
return not notification.close()
end
end,
raise = function(self)
if self:has_error() then
notification.close()

self:reset_error()
self:release()
end
return notification.options.show_errors
end
}

Expand Down Expand Up @@ -459,7 +505,7 @@ end
---
---@return Key[] keys
function notification.group_keys()
return vim.tbl_map(function(group) return group.key end, state.groups)
return vim.iter(state.groups):map(function(group) return group.key end):totable()
end

return notification
Loading