Skip to content

About

Excalidraw inside Emacs 32: edit .excalidraw files, drawn through the new canvas API

Topics

Resources

Stars

64 stars

Watchers

0 watching

Forks

Latest commit

 

History

72 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

excali-mode — Excalidraw inside Emacs

Drawing, binding arrows and growing a flowchart in excali-mode

excali-mode opens, edits and saves Excalidraw drawings in Emacs. The .excalidraw files are the same ones the web app reads and writes, and the drawing looks the same: rough.js strokes and hachure fills, the hand-drawn fonts, and arrows that stay attached to shapes. Elisp holds the scene and every editing command; a small C module draws it with Cairo and Pango straight into the pixel buffer of Emacs 32’s new canvas image type.

Two things are built on top of the drawing: Org boards, a whiteboard whose cards are Org headings that stay in their own files (in the spirit of Obsidian Canvas, for Org), and Excali DSL, diagrams written as text, also as Org Babel blocks.

It is an unofficial port, not affiliated with the Excalidraw project. There is no browser or webview in it. It needs Emacs 32 (master) — the first Emacs with the canvas API — and a C compiler to build the module; see Requirements below.

Org boards (0.3.0)

An Org heading placed on a board as a card; editing the heading in its file updates the card

excali-board-mode puts Org headings on a board as cards. A heading stays in its Org file: edit it there and the card follows, or jump from the card back to its source.

Watch the full 45-second demo (1080p, English captions, no audio), which also shows labelled arrows between cards, named regions and file cards. The details are under Org boards below.

The video is also included at demo/excali-v0.3.0.mp4.

0.2.0 demo: placement, styling and Org

Watch the 45-second demo (1080p, English captions, no audio).

The custom DSL lets you specify relative node positions rather than leaving arrangement entirely to automatic layout. The demo changes right of to left of, adds a node style block, then uses the same source and styles in Org Babel and regenerates the inline image after another placement change.

The video is also included at demo/excali-v0.2.0.mp4.

Features

  • Every Excalidraw tool: rectangle, diamond, ellipse, arrow, line, pen, text, image, frame, sticky note, eraser, laser pointer, eye dropper, bucket fill, autoshape and lasso selection.
  • Arrows that bind to shapes and follow them, including elbow arrows routed around obstacles; Cmd+arrow grows a flowchart node by node.
  • Selection, groups, resize and rotate handles, align and distribute, flip, lock, snapping and grid, z-order, copy and paste of elements and styles, undo and redo.
  • Labels in shapes and on arrows, text wrapping and fonts as upstream lays them out (CJK included), dark mode.
  • Images (PNG, SVG, JPEG, GIF, WebP), frames with clipping, the element library (.excalidrawlib) with the official collection of libraries.excalidraw.com one command away, and PNG/SVG export with the scene embedded so Excalidraw can open the export again.
  • Diagrams from text: Excali DSL (.excalidsl), with readable relative placement, dotted containers, explicit style blocks, and editable bound arrows.
  • Org boards (excali-board-mode): Org headings as cards that show their subtree and follow edits to the source file, independent Org notes, labelled arrows between cards, named groups and regions, and static previews of PDF, image, video and audio files. Boards are saved as .excalidraw files.
  • Smooth on large scenes: only damaged areas repaint, panning reuses pixels, zooming shows an instant preview, and on macOS a CoreAnimation layer bypasses Emacs’s image refresh.

