mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-07 14:35:08 -04:00
If a branch forks off a merge commit, the graph can't show both lines in the merge commit's row. The line from the branch's first commit ends in the same cell in which the merge's line to its second parent starts. The box drawing characters have no symbol for a line from above that bends to the left combined with a line from the left that bends down, so one of the two gets lost. Today the cell shows a plain vertical line, and the branch looks as if it sat on the merge's second parent (#5497). The branch drawing symbols in the Unicode Private Use Area (U+F5D0 to U+F60D) have a symbol for every way in which lines meet in a cell. kitty introduced them for git graph viewers like vim-flog, and Ghostty, Contour, the terminal of VS Code and nightly builds of WezTerm render them too. Other terminals need a font that contains them. Add the gui.commitGraphStyle config. Its 'detailed' style draws the graph with these symbols, and the commit symbols of the set connect to their lines. Commits are drawn as hollow circles and merge commits as filled ones. The default stays the 'classic' style with the box drawing characters, because not every terminal can display the others. A cell has only one colour. Where two lines meet, the symbol takes the colour of the line from above. With the box drawing characters, highlighting the lines of the selected commit clears the cells they run through. The branch drawing symbols keep the other lines. A cell in which a highlighted line bends is highlighted as a whole. Where a highlighted line crosses the vertical line of another commit, it is drawn over that line, so that it reads as one line. In a cell in which another line bends, the symbol keeps the colour of that line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
225 lines
5.1 KiB
Go
225 lines
5.1 KiB
Go
package graph
|
|
|
|
import (
|
|
"io"
|
|
"sync"
|
|
|
|
"github.com/gookit/color"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
)
|
|
|
|
const (
|
|
MergeSymbol = '◎'
|
|
CommitSymbol = '○'
|
|
)
|
|
|
|
// The characters that the commit graph is drawn with
|
|
type SymbolSet uint8
|
|
|
|
const (
|
|
BoxDrawingSymbols SymbolSet = iota
|
|
BranchDrawingSymbols
|
|
)
|
|
|
|
type cellType int
|
|
|
|
const (
|
|
CONNECTION cellType = iota
|
|
COMMIT
|
|
MERGE
|
|
)
|
|
|
|
// How a line that touches the top or bottom edge of a cell runs within it
|
|
type verticalLine uint8
|
|
|
|
const (
|
|
noLine verticalLine = iota
|
|
// On to the opposite edge, or into the commit symbol
|
|
straightLine
|
|
// Bends towards the left edge
|
|
lineToLeft
|
|
// Bends towards the right edge
|
|
lineToRight
|
|
)
|
|
|
|
type Cell struct {
|
|
// The lines that touch the top and bottom edges
|
|
up, down verticalLine
|
|
// Whether lines touch the left and right edges
|
|
left, right bool
|
|
// Whether a line passes through from the left edge to the right edge
|
|
horizontal bool
|
|
// Whether that line is drawn over the vertical line that it crosses
|
|
horizontalOnTop bool
|
|
cellType cellType
|
|
rightStyle *style.TextStyle
|
|
style *style.TextStyle
|
|
}
|
|
|
|
func (cell *Cell) render(writer io.StringWriter, symbolSet SymbolSet) {
|
|
var first, second string
|
|
switch symbolSet {
|
|
case BoxDrawingSymbols:
|
|
first, second = cell.boxDrawingChars()
|
|
case BranchDrawingSymbols:
|
|
first, second = cell.branchDrawingChars()
|
|
}
|
|
|
|
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, first))
|
|
_, _ = writer.WriteString(styledSecondChar)
|
|
}
|
|
|
|
func (cell *Cell) boxDrawingChars() (string, string) {
|
|
first, second := getBoxDrawingChars(cell.up != noLine, cell.down != noLine, cell.left, cell.right)
|
|
switch cell.cellType {
|
|
case COMMIT:
|
|
return string(CommitSymbol), second
|
|
case MERGE:
|
|
return string(MergeSymbol), second
|
|
default:
|
|
return first, second
|
|
}
|
|
}
|
|
|
|
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 = noLine
|
|
cell.down = noLine
|
|
cell.left = false
|
|
cell.right = false
|
|
cell.horizontal = false
|
|
cell.horizontalOnTop = false
|
|
}
|
|
|
|
func (cell *Cell) setUp(style *style.TextStyle, line verticalLine) *Cell {
|
|
cell.up = line
|
|
cell.style = style
|
|
return cell
|
|
}
|
|
|
|
func (cell *Cell) setDown(style *style.TextStyle, line verticalLine) *Cell {
|
|
cell.down = line
|
|
cell.style = style
|
|
return cell
|
|
}
|
|
|
|
func (cell *Cell) setLeft(style *style.TextStyle) *Cell {
|
|
cell.left = true
|
|
if cell.up == noLine && cell.down == noLine {
|
|
// vertical trumps left
|
|
cell.style = style
|
|
}
|
|
return cell
|
|
}
|
|
|
|
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) setHorizontal(style *style.TextStyle, overrideRightStyle bool) *Cell {
|
|
cell.horizontal = true
|
|
return cell.setLeft(style).setRight(style, overrideRightStyle)
|
|
}
|
|
|
|
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")
|
|
}
|