SixFoisNeuf

Totally irregular blog on computers and security


Handling Gram extensions building with Nix and Home-Manager

Posted on Oct 1, 2026

With Neovim having some unsavoury drama the past month (in addition to their embrace of LLM-assisted development, which I personally dislike), I’ve been shopping around for a replacement.

I initially switched to Neovim from VS Code because it was getting too heavy to comfortably use on my X230: the fans were ramping up as soon as I started it! In addition, the heavy use of padding meant that a lot of screen real estate was lost (and it’s a premium, because I’m still rocking the original 1366x768, 12.5" screen).

So my requirements were:

  • Light-ish
  • Supports LSPs
  • Has a Vim mode (I got used to those keybindings very quickly)

Lots of text editors fit the bill, however I did read about Gram (gram-editor.com) on Mastodon some months back, and I thought that it would be a great time to give it a go: I was really hyped for Zed until I actually tried it and the nags and upsells were simply too much.

Gram is simply a hard fork of Zed, focused on cleaning up the code and getting rid of all its third-party integrations (like GitHub, LLMs, live collaboration, etc.).

Simon from the future here! I have switched to Emacs with Doom Emacs, because Gram did not work on my X230. I still think this might be of interest, so I’m posting it anyway.

This is not a post about moving to Gram, so let’s get right into what I wanted to talk about.

Gram extensions

Gram supports extensions (as of now, they’re exactly the same as Zed’s). Extensions are WASM blobs which can do pretty much anything, but the cool thing is they’re fully self-contained (you only need to build them once, and then ship the output). As a matter of fact, Zed distributes its extensions in WASM form directly through their Store.

Understandably, Gram does not have an extension store, so we have to build the extensions ourself if we want to use them. Building Zed extensions is a pain and a half: you need clang, Rust with the wasm target, wasm-component-ld, etc.

Thankfully, some cool guy (Niklas Korz) made nix-gram-extensions, a Nix module which exposes functions to easily build Gram extensions!

For example, this derivation builds the Catppuccin Zed theme as an extension:

{
  lib,
  buildGramExtension,
  fetchFromGitHub,
}:

buildGramExtension (finalAttrs: {
  id = "catppuccin";
  version = "0.2.25";

  src = fetchFromGitHub {
    owner = "catppuccin";
    repo = "zed";
    tag = "v${finalAttrs.version}";
    hash = "sha256-+SO9W98LQbG6Oz5YStzdgYpVgT77pGZHaTsH3HhZr60=";
  };

  meta = {
    homepage = "https://github.com/catppuccin/zed-icons";
    description = "Soothing pastel theme for Gram";
    license = lib.licenses.mit;
  };
})

The result contains everything needed by Gram: the extension.json, as well as all the WASM files.

Setting that up in Home-Manager

I ran into a first issue when trying out this Nix module: building Gram extensions is heavy. The wasip2 version of rust-wasm is not apparently in nixpkgs yet, so we need to download Niklas’s fork, which cascades into a lot of derivations to get to the Rust stuff. Then, building the extension itself takes a bit of time, even on my desktop computer.

For this reason, I decided to separate building extensions and using them in Gram. After all, I shouldn’t need to build new extensions every day, and once they’re built, I could just commit the WASM files. This also means I could build extensions on my desktop, push them to my Git repository, and use them on my laptop without having to compile them again!

Here’s what my gram module looks like:

.
├── default.nix
├── extensions
│   ├── build
│   ├── default.nix
│   └── nix
│       └── kdl
│           └── default.nix
└── scripts
    └── build-gram-extensions.sh

The root default.nix simply defines a Home-Manager module which links ~/.local/share/gram/extensions/installed to the build/ folder.

{ pkgs, config, lib, ... }:
let
  cfg = config.programs.gram;
in
{
  options.programs.gram = {
    enable = lib.mkEnableOption "gram, a hard fork of the Zed editor";

    package = lib.mkOption {
      type = lib.types.package;
      default = pkgs.gram;
      description = "The gram package to use";
    };
  };

  config = lib.mkIf cfg.enable {
    home.packages = [ cfg.package ];

    xdg.dataFile."gram/extensions/installed" = {
      source = ./extensions/build;
      onChange = ''
        cd "${config.xdg.dataHome}/gram/extensions"
        rm -f index.json
      '';
    };
  };
}

The cool stuff happens in extensions/default.nix: this file imports Niklas’s module and uses it to build a set of extensions. Custom extension derivations are stored in the nix/ folder. linkGramExtensions bundles all the extensions into a single folder, which makes it easy to then import in Gram.

# extensions/default.nix
let
  pkgs = import <nixpkgs> {};

  gram-extensions-tarball = fetchTarball "https://tangled.org/niklaskorz.eu/nix-gram-extensions/archive/main.tar.gz";
  gram-extensions = import gram-extensions-tarball {};

  kdl = pkgs.callPackage ./nix/kdl { inherit (gram-extensions) buildGramExtension; };

  extensions = [ 
    kdl 
    gram-extensions.php
  ];
in
gram-extensions.linkGramExtensions extensions

Extensions are pretty easy to define, especially the ones which are just a Tree-Sitter grammar (here, an example for github:elkowar/zed-kdl, which adds KDL support).

# extensions/nix/kdl/default.nix
{ lib
, buildGramExtension
, fetchFromGitHub
}:
buildGramExtension (final: {
  id = "kdl";
  version = "e22910467b5aa07d6d91b5ef4bb066630f05fd9e";

  src = fetchFromGitHub {
    owner = "elkowar";
    repo = "zed-kdl";
    rev = final.version;
    hash = "sha256-p3kNJZUg9B9m04UEI/Y9w4lskjoM/Md2/RKtm3nxau8=";
  };

  grammars.kdl = fetchFromGitHub {
    owner = "tree-sitter-grammars";
    repo = "tree-sitter-kdl";
    rev = "b37e3d58e5c5cf8d739b315d6114e02d42e66664";
    hash = "sha256-irx8aMEdZG2WcQVE2c7ahwLjqEoUAOOjvhDDk69a6lE=";
  };
})

The last step is the build script, build-gram-extensions.sh. This script simply builds all extensions with nix-build (I’m using nix-output-monitor, so nom-build here) and copies the result in the build/ folder, which is the one that’s symlinked and commited to Git.

# scripts/build-gram-extensions.sh
#!/usr/bin/env bash

set -x
set -e

cd ~/.config/home-manager/modules/gram/extensions/
nom-build --show-trace

rm -f ~/.local/share/gram/extensions/index.json
rm -rf build

mkdir build
cp -rL result/* build/
rm result
find build/ -type f -exec chmod u+rw '{}' ';'
find build/ -type d -exec chmod u+rwx '{}' ';'
  • I’m chmod‘ing because the files and folder still have their Nix store perms, and I don’t like it.
  • The result/ folder is full of symlinks, but cp -L resolves all of them!

That’s it

I hope this can be of use to someone else, even if I’m not using Gram anymore 🙂. Enjoy, and happy hacking!

Send your comment via e-mail