Repository navigation
notifications
Screen-edge toast notifications with independent entry/exit animations, pulse effects, pinned toasts, and per-toast overrides.
The module is exposed as wu.Notify.
local wu = GetMod("WindowUtils")
local notify = wu.Notify
notify.info("Settings loaded")
notify.success("Preset saved!")
notify.warn("Missing configuration key")
notify.error("Failed to connect")Show an info notification (blue border). Returns the number toast ID from show().
Show a success notification (green border). Returns the number toast ID from show().
Show a warning notification (yellow border). Returns the number toast ID from show().
Show an error notification (red border). Returns the number toast ID from show().
Show a pulsing notification with default pulse settings (animated border). Returns the number toast ID from show().
The toast is created at level "info" with these defaults applied unless opts already set them: pulse = { speed = 4.0, strokeMin = 0, strokeMax = 1.5, easing = "easeInOut" }, borderColor = { 0.8, 0.2, 1.0, 0.9 }, and icon = IconGlyphs.LightningBoltCircle.
Show a notification with a specific level or registered type.
| Parameter | Type | Default | Description |
|---|---|---|---|
| message | string | - | Notification text |
| level | string | "info" | Level name or registered type name |
| opts.ttl | number | config.ttl | Seconds before exit animation starts |
| opts.pinned | boolean | false | Stay visible until dismissed |
| opts.borderColor | table | nil | RGBA border color override |
| opts.pulse | table | nil | Pulse config: {speed, strokeMin, strokeMax, easing} |
| opts.padding | number | config.toastPadding | Inner padding override |
| opts.rounding | number | config.windowRounding | Corner radius override |
| opts.borderSize | number | config.windowBorderSize | Border thickness override |
| opts.width | number|"auto" | config.toastWidth | Width override |
| opts.height | number|"auto" | config.toastHeight | Height override |
| opts.icon | string|false|nil | nil | Custom glyph, false to hide, nil for level default |
| opts.category | string|nil | nil | Category for clearCategory() |
| opts.onDismiss | function|nil | nil | Callback fired when the toast is removed, on every removal path (see Callback Timing) |
These are the only keys show() reads. Anything else in opts is ignored, except that keys coming from a registered type are merged in first (see registerType).
Validation notes:
-
opts.iconis used only when it is a string orfalse. Any other type is treated asnil(level default glyph). -
opts.borderColoris used only when it is a table whose first element is a number, otherwise the level color is used. -
opts.pulseis used only when it is a table. -
opts.categoryis used only when it is a string. -
opts.pinnedmust be exactlytrue. A pinned toast ignoresopts.ttl(its ttl is set to infinity).
Level fallback: an unrecognized level (one that is neither info/success/warn/error nor a registered type) renders with the info glyph and the blue palette color. Register the level with registerType or pass icon/borderColor to control its look.
Queue overflow: toasts past maxVisible stay in the queue and are simply not rendered until earlier ones expire. When the queue grows beyond maxVisible * 2, the oldest unpinned toast is dropped; the scan skips past pinned entries rather than stopping at them, so a pinned toast at the head cannot let the queue grow without bound. Pinned toasts are never evicted. Dropped toasts fire onDismiss.
Returns: number - toast ID (use with dismiss(), getPulseConfig(), setPulseConfig(), setBorderColor())
Remove a specific toast by ID. Useful for pinned toasts.
Removal is immediate: the toast is pulled straight out of the queue, so no exit animation plays, but its onDismiss callback still fires.
Returns: boolean - true if found and removed
Retrieve the mutable pulse config table for a toast by ID. Returns the direct table reference, so mutations are reflected on the next frame.
Returns: table|nil - reference to the toast's pulse table, or nil if not found / no pulse
local pulse = notify.getPulseConfig(toastId)
if pulse then
pulse.speed = 8.0
pulse.strokeMax = 3.0
pulse.easing = "bounce"
endSet or clear a toast's pulse config without dismissing it. Pass a table to enable pulse, or nil to disable it.
| Parameter | Type | Description |
|---|---|---|
| id | number | Toast ID |
| pulse | table|nil | Pulse config table, or nil to disable pulse |
Returns: boolean - true if toast found and updated, false if not found or invalid
-- Enable pulse on an existing toast
notify.setPulseConfig(toastId, { speed = 6.0, strokeMin = 0, strokeMax = 2.0, easing = "bounce" })
-- Disable pulse
notify.setPulseConfig(toastId, nil)Update a toast's border color without dismissing it.
| Parameter | Type | Description |
|---|---|---|
| id | number | Toast ID |
| color | table | RGBA color array, e.g. {1.0, 0.5, 0.0, 1.0} |
Returns: boolean - true if toast found and updated, false if not found or invalid color
notify.setBorderColor(toastId, { 1.0, 0.0, 0.0, 1.0 })Register a reusable notification type with preset defaults. The type name can then be used as the level parameter in show().
notify.registerType("achievement", {
icon = IconGlyphs.Trophy,
borderColor = { 1.0, 0.84, 0.0, 1.0 },
pulse = { speed = 3.0, strokeMin = 0, strokeMax = 2.0, easing = "easeInOut" },
ttl = 5.0,
})
-- Use it
notify.show("Achievement unlocked!", "achievement")name must be a string and defaults must be a table, otherwise the call is logged and ignored. Registering the same name again replaces the previous defaults. The defaults table is stored by reference and merged into opts on every show() call for that level, with opts winning on key collisions, so it may hold any key show() reads.
Change notification defaults. Only provided keys are updated; omitted keys retain their current values. Keys that are not part of the config table below are ignored silently, so a typo has no effect and reports no error.
| Option | Type | Default | Description |
|---|---|---|---|
| position | string | "topRight" | "topRight", "topLeft", "bottomRight", "bottomLeft" |
| maxVisible | number | 5 | Maximum visible toasts |
| ttl | number | 3.0 | Default time-to-live (seconds) |
| safeZone | number | 20 | Pixels from screen edge |
| toastWidth | number|"auto" | "auto" | Toast window width (0 or "auto" for content-based) |
| toastHeight | number|"auto" | "auto" | Toast window height (0 or "auto" for content-based) |
| toastPadding | number | 24 | Internal padding |
| spacing | number | 10 | Vertical gap between toasts |
| windowRounding | number | 0 | Corner rounding (0 = auto-detect from ImGui style) |
| windowBorderSize | number | 3.0 | Border thickness |
| borderOpacity | number | 0.8 | Border color opacity (0.0 to 1.0) |
| entryAnimation | string | "slideUp" | Entry animation type |
| entryEasing | string | "easeIn" | Entry easing curve |
| entryDuration | number | 0.2 | Entry animation duration (seconds) |
| exitAnimation | string | "fade" | Exit animation type |
| exitEasing | string | "easeOut" | Exit easing curve |
| exitDuration | number | 0.5 | Exit animation duration (seconds) |
Remove all pending notifications. Pinned toasts are preserved unless force = true. Removal is immediate, with no exit animation, but every removed toast fires its onDismiss.
Remove all pending toasts with a specific category, pinned ones included. Removal is immediate, with no exit animation, but every removed toast fires its onDismiss. Logs and returns without clearing anything if category is not a string.
Every callback registered with onClear is invoked afterwards, even when nothing matched the category.
Register a callback invoked when clearCategory() is called. Receives the category string. clear() does not invoke these callbacks. Non-function arguments are logged and ignored.
Returns: number - current notification count in the queue
Render all active notifications. Called automatically each frame by WindowUtils.
Entry and exit animations are configured independently. Each has its own animation type, easing curve, and duration.
| Type | Entry Behavior | Exit Behavior |
|---|---|---|
| "slideUp" | Slides up from below | Slides upward out |
| "slideDown" | Slides down from above | Slides downward out |
| "slideLeft" | Slides in from the right | Slides leftward out |
| "slideRight" | Slides in from the left | Slides rightward out |
| "fade" | Fades in from transparent | Fades out to transparent |
| Curve | Description |
|---|---|
| "linear" | Constant rate |
| "easeIn" | Quadratic acceleration |
| "easeOut" | Quadratic deceleration |
| "easeInOut" | Quadratic ease both ends |
| "exponential" | Fast start, eases to end position: 1 - 2^(-10*t) |
| "bounce" | Bounce-out deceleration (4-segment piecewise) |
Unrecognized easing names fall back to linear.
notify.configure({
entryAnimation = "slideDown",
entryEasing = "exponential",
entryDuration = 0.3,
exitAnimation = "slideUp",
exitEasing = "bounce",
exitDuration = 0.8,
})When a toast disappears, remaining toasts smoothly animate to fill the gap (reflow animation).
Pinned toasts stay visible until the user clicks the dismiss button or dismiss(id) is called programmatically. Both paths fire onDismiss; they differ only in animation:
- Clicking the X unpins the toast and sets its ttl to zero, so the configured exit animation plays and
onDismissfires once the toast is removed. -
dismiss(id)removes the toast from the queue on the spot, firingonDismissimmediately with no exit animation.
local id = notify.show("Download complete", "success", { pinned = true })
-- Later, remove it immediately (no exit animation, onDismiss still fires)
notify.dismiss(id)
-- Or react to the user dismissing via the X button
notify.show("Action required", "warn", {
pinned = true,
onDismiss = function(toastId)
-- Fires after the exit animation completes and the toast is removed
print("Toast " .. toastId .. " fully dismissed")
end,
})The X button is drawn into the window draw list and hit-tested manually against the mouse position, so it only responds while the CET overlay is drawing toasts.
onDismiss fires on every removal path, through a shared internal helper:
| Path |
onDismiss fires |
|---|---|
| Toast reaches its ttl and finishes the exit animation | yes |
| Pinned toast dismissed with the X button | yes (after the exit animation) |
dismiss(id) |
yes (immediately, no exit animation) |
clear() / clear(true)
|
yes (immediately, no exit animation) |
clearCategory(category) |
yes (immediately, no exit animation) |
| Evicted by queue overflow | yes |
So onDismiss is not pinned-specific and not expiry-specific: it fires once for any toast that leaves the queue.
Any toast can have an animated pulsing border. The pulse oscillates border opacity and thickness using a sine wave.
While a toast is pulsing, the borderOpacity config value is bypassed: border alpha comes from the pulse curve instead (0.4 to 1.0, scaled by the toast's fade alpha). strokeMin/strokeMax are added on top of the effective border size rather than replacing it. Pulse defaults when a key is omitted: speed = 4.0, strokeMin = 0, strokeMax = 1.5, easing = "linear".
-- Quick pulse via shortcut
notify.pulse("Attention needed!")
-- Custom pulse parameters
notify.show("Critical alert", "error", {
pulse = {
speed = 6.0, -- oscillation speed (radians/sec)
strokeMin = 0.5, -- minimum added border thickness
strokeMax = 3.0, -- maximum added border thickness
easing = "easeInOut" -- any supported easing curve
}
})Pulse parameters can be changed on a live toast without dismissing it:
local id = notify.show("Monitoring...", "info", {
pinned = true,
pulse = { speed = 4.0, strokeMin = 0, strokeMax = 1.5, easing = "linear" }
})
-- Later, adjust pulse in real-time
local pulse = notify.getPulseConfig(id)
if pulse then
pulse.speed = 8.0
pulse.easing = "bounce"
end
-- Change border color without recreating
notify.setBorderColor(id, { 1.0, 0.0, 0.0, 1.0 })Override the level-based border color for any toast:
notify.show("Custom color", "info", {
borderColor = { 1.0, 0.5, 0.0, 1.0 } -- orange
})Register reusable presets to avoid repeating opts:
-- Register once at init
notify.registerType("download", {
icon = IconGlyphs.Download,
borderColor = { 0.3, 0.8, 1.0, 1.0 },
ttl = 8.0,
pinned = true,
})
-- Use anywhere
notify.show("Downloading update...", "download")-- Let ImGui determine width from content (either form works)
notify.configure({ toastWidth = "auto" })
notify.configure({ toastWidth = 0 })Width also decides how the message is drawn. Auto width renders the message as a single unwrapped line, so a long string produces a very wide toast. An explicit numeric width renders it wrapped. Height is treated as auto when it is "auto", 0, or nil.
| Level | Glyph | Fallback |
|---|---|---|
| info | InformationOutline | i |
| success | CheckCircleOutline | ok |
| warn | AlertOutline | ! |
| error | AlertOctagonOutline | X |
Any other level, including a name registered with registerType, falls back to the info glyph unless the type defaults or the per-toast opts supply an icon.