I run Claude Code inside Emacs via vterm. Claude Code's TUI uses various Unicode symbols for its thinking spinner and status indicators. My monospace font doesn't have all of them, so Emacs falls back to a proportional font like Arial Unicode MS. The fallback glyphs get their native width instead of the monospace cell width, and you end up with jittery columns and spinner animations that wobble around.
iTerm2 doesn't have this issue because terminal emulators force every glyph into a fixed cell grid. Emacs GUI just… doesn't.
I wrote a two-part patch for emacs-macport to fix this.
What's going on
You can check which font Emacs picks for any character:
|
|
Most coding fonts, even Nerd Font patched ones, don't cover the Dingbats block (U+2700-U+27BF) and a bunch of other ranges. Emacs falls back to whatever has the glyph, and that font's advance width won't match your cell grid.
Part 1: fix the layout (xdisp.c)
The function gui_produce_glyphs computes pixel width for each character. Right
after it sets it->pixel_width from the font metrics, I clamp it to the cell
grid when a fallback font is in use:
|
|
A few things I learned the hard way:
- Use
face->ascii_face->font, not the global default face. Otherwise you breakmixed-pitch-modewhere some faces are supposed to be proportional. - Check both fonts' spacing: the face must be mono, and the fallback must be
proportional. CJK fonts report
spacing=0(proportional) even though they're intentionally assigned by the fontset - so we also restrict to single-width chars (cols =1=) to avoid clamping CJK. - Don't touch
pcm->width. That pointer goes to the font's shared glyph cache. If you modify it, you corrupt every future render of that character across all buffers.
This alone gets rid of the jitter for single-width fallback glyphs. They now occupy exactly one cell regardless of their native advance width. They might visually overflow into the next cell, but the grid is stable.
Part 2: scale the glyphs (macfont.m)
To deal with the overflow, I scale down oversized fallback glyphs in
macfont_draw, which is where Core Text actually renders things.
Two things bit me here:
GCD threading. The macport dispatches drawing to a GCD block on a separate
thread. You can't read Lisp_Object values from there because the garbage
collector runs on the main thread. I hit a SIGSEGV before figuring this out. The
fix: compute everything you need before MAC_BEGIN_DRAW_TO_FRAME and pass plain
C scalars into the block.
CTM leaking. Any CGContextScaleCTM you apply inside the drawing block
persists for the rest of that draw call. Wrap it in
CGContextSaveGState / CGContextRestoreGState.
|
|
The translate-scale-translate trick scales uniformly around the text origin. Don't
manually scale the positions array on top of this - the CTM already handles it
and you'd end up double-scaling.
The clip rect prevents sub-pixel bleeding when the cursor redraws the glyph. Without it, you get visual artifacts on cursor movement. The rect and all Lisp_Object accesses are precomputed on the Lisp thread because the drawing block runs on a separate GCD thread where the garbage collector could invalidate Lisp_Objects.
I also only scale proportional fonts. Monospace fonts should never need it.
Applying with Nix
|
|
Limitations
The xdisp.c part works on all platforms since gui_produce_glyphs is shared
code. The macfont.m scaling only works on macport. The NS/Cocoa build has a
different code path in the same file, and Linux (X11/GTK/PGTK) uses completely
different font backends (xftfont.c, ftcrfont.c). Those would need their own
versions of the scaling patch.
Also the uniform scaling makes fallback glyphs a bit smaller than surrounding text. iTerm2 seems to handle it slightly differently but I haven't dug into exactly how yet.
Full patch
The full patch (for emacs-macport 30.x):
--- a/src/xdisp.c 2026-08-30 06:56:17.853239362 +0900
+++ b/src/xdisp.c 2026-08-30 17:16:24.807600073 +0900
@@ -32724,6 +32724,30 @@
it->phys_ascent = pcm->ascent + boff;
it->phys_descent = pcm->descent - boff;
it->pixel_width = pcm->width;
+ {
+ struct face *ascii_face = face->ascii_face;
+ if (ascii_face && ascii_face->font
+ && font != ascii_face->font)
+ {
+ Lisp_Object spacing
+ = ascii_face->font->props[FONT_SPACING_INDEX];
+ Lisp_Object fb_spacing
+ = font->props[FONT_SPACING_INDEX];
+ bool face_is_mono
+ = FIXNUMP (spacing)
+ && XFIXNUM (spacing) >= FONT_SPACING_MONO;
+ bool fallback_is_proportional
+ = !FIXNUMP (fb_spacing)
+ || XFIXNUM (fb_spacing) < FONT_SPACING_MONO;
+ if (face_is_mono && fallback_is_proportional)
+ {
+ int cell = ascii_face->font->space_width;
+ int cols = CHARACTER_WIDTH (it->char_to_display);
+ if (cols == 1 && cell > 0)
+ it->pixel_width = cell * cols;
+ }
+ }
+ }
/* Don't use font-global values for ascent and descent
if they result in an exceedingly large line height. */
if (it->override_ascent < 0)
--- a/src/macfont.m 2026-08-30 14:43:00.724157934 +0900
+++ b/src/macfont.m 2026-08-30 17:04:48.065168085 +0900
@@ -2906,11 +2906,11 @@
background_rect = CGRectNull;
text_position = CGPointMake (x, -y);
+ CGFloat total_width = 0;
glyphs = xmalloc (sizeof (CGGlyph) * len);
{
CGFloat advance_delta = 0;
int i;
- CGFloat total_width = 0;
positions = xmalloc (sizeof (CGPoint) * len);
for (i = 0; i < len; i++)
@@ -2928,6 +2928,23 @@
}
}
+ bool should_scale = false;
+ CGFloat scale_factor = 1.0;
+ CGRect scale_clip_rect = CGRectNull;
+ {
+ Lisp_Object spc = s->font->props[FONT_SPACING_INDEX];
+ bool font_is_proportional
+ = !FIXNUMP (spc) || XFIXNUM (spc) < FONT_SPACING_MONO;
+ if (font_is_proportional && total_width > 0 && s->width > 0
+ && total_width > s->width)
+ {
+ should_scale = true;
+ scale_factor = (CGFloat) s->width / total_width;
+ scale_clip_rect = CGRectMake (x, -y - FONT_DESCENT (s->font),
+ s->width, FONT_HEIGHT (s->font));
+ }
+ }
+
/* We assume `macfont_info' is pointing to valid data during the
execution of the code between MAC_BEGIN_DRAW_TO_FRAME and
MAC_END_DRAW_TO_FRAME in a non-main thread, because the thread
@@ -2974,6 +2991,17 @@
CGContextSetTextMatrix (context, atfm);
CGContextSetTextPosition (context, text_position.x, text_position.y);
+ if (should_scale)
+ {
+ CGContextSaveGState (context);
+ CGContextClipToRect (context, scale_clip_rect);
+ CGContextTranslateCTM (context, text_position.x,
+ text_position.y);
+ CGContextScaleCTM (context, scale_factor, scale_factor);
+ CGContextTranslateCTM (context, -text_position.x,
+ -text_position.y);
+ }
+
if (macfont_info->color_bitmap_p || macfont_info->svg_p)
{
if (len > 0)
@@ -2986,6 +3014,9 @@
CGContextSetFontSize (context, font_size);
CGContextShowGlyphsAtPositions (context, glyphs, positions, len);
}
+
+ if (should_scale)
+ CGContextRestoreGState (context);
}
#if defined (XMALLOC_BLOCK_INPUT_CHECK) && DRAWING_USE_GCD
Last modified on 2026-08-30