DEV Community

Cover image for What's new in Vel: hot reload, an SDK, and a real editor
Sai Chandan Kadarla
Sai Chandan Kadarla

Posted on • Originally published at saichandankadarla.com

What's new in Vel: hot reload, an SDK, and a real editor

When I posted Building Vel a month ago, the language and runtime worked end-to-end — but everything around them was painful. You had to clone the repo, wait 30–60 minutes for vcpkg to build Skia, restart the binary on every change, and write .vel files in a plain text editor with no help.

Here's what shipped to fix that. None of it changed what Vel is. All of it changed what using Vel feels like.

A Flutter-style SDK

The install path is now one line:

curl -fsSL https://raw.githubusercontent.com/chan27-2/vel/main/scripts/install-vel.sh | bash
echo 'eval "$(vel shell-init bash)"' >> ~/.zshrc
vel doctor
Enter fullscreen mode Exit fullscreen mode

The script downloads a prebuilt SDK archive for your OS and arch — vel-sdk-<ver>-darwin-arm64.tar.gz, linux-x64.tar.gz, windows-x64.zip — drops it at ~/.vel/sdk, and adds vel + velc to your PATH. No Skia compile. No vcpkg bootstrap. First run to working compiler: seconds, not hours.

Windows users get the equivalent PowerShell line:

irm https://raw.githubusercontent.com/chan27-2/vel/main/scripts/install-vel.ps1 | iex
Enter fullscreen mode Exit fullscreen mode

The model is Flutter's, deliberately:

