Turn the window margins into a flexible, multi-column gutter that many
independent sources ("providers") can draw into, with their indicators packed
side by side on the same line. Supports Emacs 29.1+ (the default SVG renderer
needs a graphical frame; the text renderer also works in the terminal).
Overview
Unlike the fringe — which renders only monochrome bitmaps and shows a single bitmap per line per side — a margin can display arbitrary SVG. svg-margin composites every indicator for a line/side into one SVG image at exact pixel coordinates, on either the left or the right margin:
- Multi-column packing — several indicators on the same line stack side by side into columns (densely, or into dedicated per-provider lanes — see Arrangement); the margin grows to the widest line.
- Decoupled providers — independent packages can each contribute to the same gutter without knowing about one another.
- Any drawing — built-in shapes (dot, ring, bar, box, triangle), centred
text/glyphs (e.g. a Nerd Font icon), or a fully custom
:drawfunction. - Interactive indicators — per-indicator hover help, a left-click action, and a right-click context menu.
- No jitter — margin/fringe widths are reserved buffer-locally so switching to a buffer doesn't shift its text as indicators render in.
svg-margin is the rendering engine only: it ships no providers and no
colours. You (or a small adapter) supply providers. The column-allocation
compositor — the provider registry, indicator collection, and per-line
arrangement — lives in the renderer-independent svg-margin-core; this package
is the SVG renderer built on top of it.
Installation
With elpaca (use-package)
(use-package svg-margin :ensure (:host github :repo "chiply/svg-margin"))
With straight.el (use-package)
(use-package svg-margin :straight (:host github :repo "chiply/svg-margin"))
Manual
(add-to-list 'load-path "/path/to/svg-margin") (require 'svg-margin)
Quick start
A provider is just a function of one argument BUFFER that returns a list of indicator plists. Register one, then enable the mode:
(svg-margin-register-provider 'todo (lambda (_buffer) (list (list :line 10 :shape 'dot :color "#cc3333") (list :line 10 :shape 'bar :color "#3333cc" :column 1) (list :line 25 :text "!" :side 'left :face 'warning)))) (svg-margin-mode 1) ; or (global-svg-margin-mode 1)
Indicators sharing a (line, side) are packed into columns and drawn into a
single composite image; the margin width on that side grows to the widest line.
global-svg-margin-mode enables the mode in file-visiting buffers by default;
set svg-margin-global-predicate to a function of your own to change which
buffers qualify.
Indicator plists
An indicator plist recognises:
| Key | Meaning |
|---|---|
:pos/:line |
buffer position or 1-based line (one is required) |
:side |
left (default svg-margin-default-side) or right |
:column |
column index (0 = nearest the text); soft hint in fill, a dedicated lane in fixed (see Arrangement) |
:priority |
higher is packed first (default 0) |
:shape |
a registered shape symbol (see svg-margin-define-shape) |
:text |
a short string drawn centred (e.g. an icon glyph or mark letter) |
:font |
font family for :text (e.g. a Nerd Font); defaults to default |
:scale |
multiplies the glyph height fraction (raise for icon glyphs) |
:weight |
font weight for :text (default "bold") |
:draw |
a function (SVG X Y W H COLOR) for full control |
:color/:face |
fill colour, or a face whose foreground is used |
:help |
tooltip string (shown when hovering just this indicator) |
:action |
a command run on left/middle click (also gives a hand pointer) |
:action-help |
a short verb phrase, e.g. "jump"; tooltip reads "… click to jump" |
:menu |
an alist of (LABEL . COMMAND); right-click pops up a context menu |
Built-in shapes
dot, circle (hollow ring), bar, box, triangle. Register your own with
svg-margin-define-shape:
(svg-margin-define-shape 'diamond (lambda (svg x y w h color) (let ((cx (+ x (/ w 2.0))) (cy (+ y (/ h 2.0))) (r (* (min w h) 0.34))) (svg-polygon svg (list (cons cx (- cy r)) (cons (+ cx r) cy) (cons cx (+ cy r)) (cons (- cx r) cy)) :fill color))))
Per-provider defaults
A provider can set defaults so it need not stamp every indicator, and users can relocate any provider's margin declaratively (without editing it):
(svg-margin-register-provider 'marks #'my-marks-fn :side 'right :priority 5) ;; Move a third-party provider to the other margin, no source edit: (setq svg-margin-provider-sides '((some-other-provider . right)))
Arrangement: fill vs fixed columns
svg-margin-arrangement controls how indicators sharing a line are assigned to
columns:
fill(default) — pack indicators densely from the column nearest the text, ordered by:priority. An explicit:columnis a soft hint that is bumped aside when its slot is already taken. Best when you just want indicators to stack tightly.fixed— treat:columnas a dedicated lane kept on every line. Each indicator stays in its assigned column, empty lanes are left empty, and indicators without a:columnfill the free lanes by priority. When two indicators claim the same lane the higher:prioritykeeps it (seesvg-margin-fixed-collisionto re-flow the loser into a free lane instead of dropping it). This gives each provider a stable column the eye can track — the behaviour requested for shared margins, where flymake, outline and hideshow indicators each want their own column.
Assign a provider's lane declaratively with svg-margin-provider-columns — no
need to edit the provider — then switch on the fixed arrangement:
(setq svg-margin-arrangement 'fixed svg-margin-provider-columns '((flymake . 0) ; 0 = nearest the text (outline . 1) (hideshow . 2)))
Each margin can use a different arrangement by giving an alist instead of a symbol:
(setq svg-margin-arrangement '((left . fixed) (right . fill)))
Renderers: SVG or built-in margin text
svg-margin-renderer selects how the composed indicators are drawn:
svg(default) — composite each line into one SVG image: arbitrary shapes, colours and exact pixel placement. Needs a graphical frame with SVG support.text— draw each indicator's glyph straight into the built-in margin as ordinary characters, with no image. Works in a terminal (emacs -nw) as well as graphical frames. An indicator shows its:textglyph, the character mapped for its:shapeinsvg-margin-shape-characters, orsvg-margin-text-fallback;:color/:facecolour it (:draw,:font,:weightand:scaleare honoured only by the SVG renderer).
(setq svg-margin-renderer 'text)
Glyph width matters. The built-in margin reserves width in whole character cells, so only glyphs with a single, consistent cell advance line up. Many Nerd Font, emoji and CJK icon glyphs are 1.5–2 cells wide (and ligatures vary), so they overflow or misalign their column under the
textrenderer — keep thesvgrenderer for arbitrary icons.
Switching arrangement / renderer at runtime
svg-margin-arrangement and svg-margin-renderer apply immediately — the
display re-renders whether you change them via M-x customize or these
commands:
| Command | Effect |
|---|---|
svg-margin-set-arrangement |
pick fill or fixed |
svg-margin-toggle-arrangement |
flip fill ⇄ fixed |
svg-margin-set-renderer |
pick svg or text |
svg-margin-toggle-renderer |
flip svg ⇄ text |
Arrangement and renderer are independent, so the two toggles cover all four
combinations — handy for eyeballing each while developing a provider. (A plain
setq of these variables does not re-render on its own; use the commands,
Customize, or follow it with M-x svg-margin-refresh-all.)
Reclaiming the fringe
To move what a package draws in the fringe into the margin, write a provider
that reads that package's data and set svg-margin-disable-fringe to reclaim
the fringe space:
;; A provider that mirrors evil's marks into the left margin. (svg-margin-register-provider 'evil-marks (lambda (buffer) (with-current-buffer buffer (cl-loop for (ch . m) in (bound-and-true-p evil-markers-alist) when (markerp m) collect (list :pos (marker-position m) :text (char-to-string ch) :side 'left :face 'font-lock-keyword-face)))) :side 'left) (setq svg-margin-disable-fringe 'left) ; reclaim the left fringe
Hover highlight (opt-in)
A margin only delivers mouse enter/leave through the help-echo machinery, so the
hover highlight needs a show-help-function hook. The easy way is the global
minor mode — it installs that hook (chaining any existing one) and sets
svg-margin-hover-highlight:
(svg-margin-hover-mode 1)A svg-margin-hover-color background is then drawn behind the indicator under
the mouse. (Clicks and tooltips work regardless of this mode.)
If you already maintain your own show-help-function wrapper, call the public
svg-margin-note-help from it and set svg-margin-hover-highlight yourself
instead of enabling the mode:
(setq svg-margin-hover-highlight t) (let ((orig show-help-function)) (setq show-help-function (lambda (help) (svg-margin-note-help help) (when orig (funcall orig help)))))
License
GPL-3.0. See LICENSE.