Skip to content

About

Fireworks in your buffer every time you save. A joke UI plugin for Neovim.

Topics

Resources

Stars

15 stars

Watchers

0 watching

Forks

Latest commit

 

History

21 Commits

Folders and files

Repository files navigation

fireworks.nvim

CI

Fireworks in your buffer every time you save. A joke UI plugin drawn on an extmark canvas: overlay virtual text on real lines and a virt_lines block that claims the blank rows below EOF, so a three line file still fills the window.

A save launching three shells over a Lua buffer, with the light falling across both split windows

A rocket climbs from the bottom row, slows as it nears its apex, hangs dark for a beat, and breaks somewhere in the top 60 percent. Its trail cools from the lift charge's gold into the colour it is about to break in. The light of the burst falls on every visible window, not only the one you saved. Text near the burst is tinted toward the firework's own colour, brighter on the side facing it, and fades out over about a second. About one in twenty rockets fails: a dud that sputters and falls back, a premature pop right above the tube, or a fizzle. Failures scorch instead of lighting, leaving an ash coloured burn and a little smoke for two seconds.

Nothing it draws touches your file. Every glyph and every tint is an extmark in the plugin's own namespaces, cleared when the show ends.

Requirements

Neovim 0.10 or newer. Tested against 0.10, 0.11, and stable on every push.

On 0.11 and newer, lines hidden with conceal_lines are accounted for, so plugins that collapse blocks that way stay aligned with the effect.

Installation

lazy.nvim

return {
  "btj93/fireworks.nvim",
  event = "VeryLazy",
  opts = {},
}

Local checkout:

return {
  dir = vim.fn.expand("~/nvim-dev/fireworks.nvim"),
  event = "VeryLazy",
  opts = {},
}

Commands

Command Effect
:Fireworks Launch a show over the current buffer now.
:Fireworks <type> Force a firework type: peony, chrysanthemum, willow, ring, crossette, crackle, pistil, palm, kamuro, salute.
:Fireworks fail Force a random failure. :Fireworks dud, :Fireworks premature, :Fireworks fizzle pick one.
:Fireworks toggle Arm or disarm the save trigger.
:Fireworks stop Clear a running show.

Configuration

Every key below is optional. The values shown are the defaults.

require("fireworks").setup({
  events = { "BufWritePost" },
  launch_chance = 1.0,
  rockets = { min = 1, max = 3 },
  stagger = { first = { 0.3, 0.8 }, between = { 0.4, 1.4 } },
  tail_tint = 1.0,
  types = {
    peony = 4,
    chrysanthemum = 3,
    willow = 2,
    ring = 2,
    crossette = 2,
    crackle = 2,
    pistil = 2,
    palm = 2,
    kamuro = 1,
    salute = 1,
  },
  palettes = {
    single = 4,
    two_tone = 3,
    rainbow = 2,
    gold = 2,
  },
  sizes = {
    small = 3,
    medium = 4,
    large = 2,
  },
  colors = { "#ff5f5f", "#ff9f43", "#ffe66d", "#7bed9f", "#70a1ff", "#a29bfe", "#ff6bcb", "#eaeaea" },
  gold = { "#ffd700", "#ffb347", "#fff1a8" },
  fail_chance = 0.05,
  fps = 30,
  max_particles = 400,
  light = {
    radius = 25,
    brightness = 0.7,
    attack_ms = 80,
    duration_ms = 900,
    bg = 0.25,
    fg = 1.0,
    glow = 0.35,
    glow_radius = 6,
  },
  burn = {
    duration_ms = 2000,
    color = "#6b5d4f",
    smoke = true,
  },
  ignore_filetypes = {
    "TelescopePrompt", "TelescopeResults", "NvimTree", "neo-tree", "lazy", "mason",
    "help", "dashboard", "alpha", "starter", "notify", "noice", "trouble", "qf",
    "fugitive", "gitcommit",
  },
})
Key Meaning
events Autocommand events that launch a show. Only normal listed file buffers qualify.
launch_chance Probability in [0, 1] that a qualifying save launches at all.
rockets Inclusive range of rockets per save. The first leaves after a stagger.first delay and later ones follow at stagger.between intervals.
stagger Seconds, as {min, max} ranges, before the first rocket leaves and between rockets.
tail_tint How far the climbing rocket's trail is tinted toward its own shell colour. At 1 the head stays the lift charge's gold and the trail runs the shell colour, reaching it exactly at the tail end. 0 keeps a plain gold comet tail.
types Weights for the burst types. peony plain radial, chrysanthemum trailing, willow drooping, ring one radius, crossette splitting, crackle twinkling, pistil inner break in a second colour, palm thick rising arms, kamuro gold willow with long trails, salute a bare flash with a large light. A weight of 0 removes a type.
palettes Weights for colour schemes. single is one random colour, two_tone two, rainbow the whole colors list, gold the gold list.
sizes Weights for burst radius: small, medium, large.
colors Colours drawn from for single, two_tone, and rainbow.
gold Colours used by the gold palette.
fail_chance Probability that a rocket fails. The kind of failure is uniform over dud, premature, fizzle.
fps Animation frames per second.
max_particles Particle cap per show. A save while a running show is at the cap is dropped.
light.radius Reach of the burst flash in rows. Columns count for half a row. Cells beyond it get no extmark.
light.brightness Peak blend toward the firework colour at the burst. Intensity is brightness * (1 - d / radius)^2.
light.attack_ms Ramp of the burst flash from dark to peak as the shell opens.
light.duration_ms How long the burst flash takes to fade after its peak. It holds near full for the first part and falls away at the end. The stars keep glowing for as long as they live.
light.bg Fraction of the intensity also applied to the background, so blank cells and the area past end of line glow too. 0 tints foreground only.
light.fg Strength of the foreground blend. Text keeps its own syntax colour and is pushed toward the firework colour by this fraction of the intensity. 0 leaves foregrounds alone and lights the background only.
light.glow Peak light each star sheds on the cells around it, fading with the star's age. Overlapping stars add up, so a fresh shell glows hardest at its heart. 0 leaves only the burst flash.
light.glow_radius Reach of one star's glow in rows, twice that in columns.
burn.duration_ms How long a failure's scorch lasts.
burn.color Ash colour the scorch blends toward. Rows adjacent to the impact take a darker soot shade.
burn.smoke Emit drifting ~ smoke from the impact point.
ignore_filetypes Filetypes that never launch on save. Buffers with a non empty buftype are always skipped.

