Skip to content

notifications

CyanideX edited this page Aug 5, 2026 · 1 revision

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.

Quick Start

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")

API Reference

info(message, opts?)

Show an info notification (blue border). Returns the number toast ID from show().

success(message, opts?)

Show a success notification (green border). Returns the number toast ID from show().

warn(message, opts?)

Show a warning notification (yellow border). Returns the number toast ID from show().

error(message, opts?)

Show an error notification (red border). Returns the number toast ID from show().

pulse(message, opts?)

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(message, level?, opts?)

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.icon is used only when it is a string or false. Any other type is treated as nil (level default glyph).
  • opts.borderColor is used only when it is a table whose first element is a number, otherwise the level color is used.
  • opts.pulse is used only when it is a table.
  • opts.category is used only when it is a string.
  • opts.pinned must be exactly true. A pinned toast ignores opts.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())

dismiss(id)

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

getPulseConfig(id)

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"
end

setPulseConfig(id, pulse)

Set 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)

setBorderColor(id, color)

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 })

registerType(name, defaults)

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.

configure(opts)

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)

clear(force?)

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.

clearCategory(category)

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.

onClear(callback)

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.

count()

Returns: number - current notification count in the queue

draw()

Render all active notifications. Called automatically each frame by WindowUtils.

Entry and Exit Animations

Entry and exit animations are configured independently. Each has its own animation type, easing curve, and duration.

Animation Types

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

Easing Curves

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.

Configuration Example

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

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 onDismiss fires once the toast is removed.
  • dismiss(id) removes the toast from the queue on the spot, firing onDismiss immediately 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.

Callback Timing

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.

Pulse Effect

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
    }
})

Real-Time Pulse Mutation

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 })

Custom Border Color

Override the level-based border color for any toast:

notify.show("Custom color", "info", {
    borderColor = { 1.0, 0.5, 0.0, 1.0 }  -- orange
})

Custom Types

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")

Auto-size Width

-- 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.

Icons

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.

Clone this wiki locally