Guidance for AI coding agents (and humans) working in this repository. Keep this file in sync with the code when behaviour changes.
neo-processing is a native desktop application that lets a user write p5.js sketches in an embedded code editor and run them in a live preview, aimed at real-world / full-screen deployment. It is a modern, JavaScript-based take on the Java Processing IDE.
The whole product is a single C++ executable that:
- Starts a local HTTP server bound to
127.0.0.1on an OS-assigned port. - Serves a small web frontend (Ace editor + p5.js runner) that is embedded
into the binary at build time —
public/is not read from disk at runtime. - Opens a native webview window pointed at that local server (WebView2 on Windows, WebKitGTK on Linux).
There is no separate backend process and no external network dependency at runtime.
┌─────────────────────────────────────────────┐
│ neo-processing (single native executable) │
│ │
│ webview window ──HTTP──► httplib server │
│ (WebView2 / 127.0.0.1:<port>│
│ WebKitGTK) │ │
│ ├─ GET /health │
│ ├─ POST /api/save-script
│ ├─ POST /api/save-media
│ └─ static assets (embedded from public/)
│ │
│ boost::asio io_context (idle infra thread) │
└─────────────────────────────────────────────┘
src/main.cpp— the entire C++ application. Entry point, HTTP routes, window creation, per-OS icon handling, and graceful shutdown.public/— the frontend, embedded into the binary viacpp-embedlib:index.html— layout: menu bar (Run, Stop, File, Examples), Ace editor + p5 version label (left), sketch preview (right), a draggable horizontal splitter, a resizable status/terminal row (bottom), and a collapsible side panel with a Capture section (Record / Capture PNG / Full Window / Fullscreen), a Sketch section (background-colour picker for the area behind the canvas), and a Libraries section (p5.js build picker). Stop tears down the sketch iframe; the.splitterresizes the editor/preview split and.h-splitterthe terminal height (both drive CSS custom properties on the grid).libraries.json— manifest of injectable p5.js builds ({ id, name, version, url, isLocal }); see the Libraries section below.script.js— all UI logic: editor setup, menus, file open/save, panel splitter, the sketch runner, and the capture/fullscreen controls.style.css— styling.libs/— vendored third-party JS (Ace editor, p5.js). These are committed. The bundled p5 version is declared once asP5_VERSIONinscript.js, which drives both the version label and the<script>URL the sketch iframe loads; keep it in sync with thepublic/libs/p5-<version>.min.jsfilename.
outputs/— where saved sketches are written at runtime (POST /api/save-script). Treated as scratch output; safe to clear.icons/—.icofiles copied next to the executable on Windows and loaded at runtime for the window/taskbar icon.assets/— screenshots for the README only.
| Route | Method | Body | Success | Errors |
|---|---|---|---|---|
/health |
GET | — | 200 ok |
— |
/api/save-script |
POST | sketch as text | 200 <filename> |
403 bad origin, 400 empty, 413 too large, 500 write failure |
/api/save-media?ext= |
POST | binary (PNG/WebM) | 200 <filename> |
403 bad origin, 400 bad/empty ext or body, 413 too large, 500 write failure |
| everything else | GET | — | embedded static asset | 404 |
Both write endpoints reject requests whose Origin header isn't the app's own
page origin (http://127.0.0.1:<port>). The editor UI's POSTs carry that origin;
a sketch's opaque-origin iframe sends Origin: null and is denied, so sketch
code cannot reach these endpoints even via a fire-and-forget request.
Saved files are named server-side from the clock — scripts as
YYYY-MM-DD-HH-MM-SS_p5.js, media as YYYY-MM-DD-HH-MM-SS-mmm_<capture|recording>.<ext>.
The client never supplies a path, so there is no path-traversal surface. For
/api/save-media the ext query param is sanitised to lower-case alphanumerics
and checked against an allow-list (png, jpg, jpeg, webm, mp4); anything
else is rejected.
runSketch() in public/script.js injects the user's code into an <iframe>
via srcdoc with sandbox="allow-scripts" (intentionally without
allow-same-origin). This means user sketches:
- run in an opaque origin — they cannot read cookies/storage or call the
local HTTP API (
/api/save-script, etc.); - can still load
/libs/p5-1.11.3.min.jsas a subresource and report errors to the parent viapostMessage.
Do not add allow-same-origin back unless you have a deliberate reason and a
replacement isolation strategy — it would let arbitrary user code escape the
sandbox and reach the local server.
Because the iframe is opaque-origin, the parent cannot touch the sketch's
canvas directly. Recording and PNG capture therefore happen inside the
iframe: a small captureController (also in script.js) is injected ahead of
the user code and listens for postMessage commands from the parent.
Output media is rendered at the sketch's logical size (canvas.clientWidth/ Height, e.g. 400×400), not the device-pixel-inflated backing store, so files
match the dimensions the sketch declares.
- Record (toggle button in the right side panel) → an offscreen canvas at the
logical size is fed each frame from the live canvas (
drawImage), andoffscreen.captureStream(fps)+MediaRecorderproduce a WebM blob (GPU-backed; no per-frame pixel copying to JS). On stop, the bytes are transferred to the parent and POSTed to/api/save-media?ext=webm. - Capture PNG → the live canvas is drawn once onto a logical-size offscreen
canvas and exported with
toBlob('image/png'), transferred to the parent and POSTed to/api/save-media?ext=png.
Both land in outputs/. The controller feature-detects captureStream /
MediaRecorder and reports a record-error if the webview lacks them (WebKitGTK
support varies by version). Known limitation: capture of WEBGL sketches may
be blank because p5 does not set preserveDrawingBuffer (this affects both the
PNG snapshot and the per-frame drawImage used for recording).
There are two fullscreen modes, both calling requestFullscreen() on the preview
pane (.right-panel), not the sandboxed iframe — so neither needs an iframe
fullscreen permission. The sketch iframe's body centres its canvas on a white
background, so the canvas shows at its exact pixel size in the middle. Esc exits.
- Full Window fills the WebView viewport (the app's content area). The native window is untouched, so it covers only the app window.
- Fullscreen does the same and drives the native OS window into borderless
full-screen via
window.neoSetDesktopFullscreen(bool)— a function bound in C++ (main.cpp) that strips the window frame and stretches it over the monitor (Win32) or callsgtk_window_fullscreen(Linux), so the sketch covers the whole desktop. On exit, thefullscreenchangehandler restores the native window. This is the only JS→C++ bridge in the app.
The side panel's Libraries section lets the user choose which p5.js build the
sketch iframe loads. The options come from public/libraries.json — a manifest
of { id, name, version, url, isLocal } entries that doubles as the allow-list
of injectable builds (the JS-first approach from issue #3). script.js tracks the
chosen build in activeLibrary; runSketch() uses activeLibrary.url for the
iframe's p5 <script>, and the label under the editor reflects it. Apply swaps
the active build and reloads any running sketch.
The bundled build (/libs/p5-1.11.3.min.js, isLocal: true) is the default and
keeps the app fully offline. Selecting an online build loads p5 from a CDN — this
trades offline-first for version flexibility and is loaded inside the same
sandboxed, opaque-origin iframe (so it cannot reach the parent or the local API).
Keep the bundled entry's version/url in sync with the actual file in
public/libs/ and with P5_VERSION in script.js.
Prerequisites: CMake ≥ 3.20, a C++20 compiler, Git (CMake FetchContent
downloads dependencies), and internet access on the first configure.
- Windows: Visual Studio 2022 Build Tools (MSVC) + Microsoft Edge WebView2 Runtime.
- Linux: GCC/Clang with C++20, plus GTK 3 and WebKit2GTK development files.
# Configure + build (Debug)
cmake -B build
cmake --build build --target neo-processing -j --config Debug
# Run
# Windows: .\build\Debug\neo-processing.exe
# Linux: ./build/neo-processing
# One-shot helper scripts (configure + build Debug + run):
# Windows: .\build_and_run.bat (also initialises the MSVC environment)
# Linux: ./build_and_run.shbuild_and_distribute.bat (Windows) builds Release and copies the resulting
build\Release folder (executable, icons, runtime DLLs) to
%USERPROFILE%\Desktop\neo-processing.
| Dependency | Purpose |
|---|---|
yhirose/cpp-httplib |
local HTTP server |
webview/webview |
native window + system webview |
yhirose/cpp-embedlib |
embeds public/ into the binary |
Boost (asio, system) |
async infrastructure thread (see below) |
None of these are vendored in-repo (except the frontend libs/); they are
downloaded into the build tree.
public/is embedded at build time. Editing a file underpublic/requires a rebuild to take effect — there is no live reload. Adding a new file topublic/makes it served automatically (it is part of the embedded FS).- Boost.Asio is intentional but currently idle.
main()spins up anio_contexton its own thread as the foundation for future async work. It does no work yet. Don't reintroduce console-spam heartbeats; post real tasks toiocwhen async behaviour is actually needed. - Loopback only. The server binds
127.0.0.1. Never change it to0.0.0.0or a public interface — it is not designed to be reachable off-host. - Server-side limits. Request bodies are capped at
kMaxScriptBytes(5 MiB) and read/write timeouts are set. Error responses to the client are generic; detailed errors go tostd::cerr(avoid leaking internals to the client). - Windows MSVC vs MinGW.
CMakeLists.txtandbuild_and_run.batactively guard against MinGW/MSYS2 headers leaking into MSVC's include path. If you seecorecrt.h/winnt.herrors, build from a clean VS Developer Command Prompt (or just usebuild_and_run.bat). - Per-OS code in
main.cppis guarded with#ifdef _WIN32/#ifdef __linux__. macOS is partially wired in CMake (frameworks) but not exercised. - No console window. On Windows the target is built as a GUI app
(
WIN32_EXECUTABLE, with/ENTRY:mainCRTStartupsoint main()stays the entry point), so launching the.exeopens no terminal.std::cerr/std::coutlogging therefore isn't visible from a console — attach a debugger if you need it during development. - Platform. Primary development is on Windows (PowerShell). The Bash tool is available for POSIX scripts.
- After touching
src/main.cppor anything underpublic/, rebuild theneo-processingtarget and confirm it compiles before reporting done. - Keep the HTTP contract table above and the security notes accurate if you change routes, limits, or the sketch sandbox.
- There is no automated test suite. Verify behaviour by building and running the
app (the right-hand preview should render a sketch; saving writes to
outputs/).