Recipes

Fireworks when your tests pass

Set events = {} to drop the save trigger, then launch from whatever you would rather celebrate. With neotest, that is a consumer: a passing run breaks a shell, a failing one sends up a dud that sputters, falls back, and scorches the buffer.

Neotest fires no autocommands for results, and consumers can only be registered inside neotest.setup, so this goes in your neotest config rather than here:

require("neotest").setup({
  adapters = { --[[ ... ]] },
  consumers = {
    fireworks = function(client)
      client.listeners.results = function(adapter_id, results, partial)
        if partial then
          return
        end
        local tree = client:get_position(nil, { adapter = adapter_id })
        if not tree then
          return
        end
        local passed, failed = 0, 0
        for pos_id, result in pairs(results) do
          local node = tree:get_key(pos_id)
          if node and node:data().type == "test" then
            if result.status == "passed" then
              passed = passed + 1
            elseif result.status == "failed" then
              failed = failed + 1
            end
          end
        end
        vim.schedule(function()
          local fireworks = require("fireworks")
          if not fireworks.enabled or vim.bo.buftype ~= "" then
            return
          end
          if failed > 0 then
            fireworks.launch({ fail = "dud" })
          elseif passed > 0 then
            fireworks.launch({ count = math.min(3, passed) })
          end
        end)
      end
    end,
  },
})

Four lines in there carry the weight:

  • if partial then return end skips streamed results. Adapters that report each test as it finishes emit a results event per test, and without this you get a rocket per test instead of one show per run.
  • node:data().type == "test" filters the results table, which is keyed by position and also carries file and directory aggregates. Counting raw keys double counts.
  • vim.schedule gets you out of neotest's async context, where most of the API is unavailable.
  • fireworks.enabled and the buftype check are what on_write applies and launch does not, so :Fireworks toggle still disarms this and a run started from the neotest summary window does not paint a show over the summary.

Pair it with neotest's watch consumer and the plugin goes back to being save triggered: you save, the tests run, and the shell only breaks if they are green.

One show is launched per adapter, since each adapter reports its own run.

How the light works

The light is a byproduct of the particles. Every frame each star deposits a small glow around its screen cell, scaled by its age, and the burst adds a short bright flash at the break point. Contributions add up into one light field in editor screen space, and where two colours overlap a cell takes their intensity weighted mean, so a two tone shell blends through its seam. The glow expands with the shell, drifts and droops with the stars, and dies with them. Every window then reads the field cell by cell: neighbouring cells with the same intensity bucket and colour are merged into one extmark, so a row is brighter on the side facing the light and the glow past end of line fades cell by cell. Buckets map to cached highlight groups whose foreground is the colour already on that cell, taken from the treesitter highlighter and from highlight extmarks such as LSP semantic tokens, blended toward the palette colour, with a lighter touch of the same colour on the background. Syntax colours stay readable while the light is on; a keyword just goes warmer. Two tone fireworks light each half of the screen with the colour that flew that way. Extmarks are set at priority 200 so the tint wins over treesitter while it lasts.

Rows are mapped through screenpos, so lines hidden by conceal_lines (as no-go.nvim does), closed folds, wrapped continuation rows, and virt_lines from other plugins are accounted for: the rows below them stay aligned, and those rows themselves get no particles or light. Inline and end of line virtual text from other plugins is lit too: the plugin draws an overlay copy of the same text at the same cells with the original highlight stacked under the tint, and the glow past end of line starts after it. Rows with more than one end of line virtual text are left alone, since their drawing order is not knowable.

Windows shorter than 10 rows or narrower than 20 columns are skipped. Rendering never raises inside the autocommand; every extmark write is wrapped in pcall.

Documentation

:help fireworks covers the same ground as this README, plus the Lua API and the highlight groups.

Development

make test          # plenary specs under tests/
make lint          # luacheck
make format        # stylua
make format-check

CI runs all three against Neovim 0.10.4, 0.11.4, stable, and nightly. Nightly is informational and cannot fail the build; the pinned versions and stable must stay green.

Credits

The idea and the drawing technique come from starfall.nvim. Claiming the blank rows below the last line with a virt_lines block, so a short file still has somewhere for things to happen, is theirs.

The two run side by side happily on a file long enough to fill the window. On a short file they both want the space below the last line, and only one of them gets it: Neovim renders whichever block comes first and pushes the other off screen. Nothing errors, but expect part of a show to be hidden while starfall is running over a short buffer.

License

Apache 2.0. See LICENSE.

About

Fireworks in your buffer every time you save. A joke UI plugin for Neovim.

Topics

Resources

Stars

15 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages