diff --git a/pkg/fonts/embedded/JetBrainsMono-Regular.ttf b/pkg/fonts/embedded/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..dff66cc Binary files /dev/null and b/pkg/fonts/embedded/JetBrainsMono-Regular.ttf differ diff --git a/pkg/fonts/embedded/MonaspaceArgon-Regular.ttf b/pkg/fonts/embedded/MonaspaceArgon-Regular.ttf deleted file mode 100644 index 92646e1..0000000 Binary files a/pkg/fonts/embedded/MonaspaceArgon-Regular.ttf and /dev/null differ diff --git a/pkg/fonts/embedded/MonaspaceNeon-Regular.ttf b/pkg/fonts/embedded/MonaspaceNeon-Regular.ttf deleted file mode 100644 index c6c859d..0000000 Binary files a/pkg/fonts/embedded/MonaspaceNeon-Regular.ttf and /dev/null differ diff --git a/pkg/fonts/fonts.go b/pkg/fonts/fonts.go index 5b819b6..5c07a2d 100644 --- a/pkg/fonts/fonts.go +++ b/pkg/fonts/fonts.go @@ -65,16 +65,6 @@ const ( AxisLigatures = "liga" // Ligatures ) -// Monaspace specific ranges -const ( - MonaspaceWeightMin = 200 - MonaspaceWeightMax = 800 - MonaspaceWidthMin = 100 - MonaspaceWidthMax = 125 - MonaspaceSlantMin = -11 - MonaspaceSlantMax = 1 -) - // Font represents a loaded font with its metadata type Font struct { Name string // Name of the font family @@ -209,7 +199,7 @@ const ( FallbackMono FallbackVariant = "mono" ) -// GetFallback returns either Monaspace Argon or Neon as the fallback font +// GetFallback returns either JetBrainsMono or Inter as the fallback font func GetFallback(variant FallbackVariant) (*Font, error) { var filename string var fontName string @@ -217,25 +207,11 @@ func GetFallback(variant FallbackVariant) (*Font, error) { switch variant { case FallbackMono: - filename = "MonaspaceNeon-Regular.ttf" - fontName = "Monaspace Neon" - variations = map[string]float32{ - AxisWeight: 400, // Regular weight - AxisWidth: 100, // Normal width - AxisSlant: 0, // No slant - AxisTexture: 0, // No texture healing - AxisLigatures: 0, // Disable ligatures - } + filename = "JetBrainsMono-Regular.ttf" + fontName = "JetBrainsMono" default: // FallbackSans - filename = "MonaspaceArgon-Regular.ttf" - fontName = "Monaspace Argon" - variations = map[string]float32{ - AxisWeight: 400, // Regular weight - AxisWidth: 100, // Normal width - AxisSlant: 0, // No slant - AxisTexture: 0, // No texture healing - AxisLigatures: 0, // Disable ligatures - } + filename = "Inter-Regular.ttf" + fontName = "Inter" } data, err := embeddedFonts.ReadFile("embedded/" + filename) @@ -709,17 +685,20 @@ func (f *Font) GetFontFace(size float64) (font.Face, error) { return fallback.GetFontFace(size) } - // Create face options - opts := &opentype.FaceOptions{ - Size: size, - DPI: 72, + // Convert to TrueType for better hinting support + ttf, err := f.ToTrueType() + if err != nil { + return nil, fmt.Errorf("failed to convert to TrueType: %v", err) } - face, err := opentype.NewFace(f.Font, opts) - if err != nil { - return nil, fmt.Errorf("failed to create font face: %v", err) + // Create face options with hinting enabled + opts := &truetype.Options{ + Size: size, + DPI: 72, + Hinting: font.HintingFull, } + face := truetype.NewFace(ttf, opts) return face, nil } diff --git a/pkg/syntax/render.go b/pkg/syntax/render.go index 15fdf18..eec86fb 100644 --- a/pkg/syntax/render.go +++ b/pkg/syntax/render.go @@ -11,6 +11,7 @@ import ( "github.com/golang/freetype/truetype" "golang.org/x/image/font" "golang.org/x/image/font/gofont/gomono" + "golang.org/x/image/math/fixed" ) // RenderConfig holds configuration for rendering highlighted code to an image @@ -536,13 +537,44 @@ func (h *HighlightedCode) RenderToImage(config *RenderConfig) (image.Image, erro if config.ShowLineNumbers { x += lineNumberOffset } + + // Check if we're using a monospace font + isMono := isMonospace(config.FontFace) + + // Draw each token for _, token := range line { c.SetSrc(image.NewUniform(token.Color)) pt := freetype.Pt(x, y) - c.DrawString(token.Text, pt) - x += font.MeasureString(config.FontFace, token.Text).Round() + + if isMono { + // For monospace fonts, use fixed character spacing + charWidth := font.MeasureString(config.FontFace, "M").Round() + for _, ch := range token.Text { + c.DrawString(string(ch), pt) + pt.X += fixed.Int26_6(charWidth << 6) + } + x += charWidth * len([]rune(token.Text)) + } else { + // For proportional fonts, use natural spacing + c.DrawString(token.Text, pt) + x += font.MeasureString(config.FontFace, token.Text).Round() + } } } return img, nil } + +// isMonospace checks if the font is monospace by sampling character widths +func isMonospace(fontFace font.Face) bool { + isMonospace := true + samples := []rune{'M', 'i', '.', ' ', 'W'} + width := font.MeasureString(fontFace, string(samples[0])).Round() + for _, ch := range samples[1:] { + if font.MeasureString(fontFace, string(ch)).Round() != width { + isMonospace = false + break + } + } + return isMonospace +} diff --git a/pkg/syntax/syntax.go b/pkg/syntax/syntax.go index 9b587ae..6a719c8 100644 --- a/pkg/syntax/syntax.go +++ b/pkg/syntax/syntax.go @@ -251,12 +251,32 @@ func (f *customFormatter) addToken(text string, tokenType chroma.TokenType, styl expandedText, newColumn := expandTabs(text, f.currentColumn, f.tabWidth) f.currentColumn = newColumn + // Check if this token should be joined with the previous token + if len(f.currentLine.Tokens) > 0 && shouldJoinTokens(tokenType) { + lastToken := &f.currentLine.Tokens[len(f.currentLine.Tokens)-1] + // Only join if the colors match + if lastToken.Color == f.createToken(expandedText, tokenType, style).Color { + lastToken.Text += expandedText + return + } + } + // Add the token with expanded text if expandedText != "" { f.currentLine.Tokens = append(f.currentLine.Tokens, f.createToken(expandedText, tokenType, style)) } } +func shouldJoinTokens(tokenType chroma.TokenType) bool { + // Join punctuation tokens + return tokenType == chroma.Punctuation || + strings.Contains(tokenType.String(), "Punctuation") || + strings.Contains(tokenType.String(), "Operator") || + strings.Contains(tokenType.String(), "Parenthesis") || + strings.Contains(tokenType.String(), "Bracket") || + strings.Contains(tokenType.String(), "Brace") +} + func (f *customFormatter) processNewlines(text string, tokenType chroma.TokenType, style *chroma.Style) (Line, bool) { if !strings.Contains(text, "\n") { return Line{}, false