mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-28 10:15:32 -05:00
The commit graph used '⏣' (U+23E3 BENZENE RING WITH CIRCLE) for merge commits and '◯' (U+25EF LARGE CIRCLE) for regular commits. Both have very poor coverage in popular monospace fonts: - '⏣' lives in the Misc Technical block and is essentially absent from every common monospace font (Source Code Pro, JetBrains Mono, Fira Code, Cascadia Code, Hack, Iosevka, Menlo, Consolas, Monaco, IBM Plex Mono, Ubuntu Mono, Noto Sans Mono, Inconsolata). It is always drawn from a system fallback font. - '◯' is the late-addition LARGE CIRCLE codepoint. It is present in some fonts (Cascadia, Fira Code, Hack, Iosevka, Menlo, Noto Sans Mono) but missing from many others, including Source Code Pro and most Nerd Font derivatives based on it. This is why the graph renders inconsistently across platforms even when the same monospace font is configured: each OS picks a different fallback font (Apple Symbols on macOS, Segoe UI Symbol on Windows, Noto/DejaVu/Symbola on Linux), and the substituted glyphs differ in shape, weight, and advance width. '◯' is also East Asian Ambiguous width, so some terminals render it wider than one cell, exaggerating the misalignment. Replace the symbols with codepoints from the foundational 1991 Geometric Shapes block, which has far broader font coverage: - Merge: '◎' U+25CE BULLSEYE -- concentric circles, the visually closest cousin to the previous benzene-ring glyph. - Commit: '○' U+25CB WHITE CIRCLE -- the same hollow-circle silhouette as before, just a more universally available codepoint. The new symbols are present in the font directly in significantly more cases; and when fallback is still required, they are universally well-drawn (unlike '⏣', which many fallback fonts also lack). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
184 lines
3.9 KiB
Go
184 lines
3.9 KiB
Go
package graph
|
|
|
|
import (
|
|
"io"
|
|
"sync"
|
|
|
|
"github.com/gookit/color"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
)
|
|
|
|
const (
|
|
MergeSymbol = '◎'
|
|
CommitSymbol = '○'
|
|
)
|
|
|
|
type cellType int
|
|
|
|
const (
|
|
CONNECTION cellType = iota
|
|
COMMIT
|
|
MERGE
|
|
)
|
|
|
|
type Cell struct {
|
|
up, down, left, right bool
|
|
cellType cellType
|
|
rightStyle *style.TextStyle
|
|
style *style.TextStyle
|
|
}
|
|
|
|
func (cell *Cell) render(writer io.StringWriter) {
|
|
up, down, left, right := cell.up, cell.down, cell.left, cell.right
|
|
|
|
first, second := getBoxDrawingChars(up, down, left, right)
|
|
var adjustedFirst string
|
|
switch cell.cellType {
|
|
case CONNECTION:
|
|
adjustedFirst = first
|
|
case COMMIT:
|
|
adjustedFirst = string(CommitSymbol)
|
|
case MERGE:
|
|
adjustedFirst = string(MergeSymbol)
|
|
}
|
|
|
|
var rightStyle *style.TextStyle
|
|
if cell.rightStyle == nil {
|
|
rightStyle = cell.style
|
|
} else {
|
|
rightStyle = cell.rightStyle
|
|
}
|
|
|
|
// just doing this for the sake of easy testing, so that we don't need to
|
|
// assert on the style of a space given a space has no styling (assuming we
|
|
// stick to only using foreground styles)
|
|
var styledSecondChar string
|
|
if second == " " {
|
|
styledSecondChar = " "
|
|
} else {
|
|
styledSecondChar = cachedSprint(*rightStyle, second)
|
|
}
|
|
|
|
_, _ = writer.WriteString(cachedSprint(*cell.style, adjustedFirst))
|
|
_, _ = writer.WriteString(styledSecondChar)
|
|
}
|
|
|
|
type rgbCacheKey struct {
|
|
*color.RGBStyle
|
|
str string
|
|
}
|
|
|
|
var (
|
|
rgbCache = make(map[rgbCacheKey]string)
|
|
rgbCacheMutex sync.RWMutex
|
|
)
|
|
|
|
func cachedSprint(style style.TextStyle, str string) string {
|
|
switch v := style.Style.(type) {
|
|
case *color.RGBStyle:
|
|
rgbCacheMutex.RLock()
|
|
key := rgbCacheKey{v, str}
|
|
value, ok := rgbCache[key]
|
|
rgbCacheMutex.RUnlock()
|
|
if ok {
|
|
return value
|
|
}
|
|
value = style.Sprint(str)
|
|
rgbCacheMutex.Lock()
|
|
rgbCache[key] = value
|
|
rgbCacheMutex.Unlock()
|
|
return value
|
|
case color.Basic:
|
|
return style.Sprint(str)
|
|
case color.Style:
|
|
value := style.Sprint(str)
|
|
return value
|
|
}
|
|
return style.Sprint(str)
|
|
}
|
|
|
|
func (cell *Cell) reset() {
|
|
cell.up = false
|
|
cell.down = false
|
|
cell.left = false
|
|
cell.right = false
|
|
}
|
|
|
|
func (cell *Cell) setUp(style *style.TextStyle) *Cell {
|
|
cell.up = true
|
|
cell.style = style
|
|
return cell
|
|
}
|
|
|
|
func (cell *Cell) setDown(style *style.TextStyle) *Cell {
|
|
cell.down = true
|
|
cell.style = style
|
|
return cell
|
|
}
|
|
|
|
func (cell *Cell) setLeft(style *style.TextStyle) *Cell {
|
|
cell.left = true
|
|
if !cell.up && !cell.down {
|
|
// vertical trumps left
|
|
cell.style = style
|
|
}
|
|
return cell
|
|
}
|
|
|
|
//nolint:unparam
|
|
func (cell *Cell) setRight(style *style.TextStyle, override bool) *Cell {
|
|
cell.right = true
|
|
if cell.rightStyle == nil || override {
|
|
cell.rightStyle = style
|
|
}
|
|
return cell
|
|
}
|
|
|
|
func (cell *Cell) setStyle(style *style.TextStyle) *Cell {
|
|
cell.style = style
|
|
return cell
|
|
}
|
|
|
|
func (cell *Cell) setType(cellType cellType) *Cell {
|
|
cell.cellType = cellType
|
|
return cell
|
|
}
|
|
|
|
func getBoxDrawingChars(up, down, left, right bool) (string, string) {
|
|
if up && down && left && right {
|
|
return "│", "─"
|
|
} else if up && down && left && !right {
|
|
return "│", " "
|
|
} else if up && down && !left && right {
|
|
return "│", "─"
|
|
} else if up && down && !left && !right {
|
|
return "│", " "
|
|
} else if up && !down && left && right {
|
|
return "┴", "─"
|
|
} else if up && !down && left && !right {
|
|
return "╯", " "
|
|
} else if up && !down && !left && right {
|
|
return "╰", "─"
|
|
} else if up && !down && !left && !right {
|
|
return "╵", " "
|
|
} else if !up && down && left && right {
|
|
return "┬", "─"
|
|
} else if !up && down && left && !right {
|
|
return "╮", " "
|
|
} else if !up && down && !left && right {
|
|
return "╭", "─"
|
|
} else if !up && down && !left && !right {
|
|
return "╷", " "
|
|
} else if !up && !down && left && right {
|
|
return "─", "─"
|
|
} else if !up && !down && left && !right {
|
|
return "─", " "
|
|
} else if !up && !down && !left && right {
|
|
return "╶", "─"
|
|
} else if !up && !down && !left && !right {
|
|
return " ", " "
|
|
}
|
|
|
|
panic("should not be possible")
|
|
}
|