The road to Emacs maximalism

Building an Emacs Utopia with hot glue, duct tape and Nix

I’ve stumbled across a method of composing programs that excites me very much. In fact, my enthusiasm is so great that I must warn the reader to discount much of what I shall say as the ravings of a fanatic who thinks he has just seen a great light. (Knuth 1984, 1)

Introduction

A couple years ago I've started watching David Wilson's channel (also know as SystemCrafters), originally his channel focused heavilly on F# (and that's how I found him), but eventually he started posting more and "GNU slash Emacs" content. I lost interest on the channel at the time, while he did indeed showcase a level of productivity that was way above what I had, I felt like it could be easily emulated with a combination of Neovim and some other Linux tools.

Everything changed once I saw his presentation about org-roam, my brain got hijacked by the idea of testing Emacs. I had previously heard about orgmode before, but it was never on my radar since it felt like some "Emacs weirdo" cool-aid. What got me impressed was that I always needed some tool to help me organize my notes (specially during college where most of my annotations got lost). To fill this gap, such a tool would need to have the following properties:

  1. Document everything that I've done.
  2. Register and link everything that I've read, preferably by integrating:
    • Text
    • Math
    • Code
    • Citations
  3. Manage and track figures, while maintaing referential integrity.
  4. Be free (as in freedom) or open source.

Obsidian tickles most of these (if you consider its plugins), but the license is a little bit worrying and you can bet they will pull an insomnia in the future. Logseq is a much better alternative in this regard, plus you have the option of using something much better than markdown or reStructuredText, and that is org.

(…) trivial usage of Org-mode is nothing more than text editing, from which point the user can start to add special plain text Org-mode elements to the document. Org-mode is therefore easy to adopt and aims to be a general solution for authoring projects with mixed computational and natural languages. It supports multiple programming languages, export targets, and work flows. (Schulte et al. 2012, 2)

Org is a major mode in Emacs, it is also a powerful markup language and I would argue a better tool than any of the current alternatives. If properly configured, orgmode can combine writing, planning, scheduling, linking and programming into a cohesive workflow.

Org-mode extends Emacs with a simple and powerful markup language that turns it into a language for creating, parsing, and interacting with hierarchically-organized text documents. Its rich feature set includes text structuring, project management, and a publishing system that can export to a variety of formats. Source code and data are located in active blocks, distinct from text sections, where "active" here means that code and data blocks can be evaluated to return their contents or their computational results. The results of code block evaluation can be written to a named data block in the document, where it can be referred to by other code blocks, any one of which can be written in a different computing language. In this way, an Org-mode buffer becomes a place where different computer languages communicate with one another. Like Emacs, Org-mode is extensible: support for new languages can be added by the user in a modular fashion through the definition of a small number of Emacs Lisp functions. (Schulte et al. 2012, 7)

Literate Programming

I believe that the time is ripe for significantly better documentation of programs, and that we can best achieve this by considering programs to be works of literature. Hence, my title: "Literate Programming." (Knuth 1984, 1)

I must admit that, even though the idea itself seems interesting, time has proven that "literate programming" the way Knuth is suggesting is impractical, but lets forgive him, it was 1984. When it comes to documentation, I would rather deal with a good type system (like the ones inpired by the ML-family of languages) and/or have proper documentation tooling (hexdocs is the gold standard as far as I could experience).

I'm emphasizing this because org-babel (which is part of org-mode) is a practical tool to make Emacs a literate programming environment.

Borrowing terms from the literate programming literature, Org-mode supports both weaving - the exportation of a mixed code/prose document to a format suitable for reading by a human - and tangling - the exportation of a mixed code/prose document to a pure code file suitable for execution by a computer. (Schulte et al. 2012, 12)

Orgmode

Arbitrary Code Execution and Generation

Here's what my blog.org file looks like:

    Here you'll find my latest content, projects, tutorials and ramblings.

    #+header: :exports results
    #+header: :results html
    #+NAME: export-posts
    #+BEGIN_SRC shell
    dotnet fsi posts.fsx
    #+END_SRC