Flutter Vel
~/flutter ~/.vel/sdk
stable / beta channels `vel channel stable\
{% raw %}flutter upgrade vel upgrade
flutter doctor vel doctor

The piece Vel doesn't ship: your final binary still links Skia/GLFW via vcpkg, same as Flutter's platform embedders. The SDK gets you the compiler and the framework — your CMake handles the application link.

Hot reload via dynamic plugins

The biggest architectural change since the introduction post. libvel is now a shared library, and the app shell can dlopen a plugin — your widget tree compiled into its own dylib.

The plugin ABI is one symbol:

extern "C" vel::Widget* vel_create_root();
Enter fullscreen mode Exit fullscreen mode

The host watches the dylib's mtime every 50ms. When you save a .vel file, a watch loop runs cmake --build build --target showcase_plugin (typically 1–2s for a small edit). The dylib mtime ticks. The host destroys the current root, dlcloses the old handle, dlopens the new one, and calls vel_create_root() again. The window updates in place.

One script wires it all together:

./scripts/dev-hot.sh
Enter fullscreen mode Exit fullscreen mode

That's the dev loop now: edit, save, see the UI change in under two seconds without restarting.

What it costs. Signal state is reset on reload. If you had a form half-filled, those values are gone after the dylib swap — the new root is a fresh tree. This is a deliberate tradeoff: serializing state across an ABI boundary is the kind of feature that ends up dictating every widget's API. For a UI dev loop, "edit structure → see new structure" is what matters; the state comes back when you fill the form once.

The other cost: RTLD_LOCAL matters. Plugins are loaded with dlopen(path, RTLD_NOW | RTLD_LOCAL) so symbol tables don't leak across reloads. Without it, the second load picks up stale vtables from the first plugin and crashes on the first virtual call. The teardown order also matters — destroy the old root before dlclose, because the destructors run vtables that live in the plugin's text segment.

A VS Code extension with real diagnostics

Vel has a VS Code extension now — syntax highlighting, completion, hover, live diagnostics.

The interesting part is where the data comes from. Two things were already true about velc:

  1. velc --diagnostics-json file.vel emits structured parse + typecheck errors.
  2. velc --docs-json emits the complete widget registry — every widget, every prop, every event, every example — as JSON.

The extension is mostly a thin TypeScript shim over those two outputs. The language server runs velc --diagnostics-json on save and surfaces the errors. Completion and hover read from widgets.json — the same file the docs site is built from. There is one source of truth for "what widgets and props exist," and the compiler owns it.

This is the part I'm proudest of. The compiler doing double duty as the registry source means the editor experience never drifts from the language. Add a widget; the editor knows about it on the next sync. Rename a prop; the diagnostics flag every old call site.

A live theme configurator

The framework now ships a ThemeConfig global — accent, radius scale, dark/light — that any widget can flip:

Btn "Rose"   variant=outline -> click => vel::useAccentRose()
Btn "Lg"     variant=outline -> click => vel::useRadiusLg()
Btn "Light"  variant=outline -> click => vel::useLightTheme()
Enter fullscreen mode Exit fullscreen mode

There are 7 accents (Indigo, Blue, Violet, Rose, Emerald, Amber, Slate), 6 radius scales (None → Full), and dark/light. A single setter call rebuilds the Theme from the config and re-applies it across the tree. The whole UI re-tints in one frame.

The detail that took thought: this used to crash. The old path called root->onThemeChanged() synchronously from inside the click dispatch, which rebuilt the entire widget tree — freeing the button that was still executing its own onClick handler. Use-after-free, every time.

The fix is deferred rebuild: set a pendingThemeChange_ flag and apply the theme at the start of the next frame, after the current event dispatch has unwound. It's now the pattern for any widget action that mutates global state — locale switching will use it next.

EditableText: one widget, two surfaces

Input and Textarea shared ~95% of their code — UTF-8 cursor math, blink, key handling, focus sync, char input. They were drifting. Both have been refactored onto a shared EditableText base:

class EditableText : public Interactable {
public:
    std::string text;
    std::string placeholder;
    std::function<void(const std::string&)> onChange;
    std::function<void(const std::string&)> onSubmit;

    bool onMouseDown(Point, MouseButton) override;     // final
    bool onKey(int, int, bool) override;               // final
    bool onChar(unsigned int) override;                // final
    void tick(double now) override;                    // final

protected:
    virtual int  byteIndexAt(Point pos) const;         // override per layout
    virtual void onEnterPressed();                     // Input submits, Textarea inserts \n
};
Enter fullscreen mode Exit fullscreen mode

Subclasses now only own their rendering and the click-to-cursor mapping. One place to fix backspace-at-position-zero. One place to keep the cursor on a UTF-8 boundary. Adding a PasswordInput or a syntax-highlighted code editor is much smaller now — the base class owns the hard parts.

CI on Linux, macOS, and Windows

Less glamorous, but it matters: GitHub Actions builds Vel on Ubuntu, macOS, and Windows on every push and PR. Each platform builds velc, builds libvel, runs CTest, and runs the showcase smoke test where a display exists.

Two things were painful to get right:

  • vcpkg caching. Earlier iterations cached only installed/, which left the cache unable to bootstrap on restore — the .git directory and scripts/ were missing. Now the entire vcpkg-root is cached, keyed on the manifest hash. CI cold start dropped from a full Skia build to a tar extract.
  • Headless runners. macOS CI has no display; Windows Server runners ship without a usable OpenGL driver, so glfwCreateWindow aborts the showcase. The vel run script now detects MINGW*/MSYS*/CYGWIN* and macOS CI and skips the GUI launch — compile + CTest still cover the toolchain.

Multi-platform CI is the kind of thing you don't notice until it breaks. It took three commits to get right, and I hope to never touch it again.

The showcase, reorganized

A small one but worth flagging: examples/showcase.vel used to be one long scrolling page exercising every primitive. It's now a multi-page demo with a sidebar:

  • Form Controls
  • Buttons & Cards
  • Data
  • Overlays
  • Theme

Build, then ./scripts/vel run ./build/showcase — that's the first thing you see. It's also the integration test that exercises every widget, every prop, and every event.

What's next

  • Per-widget damage rectangles. Frame-level damage tracking is great for idle apps; per-widget would let animations not repaint the whole screen.
  • vel.json package manifest, so third-party registries become installable instead of vendored.
  • A vel new template generator. The boilerplate to start a Vel project is small but not zero.
  • More registry primitives — Video, SVG, a devtools overlay, a syntax-highlighted code editor (which the EditableText refactor now makes practical).

If you tried Vel a month ago and bounced off the install, give it another go. curl -fsSL .../install-vel.sh | bash, run vel doctor, open the showcase. The first ten minutes look very different now.

Top comments (0)