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).
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.
(use-package svg-margin
:ensure (:host github :repo "chiply/svg-margin"))(use-package svg-margin
:straight (:host github :repo "chiply/svg-margin"))(add-to-list 'load-path "/path/to/svg-margin")
(require 'svg-margin)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.
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 |
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))))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)))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)))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.
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.)
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 fringeA 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)))))GPL-3.0. See LICENSE.