it's a single text line that also calls a shell command, dotnet fsi posts.fsx, posts.fsx is an F# file that generates html content as a huge string:

  $ dotnet fsi posts.fsx 

      <div class="stub">
        <h2>
          <a href="./blog/20241231-the_road_to_emacs_maximalism.html"> The road to Emacs maximalism </a>
        </h2>
        <small>2024-12-31</small>
      </div>
      

      <div class="stub">
        <h2>
          <a href="./blog/20240916-you_have_10_seconds_to_nixify_your_dotnet_project.html"> You have 10 seconds to nixify your dotnet project </a>
        </h2>
        <small>2024-09-16</small>
      </div>

      # And so on...

This could have been done in any language really, but I felt more comfortable quickly pulling this in F#, the #+header: :results html makes sure this will be correctly exported to html once we run the publish.el file (either locally on in CI).

Roam

Similar to the previous section, my notes are also generated via some hacky F# script:

  This is the place where I dump my Org ROAM notes.

  #+INCLUDE: ./static/html/graph.html export html

  #+header: :exports results
  #+header: :results html
  #+NAME: export-posts
  #+BEGIN_SRC shell
    dotnet fsi notes.fsx
  #+END_SRC

The difference being that there is an extra #+INCLUDE: directive importing actual html code. That's where the d3.js graph setup is.

Stealing the Graph

notes.png
Figure 1: My crappy graph

I've blatantly copied from Hugo Cisnero's awesome blogpost a couple years ago and I really like how he generated a graph out of the sqlite db already used by org-roam. Some minimal changes were required to render my graph, it is sparser than his, so forcing a minimum number of communites doesn't look that good (a quick hack is taking the number of weakly connected components). To build the graph one just needs to run:

  just graph
  # or
  graph
  # inside the Nix shell

Note that this used to be an feature request on Github, until someone created a publish-org-roam-ui github action. Maybe that's what most people need, but it won't work for me, at least in this current iteration of the blog.

Diagrams as Code

For my current needs graphviz is enough, but I can keep adding similar tools later:

  digraph {
    a -> b;
    b -> c;
    c -> a;
  }
graphviz_example.png

Exports

