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.
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.
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.
- 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+arrowgrows 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.excalidrawfiles. - 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.
- Emacs 32 (master) built with module support and the
canvasimage type. To check, evaluate this in a graphical Emacs (M-:); it should returnt:(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).
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 -Qmake 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.
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.
- Evaluate the configuration, or restart Emacs.
- 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-modein its buffer. - In the DSL source buffer, press
C-c C-cto preview; press it again to refresh the same canvas. These bindings do not belong to the canvas buffer. - Press
C-c C-ein the source buffer and choose an.excalidrawoutput file.
| Extension | Contents | How to open |
|---|---|---|
.excalidsl | Excali DSL text source | excali-dsl-mode; C-c C-c previews |
.excalidraw | Native JSON scene | M-x excali-open |
.edsl | Former DSL syntax | No 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))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.
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.txtAll 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 CoreAnimationlayerbackend 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/binmust be on Emacs’PATH. Fonts infonts/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.
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.
| Key | Action |
|---|---|
v 1, h, r 2, d 3, o 4, a 5, l 6, p x 7, t 8 | select, hand, rectangle, diamond, ellipse, arrow, line, pen, text |
a again | cycle sharp / round / elbow arrows |
f, n, e 0 | frame, sticky note, eraser (e again: previous tool) |
k | laser pointer: a fading red trail, never saved |
b | bucket fill: click inside a closed area (b again: next color, M-click: pick the color) |
X | autoshape: a freehand stroke becomes a rectangle, ellipse, diamond or line |
i G, S | eye dropper: next click picks a background, stroke (M-click: the other) |
C-M-drag M-s-drag | lasso select; M-x excali-toggle-lasso makes it the v tool |
q | lock the tool (keep drawing) |
| drag | create; shift = square / 15° lines, meta = from center |
short drag with a=/=l, then clicks | multi-point line; click the last point, RET or ESC to finish |
click, S-click, box drag | select, toggle, box select (contain) |
| drag inside the selection box | move |
| corner handles, border, round handle | resize (shift keeps ratio, meta from center), rotate (shift snaps 15°) |
| double-click | enter group, edit text, add or edit a shape’s label, add text |
s, g, F | style 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, V | flip horizontally / vertically |
TAB | cycle rectangle / diamond / ellipse |
| arrows, =S-=arrows | nudge 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-G | duplicate, select all, group, ungroup |
s-] s-[, M-s-] M-s-[ | forward / backward, front / back |
S-s-=arrows, =M-h M-v | align, distribute |
| =s-=arrows (=C-=arrows) | flowchart: add a linked node; repeat for siblings, any other key keeps them |
| =M-=arrows | flowchart: go to the linked node that way; repeat to cycle |
s-L | lock / unlock elements |
s-k, click a link icon | set a link (URL or ?element=ID), follow it |
s-', M-s | grid, object snapping (super at the press suppresses/inverts) |
M-D | toggle the dark theme |
C-c l a, C-c l i, C-c l b, C-c l d, C-c l o | library: 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 |
9 | insert an image file at the view centre |
s-s, C-x C-s | save |
s-E, C-c C-e | export PNG or SVG (by extension; the selection if any, C-u for all) |
C-c C-b, C-c C-p | cycle backend, toggle 1x/2x (debug) |
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-cdraws it in a scene beside the source; repeat to refresh.C-c C-eexports a native, editable.excalidrawfile.- Set
excali-dsl-render-on-saveto 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.
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.
- 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.
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.
- Put point on a heading in a local, file-backed Org buffer.
- Run
M-x excali-board-add-heading. - Choose an existing board buffer,
[Open board file...], or[New board]. - Save the Org source yourself: an
IDis added if needed but never saved automatically. Save the board separately withC-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.
| Command/key | Action |
|---|---|
M-x excali-board-new | Create and save a board inside the configured vault; show it on the right |
M-x excali-board-open | Open an existing marked board on the right |
C-c C-i | Choose an Org file and heading to insert |
C-c C-n | Create an independent Org text card |
C-c C-w | Convert an independent card into a heading in a local Org file |
C-c C-o | Visit the selected card’s source heading, retaining the board pane |
Double-click an Org card / C-c C-e | Edit the Org card in a fixed popup |
M-n / M-p | Scroll selected card down/up one page |
M-x excali-board-replace-source | Choose a different heading without replacing the card or its connections |
C-c C-a | Insert a static media card or generic file attachment |
Double-click a file/media card / C-c C-f | Open the original file in another Emacs window |
M-x excali-board-media-relink | Explicitly choose a replacement file for the selected media card |
M-x excali-board-media-refresh | Discard transient media previews and retry generation |
C-c C-l | Connect two selected cards/shapes with an optionally labeled bound arrow |
Double-click a free arrow endpoint / C-c C-j | Create and connect a new Org note |
C-c C-t | Create a named region from the selection |
C-c C-v | Navigate to a named group or region |
C-c C-r | Refresh the selected card, or all cards if none is selected |
M-x excali-board-refresh-all | Refresh all cards and report source errors |
M-x excali-board-return | Return from Org to the last used board |
C-x C-s | Save 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.
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.
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.
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.
- [X] Derived mode, versioned document marker and local extension hooks.
- [X] Both insertion entry points, source navigation/sync, save/reopen.
- [X] Explicit prototype migration and regression tests.
- [X] Dedicated Org content renderer, subtree formatting, clipping/scrolling.
- [X] In-card source editing, source replacement, local attachments and bound relationships.
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.
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.
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.
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.
- 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-groupnames and groups the selected shapes.M-x excali-board-rename-grouprenames the selected group. These use standardgroupIds; the name lives in each member’scustomData.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. UseM-x excali-rename-frameto 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.
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:
| Type | Reading-state content |
|---|---|
| First-page preview only | |
| Image | Proportional thumbnail; GIFs remain static |
| Video | First-frame thumbnail, no playback |
| Audio | Embedded 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.
- Configure
excali-board-vaultsonce as shown above. - Browse an Org file inside that vault in the left pane. Run
excali-board-newand choose a filename: the board is saved inside the vault and shown on the right. Useexcali-board-openfor an existing board. - Press
C-c C-aand select a PDF, image, video or audio file. The card shows its type and filename while any background preview is being prepared. - Drag the card to position it. Double-click its body or filename to open
the original file in another Emacs window;
C-c C-fdoes the same. - 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.
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.
- [X] Independent Org cards, fixed popup editing and title-preserving conversion.
- [X] Vault selection/persistence, recursive file picker and native attachment path resolution.
- [X] Mouse acceptance for endpoint creation and following moved cards; automated binding, conversion, undo/redo and save/reload checks.
- [X] Vault-bounded Org ID relocation/repair, reverse board references and external updates with unsaved-buffer conflict protection.
- [X] Clickable reading-state Org links and inline local images. No direct checkbox or TODO toggling.
- [X] Create-and-connect at an arrow endpoint; named groups/regions.
- [X] Typed static media cards: PDF first-page preview, image/video thumbnails, audio filename plus cover or fallback icon. No playback or PDF navigation.
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).
:fileis required. Its extension selects.svg,.pngor.excalidraw. Use:results file graphicsfor images and:results filefor 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
:dirwhen provided. Use:mkdirp yesto create missing directories. Outputs must be local files. - PNG/SVG use
excali-export-background,excali-export-padding,excali-export-scaleandexcali-export-embed-scene. Embedded scenes are enabled by default, so these images can also be reopened withexcali-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
:evalare 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,
:varsubstitution,:cmd/:cmdline,:prologueand:epilogueare not supported. No automatic execution or scene-to-source synchronization is installed.
See the runnable Org example for all three output formats.
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 getcanvas-refresh. Drags also repaint only the damaged rectangle.layer(macOS) — a CoreAnimation layer over the Emacs view shows the framebuffer via IOSurface, bypassingcanvas-refresh. Mouse events still go to Emacs.auto(default) —layeron graphical macOS frames when the module was built with it,tileseverywhere 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 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.
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.
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/:
| Variable | Default relative path | Contents |
|---|---|---|
excali-library-file | excali/library.excalidrawlib | Personal element library; preserve or back up |
excali-library-thumbnail-directory | excali/thumbnails/ | Generated PNG thumbnails; can be regenerated |
excali-library-official-cache | excali/official-libraries.json | Downloaded 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 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.
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’sJSON.stringify(…, null, 2)).excali-restore.el— restore/migration of loaded scenes (port of upstreamrestore.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, andexcali--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—.excalidrawliblibrary: 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 Emacspointer).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, andexcali-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.
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.
See CHANGELOG.org.
excali-mode was indirectly inspired by video.el, which uses Emacs’s Canvas API to display images and video inside the editor.
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.