GitHub

CI License: GPL-3.0

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 :draw function.
  • 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 :column is a soft hint that is bumped aside when its slot is already taken. Best when you just want indicators to stack tightly.
  • fixed — treat :column as a dedicated lane kept on every line. Each indicator stays in its assigned column, empty lanes are left empty, and indicators without a :column fill the free lanes by priority. When two indicators claim the same lane the higher :priority keeps it (see svg-margin-fixed-collision to 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 :text glyph, the character mapped for its :shape in svg-margin-shape-characters, or svg-margin-text-fallback; :color/:face colour it (:draw, :font, :weight and :scale are 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 text renderer — keep the svg renderer 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 fillfixed
svg-margin-set-renderer pick svg or text
svg-margin-toggle-renderer flip svgtext

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.

Read the original on github.com ↗