You take an org file and export it to different formats like: html (that's how this blog is made), LaTeX, markdown, etc. Currently I only care about the html export and you can find the publish.el file here.

The Infrastructure

Knuth has a point about some of the portability issues on his "Literate Programming" paper, even though the markup language (WEB) was portable to different systems, the same could not be said of the PASCAL compilers that were generating the code:

Furthermore, many of the world's PASCAL compilers are incredibly bizarre. Therefore it is quite naive to believe that a single program TANGLE.PAS could actually work on very many different machines, or even that one single source file TANGLE.WEB could be adequate; some system-dependent changes are inevitable. (Knuth 1984, 10)

Technically, any modern literate programming environment (heck any programming environment in general) is going to suffer from similar issues (instead of multiple compilers we have multiple package managers and DLL hell). If you use a python notebook and never bothered to pin your dependencies, or worse, if no one knows which version of the interpreter was used initially, then give it a couple months and there's a pretty good chance it will never run.

Nix

To make this less likelly to happen, my development environment heavilly relies on Nix and devenv. Everything is set in a single flake.nix file, a LaTeX environment with a couple dependencis, some .NET and Python libs, sqlite, just and even a custom Emacs to be used in CI, this might seem cursed but it's really easy to pull this off on Nix:

  # (...)
  customEmacs = (pkgs.emacsPackagesFor pkgs.emacs-nox).emacsWithPackages (
    epkgs:
    with epkgs.melpaPackages;
    [
      citeproc
      htmlize
      ox-rss
    ]
    ++ (with epkgs.elpaPackages; [
      org
      org-roam
      org-roam-ui
    ])
  );
  # (...)

this is only used the CI shell, where we require Emacs with a minimum set of plugins to publish the website, the default (impure) development shell is still going to pull your local Emacs:

  {
    # `nix develop .#ci`
    # Reduce the number of packages to the bare minimum needed for CI,
    # by removing LaTeX and not using my own Emacs configuration, but
    # a custom package with just enough tools for org-publish.
    ci = pkgs.mkShell {
      ENVIRONMENT = "prod";
      OUT_URL = "https://schonfinkel.github.io/";
      DOTNET_ROOT = "${dotnet}";
      DOTNET_CLI_TELEMETRY_OPTOUT = "1";
      LANG = "en_US.UTF-8";
      buildInputs = [ dotnet customEmacs ] ++ tooling;
    };

    # `nix develop --impure`
    # This is the development shell, meant to be used as an impure
    # shell, so no custom Emacs here, just use your global package
    # switch back to the CI shell for builds.
    default = devenv.lib.mkShell {
      inherit inputs pkgs;
      modules = [
        (
          { pkgs, lib, ... }:
          {
            packages = [ dotnet texenv ] ++ tooling;

            env = {
              ENVIRONMENT = "dev";
              DOTNET_ROOT = "${dotnet}";
              DOTNET_CLI_TELEMETRY_OPTOUT = "1";
              LANG = "en_US.UTF-8";
            };

            scripts = {
              build.exec = "just build";
              graph.exec = "just graph";
              clean.exec = "just clean";
            };

            enterShell = ''
              echo "Starting environment..."
            '';
          }
        )
      ];
  };

the full setup can be found in the main repo.

Continous Integration

Again, I benefit from a somewhat easy to setup CI pipeline thanks to install-nix, it's a copy of what I already do locally. And you can also benefit from faster builds with the magic-nix-cache.

      - name: Install Nix
        uses: cachix/install-nix-action@v27

      - name: Install Nix Cache
        uses: DeterminateSystems/magic-nix-cache-action@main

      - name: Build website
        run: |
          mkdir -p "$HOME/.emacs.d/"
          touch "$HOME/.emacs.d/.org-id-locations"
          nix develop .#ci -c just build

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./public

What is Still Missing

  • Anki-Like Flashcards: With either org-drill or org-fc.
  • Integration with org-ref: org-ref offers a suite of tools that would make keeping track of references easier as the number of notes and posts increases.
  • Remove some of the Polyglot Templating: Although the polyglot usage of different programming languages here was a good way to show some of orgmode's source block features, I know my usage of F# is unecessary, I could have done the same in pure elisp, but I still suck at it.
  • Not related to the blog itself, but org-agenda looks slick.

Notable Mentions

Hugo

This blog was originally fully integrated with hugo and ox-hugo (first stolen from my close friend), but eventually I've started facing issues since the way I organize files (one per post) is not recommended by ox-hugo. The "per-post" setup actually worked, but hugo is also a project that moves very fast and I quickly faced a situation where upgrading it broke my workflow, luckily I develop in a sandbox environment and was able to ignore this versioning issue for a couple months.

Quartz

Similar to hugo, although Quartz is also built to support Obsidian-like notes.

Emanote

Before doing the full refactor and moving it back to a pure org-publish workflow, I found out about emanote. It is similar to Quartz, but it feels overall better since the license is AGPL v3. It's also built atop of Markdown, but there are steps on how to configure this to use org as well. May be a good choice for people already familiar with Haskell and Nix.

Conclusion

While I haven't moved all my development workflow to Emacs (it might be a matter a time), Emacs already stole all my note-taking and blogging capabilities and I'll probably stick with it for a long time. I still hope Neovim gets something similar, it is already a huge improvement above vanilla vim offered thanks to many new features (and allowing a real language for configuration instead of VimL). There is also some work being done in replicating org using lua, but it would be interesting to see if the community can pull some similar plugins as well.

References

Knuth, Donald Ervin. 1984. “Literate Programming.” The Computer Journal 27 (2): 97–111.
Schulte, Eric, Dan Davison, Thomas Dye, and Carsten Dominik. 2012. “A Multi-Language Computing Environment for Literate Programming and Reproducible Research.” Journal of Statistical Software 46: 1–24.