Render the tab-bar, tab-line, header-line and mode-line as SVG images instead of laid-out text. Supports Emacs 29.1+ (graphical only).
An SVG image can be any height and is positioned at exact pixel coordinates, so svg-line makes possible things the text engine cannot do uniformly:
- Multi-line bars of arbitrary height (a 3-row tab-bar, a 2-row breadcrumb header-line, a multi-row mode-line).
- Per-line left / centre / right alignment on every row — not just the
last, and without the
:align-to-on-a-non-final-line redisplay freeze. - Tab lines that wrap overflowing tabs onto new rows instead of truncating or horizontally scrolling, with optional single-row centring.
- Interactive indicators: any element can carry a left-click action, a right-click menu, hover help and a hover highlight — across all four bars, including the otherwise–uncooperative tab-bar.
svg-line is the rendering engine only: it ships no content and no colours of
its own. You supply a :content function and styling, then bind it to a target.
(use-package svg-line
:ensure (:host github :repo "chiply/svg-line"))(use-package svg-line
:straight (:host github :repo "chiply/svg-line"))(add-to-list 'load-path "/path/to/svg-line")
(require 'svg-line)Define a line and activate it:
(svg-line-define 'my-mode-line
:target 'mode-line
:layout 'lines
:content (lambda ()
;; one or more rows; each is (LEFT . RIGHT) or a
;; (:left L :center C :right R) plist for a centred middle
(list (cons (list (buffer-name))
(list (format-mode-line "%l:%c")))))
:active #'mode-line-window-selected-p
:background (lambda () "#e7edf6")
:foreground (lambda () "#2a4d77"))
(svg-line-activate 'my-mode-line) ; M-x svg-line-deactivate / svg-line-toggleColour and font options accept a literal value or a zero-argument function evaluated on every render, so theme-dependent colours live in your config and the engine stays theme-agnostic.
lines— rows of(LEFT . RIGHT)(or[LEFT CENTER RIGHT]/ a:left/:center/:rightplist). A segment is a string, a zero-argument function, a bound-variable symbol, or a token:(:svg-bar FRAC W FILL BG),(:svg-pie FRAC FILL BG), or an interactive(:svg-seg TEXT . PLIST)built withsvg-line-seg. Used for the mode-line, header-line and tab-bar.wrap— a flow of(LABEL . STATE)items wrapped across rows, with per-item "current"/"modified" highlighting. Used for tab lines.
Build a clickable segment with svg-line-seg:
(svg-line-seg "buffer.el"
:id 'ml-buffer ; unique hover/identity key
:help "buffer: buffer.el"
:action #'switch-to-buffer ; left/middle click
:action-help "switch buffer" ; the "click to …" hint
:menu '(("Save" . save-buffer) ; right click
("Kill" . kill-this-buffer)))For wrap lines, put :id/:help/:action/:action-help/:menu in each
item's STATE plist. Enable the hover highlight with svg-line-hover-highlight
(it needs show-help-function wired to call svg-line--note-help; see the
docstring). Clicks select the window they land in, and the tab-bar's clicks and
hover are wired up automatically when a tab-bar line is activated.
Mode-line content you already have — a breadcrumb header line, which-func, a
VC indicator — carries keymap/help-echo text properties. Hand the
propertized string to svg-line-segs-from-string and each clickable region
becomes an interactive segment, so it renders through svg-line with its click
and hover intact:
(svg-line-segs-from-string (format-mode-line '(:eval (breadcrumb--header-line))))svg-line positions right-aligned content, inline pies/bars, interactive
segments and wrap breakpoints using svg-line-char-advance — the assumed
pixel width of one character. It can't be measured (librsvg rasterises text
with its own font stack), so it's a calibration constant. By default (nil) it
is derived from the font size via svg-line-char-advance-ratio (0.6, a typical
monospace), which keeps layout aligned across font sizes; pin a number for a
specific font if right-aligned or hover content sits slightly off.
GPL-3.0. See LICENSE.