Requirements

  • Emacs 32 (master) built with module support and the canvas image type. To check, evaluate this in a graphical Emacs (M-:); it should return t:
    (and (fboundp 'module-load)
         (fboundp 'canvas-refresh)
         (image-type-available-p 'canvas))
        
  • GNU make, pkg-config, a C11 compiler, cairo, pangocairo and zlib. The packages for each platform are listed under Platforms.
  • Optional, for PDF and video/audio previews on Org boards: Poppler (pdftoppm) and FFmpeg.

What has been run: excali-mode is developed and used on macOS (Apple silicon) with Homebrew’s emacs-plus@32; the build last tested against is from 2026-09-10. That is a tested configuration, not a minimum version. On Linux and Windows the sources compile, but the author has not run them there. One user reported 0.1.1 working with an .so build on Emacs 31 plus the canvas patch (see With elpaca).

Installation

excali-mode is not on MELPA yet. Build it from a clone and try it:

git clone https://github.com/yibie/excali-mode.git
cd excali-mode
make            # the C module and the byte-compiled Lisp; see Platforms
make fonts      # optional but recommended: Excalidraw's fonts (network)
make try        # opens the sample drawing in a separate Emacs started with -Q

make try does not touch your configuration. Without make fonts text falls back to system fonts, so drawings do not look like the pictures above and text wraps differently from the web app.

Run make again after pulling: excali loaded from uncompiled .el files runs several times slower, which large scenes feel. Emacs picks the .elc files up by itself, and native-compiles them in the background where it can.

Local clone: example configuration

Replace /path/to/excali-mode with your clone directory. This configuration checks for Canvas support and the built module, keeps the drawing commands lazy-loaded, and registers the DSL extension before the package is loaded:

(let ((excali-directory (expand-file-name "/path/to/excali-mode")))
  (when (and (>= emacs-major-version 32)
             (fboundp 'canvas-refresh)
             (image-type-available-p 'canvas)
             (file-exists-p
              (expand-file-name (concat "excali-module" module-file-suffix)
                                excali-directory)))
    (add-to-list 'load-path excali-directory)

    (use-package excali
      :defer t
      :commands (excali-open excali-new))

    ;; Text source files, NOT .excalidraw scene files.
    (autoload 'excali-dsl-mode "excali-dsl" nil t)
    (add-to-list 'auto-mode-alist
                 '("\\.excalidsl\\'" . excali-dsl-mode))))

M-x excali-open opens a .excalidraw file (or a PNG/SVG exported with an embedded scene); M-x excali-new starts an empty drawing.

The explicit autoload and auto-mode-alist entries let an .excalidsl file activate its mode even before you have opened a canvas. The registration inside excali-dsl.el alone is not enough when a local clone is lazy-loaded without generated package autoloads. If any guard above fails, the setup is skipped; check your Emacs build, clone path, and make output.

If you already configure excali with use-package and a working load path, adding :mode ("\\.excalidsl\\'" . excali-dsl-mode) to that declaration is an alternative to the two explicit DSL registration forms above.

First DSL preview

  1. Evaluate the configuration, or restart Emacs.
  2. Open examples/excali-dsl/application.excalidsl. The mode line should show Excali DSL, not Fundamental. For a file already open before configuration, run M-x normal-mode in its buffer.
  3. In the DSL source buffer, press C-c C-c to preview; press it again to refresh the same canvas. These bindings do not belong to the canvas buffer.
  4. Press C-c C-e in the source buffer and choose an .excalidraw output file.
ExtensionContentsHow to open
.excalidslExcali DSL text sourceexcali-dsl-mode; C-c C-c previews
.excalidrawNative JSON sceneM-x excali-open
.edslFormer DSL syntaxNo longer supported; rewrite using Excali DSL

Do not associate .excalidraw with excali-dsl-mode. If you previously evaluated that incorrect association, remove it from your initialization file and evaluate this once to clean up the current session:

(setq auto-mode-alist
      (delete '("\\.excalidraw\\'" . excali-dsl-mode)
              auto-mode-alist))

With elpaca

u/ZeStig2409 shared this setup on r/emacs, noting it works on Emacs 31 too with the Emacs patch. Elpaca builds the package in its own directory, so the compiled module is linked in there:

cd /path/to/excali-mode
make
ln -sf /path/to/excali-mode/excali-module.so ~/.config/emacs/elpaca/builds/excali/excali-module.so
(use-package excali
  ;; :vc :url "https://github.com/yibie/excali-mode" :branch "v0.3.0"
  :ensure (:repo "/path/to/excali-mode"
           :pre-build (("ln" "-s" "/path/to/excali-mode/excali-module.so" "."))) ;; you could do this manually as well.
  :mode ("\\.excalidsl\\'" . excali-dsl-mode)
  :config
  (add-hook 'excali-mode-hook
            (lambda ()
              (when (fboundp 'evil-local-mode)
                (evil-local-mode -1))
              (when (fboundp 'which-key-mode)
                (which-key-mode -1)))))

The hook turns off Evil and which-key in excali buffers, whose keys excali uses itself. Note that which-key-mode is global: turning it off there turns it off in every buffer until you turn it on again. On macOS the module is excali-module.dylib.

Build and test

make            # builds excali-module (.so/.dylib/.dll per Emacs; needs cairo, pangocairo, zlib)
make compile    # byte-compiles the Lisp (part of make)
make info       # shows the detected platform, Emacs, header and module name
make test       # batch ERT: round-trip, text, rendering, tiles, resizing
make try        # opens the sample in a clean GUI Emacs
make fonts      # downloads Excalidraw's fonts into fonts/ (see fonts/README)
make bench      # benchmarks backends in a clean GUI Emacs, writes bench.txt

Platforms

All platforms need an Emacs 32 (master) built with module support — its emacs-module.h must have canvas_data — plus GNU make, pkg-config, a C11 compiler, cairo, pangocairo and zlib. librsvg, gdk-pixbuf and libwebp (SVG, GIF/JPEG and WebP images) and fontconfig (the fonts in fonts/ on Linux) are used when pkg-config finds them; EXCALI_WITH_RSVG=no etc. leave one out. Set EMACS=/path/to/emacs to build against a particular Emacs; the header is looked up next to it (../include/ when installed, or src/ in a build tree), else set EMACS_MODULE_INCLUDE.

  • macOS (Homebrew): brew install pkg-config cairo pango librsvg gdk-pixbuf webp. Adds the CoreAnimation layer backend and registers fonts with CoreText.
  • Linux, Debian/Ubuntu: =apt install build-essential pkg-config libcairo2-dev libpango1.0-dev zlib1g-dev librsvg2-dev libgdk-pixbuf-2.0-dev libwebp-dev libfontconfig-dev=; Fedora: =dnf install gcc make pkgconf cairo-devel pango-devel zlib-devel librsvg2-devel gdk-pixbuf2-devel libwebp-devel fontconfig-devel=. The BSDs build the same way with gmake.
  • Windows, MSYS2 UCRT64 shell: pacman -S make mingw-w64-ucrt-x86_64-{gcc,pkgconf,cairo,pango,zlib,librsvg,gdk-pixbuf2,libwebp}, with an Emacs 32 built in the same environment. The module links against the UCRT64 DLLs, so /ucrt64/bin must be on Emacs’ PATH. Fonts in fonts/ are registered with GDI (AddFontResourceEx).

Without CoreAnimation the auto backend is tiles. What has been checked: the full build and make test on macOS; the non-macOS configuration (make PLATFORM=unix, no CoreAnimation or CoreText) built with GCC and passing make test on macOS; every source compiling cleanly for x86_64 glibc, aarch64 musl and x86_64 mingw-w64 (zig cc with those libcs’ headers). Not yet run by the author on Linux or Windows: linking there, the GUI, and on Windows whether Pango sees GDI-registered fonts. One user reported 0.1.1 working with an .so build on Emacs 31 plus the canvas patch (see With elpaca); reports from other setups are welcome.

Keys

excali-open also opens PNG and SVG files exported with an embedded scene (by excali or Excalidraw); such scenes are saved to a new .excalidraw file.

Plain keys follow Excalidraw; its Mod shortcuts use super (Cmd on macOS), and the usual Emacs keys work too. ? lists everything.

KeyAction
v 1, h, r 2, d 3, o 4, a 5, l 6, p x 7, t 8select, hand, rectangle, diamond, ellipse, arrow, line, pen, text
a againcycle sharp / round / elbow arrows
f, n, e 0frame, sticky note, eraser (e again: previous tool)
klaser pointer: a fading red trail, never saved
bbucket fill: click inside a closed area (b again: next color, M-click: pick the color)
Xautoshape: a freehand stroke becomes a rectangle, ellipse, diamond or line
i G, Seye dropper: next click picks a background, stroke (M-click: the other)
C-M-drag M-s-draglasso select; M-x excali-toggle-lasso makes it the v tool
qlock the tool (keep drawing)
dragcreate; shift = square / 15° lines, meta = from center
short drag with a=/=l, then clicksmulti-point line; click the last point, RET or ESC to finish
click, S-click, box dragselect, toggle, box select (contain)
drag inside the selection boxmove
corner handles, border, round handleresize (shift keeps ratio, meta from center), rotate (shift snaps 15°)
double-clickenter group, edit text, add or edit a shape’s label, add text
s, g, Fstyle panel (as upstream’s; each property opens a panel of its values, colors a picker: q..=b= colors, 1..=5= shades, # hex), background, font
H, Vflip horizontally / vertically
TABcycle rectangle / diamond / ellipse
arrows, =S-=arrowsnudge 1 / 5
s-z s-Z (C-/ C-?)undo, redo
s-c s-x s-v (M-w C-w C-y)copy, cut, paste (Excalidraw clipboard)
s-d, s-a, s-g, s-Gduplicate, select all, group, ungroup
s-] s-[, M-s-] M-s-[forward / backward, front / back
S-s-=arrows, =M-h M-valign, distribute
=s-=arrows (=C-=arrows)flowchart: add a linked node; repeat for siblings, any other key keeps them
=M-=arrowsflowchart: go to the linked node that way; repeat to cycle
s-Llock / unlock elements
s-k, click a link iconset a link (URL or ?element=ID), follow it
s-', M-sgrid, object snapping (super at the press suppresses/inverts)
M-Dtoggle the dark theme
C-c l a, C-c l i, C-c l b, C-c l d, C-c l olibrary: add selection, insert items, browse, delete items, official collection
M-s-c M-s-v, s-< s->copy / paste styles, font size
wheel (S-=wheel sideways), =C-=wheel / pinch, middle or right drag | pan (the way Emacs scrolls a buffer; =mouse-wheel-flip-direction swaps sideways), zoom, pan
s-= s-- s-0 (C-x C- - 0=), ! @ #zoom, zoom to fit all / selection
PgUp PgDn (with shift: horizontal)page
9insert an image file at the view centre
s-s, C-x C-ssave
s-E, C-c C-eexport PNG or SVG (by extension; the selection if any, C-u for all)
C-c C-b, C-c C-pcycle backend, toggle 1x/2x (debug)

Drawing from text

Excali DSL (.excalidsl) is a text language designed for excali-mode, with relative placement inspired by reladraw. It replaces the old .edsl syntax; this is not a reladraw compatibility layer.

Open an .excalidsl file in excali-dsl-mode:

  • C-c C-c draws it in a scene beside the source; repeat to refresh.
  • C-c C-e exports a native, editable .excalidraw file.
  • Set excali-dsl-render-on-save to refresh an existing preview on save.
style service {
  fill: "#dbeafe"
  fill-style: hachure
}

node system "Application"
node system.ui "Interface" { style: service }
node system.api "API" below system.ui { style: service }
node database "Database" right of system level with system.api

edge system.ui -> system.api "request" from: bottom to: top
edge system.api -> database "query" from: right to: left {
  routing: elbow
}

Dotted identifiers define containment. Placement and attachment sides stay outside style blocks; appearance uses explicit { ... } blocks. Properties are separated by newlines or semicolons. Reusable style and default node / default edge blocks are document-wide.

Layout jointly solves positions and container sizes, with measured text, a minimum 80-unit directional gap, and 30-unit container padding. Contradictory constraints and disconnected nodes are errors, not silently rearranged. Arrows stay bound to shapes, and leaf/edge labels are bound text. Container titles are grouped with their boundary; containers are not clipping frames. Straight edges do not avoid obstacles automatically.

From Lisp, (excali-dsl-elements STRING) returns elements, (excali-dsl-scene STRING) returns a document, and (excali-dsl-write STRING FILE) exports atomically without opening a canvas. In an excali buffer, M-x excali-dsl-yank and M-x excali-dsl-insert-file insert a diagram at the view center.

See the language reference and runnable examples. The source remains authoritative: regeneration replaces manual canvas changes. For diagrams embedded in documents, see the Org Babel setup below.

Org boards

excali-board-mode is a derived major mode of =excali-mode=, not a second file format or an Org feature globally enabled in ordinary drawings. It inherits drawing, selection, transforms, arrows, groups and undo. Boards use .excalidraw files, not JSON Canvas.

Cards display the complete referenced subtree using a native Cairo/Pango renderer: headings, emphasis, paragraphs, lists/checkboxes, aligned tables and literal code/example blocks. Content is clipped inside the card rather than growing its rectangle. Hover over a card body and use the wheel to scroll it, without selecting it; Shift-wheel scrolls wide tables/code sideways. Elsewhere the wheel pans the board, and Control-wheel still zooms. In the selection tool, drag either scrollbar thumb or click its track to reposition the content without moving the card.

Referenced cards edit the actual Org subtree through a shared indirect buffer in a child frame, not an editable cache. Independent cards own their Org text in the board instead. Both support double-click editing; closing the editor does not automatically save a file.

This is a focused Org board, not a complete Obsidian Canvas clone or a complete Org export backend. Drawers/planning are hidden, code is never executed, and supported Org links are clickable in reading state. Local inline images and static PDF/image/video/audio cards are supported. LaTeX rendering, web embeds, media playback and PDF page navigation are intentionally not provided.

Board features at a glance

  • Org cards: reference existing headings or write independent Org notes; convert a note to a heading without re-entering its title.
  • Reading and editing: scroll inside cards, follow supported Org links, display local inline images, and double-click Org cards for popup editing.
  • Vault and references: recursively discover files within a chosen directory, explicitly repair moved Org references by ID, and find boards referencing a heading. External updates preserve unsaved edits when conflicts occur.
  • Spatial organization: connect cards with bound arrows, create a new note at a free arrow endpoint, and organize the board with named groups/regions.
  • File cards: display typed PDF, image, video and audio previews. Double-click a file card to open its original file in another Emacs window.

Configuration

Rebuild the native module with make module and restart Emacs when upgrading to rich cards; an already-loaded dynamic module cannot be replaced by loading the Lisp files again. No byte-compilation is required.

After adding this repository to load-path as described above:

(use-package excali-board
  :commands (excali-board-new
             excali-board-open
             excali-board-add-heading
             excali-board-from-scene
             excali-board-return)
  :init
  (setq excali-board-vaults
        '(("Personal" . "~/Documents/notes/")
          ("Work" . "~/Documents/work/")
          ("Research" . "~/Documents/research/"))))

Configure excali-board-vaults with your existing local directories before creating boards. Names are labels, not folders to create. There is no separate “enter vault” command, global active-vault state, or per-creation vault picker. excali-board-new automatically matches the current Org file’s directory to a configured root, asks only for a new filename, saves the .excalidraw file immediately, and displays it on the right. The filename picker starts in the source directory. Without a file-backed buffer, default-directory is used; from an existing board, its file directory is used.

For nested roots, the deepest matching directory wins, independent of list order. Paths are canonicalized, including symlinks. The destination must stay in that same matched vault, not another configured vault (including a nested one). Creation outside every configured root is blocked before prompting or splitting windows. Update the configuration or use excali-new for an ordinary drawing anywhere. Existing files are never overwritten and Org sources are not automatically saved.

All configured roots must exist and be local absolute paths (~/ is allowed). An invalid entry reports a configuration error rather than silently selecting a broader root. Existing boards keep their persisted vault roots even if this list changes; their file discovery and reference repair still use those roots, not a combined search across the configured list.

For one vault, use the same variable with a single entry:

(setq excali-board-vaults
      '(("Notes" . "~/Documents/notes/")))

An empty list means no vault is configured: new board creation is blocked until a root is configured. There is no alternate directory setting or implicit fallback to the current directory.

Or simply (require 'excali-board) and configure the vaults above. Do not map every .excalidraw file to the board mode: excali-open detects the document’s versioned board marker and loads the derived mode when appropriate. Unmarked drawings remain in excali-mode. Enter through the new/open commands; the mode initializer itself is intentionally non-interactive, to avoid resetting a live drawing.

From Org to a board

  1. Put point on a heading in a local, file-backed Org buffer.
  2. Run M-x excali-board-add-heading.
  3. Choose an existing board buffer, [Open board file...], or [New board].
  4. Save the Org source yourself: an ID is added if needed but never saved automatically. Save the board separately with C-x C-s.

Cancelling target selection does not assign an ID. Duplicate heading titles are allowed: references use Org IDs, and the insertion picker displays outline paths and line numbers. Each invocation creates a new card; several cards or boards may reference the same heading.

From a board to Org

Command/keyAction
M-x excali-board-newCreate and save a board inside the configured vault; show it on the right
M-x excali-board-openOpen an existing marked board on the right
C-c C-iChoose an Org file and heading to insert
C-c C-nCreate an independent Org text card
C-c C-wConvert an independent card into a heading in a local Org file
C-c C-oVisit the selected card’s source heading, retaining the board pane
Double-click an Org card / C-c C-eEdit the Org card in a fixed popup
M-n / M-pScroll selected card down/up one page
M-x excali-board-replace-sourceChoose a different heading without replacing the card or its connections
C-c C-aInsert a static media card or generic file attachment
Double-click a file/media card / C-c C-fOpen the original file in another Emacs window
M-x excali-board-media-relinkExplicitly choose a replacement file for the selected media card
M-x excali-board-media-refreshDiscard transient media previews and retry generation
C-c C-lConnect two selected cards/shapes with an optionally labeled bound arrow
Double-click a free arrow endpoint / C-c C-jCreate and connect a new Org note
C-c C-tCreate a named region from the selection
C-c C-vNavigate to a named group or region
C-c C-rRefresh the selected card, or all cards if none is selected
M-x excali-board-refresh-allRefresh all cards and report source errors
M-x excali-board-returnReturn from Org to the last used board
C-x C-sSave the board, not its Org sources
!Fit the drawing in the current viewport

New boards split the current window left/right, or reuse a board pane already on the right. Repeating the command does not keep splitting that board pane. Unrelated panes are not replaced; if the window cannot be split, the command fails without replacing the source.

Click a card to focus the board; drag to move or resize it. Edits in watched Org buffers update referencing boards after 0.35 idle seconds, including unsaved edits. Refresh is undoable, preserves position and style, and does not save either file. Source updates and resizing never expand a card merely to fit its content. Scroll offsets are saved with the board and clamped to the current content size. Deleting a card never deletes its source heading.

Double-click a card (or use C-c C-e on the board) to open a fixed popup editor, centered in the parent frame. It is independent of the card’s position, rotation and zoom: panning the board never moves or hides the popup. Inside the editor, C-c C-e expands the same Org buffer into an ordinary Emacs window, preserving point, undo history and folding. C-c C-c or Escape closes the editor without saving; edits remain live in the card/source. C-x C-s explicitly saves the Org source for a reference card, or the board for an independent note. Closing is not a discard operation; use Org undo to revert edits. Popup editing requires graphical Emacs. C-c C-o on the board still visits the referenced source heading.

Relationships use ordinary Excalidraw bound arrows, so moving/resizing cards keeps their connections. The inherited arrow tools, grouping and directional flowchart navigation remain available. Board relationships do not write Org links. Board attachments store an absolute local path, not a copy of the file. The ordinary excali-insert-image command remains available when an embedded image rather than a linked media card is wanted.

Missing files/IDs or unsupported references retain the cached preview and show an error count in the mode line. Use excali-board-refresh-all for details. Source paths are absolute local paths in version 1; automatic file relocation, remote sources, and watching external disk edits are not supported. Reverting or reopening a source buffer reconnects its watches. Modified source text must be saved separately before closing Emacs.

Independent Org cards

Use C-c C-n (excali-board-new-note) to start a note without creating an Org file or heading first. Double-click to edit it. Text updates the card live and is stored inside the board’s .excalidraw file. In this editor, C-x C-s saves the board; C-c C-c or Escape closes the editor without saving. In a referenced card’s editor, C-x C-s still saves its Org source.

Select an independent note and use C-c C-w (excali-board-note-to-heading). Choose a local .org file (existing or new); no title prompt is needed. Conversion:

  • Appends a new top-level heading with an Org ID.
  • Reuses the leading Org heading and its subtree without duplicating the title. Additional root headings become children. Plain notes use the first nonblank line as a title while retaining their body; empty notes use New note.
  • Preserves the card’s identity, layout, styling and bound connections.
  • Replaces owned text with a normal heading reference.
  • Does not automatically save either file: save Org and board separately.

Close the editor before converting. Failed conversion rolls back the inserted Org text and preserves the independent card. Undoing a successful conversion in the board restores its independent text, but never deletes the new Org heading; Org and board have separate undo histories.

Existing prototype or drawing

Open the scene, then run M-x excali-board-from-scene. This explicitly creates a new, unsaved board copy, preserving shapes, connections and layout, and migrates known excaliOrgPrototype references. Save to a different filename; the copy’s save hook rejects overwriting its original file. The original drawing/prototype is not converted in place.

The experimental module is no longer required. Prefer a fresh Emacs session without loading it when trying the formal mode; already-loaded prototype advice does not disappear just because the new module was loaded.

Format and compatibility

The top-level excaliBoard object stores { "version": 1 }. Each Org card uses customData.excaliOrg with version, scope: "heading", file and id. customData.excaliBoardContent caches version 1 formatted blocks and the full subtree source; customData.excaliBoardView stores internal scroll offsets. Standard rectangle/text elements provide a short static fallback preview; the Org file remains authoritative for referenced cards. Independent cards use customData.excaliBoardNote with version: 1 and owned text; their board file is authoritative until conversion. Unknown document fields are retained by the local serializer. Unknown board versions are rejected rather than silently rewritten.

Other Excalidraw editors can display the standard shapes/text, but do not gain Org navigation or synchronization. Preservation of extension metadata after editing in other applications is not guaranteed. Board files contain source paths and copied text, including raw subtree properties hidden from the card: review them before sharing. PNG/SVG exports from board mode render the rich card viewport; other Excalidraw editors display only the fallback shapes/text.

Implementation sequence

  1. [X] Derived mode, versioned document marker and local extension hooks.
  2. [X] Both insertion entry points, source navigation/sync, save/reopen.
  3. [X] Explicit prototype migration and regression tests.
  4. [X] Dedicated Org content renderer, subtree formatting, clipping/scrolling.
  5. [X] In-card source editing, source replacement, local attachments and bound relationships.

Vault: bounded file discovery

A vault is an ordinary local directory including its subdirectories. Configure excali-board-vaults once, before creating boards. Each new board automatically stores its matched root as excaliBoard.vaultRoot. There is no required folder layout and no files are moved or imported. Existing boards without a vault continue to work; ordinary drawings are unaffected.

M-x excali-board-vault-find-file searches all files recursively through completion, showing vault-relative paths so duplicate basenames remain distinct. Heading insertion (C-c C-i) and attachment insertion (C-c C-a) use this picker when a vault is configured. Discovery is on demand, not a persistent database. VCS directories and .excali-cache are excluded; directory symlinks are not traversed and file symlinks escaping the root are excluded. An unavailable root reports an error rather than searching elsewhere. After relocating a vault, update its entry in excali-board-vaults for future boards. For an existing board, excali-board-set-vault explicitly repairs its stored root; save the board to persist that change. This command is a migration/repair tool, not a routine creation step.

The shared file resolver preserves native Org semantics: ordinary source links are relative to their Org file; attachment: paths use the source heading’s Org DIR or ID attachment directory, including Org’s inheritance settings. Independent notes use the vault root for relative file paths. These paths also drive reading-state links and inline image previews. Setting a vault does not automatically repair old absolute references.

Reading-state links and images

Click an underlined link in a card’s body to follow it, without opening the popup editor. Link hit testing uses the native text layout, including wrapped lines, tables, card rotation and internal scrolling. A drag does not activate a link; use the card header or other non-link area to move the card. Shift-click retains the ordinary selection gesture.

Supported targets are HTTP/HTTPS, local file: and attachment: links, vault-scoped id: links and internal Org heading/search links. File links open in Emacs without shell file handlers. Unsupported/custom link types, including elisp: and shell:, report an error rather than execute code. A stale cached link is checked against the current Org source before opening.

Undescribed local image links such as [[file:images/diagram.png]] or [[attachment:diagram.png]] render directly in the card. Supported formats depend on the native image decoders (PNG/JPEG/GIF/WebP/SVG, etc.). Previews preserve aspect ratio and fit the body width, with a 320-scene-unit height limit. Described image links remain clickable text links. Remote images are not downloaded. Missing, unsupported or oversized images show a placeholder.

Org attachment inheritance follows org-attach-use-inheritance; a child heading does not automatically inherit a parent’s DIR if Org inheritance is disabled. Independent notes resolve relative file links against their vault. Use C-c C-r to refresh after changing an image file externally. Image bytes are held in a session cache, not copied into the board’s document; PNG/SVG exports include the visible image preview.

Repair and reverse references

Run M-x excali-board-repair-references in the board after moving an Org file or heading. It scans only the current vault for exact Org IDs, including unsaved visiting Org buffers. Unique matches update the card’s source path, not its identity, geometry or connections. Missing or duplicate IDs are reported, never guessed. Dirty old sources are not silently detached. Save the board separately after repair; Org files are never auto-saved.

At an Org heading, run M-x excali-board-find-backlinks and select the vault. The results list referencing cards with buttons to open their boards and select the cards. Open boards’ live state takes precedence over their saved files; unopened board documents are also searched. An existing heading ID is required; searching never creates one. Malformed board files abort the query with an error rather than silently omitting potentially relevant data.

External edits and conflicts

While boards are open, one shared two-second timer checks their referenced source buffers’ visited-file modification times. It does not recursively rescan the vault. A clean buffer is safely reverted when its file changes, then its cards refresh. Missing files retain cached content until repaired. If the buffer has unsaved edits, no revert or merge occurs: the card keeps its cached content and the board reports a conflict. Resolve the source conflict explicitly, then refresh. The timer stops when the last board closes. This uses Emacs modification-time detection, not content hashing.

Connected cards and named organization

  • Double-click an arrow’s free endpoint to create an independent note there and bind that endpoint to it. Alternatively select the arrow and press C-c C-j (excali-board-note-at-arrow-end). If both ends are free, the command asks which end to use. Bound endpoints are never silently replaced. The existing arrow, relationship label and opposite binding are preserved. Creation and binding form one undo step; edit the new note using the popup.
  • M-x excali-board-name-group names and groups the selected shapes. M-x excali-board-rename-group renames the selected group. These use standard groupIds; the name lives in each member’s customData.excaliBoardGroups. Ordinary grouping/ungrouping still works. Group names are navigation labels, not extra visible text on the canvas.
  • C-c C-t (excali-board-create-region) creates a visible named region around the selection, including bound labels and arrows between selected shapes. Regions are standard Excalidraw frames: moving the region moves its contents, and deleting the region releases rather than deletes its cards. Use M-x excali-rename-frame to rename it. Regions cannot nest; select the whole group rather than splitting it across regions.
  • C-c C-v (excali-board-goto-organization) chooses a named group or region, selects it and brings it into view. Names need not be unique: the picker includes IDs to disambiguate them.

These actions only change the board; they never create or modify Org source headings. Save the board normally. Other Excalidraw editors retain standard groups, frames and bindings but may not display or preserve custom group names.

Static media cards

Use C-c C-a (or M-x excali-board-insert-media) to choose a local file; with a vault configured, the picker includes its subdirectories. Media cards show their type in the header and the filename in the body:

TypeReading-state content
PDFFirst-page preview only
ImageProportional thumbnail; GIFs remain static
VideoFirst-frame thumbnail, no playback
AudioEmbedded cover when available; otherwise a type icon and filename

PDF previews use pdftoppm (Poppler); video/audio previews use ffmpeg. Emacs must find these optional programs through exec-path. No dependencies are downloaded automatically. Missing tools, missing/corrupt files and absent audio covers produce a labeled placeholder rather than blocking the board. Preview processes run in the background, at most two per board, with a 20-second timeout. Closing the board cancels jobs and removes its temporary preview files. Native image previews obey excali-image-max-file-size.

Cards remain movable, resizable, groupable and connectable. Double-click a file/media card, or press C-c C-f, to visit the original file in another Emacs window. This does not invoke a system player or edit the filename. There are no in-card playback controls, PDF page controls or media editing tools. Org cards retain their double-click popup editing behavior.

Quick example: add and open a file

  1. Configure excali-board-vaults once as shown above.
  2. Browse an Org file inside that vault in the left pane. Run excali-board-new and choose a filename: the board is saved inside the vault and shown on the right. Use excali-board-open for an existing board.
  3. Press C-c C-a and select a PDF, image, video or audio file. The card shows its type and filename while any background preview is being prepared.
  4. Drag the card to position it. Double-click its body or filename to open the original file in another Emacs window; C-c C-f does the same.
  5. Save subsequent board edits with C-x C-s. This saves the reference, not a media copy; the Org source is saved separately.

Opening a file uses Emacs’s normal file handling and your installed modes, not the operating system’s default application. Missing files produce an error rather than creating a new empty file. Double-clicking an Org card continues to open its popup editor; use C-c C-o to visit its source heading.

Persistence and repair

Only the media type and local source path are persisted in customData; previews are regenerated on reopening and are not embedded in the drawing. C-c C-r on a selected media card (or with no selection) retries previews; M-x excali-board-media-refresh also works. Changed files are detected when the board renders; there is no separate media-directory watcher. To repair a moved/missing source, select the card and run excali-board-media-relink. This is an explicit replacement: no same-named file is silently substituted. Org ID reference repair does not guess media paths.

Implemented scope (0.3.0)

  1. [X] Independent Org cards, fixed popup editing and title-preserving conversion.
  2. [X] Vault selection/persistence, recursive file picker and native attachment path resolution.
  3. [X] Mouse acceptance for endpoint creation and following moved cards; automated binding, conversion, undo/redo and save/reload checks.
  4. [X] Vault-bounded Org ID relocation/repair, reverse board references and external updates with unsaved-buffer conflict protection.
  5. [X] Clickable reading-state Org links and inline local images. No direct checkbox or TODO toggling.
  6. [X] Create-and-connect at an arrow endpoint; named groups/regions.
  7. [X] Typed static media cards: PDF first-page preview, image/video thumbnails, audio filename plus cover or fallback icon. No playback or PDF navigation.

Org Babel: diagrams in documents

ob-excali.el adds the excali source-block language. It uses the same DSL, layout and renderer as .excalidsl files; no external process or interactive canvas is needed. Build with make and configure the clone’s load-path as above, then enable it without discarding your other Babel languages:

(with-eval-after-load 'org
  (org-babel-do-load-languages
   'org-babel-load-languages
   (cons '(excali . t)
         (assq-delete-all 'excali (copy-tree org-babel-load-languages)))))

Add this block to an Org document:

node system "Application"
node system.ui "Interface"
node system.api "API" below system.ui
node database "Database" right of system level with system.api

edge system.ui -> system.api "request" from: bottom to: top
edge system.api -> database "query" from: right to: left

In the Org buffer, C-c C-c executes the block using normal Babel confirmation and inserts a result link. C-c ' opens the source in excali-dsl-mode; in that edit buffer C-c ' returns to Org, while C-c C-c follows Org’s edit-buffer save-and-exit binding. Use C-c C-x C-v in Org to toggle inline images (SVG display depends on your Emacs image support).

  • :file is required. Its extension selects .svg, .png or .excalidraw. Use :results file graphics for images and :results file for a native scene. The default is :results file :exports results.
  • Relative output paths use Babel’s execution directory: normally the Org file’s directory, or :dir when provided. Use :mkdirp yes to create missing directories. Outputs must be local files.
  • PNG/SVG use excali-export-background, excali-export-padding, excali-export-scale and excali-export-embed-scene. Embedded scenes are enabled by default, so these images can also be reopened with excali-open.
  • Generation uses a temporary file and an atomic rename. Parse, layout or rendering failures preserve an existing output and its Org result link. DSL errors identify the Org file, block start and line/column within the expanded DSL body (after any Babel noweb expansion).
  • Execution confirmation and :eval are controlled by Org, not disabled by this adapter. Standard Babel header expressions and noweb references still obey Org’s own processing rules: execute only trusted documents.
  • Sessions, :var substitution, :cmd / :cmdline, :prologue and :epilogue are not supported. No automatic execution or scene-to-source synchronization is installed.

See the runnable Org example for all three output formats.

Presentation backends

The module renders into its own offscreen framebuffer; excali-backend chooses how that reaches the screen:

  • canvas — copy every frame into one Canvas image.
  • tiles — split the window into Canvas tiles; the module diffs each tile against its last contents and only changed tiles get canvas-refresh. Drags also repaint only the damaged rectangle.
  • layer (macOS) — a CoreAnimation layer over the Emacs view shows the framebuffer via IOSurface, bypassing canvas-refresh. Mouse events still go to Emacs.
  • auto (default) — layer on graphical macOS frames when the module was built with it, tiles everywhere else. An explicit choice wins.

Independently of the backend, rendering only repaints what changed: elements outside the view are culled, drags repaint the damaged rectangle, and panning by whole device pixels shifts the framebuffer in place and paints only the newly exposed strips (sub-pixel trackpad deltas accumulate first). Zooming (C-wheel, pinch, === / -) shows a preview first: the module scales the pixels of the last full render about the anchor with bilinear filtering (about 0.5 ms at 1x, 2 ms at 2x), and a crisp full render follows once no zoom step came for excali-zoom-preview-delay (0.1 s; nil renders every step fully). Steps more than excali-zoom-preview-limit (4x) away from that render render fully.

A scene shown in several windows gets a view per window, each with its own zoom, scroll, framebuffer and surfaces; a change made in one window redraws the others after the command.

M-x excali-bench-backends in an excali buffer compares them; make bench does the same in a fresh GUI Emacs and writes bench.txt.

Text and fonts

Text follows Excalidraw’s layout exactly wherever it does not depend on glyph widths: per-family line heights and baselines (FONT_METADATA, getVerticalOffset), heights of lines × size × line height, the tokenizer-based wrapText (CJK, emoji, hyphen and whitespace rules), fixed-width (autoResize false) text, and labels in rectangles, ellipses, diamonds, sticky notes and on arrows (padding, maximum sizes, alignment, container growth, labelPosition). Pango only measures and draws single lines, so widths match the web app when the same fonts are installed. make fonts downloads them into fonts/, which is registered with the font backend (CoreText on macOS with Homebrew Pango, fontconfig elsewhere) when excali loads; without them text falls back to system fonts, see excali-font-families and M-x excali-font-report.

Element library

The personal library lives in excali-library-file in Excalidraw’s .excalidrawlib format, so it moves freely between excali and excalidraw.com. C-c l a adds the selection, C-c l i inserts items, M-x excali-library-import / excali-library-export merge and write library files.

C-c l b browses it as thumbnails: RET or a click inserts the item into the scene, d deletes it (after asking), g redraws, o opens the official collection. C-c l d deletes items by name.

C-c l o (excali-library-browse-official) lists the official collection of libraries.excalidraw.com: name, item count, downloads, last update, authors and description. RET previews a library (the site’s picture and every item), a adds all of it, / filters by a regexp over names, descriptions, authors and item names, g fetches the index again, o shows the library on the site; S sorts by the column at point; a ✓ marks libraries wholly in the personal library. The preview takes the list’s place: RET inserts the item at point, + adds it to the personal library, a adds them all, q goes back to the list where you left it. Items the library holds say ✓ in library; added ones say ✓ added and light up. Items keep their names; unnamed ones take the library’s. Adding a library twice adds nothing twice.

Thumbnails are cached as PNG files in excali-library-thumbnail-directory; those not cached yet show as grey squares and are drawn in the background, those in view first, so large libraries open at once. The index is fetched from excali-library-official-url (the files the site’s “Add to Excalidraw” button hands to excalidraw.com) and cached in excali-library-official-cache.

Storage configuration

Library storage paths are customizable (including in v0.2.0). The defaults below are relative to user-emacs-directory, commonly ~/.emacs.d/ or ~/.config/emacs/:

VariableDefault relative pathContents
excali-library-fileexcali/library.excalidrawlibPersonal element library; preserve or back up
excali-library-thumbnail-directoryexcali/thumbnails/Generated PNG thumbnails; can be regenerated
excali-library-official-cacheexcali/official-libraries.jsonDownloaded official collection index; can be fetched again

To keep this data outside your Emacs configuration directory, put the following in your init file, before first using the library (or in your use-package :init section). These are example locations, not additional dependencies:

(setq excali-library-file
      (expand-file-name "~/.local/share/excali/library.excalidrawlib")
      excali-library-thumbnail-directory
      (expand-file-name "~/.cache/excali/thumbnails/")
      excali-library-official-cache
      (expand-file-name "~/.cache/excali/official-libraries.json"))

To disable thumbnail caching on disk instead, set:

(setq excali-library-thumbnail-directory nil)

Thumbnails are then kept in memory for the session and rendered again when needed in a later session. This setting does not delete existing thumbnails.

Changing paths does not migrate existing files. Before restarting Emacs with the new settings, back up and copy or move the existing library.excalidrawlib to the new location (create its parent directory if needed). Do not overwrite another library already at that destination; use excali-library-import to merge libraries instead. The library is user data, not a disposable cache. Old thumbnails and the downloaded index can be removed once Emacs is no longer using the old paths; they will be regenerated or downloaded as needed. Restart after changing paths if the library has already been used, since its contents are also cached in memory.

Images, frames and export

Images show files[fileId].dataURL; each file is decoded once per session in the module and freed when no excali buffer uses it. PNG is decoded by Cairo; SVG needs librsvg, WebP libwebp, and JPEG, GIF (first frame), BMP, ICO etc. gdk-pixbuf. These are optional: the Makefile uses whichever pkg-config finds (disable with EXCALI_WITH_RSVG=no, EXCALI_WITH_PIXBUF=no, EXCALI_WITH_WEBP=no), and undecodable images show upstream’s error placeholder. Flips (scale), crop, rounded corners and opacity follow upstream. excali-insert-image ids files by SHA-1 and scales raster images larger than excali-image-max-size (1440) down, as PNG.

Frames draw upstream’s outline (2px, radius 8, constant on screen) and their name above (14px, cut with an ellipsis), clip their children and multiply their opacity into them.

excali-export-png / excali-export-svg follow upstream’s export: padding excali-export-padding (10), scale excali-export-scale (1–3), excali-export-background, frame names as Helvetica 14px text, a single selected frame exported alone, and excali-export-embed-scene (on by default) embedding the scene as upstream does (PNG tEXt chunk application/vnd.excalidraw+json with a zlib-compressed payload; SVG <metadata> payload version 2). The SVG is Cairo’s (text as glyph paths, images embedded as PNG) with upstream’s root size and metadata. No dark-mode export yet.

Layout

  • src/excali-render.c — roughjs-style strokes, hachure fills, Pango text.
  • src/excali-text.c — font registration, Pango line measuring and drawing.
  • src/excali-module.c — module glue, framebuffer, tile diffing.
  • src/excali-preview.c — zoom previews scaled from rendered pixels.
  • src/excali-layer.m — CoreAnimation overlay backend (macOS).
  • src/excali-image.c — image decoding, the image cache, image and placeholder drawing.
  • src/excali-frame.c — frame outlines, names and child clipping.
  • src/excali-export.c — PNG/SVG export surfaces, tEXt chunks, zlib.
  • src/excali-fill.c — bucket-fill region search: raster flood fill, gap bridging, outline tracing with keyholed holes.
  • excali-core.el — module loading, shared state, document model, file reading and saving (JSON written like upstream’s JSON.stringify(…, null, 2)).
  • excali-restore.el — restore/migration of loaded scenes (port of upstream restore.ts); unknown element types and fields are kept.
  • excali-index.el — fractional z-order indices (fractional-indexing, syncInvalidIndices, syncMovedIndices).
  • excali-text.el — fonts, text measurement, wrapping, bound text and arrow labels.
  • excali-view.el — framebuffer, presentation backends, damage, scroll reuse, pan/zoom.
  • excali-select.el — selection model, groups, box selection, overlays.
  • excali-handles.el — selection UI geometry, transform handles.
  • excali-transform.el — resize / rotate math.
  • excali-hit.el — hit testing.
  • excali-create.el — drawing tools, click-click lines, tool lock.
  • excali-actions.el — flip, align, distribute, lock, styles, zoom to fit.
  • excali-binding.el — arrow binding, and excali--follow (arrows and labels following moved shapes).
  • excali-linear.el — point editor for lines and arrows.
  • excali-snap.el — grid mode and object snapping.
  • excali-frame.el — frame membership and behavior.
  • excali-erase.el — eraser and element links.
  • excali-tools.el — laser, eye dropper, autoshape, lasso.
  • excali-bucket.el — bucket fill.
  • excali-library.el — .excalidrawlib library: add, insert, import, export, browse, delete; the official collection.
  • excali-edit.el — drags, mouse commands, text, grouping, z-order.
  • excali-cursor.el — pointer shapes (upstream’s CSS cursors; on macOS the module shows them over the canvas, elsewhere the nearest Emacs pointer).
  • excali-history.el — undo/redo with shared per-element snapshots.
  • excali-clipboard.el — copy/cut/paste/duplicate.
  • excali-image.el — image render data, the files map and the module’s image cache, excali-insert-image.
  • excali-frame-render.el — frame titles, name label bounds, frame render data.
  • excali-export.el — PNG/SVG export and import of embedded scenes.
  • excali-dsl.el — Excali DSL model, native drawing, and excali-dsl-mode.
  • excali-board.el — optional Org board derived mode, references and live previews.
  • ob-excali.el — optional Org Babel adapter for SVG, PNG and native scenes.
  • excali-dsl-parser.el — lexer, parser, style schema and validation.
  • excali-dsl-constraints.el — joint relative-position/container-size solver.
  • excali-bench.el — backend benchmarks.
  • excali.el — keymap, major mode and entry points.

Known gaps

Not ported: collaboration, AI / magic frames, embeddables, Mermaid, the app shell, and interactive image cropping (a stored crop renders). Autoshape recognition is a heuristic rather than upstream’s convertToShape. Text is typed in the minibuffer with a live preview rather than on the canvas. Rough strokes resemble but do not match the web renderer pixel for pixel. Without make fonts, text uses system fallback fonts, so widths and wrapping differ from the web app. Native pointer shapes need macOS; elsewhere the nearest Emacs pointer shape stands in. A stylus draws as a mouse does: pen pressure is not read, and freedraw strokes use simulated pressure. Excali DSL is one-way: regenerating from the source replaces manual changes on the canvas. Linux and Windows builds are cross-compiled but not run by the author; see Platforms.

Changes

See CHANGELOG.org.

Acknowledgements

excali-mode was indirectly inspired by video.el, which uses Emacs’s Canvas API to display images and video inside the editor.

License

GPL-3.0-or-later; see LICENSE. excali-mode ports code from Excalidraw, roughjs and perfect-freehand (MIT) and fractional-indexing (CC0); their notices are in THIRD-PARTY-NOTICES. The fonts make fonts downloads keep their own licenses, listed in fonts/README.

About

Excalidraw inside Emacs 32: edit .excalidraw files, drawn through the new canvas API

Topics

Resources

Stars

64 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages