📜ActivityBlogGarden
Back to Projects
P5.NVIM

Better editor support for P5.js in Neovim

Source|
luadxprocessingp5jsneovim-plugin
Feb 28, 2026

Introduction

p5.nvim is an attempt to bring a better DX when working with p5.js in Neovim .

🌌important
What's a sketchspace ? Because naming things is hard in programming (is it easier in any other field ?) , the term sketchspace is a combination of sketch + workspace. I didn't want the "work" part because that just makes it sound less fun. So yes, its a completely 'imaginary' term just like "algorist" (algorithm + artist).

Modules overview

The plugin consists of Lua modules that integrate with Neovim and a standalone Python server that handles live reload and console streaming.

ModulePurpose
init.luaCommand registration, setup, and orchestration
core.luaShared utilities, notifications, path handling
project.luaSketchspace creation and management
server.luaLive server lifecycle management
console.luaBrowser console output display
libraries.luap5.js contributor library management
gist.luaGitHub Gist integration for sharing
health.luaNeovim health checks

Automated assets update

The plugin bundles p5.js core modules, type definitions, and manpages. This allows users to have all they need to get started using p5.js in one place. These assets are automatically generated via GitHub Actions in a separate repository (automata):

Process:

  1. Trigger: p5.js GitHub release OR manual workflow dispatch
  2. Extract: documentation from @types/p5 TypeScript definitions
  3. Generate: manpages & tags (doc/p5-*.txt)
  4. Fetch: latest p5.js release
  5. Push: updates to p5.nvim repository

Design Decisions

Command Pattern (single command with sub-commands)

Instead of multiple commands like :P5Create, :P5Install, :P5Server, I used a more 'CLI-like' approach where a single command (P5) has sub-commands:

This pattern provides:

  • Consistent command interface
  • Built-in argument handling
  • Tab completion out of the box
sh
:P5 create my-sketch # Create new sketchspace :P5 install p5.play # Install a library :P5 server # Start live server :P5 console # Toggle console :P5 gist "my sketch" # Share as Gist

Live server (python3)

Initially, the plugin supported multiple server backends:

  • Python HTTP server
  • live-server (npm)
  • Bun
  • Deno

However, all these options proved to be overkill and a not needed complexity and decided tp just use Python and call it a day

Console streaming with SSE

The original implementation used WebSocket for streaming browser console logs to the editor. This was refactored to use Server-Sent Events (SSE) because:

  • SSE works with the existing HTTP server (no separate WebSocket server needed)
  • Simpler implementation
  • Better compatibility with Neovim's terminal API

p5.json (a ripoff of the package.json)

Sketchspace info is stored in a simple p5.json file:

json
{ "version": "1.9.0", "libs": { "p5.play": "3.3.1" } }

It allows the user to:

  • Setup a sketchspace using the :P5 setup command
  • Save the libraries, Gist URL (optional) used in the sketchspace.

It is also used by the assets/libs.js module to load the contributor libraries into the index.html.

Gotchas

Below is a summary of some of the issues I encountered and how they were resolved.

E488 : command prefix conflict

Running any :P5* command threw E488: Arguments not accepted.The plugin registered both P5 (for the picker) and P5Create, P5Install, etc. as separate commands. Neovim's command prefix matching caused conflicts.

🌬note
I had previously defined the user commands to have the following format P5Create, P5Install, etc but it felt unnatural given the amount of sub-commands I wanted.

The solution looked something like this:

lua
vim.api.nvim_create_user_command('P5', function(opts) local subcommand = opts.fargs[1] if subcommand == 'create' then -- handle create elseif subcommand == 'install' then -- handle install end end, { nargs = '+' })

Gist Creation Fails on Different Platforms

Creating a Gist failed on Windows or when the temp directory had unusual paths.

The root cause(s) were:

  • Hardcoded path separators (/), which are not compatible with Windows
  • Using os.tmpname() which creates paths the gh CLI couldn't read

In the end, vim.fn.tempname() was the solution for cross-platform temp paths.

CDN Links Breaking

Installing some libraries failed with "invalid response". CDN links (jsdelivr, unpkg) occasionally changed or deprecated libraries. The plugin was blindly downloading without validation.

I added regex validation to reject error pages masquerading as valid JavaScript files (but maybe checking the status code would have had been wiser).

Path Traversal Vulnerability

Root Cause: The Python server used os.path.join() directly without validating that resolved paths stayed within the sketchspace directory.

Fix: Added path resolution validation:

py
resolved = Path(self.directory, path.lstrip('/')).resolve() base_resolved = Path(self.directory).resolve() if not resolved.is_relative_to(base_resolved): # Return 403 Forbidden

Circular Dependency

At some point, the plugin failed to load with "module not found" errors. The cause was circular dependency, console.lua required server.lua, which in turn required console.lua. Lua's require detected the cycle and failed.

Fix: Restructured modules so that core.lua acts as an intermediary, and moved shared code to prevent circular imports.

Server Features

The Python server (server.py) handles live reload and console streaming. Key features include:

on-the-fly "index.html" generation

If index.html doesn't exist in the sketchspace, the server generates one automatically based on p5.json:

Console Integration

The server injects a script that intercepts console.log, console.error, console.warn, and console.info, then sends them to Neovim via SSE:

js
console.log = function(...args) { originalConsole.log.apply(console, args); fetch('/api/console/log', { method: 'POST', body: JSON.stringify({ level: 'log', message: ... }) }); };

Live Reload

File changes trigger a WebSocket message to the browser, which reloads the page automatically:

Available Commands

sh
:P5 # Interactive picker :P5 create [name] # Create sketchspace :P5 setup # Setup assets/download gist :P5 install [libs] # Install libraries :P5 uninstall [libs] # Remove libraries :P5 server [port] # Start/stop server :P5 console # Toggle browser console :P5 sync [gist|libs] # Sync gist or libraries :P5 gist [desc] # Create Gist :P5 docs # Open help :P5 update # Update libraries

The plugin is usable today and covers the core p5.js development workflow. Contributions and feedback are welcome 💙


References

  • p5.nvim
  • p5.js
  • Neovim
<<EOF
last seen on: Mar 9, 2026 at 11:09 AM| build: dd83f93
© 2026,GitHub