render commit graph

This commit is contained in:
Jesse Duffield 2021-11-02 16:39:15 +11:00
parent 2fc1498517
commit 802cfb1a04
53 changed files with 543 additions and 284 deletions

View file

@ -1,5 +1,5 @@
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)

4
go.mod
View file

@ -20,7 +20,7 @@ require (
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20211031223253-24baf341da75
github.com/jesseduffield/gocui v0.3.1-0.20211102081536-e4eee64f4d13
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
@ -40,7 +40,7 @@ require (
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20211031064116-611d5d643895 // indirect
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0

8
go.sum
View file

@ -71,8 +71,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20211031223253-24baf341da75 h1:zu+WBGwscCwu7GEuxANGl8E51HbW6ueqTF1XdAoqnZs=
github.com/jesseduffield/gocui v0.3.1-0.20211031223253-24baf341da75/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211102081536-e4eee64f4d13 h1:JB1nYX2l3s9aBtw4Ymc7KXp/Hk3IukO4u+APok6WWjo=
github.com/jesseduffield/gocui v0.3.1-0.20211102081536-e4eee64f4d13/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
@ -178,8 +178,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211031064116-611d5d643895 h1:iaNpwpnrgL5jzWS0vCNnfa8HqzxveCFpFx3uC/X4Tps=
golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c h1:QOfDMdrf/UwlVR0UBq2Mpr58UzNtvgJRXA4BgPfFACs=
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View file

@ -406,7 +406,7 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
return c.OSCommand.ExecutableFromString(
fmt.Sprintf(
"git log %s --oneline %s %s --abbrev=%d %s",
"git log --topo-order %s --oneline %s %s --abbrev=%d %s",
c.OSCommand.Quote(opts.RefName),
prettyFormat,
limitFlag,

View file

@ -10,6 +10,9 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// after selecting the 200th commit, we'll load in all the rest
const COMMIT_THRESHOLD = 200
// list panel functions
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
@ -23,7 +26,7 @@ func (gui *Gui) getSelectedLocalCommit() *models.Commit {
func (gui *Gui) handleCommitSelect() error {
state := gui.State.Panels.Commits
if state.SelectedLineIdx > 290 && state.LimitCommits {
if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
state.LimitCommits = false
go utils.Safe(func() {
if err := gui.refreshCommitsWithLimit(); err != nil {

View file

@ -395,7 +395,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
Tags: &tagsPanelState{listPanelState{SelectedLineIdx: -1}},
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, LimitCommits: true},
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, LimitCommits: true},
ReflogCommits: &reflogCommitPanelState{listPanelState{SelectedLineIdx: 0}},
SubCommits: &subCommitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, refName: ""},
CommitFiles: &commitFilesPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, refName: ""},

View file

@ -14,6 +14,10 @@ type ListContext struct {
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
SelectedItem func() (ListItem, bool)
OnGetPanelState func() IListPanelState
// if this is true, we'll call GetDisplayStrings for just the visible part of the
// view and re-render that. This is useful when you need to render different
// content based on the selection (e.g. for showing the selected commit)
RenderSelection bool
Gui *Gui
@ -60,10 +64,17 @@ type ListItem interface {
func (self *ListContext) FocusLine() {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
// ignoring error for now
return
}
// we need a way of knowing whether we've rendered to the view yet.
view.FocusPoint(0, self.GetPanelState().GetSelectedLineIdx())
if self.RenderSelection {
_, originY := view.Origin()
displayStrings := self.GetDisplayStrings(originY, view.InnerHeight())
self.Gui.renderDisplayStringsAtPos(view, originY, displayStrings)
}
view.Footer = formatListFooter(self.GetPanelState().GetSelectedLineIdx(), self.GetItemsLength())
}

View file

@ -157,18 +157,29 @@ func (gui *Gui) branchCommitsListContext() IListContext {
OnClickSelectedItem: gui.handleViewCommitFiles,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
selectedCommitSha := ""
if gui.currentContext().GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
selectedCommit := gui.getSelectedLocalCommit()
if selectedCommit != nil {
selectedCommitSha = selectedCommit.Sha
}
}
return presentation.GetCommitListDisplayStrings(
gui.State.Commits,
gui.State.ScreenMode != SCREEN_NORMAL,
gui.cherryPickedCommitShaMap(),
gui.State.Modes.Diffing.Ref,
parseEmoji,
selectedCommitSha,
startIdx,
length,
)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedLocalCommit()
return item, item != nil
},
RenderSelection: true,
}
}
@ -215,18 +226,29 @@ func (gui *Gui) subCommitsListContext() IListContext {
OnFocus: gui.handleSubCommitSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
selectedCommitSha := ""
if gui.currentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY {
selectedCommit := gui.getSelectedSubCommit()
if selectedCommit != nil {
selectedCommitSha = selectedCommit.Sha
}
}
return presentation.GetCommitListDisplayStrings(
gui.State.SubCommits,
gui.State.ScreenMode != SCREEN_NORMAL,
gui.cherryPickedCommitShaMap(),
gui.State.Modes.Diffing.Ref,
parseEmoji,
selectedCommitSha,
0,
len(gui.State.SubCommits),
)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedSubCommit()
return item, item != nil
},
RenderSelection: true,
}
}

View file

@ -2,120 +2,152 @@ package presentation
import (
"strings"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/kyokomi/emoji/v2"
)
func GetCommitListDisplayStrings(commits []*models.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string, parseEmoji bool) [][]string {
lines := make([][]string, len(commits))
type pipeSetCacheKey struct {
commitSha string
commitCount int
}
var displayFunc func(*models.Commit, map[string]bool, bool, bool) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForCommit
} else {
displayFunc = getDisplayStringsForCommit
var pipeSetCache = make(map[pipeSetCacheKey][][]*graph.Pipe)
var mutex sync.Mutex
func GetCommitListDisplayStrings(
commits []*models.Commit,
fullDescription bool,
cherryPickedCommitShaMap map[string]bool,
diffName string,
parseEmoji bool,
selectedCommitSha string,
startIdx int,
length int,
) [][]string {
mutex.Lock()
defer mutex.Unlock()
if len(commits) == 0 {
return nil
}
for i := range commits {
diffed := commits[i].Sha == diffName
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed, parseEmoji)
// given that our cache key is a commit sha and a commit count, it's very important that we don't actually try to render pipes
// when dealing with things like filtered commits.
cacheKey := pipeSetCacheKey{
commitSha: commits[0].Sha,
commitCount: len(commits),
}
pipeSets, ok := pipeSetCache[cacheKey]
if !ok {
// pipe sets are unique to a commit head. and a commit count. Sometimes we haven't loaded everything for that.
// so let's just cache it based on that.
getStyle := func(commit *models.Commit) style.TextStyle {
return authors.AuthorStyle(commit.Author)
}
pipeSets = graph.GetPipeSets(commits, getStyle)
pipeSetCache[cacheKey] = pipeSets
}
end := startIdx + length
if end > len(commits)-1 {
end = len(commits) - 1
}
filteredPipeSets := pipeSets[startIdx : end+1]
filteredCommits := commits[startIdx : end+1]
graphLines := graph.RenderAux(filteredPipeSets, filteredCommits, selectedCommitSha)
lines := make([][]string, 0, len(graphLines))
for i, commit := range filteredCommits {
lines = append(lines, displayCommit(commit, cherryPickedCommitShaMap, diffName, parseEmoji, graphLines[i], fullDescription))
}
return lines
}
func getFullDescriptionDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[string]bool, diffed, parseEmoji bool) []string {
shaColor := theme.DefaultTextColor
switch c.Status {
case "unpushed":
shaColor = style.FgRed
case "pushed":
shaColor = style.FgYellow
case "merged":
shaColor = style.FgGreen
case "rebasing":
shaColor = style.FgBlue
case "reflog":
shaColor = style.FgBlue
}
func displayCommit(
commit *models.Commit,
cherryPickedCommitShaMap map[string]bool,
diffName string,
parseEmoji bool,
graphLine string,
fullDescription bool,
) []string {
if diffed {
shaColor = theme.DiffTerminalColor
} else if cherryPickedCommitShaMap[c.Sha] {
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
shaColor = theme.CherryPickedCommitTextStyle
}
tagString := ""
secondColumnString := style.FgBlue.Sprint(utils.UnixToDate(c.UnixTimestamp))
if c.Action != "" {
secondColumnString = actionColorMap(c.Action).Sprint(c.Action)
} else if c.ExtraInfo != "" {
tagString = style.FgMagenta.SetBold().Sprint(c.ExtraInfo) + " "
}
name := c.Name
if parseEmoji {
name = emoji.Sprint(name)
}
return []string{
shaColor.Sprint(c.ShortSha()),
secondColumnString,
authors.LongAuthor(c.Author),
tagString + theme.DefaultTextColor.Sprint(name),
}
}
func getDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[string]bool, diffed, parseEmoji bool) []string {
shaColor := theme.DefaultTextColor
switch c.Status {
case "unpushed":
shaColor = style.FgRed
case "pushed":
shaColor = style.FgYellow
case "merged":
shaColor = style.FgGreen
case "rebasing":
shaColor = style.FgBlue
case "reflog":
shaColor = style.FgBlue
}
if diffed {
shaColor = theme.DiffTerminalColor
} else if cherryPickedCommitShaMap[c.Sha] {
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
shaColor = theme.CherryPickedCommitTextStyle
}
shaColor := getShaColor(commit, diffName, cherryPickedCommitShaMap)
actionString := ""
tagString := ""
if c.Action != "" {
actionString = actionColorMap(c.Action).Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(c.Tags, " ")) + " "
if commit.Action != "" {
actionString = actionColorMap(commit.Action).Sprint(commit.Action) + " "
}
name := c.Name
tagString := ""
if fullDescription {
if commit.ExtraInfo != "" {
tagString = style.FgMagenta.SetBold().Sprint(commit.ExtraInfo) + " "
}
} else {
if len(commit.Tags) > 0 {
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " "
}
}
name := commit.Name
if parseEmoji {
name = emoji.Sprint(name)
}
return []string{
shaColor.Sprint(c.ShortSha()),
authors.ShortAuthor(c.Author),
actionString + tagString + theme.DefaultTextColor.Sprint(name),
authorFunc := authors.ShortAuthor
if fullDescription {
authorFunc = authors.LongAuthor
}
cols := make([]string, 0, 5)
cols = append(cols, shaColor.Sprint(commit.ShortSha()))
if fullDescription {
cols = append(cols, style.FgBlue.Sprint(utils.UnixToDate(commit.UnixTimestamp)))
}
cols = append(
cols,
actionString,
authorFunc(commit.Author),
graphLine+tagString+theme.DefaultTextColor.Sprint(name),
)
return cols
}
func getShaColor(commit *models.Commit, diffName string, cherryPickedCommitShaMap map[string]bool) style.TextStyle {
diffed := commit.Sha == diffName
shaColor := theme.DefaultTextColor
switch commit.Status {
case "unpushed":
shaColor = style.FgRed
case "pushed":
shaColor = style.FgYellow
case "merged":
shaColor = style.FgGreen
case "rebasing":
shaColor = style.FgBlue
case "reflog":
shaColor = style.FgBlue
}
if diffed {
shaColor = theme.DiffTerminalColor
} else if cherryPickedCommitShaMap[commit.Sha] {
shaColor = theme.CherryPickedCommitTextStyle
}
return shaColor
}
func actionColorMap(str string) style.TextStyle {

View file

@ -1,11 +1,15 @@
package graph
import (
"io"
"sync"
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
const mergeSymbol = '⏣'
const commitSymbol = '⎔'
const mergeSymbol = "⏣"
const commitSymbol = "⎔"
type cellType int
@ -22,11 +26,11 @@ type Cell struct {
style style.TextStyle
}
func (cell *Cell) render() string {
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 rune
var adjustedFirst string
switch cell.cellType {
case CONNECTION:
adjustedFirst = first
@ -47,13 +51,46 @@ func (cell *Cell) render() string {
// 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 == ' ' {
if second == " " {
styledSecondChar = " "
} else {
styledSecondChar = rightStyle.Sprint(string(second))
styledSecondChar = cachedSprint(*rightStyle, second)
}
return cell.style.Sprint(string(adjustedFirst)) + styledSecondChar
_, _ = writer.WriteString(cachedSprint(cell.style, adjustedFirst))
_, _ = writer.WriteString(styledSecondChar)
}
type rgbCacheKey struct {
*color.RGBStyle
str string
}
var rgbCache = make(map[rgbCacheKey]string)
var 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() {
@ -102,39 +139,39 @@ func (cell *Cell) setType(cellType cellType) *Cell {
return cell
}
func getBoxDrawingChars(up, down, left, right bool) (rune, rune) {
func getBoxDrawingChars(up, down, left, right bool) (string, string) {
if up && down && left && right {
return '│', '─'
return "│", "─"
} else if up && down && left && !right {
return '│', ' '
return "│", " "
} else if up && down && !left && right {
return '│', '─'
return "│", "─"
} else if up && down && !left && !right {
return '│', ' '
return "│", " "
} else if up && !down && left && right {
return '┴', '─'
return "┴", "─"
} else if up && !down && left && !right {
return '╯', ' '
return "╯", " "
} else if up && !down && !left && right {
return '╰', '─'
return "╰", "─"
} else if up && !down && !left && !right {
return '╵', ' '
return "╵", " "
} else if !up && down && left && right {
return '┬', '─'
return "┬", "─"
} else if !up && down && left && !right {
return '╮', ' '
return "╮", " "
} else if !up && down && !left && right {
return '╭', '─'
return "╭", "─"
} else if !up && down && !left && !right {
return '╷', ' '
return "╷", " "
} else if !up && !down && left && right {
return '─', '─'
return "─", "─"
} else if !up && !down && left && !right {
return '─', ' '
return "─", " "
} else if !up && !down && !left && right {
return '╶', '─'
return "╶", "─"
} else if !up && !down && !left && !right {
return ' ', ' '
return " ", " "
} else {
panic("should not be possible")
}

View file

@ -1,6 +1,7 @@
package graph
import (
"runtime"
"sort"
"strings"
"sync"
@ -29,7 +30,7 @@ type Pipe struct {
var highlightStyle = style.FgLightWhite.SetBold()
func ContainsCommitSha(pipes []Pipe, sha string) bool {
func ContainsCommitSha(pipes []*Pipe, sha string) bool {
for _, pipe := range pipes {
if equalHashes(pipe.fromSha, sha) {
return true
@ -57,14 +58,14 @@ func RenderCommitGraph(commits []*models.Commit, selectedCommitSha string, getSt
return lines
}
func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style.TextStyle) [][]Pipe {
func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style.TextStyle) [][]*Pipe {
if len(commits) == 0 {
return nil
}
pipes := []Pipe{{fromPos: 0, toPos: 0, fromSha: "START", toSha: commits[0].Sha, kind: STARTS, style: style.FgDefault}}
pipes := []*Pipe{{fromPos: 0, toPos: 0, fromSha: "START", toSha: commits[0].Sha, kind: STARTS, style: style.FgDefault}}
pipeSets := [][]Pipe{}
pipeSets := [][]*Pipe{}
for _, commit := range commits {
pipes = getNextPipes(pipes, commit, getStyle)
pipeSets = append(pipeSets, pipes)
@ -73,29 +74,51 @@ func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style
return pipeSets
}
func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitSha string) []string {
lines := make([]string, len(pipeSets))
func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitSha string) []string {
maxProcs := runtime.GOMAXPROCS(0)
lines := make([]string, 0, len(pipeSets))
// splitting up the rendering of the graph into multiple goroutines allows us to render the graph in parallel
chunks := make([][]string, maxProcs)
perProc := len(pipeSets) / maxProcs
wg := sync.WaitGroup{}
wg.Add(len(pipeSets))
for i, pipeSet := range pipeSets {
wg.Add(maxProcs)
for i := 0; i < maxProcs; i++ {
i := i
pipeSet := pipeSet
go func() {
defer wg.Done()
var prevCommit *models.Commit
if i > 0 {
prevCommit = commits[i-1]
from := i * perProc
to := (i + 1) * perProc
if i == maxProcs-1 {
to = len(pipeSets)
}
line := renderPipeSet(pipeSet, selectedCommitSha, prevCommit)
lines[i] = line
innerLines := make([]string, 0, to-from)
for j, pipeSet := range pipeSets[from:to] {
k := from + j
var prevCommit *models.Commit
if k > 0 {
prevCommit = commits[k-1]
}
line := renderPipeSet(pipeSet, selectedCommitSha, prevCommit)
innerLines = append(innerLines, line)
}
chunks[i] = innerLines
wg.Done()
}()
}
wg.Wait()
for _, chunk := range chunks {
lines = append(lines, chunk...)
}
return lines
}
func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *models.Commit) style.TextStyle) []Pipe {
currentPipes := make([]Pipe, 0, len(prevPipes))
func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *models.Commit) style.TextStyle) []*Pipe {
currentPipes := make([]*Pipe, 0, len(prevPipes))
maxPos := 0
for _, pipe := range prevPipes {
// a pipe that terminated in the previous line has no bearing on the current line
@ -106,7 +129,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
maxPos = utils.Max(maxPos, pipe.toPos)
}
newPipes := make([]Pipe, 0, len(currentPipes)+len(commit.Parents))
newPipes := make([]*Pipe, 0, len(currentPipes)+len(commit.Parents))
// start by assuming that we've got a brand new commit not related to any preceding commit.
// (this only happens when we're doing `git log --all`). These will be tacked onto the far end.
pos := maxPos + 1
@ -124,7 +147,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
traversedSpots := make(map[int]bool)
if len(commit.Parents) > 0 {
newPipes = append(newPipes, Pipe{
newPipes = append(newPipes, &Pipe{
fromPos: pos,
toPos: pos,
fromSha: commit.Sha,
@ -177,7 +200,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
for _, pipe := range currentPipes {
if equalHashes(pipe.toSha, commit.Sha) {
// terminating here
newPipes = append(newPipes, Pipe{
newPipes = append(newPipes, &Pipe{
fromPos: pipe.toPos,
toPos: pos,
fromSha: pipe.fromSha,
@ -189,7 +212,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
} else if pipe.toPos < pos {
// continuing here
availablePos := getNextAvailablePosForContinuingPipe()
newPipes = append(newPipes, Pipe{
newPipes = append(newPipes, &Pipe{
fromPos: pipe.toPos,
toPos: availablePos,
fromSha: pipe.fromSha,
@ -205,7 +228,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
for _, parent := range commit.Parents[1:] {
availablePos := getNextAvailablePosForNewPipe()
// need to act as if continuing pipes are going to continue on the same line.
newPipes = append(newPipes, Pipe{
newPipes = append(newPipes, &Pipe{
fromPos: pos,
toPos: availablePos,
fromSha: commit.Sha,
@ -229,7 +252,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
last = i
}
}
newPipes = append(newPipes, Pipe{
newPipes = append(newPipes, &Pipe{
fromPos: pipe.toPos,
toPos: last,
fromSha: pipe.fromSha,
@ -253,7 +276,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
}
func renderPipeSet(
pipes []Pipe,
pipes []*Pipe,
selectedCommitSha string,
prevCommit *models.Commit,
) string {
@ -279,7 +302,7 @@ func renderPipeSet(
cells[i] = &Cell{cellType: CONNECTION, style: style.FgDefault}
}
renderPipe := func(pipe Pipe, style style.TextStyle, overrideRightStyle bool) {
renderPipe := func(pipe *Pipe, style style.TextStyle, overrideRightStyle bool) {
left := pipe.left()
right := pipe.right()
@ -313,9 +336,9 @@ func renderPipeSet(
// so we have our commit pos again, now it's time to build the cells.
// we'll handle the one that's sourced from our selected commit last so that it can override the other cells.
selectedPipes := []Pipe{}
selectedPipes := []*Pipe{}
// pre-allocating this one because most of the time we'll only have non-selected pipes
nonSelectedPipes := make([]Pipe, 0, len(pipes))
nonSelectedPipes := make([]*Pipe, 0, len(pipes))
for _, pipe := range pipes {
if highlight && equalHashes(pipe.fromSha, selectedCommitSha) {
@ -356,11 +379,13 @@ func renderPipeSet(
cells[commitPos].setType(cType)
renderedCells := make([]string, len(cells))
for i, cell := range cells {
renderedCells[i] = cell.render()
// using a string builder here for the sake of performance
writer := &strings.Builder{}
writer.Grow(len(cells) * 2)
for _, cell := range cells {
cell.render(writer)
}
return strings.Join(renderedCells, "")
return writer.String()
}
func equalHashes(a, b string) bool {

View file

@ -1,11 +1,14 @@
package graph
import (
"fmt"
"math/rand"
"strings"
"testing"
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
@ -253,7 +256,7 @@ func TestRenderPipeSet(t *testing.T) {
tests := []struct {
name string
pipes []Pipe
pipes []*Pipe
commit *models.Commit
prevCommit *models.Commit
expectedStr string
@ -261,7 +264,7 @@ func TestRenderPipeSet(t *testing.T) {
}{
{
name: "single cell",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: cyan},
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: green},
},
@ -271,7 +274,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "single cell, selected",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "selected", kind: TERMINATES, style: cyan},
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "c", kind: STARTS, style: green},
},
@ -281,7 +284,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "terminating hook and starting hook, selected",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "selected", kind: TERMINATES, style: cyan},
{fromPos: 1, toPos: 0, fromSha: "c", toSha: "selected", kind: TERMINATES, style: yellow},
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "d", kind: STARTS, style: green},
@ -294,8 +297,8 @@ func TestRenderPipeSet(t *testing.T) {
},
},
{
name: "terminating hook and starting hook, prioritise the starting one",
pipes: []Pipe{
name: "terminating hook and starting hook, prioritise the terminating one",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 0, fromSha: "c", toSha: "b", kind: TERMINATES, style: magenta},
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "d", kind: STARTS, style: green},
@ -309,7 +312,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "starting and terminating pipe sharing some space",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "b2", kind: CONTINUES, style: magenta},
@ -324,7 +327,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "starting and terminating pipe sharing some space, with selection",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "selected", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "b2", kind: CONTINUES, style: magenta},
@ -339,7 +342,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "many terminating pipes",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 0, fromSha: "b1", toSha: "a2", kind: TERMINATES, style: magenta},
@ -353,7 +356,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "starting pipe passing through",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 3, fromSha: "a2", toSha: "d3", kind: STARTS, style: yellow},
@ -368,7 +371,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "starting and terminating path crossing continuing path",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: yellow},
@ -383,7 +386,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "another clash of starting and terminating paths",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: yellow},
@ -398,7 +401,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "commit whose previous commit is selected",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
},
@ -410,7 +413,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "commit whose previous commit is selected and is a merge commit",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 1, fromSha: "selected", toSha: "b3", kind: CONTINUES, style: red},
},
@ -422,7 +425,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "commit whose previous commit is selected and is a merge commit, with continuing pipe inbetween",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 1, fromSha: "z1", toSha: "z3", kind: CONTINUES, style: green},
{fromPos: 2, toPos: 2, fromSha: "selected", toSha: "b3", kind: CONTINUES, style: red},
@ -435,7 +438,7 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "when previous commit is selected, not a merge commit, and spawns a continuing pipe",
pipes: []Pipe{
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: green},
@ -471,57 +474,27 @@ func TestRenderPipeSet(t *testing.T) {
}
}
func TestCellRender(t *testing.T) {
tests := []struct {
cell *Cell
expectedString string
}{
{
cell: &Cell{
up: true,
down: true,
cellType: CONNECTION,
style: style.FgCyan,
},
expectedString: "\x1b[36m│\x1b[0m ",
},
{
cell: &Cell{
up: true,
down: true,
cellType: COMMIT,
style: style.FgCyan,
},
expectedString: "\x1b[36m⎔\x1b[0m ",
},
}
for _, test := range tests {
assert.EqualValues(t, test.expectedString, test.cell.render())
}
}
func TestGetNextPipes(t *testing.T) {
tests := []struct {
prevPipes []Pipe
prevPipes []*Pipe
commit *models.Commit
expected []Pipe
expected []*Pipe
}{
{
prevPipes: []Pipe{
prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: STARTS, style: style.FgDefault},
},
commit: &models.Commit{
Sha: "b",
Parents: []string{"c"},
},
expected: []Pipe{
expected: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: style.FgDefault},
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: style.FgDefault},
},
},
{
prevPipes: []Pipe{
prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: style.FgDefault},
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: style.FgDefault},
{fromPos: 0, toPos: 1, fromSha: "b", toSha: "d", kind: STARTS, style: style.FgDefault},
@ -530,7 +503,7 @@ func TestGetNextPipes(t *testing.T) {
Sha: "d",
Parents: []string{"e"},
},
expected: []Pipe{
expected: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: CONTINUES, style: style.FgDefault},
{fromPos: 1, toPos: 1, fromSha: "b", toSha: "d", kind: TERMINATES, style: style.FgDefault},
{fromPos: 1, toPos: 1, fromSha: "d", toSha: "e", kind: STARTS, style: style.FgDefault},
@ -551,3 +524,47 @@ func TestGetNextPipes(t *testing.T) {
assert.EqualValues(t, test.expected, pipes)
}
}
func BenchmarkRenderCommitGraph(b *testing.B) {
commits := generateCommits(50)
getStyle := func(commit *models.Commit) style.TextStyle {
return authors.AuthorStyle(commit.Author)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
RenderCommitGraph(commits, "selected", getStyle)
}
}
func generateCommits(count int) []*models.Commit {
rand.Seed(1234)
pool := []*models.Commit{{Sha: "a", Author: "A"}}
commits := make([]*models.Commit, 0, count)
authorPool := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
for len(commits) < count {
currentCommitIdx := rand.Intn(len(pool))
currentCommit := pool[currentCommitIdx]
pool = append(pool[0:currentCommitIdx], pool[currentCommitIdx+1:]...)
// I need to pick a random number of parents to add
parentCount := rand.Intn(2) + 1
for j := 0; j < parentCount; j++ {
reuseParent := rand.Intn(6) != 1 && j <= len(pool)-1 && j != 0
var newParent *models.Commit
if reuseParent {
newParent = pool[j]
} else {
newParent = &models.Commit{
Sha: fmt.Sprintf("%s%d", currentCommit.Sha, j),
Author: authorPool[rand.Intn(len(authorPool))],
}
pool = append(pool, newParent)
}
currentCommit.Parents = append(currentCommit.Parents, newParent.Sha)
}
commits = append(commits, currentCommit)
}
return commits
}

View file

@ -4,6 +4,7 @@
package gui
import (
"io"
"os/exec"
"strings"
@ -50,14 +51,19 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
manager := gui.getManager(view)
ptmx, err := pty.Start(cmd)
if err != nil {
return err
start := func() (*exec.Cmd, io.Reader) {
ptmx, err := pty.Start(cmd)
if err != nil {
gui.Log.Error(err)
}
gui.State.Ptmx = ptmx
return cmd, ptmx
}
gui.State.Ptmx = ptmx
onClose := func() {
ptmx.Close()
gui.State.Ptmx.Close()
gui.State.Ptmx = nil
}
@ -65,7 +71,7 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
return err
}
if err := manager.NewTask(manager.NewCmdTask(ptmx, cmd, prefix, height+oy+10, onClose), cmdStr); err != nil {
if err := manager.NewTask(manager.NewCmdTask(start, prefix, height+oy+10, onClose), cmdStr); err != nil {
return err
}

View file

@ -39,19 +39,19 @@ func TestMerge(t *testing.T) {
{
"no color",
nil,
TextStyle{style: color.Style{}},
TextStyle{Style: color.Style{}},
"foo",
},
{
"only fg color",
[]TextStyle{FgRed},
TextStyle{fg: &Color{basic: &fgRed}, style: color.Style{fgRed}},
TextStyle{fg: &Color{basic: &fgRed}, Style: color.Style{fgRed}},
"\x1b[31mfoo\x1b[0m",
},
{
"only bg color",
[]TextStyle{BgRed},
TextStyle{bg: &Color{basic: &bgRed}, style: color.Style{bgRed}},
TextStyle{bg: &Color{basic: &bgRed}, Style: color.Style{bgRed}},
"\x1b[41mfoo\x1b[0m",
},
{
@ -60,7 +60,7 @@ func TestMerge(t *testing.T) {
TextStyle{
fg: &Color{basic: &fgBlue},
bg: &Color{basic: &bgRed},
style: color.Style{fgBlue, bgRed},
Style: color.Style{fgBlue, bgRed},
},
"\x1b[34;41mfoo\x1b[0m",
},
@ -69,7 +69,7 @@ func TestMerge(t *testing.T) {
[]TextStyle{AttrBold},
TextStyle{
decoration: Decoration{bold: true},
style: color.Style{color.OpBold},
Style: color.Style{color.OpBold},
},
"\x1b[1mfoo\x1b[0m",
},
@ -81,7 +81,7 @@ func TestMerge(t *testing.T) {
bold: true,
underline: true,
},
style: color.Style{color.OpBold, color.OpUnderscore},
Style: color.Style{color.OpBold, color.OpUnderscore},
},
"\x1b[1;4mfoo\x1b[0m",
},
@ -95,7 +95,7 @@ func TestMerge(t *testing.T) {
bold: true,
underline: true,
},
style: color.Style{fgBlue, bgRed, color.OpBold, color.OpUnderscore},
Style: color.Style{fgBlue, bgRed, color.OpBold, color.OpUnderscore},
},
"\x1b[34;41;1;4mfoo\x1b[0m",
},
@ -104,7 +104,7 @@ func TestMerge(t *testing.T) {
[]TextStyle{New().SetFg(rgbPink)},
TextStyle{
fg: &rgbPink,
style: color.NewRGBStyle(rgbPinkLib).SetOpts(color.Opts{}),
Style: color.NewRGBStyle(rgbPinkLib).SetOpts(color.Opts{}),
},
// '38;2' qualifies an RGB foreground color
"\x1b[38;2;255;0;255mfoo\x1b[0m",
@ -115,7 +115,7 @@ func TestMerge(t *testing.T) {
TextStyle{
fg: &rgbPink,
bg: &rgbYellow,
style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{}),
Style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{}),
},
// '48;2' qualifies an RGB background color
"\x1b[38;2;255;0;255;48;2;255;255;0mfoo\x1b[0m",
@ -130,7 +130,7 @@ func TestMerge(t *testing.T) {
bold: true,
underline: true,
},
style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{color.OpBold, color.OpUnderscore}),
Style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{color.OpBold, color.OpUnderscore}),
},
"\x1b[38;2;255;0;255;48;2;255;255;0;1;4mfoo\x1b[0m",
},
@ -140,7 +140,7 @@ func TestMerge(t *testing.T) {
TextStyle{
fg: &rgbYellow,
bg: &Color{basic: &bgRed},
style: color.NewRGBStyle(
Style: color.NewRGBStyle(
rgbYellowLib,
fgRed.RGB(), // We need to use FG here, https://github.com/gookit/color/issues/39
).SetOpts(color.Opts{}),

View file

@ -30,7 +30,9 @@ type TextStyle struct {
bg *Color
decoration Decoration
style Sprinter
// making this public so that we can use a type switch to get to the underlying
// value so we can cache styles. This is very much a hack.
Style Sprinter
}
type Sprinter interface {
@ -40,16 +42,16 @@ type Sprinter interface {
func New() TextStyle {
s := TextStyle{}
s.style = s.deriveStyle()
s.Style = s.deriveStyle()
return s
}
func (b TextStyle) Sprint(a ...interface{}) string {
return b.style.Sprint(a...)
return b.Style.Sprint(a...)
}
func (b TextStyle) Sprintf(format string, a ...interface{}) string {
return b.style.Sprintf(format, a...)
return b.Style.Sprintf(format, a...)
}
// note that our receiver here is not a pointer which means we're receiving a
@ -57,31 +59,31 @@ func (b TextStyle) Sprintf(format string, a ...interface{}) string {
// TextStyle receiver without actually modifying the original.
func (b TextStyle) SetBold() TextStyle {
b.decoration.SetBold()
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
func (b TextStyle) SetUnderline() TextStyle {
b.decoration.SetUnderline()
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
func (b TextStyle) SetReverse() TextStyle {
b.decoration.SetReverse()
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
func (b TextStyle) SetBg(color Color) TextStyle {
b.bg = &color
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
func (b TextStyle) SetFg(color Color) TextStyle {
b.fg = &color
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
@ -96,7 +98,7 @@ func (b TextStyle) MergeStyle(other TextStyle) TextStyle {
b.bg = other.bg
}
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}

View file

@ -1,6 +1,7 @@
package gui
import (
"io"
"os/exec"
"strings"
@ -20,18 +21,22 @@ func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
manager := gui.getManager(view)
r, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout
start := func() (*exec.Cmd, io.Reader) {
r, err := cmd.StdoutPipe()
if err != nil {
gui.Log.Warn(err)
}
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
return err
if err := cmd.Start(); err != nil {
gui.Log.Warn(err)
}
return cmd, r
}
if err := manager.NewTask(manager.NewCmdTask(r, cmd, prefix, height+oy+10, nil), cmdStr); err != nil {
return err
if err := manager.NewTask(manager.NewCmdTask(start, prefix, height+oy+10, nil), cmdStr); err != nil {
gui.Log.Warn(err)
}
return nil
@ -90,7 +95,6 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
view.Reset()
},
func() {
// gui.g.Draw(view) // doing this causes an issue when there's a popup panel in front of the main view.
gui.render()
},
func() {

View file

@ -308,6 +308,11 @@ func (gui *Gui) renderDisplayStrings(v *gocui.View, displayStrings [][]string) {
v.SetContent(list)
}
func (gui *Gui) renderDisplayStringsAtPos(v *gocui.View, y int, displayStrings [][]string) {
list := utils.RenderDisplayStrings(displayStrings)
v.OverwriteLines(y, list)
}
func (gui *Gui) globalOptionsMap() map[string]string {
keybindingConfig := gui.Config.GetUserConfig().Keybinding

View file

@ -14,6 +14,8 @@ import (
"github.com/sirupsen/logrus"
)
const THROTTLE_TIME = time.Millisecond * 30
type Task struct {
stop chan struct{}
stopped bool
@ -38,6 +40,12 @@ type ViewBufferManager struct {
beforeStart func()
refreshView func()
onEndOfInput func()
// if the user flicks through a heap of items, with each one
// spawning a process to render something to the main view,
// it can slow things down quite a bit. In these situations we
// want to throttle the spawning of processes.
throttle bool
}
func (m *ViewBufferManager) GetTaskKey() string {
@ -69,18 +77,31 @@ func (m *ViewBufferManager) ReadLines(n int) {
})
}
func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, prefix string, linesToRead int, onDone func()) func(chan struct{}) error {
func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead int, onDone func()) func(chan struct{}) error {
return func(stop chan struct{}) error {
if m.throttle {
m.Log.Info("throttling task")
time.Sleep(THROTTLE_TIME)
}
select {
case <-stop:
return nil
default:
}
startTime := time.Now()
cmd, r := start()
go utils.Safe(func() {
<-stop
m.throttle = time.Since(startTime) < THROTTLE_TIME
if err := oscommands.Kill(cmd); err != nil {
if !strings.Contains(err.Error(), "process already finished") {
m.Log.Errorf("error when running cmd task: %v", err)
}
}
if onDone != nil {
onDone()
}
})
loadingMutex := sync.Mutex{}

View file

@ -6,21 +6,24 @@ import (
)
var decoloriseCache = make(map[string]string)
var decoloriseMutex sync.Mutex
var decoloriseMutex sync.RWMutex
// Decolorise strips a string of color
func Decolorise(str string) string {
decoloriseMutex.Lock()
defer decoloriseMutex.Unlock()
decoloriseMutex.RLock()
val := decoloriseCache[str]
decoloriseMutex.RUnlock()
if decoloriseCache[str] != "" {
return decoloriseCache[str]
if val != "" {
return val
}
re := regexp.MustCompile(`\x1B\[([0-9]{1,3}(;[0-9]{1,3})*)?[mGK]`)
ret := re.ReplaceAllString(str, "")
decoloriseMutex.Lock()
decoloriseCache[str] = ret
decoloriseMutex.Unlock()
return ret
}

View file

@ -75,8 +75,6 @@ type GuiMutexes struct {
tickingMutex sync.Mutex
ViewsMutex sync.Mutex
drawMutex sync.Mutex
}
type PlayMode int
@ -936,8 +934,6 @@ func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error {
// flush updates the gui, re-drawing frames and buffers.
func (g *Gui) flush() error {
g.Mutexes.drawMutex.Lock()
defer g.Mutexes.drawMutex.Unlock()
// pretty sure we don't need this, but keeping it here in case we get weird visual artifacts
// g.clear(g.FgColor, g.BgColor)
@ -966,18 +962,6 @@ func (g *Gui) flush() error {
return nil
}
func (g *Gui) Draw(v *View) error {
g.Mutexes.drawMutex.Lock()
defer g.Mutexes.drawMutex.Unlock()
if err := g.draw(v); err != nil {
return err
}
Screen.Show()
return nil
}
// draw manages the cursor and calls the draw function of a view.
func (g *Gui) draw(v *View) error {
if g.suspended {

View file

@ -879,11 +879,18 @@ func (v *View) draw() error {
if v.Autoscroll && visibleViewLinesHeight > maxY {
v.oy = visibleViewLinesHeight - maxY
}
if len(v.viewLines) == 0 {
return nil
}
start := v.oy
if start > len(v.viewLines)-1 {
start = len(v.viewLines) - 1
}
y := 0
for i, vline := range v.viewLines {
if i < v.oy {
continue
}
for _, vline := range v.viewLines[start:] {
if y >= maxY {
break
}
@ -1112,14 +1119,6 @@ func (v *View) SetHighlight(y int, on bool) error {
return nil
}
func lineWidth(line []cell) (n int) {
for i := range line {
n += runewidth.RuneWidth(line[i].chr)
}
return
}
func lineWrap(line []cell, columns int) [][]cell {
if columns == 0 {
return [][]cell{line}
@ -1227,3 +1226,16 @@ func (v *View) ClearTextArea() {
_ = v.SetOrigin(0, 0)
_ = v.SetCursor(0, 0)
}
// only call this function if you don't care where v.wx and v.wy end up
func (v *View) OverwriteLines(y int, content string) {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
// break by newline, then for each line, write it, then add that erase command
v.wx = 0
v.wy = y
lines := strings.Replace(content, "\n", "\x1b[K\n", -1)
v.writeString(lines)
}

View file

@ -430,8 +430,25 @@ func GetsockoptXucred(fd, level, opt int) (*Xucred, error) {
return x, err
}
func SysctlKinfoProcSlice(name string) ([]KinfoProc, error) {
mib, err := sysctlmib(name)
func SysctlKinfoProc(name string, args ...int) (*KinfoProc, error) {
mib, err := sysctlmib(name, args...)
if err != nil {
return nil, err
}
var kinfo KinfoProc
n := uintptr(SizeofKinfoProc)
if err := sysctl(mib, (*byte)(unsafe.Pointer(&kinfo)), &n, nil, 0); err != nil {
return nil, err
}
if n != SizeofKinfoProc {
return nil, EIO
}
return &kinfo, nil
}
func SysctlKinfoProcSlice(name string, args ...int) ([]KinfoProc, error) {
mib, err := sysctlmib(name, args...)
if err != nil {
return nil, err
}

View file

@ -116,6 +116,7 @@ const (
ARPHRD_LAPB = 0x204
ARPHRD_LOCALTLK = 0x305
ARPHRD_LOOPBACK = 0x304
ARPHRD_MCTP = 0x122
ARPHRD_METRICOM = 0x17
ARPHRD_NETLINK = 0x338
ARPHRD_NETROM = 0x0
@ -472,6 +473,7 @@ const (
DM_DEV_WAIT = 0xc138fd08
DM_DIR = "mapper"
DM_GET_TARGET_VERSION = 0xc138fd11
DM_IMA_MEASUREMENT_FLAG = 0x80000
DM_INACTIVE_PRESENT_FLAG = 0x40
DM_INTERNAL_SUSPEND_FLAG = 0x40000
DM_IOCTL = 0xfd
@ -716,6 +718,7 @@ const (
ETH_P_LOOPBACK = 0x9000
ETH_P_MACSEC = 0x88e5
ETH_P_MAP = 0xf9
ETH_P_MCTP = 0xfa
ETH_P_MOBITEX = 0x15
ETH_P_MPLS_MC = 0x8848
ETH_P_MPLS_UC = 0x8847
@ -751,6 +754,21 @@ const (
ETH_P_WCCP = 0x883e
ETH_P_X25 = 0x805
ETH_P_XDSA = 0xf8
EV_ABS = 0x3
EV_CNT = 0x20
EV_FF = 0x15
EV_FF_STATUS = 0x17
EV_KEY = 0x1
EV_LED = 0x11
EV_MAX = 0x1f
EV_MSC = 0x4
EV_PWR = 0x16
EV_REL = 0x2
EV_REP = 0x14
EV_SND = 0x12
EV_SW = 0x5
EV_SYN = 0x0
EV_VERSION = 0x10001
EXABYTE_ENABLE_NEST = 0xf0
EXT2_SUPER_MAGIC = 0xef53
EXT3_SUPER_MAGIC = 0xef53
@ -789,9 +807,11 @@ const (
FAN_DELETE_SELF = 0x400
FAN_DENY = 0x2
FAN_ENABLE_AUDIT = 0x40
FAN_EPIDFD = -0x2
FAN_EVENT_INFO_TYPE_DFID = 0x3
FAN_EVENT_INFO_TYPE_DFID_NAME = 0x2
FAN_EVENT_INFO_TYPE_FID = 0x1
FAN_EVENT_INFO_TYPE_PIDFD = 0x4
FAN_EVENT_METADATA_LEN = 0x18
FAN_EVENT_ON_CHILD = 0x8000000
FAN_MARK_ADD = 0x1
@ -811,6 +831,7 @@ const (
FAN_MOVE_SELF = 0x800
FAN_NOFD = -0x1
FAN_NONBLOCK = 0x2
FAN_NOPIDFD = -0x1
FAN_ONDIR = 0x40000000
FAN_OPEN = 0x20
FAN_OPEN_EXEC = 0x1000
@ -821,6 +842,7 @@ const (
FAN_REPORT_DIR_FID = 0x400
FAN_REPORT_FID = 0x200
FAN_REPORT_NAME = 0x800
FAN_REPORT_PIDFD = 0x80
FAN_REPORT_TID = 0x100
FAN_UNLIMITED_MARKS = 0x20
FAN_UNLIMITED_QUEUE = 0x10
@ -1997,6 +2019,7 @@ const (
PR_SPEC_ENABLE = 0x2
PR_SPEC_FORCE_DISABLE = 0x8
PR_SPEC_INDIRECT_BRANCH = 0x1
PR_SPEC_L1D_FLUSH = 0x2
PR_SPEC_NOT_AFFECTED = 0x0
PR_SPEC_PRCTL = 0x1
PR_SPEC_STORE_BYPASS = 0x0
@ -2432,12 +2455,15 @@ const (
SMART_WRITE_THRESHOLDS = 0xd7
SMB_SUPER_MAGIC = 0x517b
SOCKFS_MAGIC = 0x534f434b
SOCK_BUF_LOCK_MASK = 0x3
SOCK_DCCP = 0x6
SOCK_IOC_TYPE = 0x89
SOCK_PACKET = 0xa
SOCK_RAW = 0x3
SOCK_RCVBUF_LOCK = 0x2
SOCK_RDM = 0x4
SOCK_SEQPACKET = 0x5
SOCK_SNDBUF_LOCK = 0x1
SOL_AAL = 0x109
SOL_ALG = 0x117
SOL_ATM = 0x108

View file

@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -294,6 +294,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -300,6 +300,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -290,6 +290,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -348,6 +348,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -352,6 +352,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -352,6 +352,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -281,6 +281,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -356,6 +356,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35

View file

@ -347,6 +347,7 @@ const (
SO_BPF_EXTENSIONS = 0x32
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0x400
SO_BUF_LOCK = 0x51
SO_BUSY_POLL = 0x30
SO_BUSY_POLL_BUDGET = 0x49
SO_CNX_ADVICE = 0x37

View file

@ -444,4 +444,5 @@ const (
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
)

View file

@ -366,4 +366,5 @@ const (
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
)

View file

@ -7,6 +7,7 @@
package unix
const (
SYS_SYSCALL_MASK = 0
SYS_RESTART_SYSCALL = 0
SYS_EXIT = 1
SYS_FORK = 2
@ -407,4 +408,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_PROCESS_MRELEASE = 448
)

View file

@ -311,4 +311,5 @@ const (
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
)

View file

@ -428,4 +428,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 4444
SYS_LANDLOCK_ADD_RULE = 4445
SYS_LANDLOCK_RESTRICT_SELF = 4446
SYS_PROCESS_MRELEASE = 4448
)

View file

@ -358,4 +358,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 5444
SYS_LANDLOCK_ADD_RULE = 5445
SYS_LANDLOCK_RESTRICT_SELF = 5446
SYS_PROCESS_MRELEASE = 5448
)

View file

@ -358,4 +358,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 5444
SYS_LANDLOCK_ADD_RULE = 5445
SYS_LANDLOCK_RESTRICT_SELF = 5446
SYS_PROCESS_MRELEASE = 5448
)

View file

@ -428,4 +428,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 4444
SYS_LANDLOCK_ADD_RULE = 4445
SYS_LANDLOCK_RESTRICT_SELF = 4446
SYS_PROCESS_MRELEASE = 4448
)

View file

@ -435,4 +435,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_PROCESS_MRELEASE = 448
)

View file

@ -407,4 +407,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_PROCESS_MRELEASE = 448
)

View file

@ -407,4 +407,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_PROCESS_MRELEASE = 448
)

View file

@ -309,4 +309,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_PROCESS_MRELEASE = 448
)

View file

@ -372,4 +372,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_PROCESS_MRELEASE = 448
)

View file

@ -386,4 +386,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_PROCESS_MRELEASE = 448
)

View file

@ -3264,7 +3264,8 @@ const (
LWTUNNEL_ENCAP_BPF = 0x6
LWTUNNEL_ENCAP_SEG6_LOCAL = 0x7
LWTUNNEL_ENCAP_RPL = 0x8
LWTUNNEL_ENCAP_MAX = 0x8
LWTUNNEL_ENCAP_IOAM6 = 0x9
LWTUNNEL_ENCAP_MAX = 0x9
MPLS_IPTUNNEL_UNSPEC = 0x0
MPLS_IPTUNNEL_DST = 0x1
@ -3617,7 +3618,9 @@ const (
ETHTOOL_A_COALESCE_TX_USECS_HIGH = 0x15
ETHTOOL_A_COALESCE_TX_MAX_FRAMES_HIGH = 0x16
ETHTOOL_A_COALESCE_RATE_SAMPLE_INTERVAL = 0x17
ETHTOOL_A_COALESCE_MAX = 0x17
ETHTOOL_A_COALESCE_USE_CQE_MODE_TX = 0x18
ETHTOOL_A_COALESCE_USE_CQE_MODE_RX = 0x19
ETHTOOL_A_COALESCE_MAX = 0x19
ETHTOOL_A_PAUSE_UNSPEC = 0x0
ETHTOOL_A_PAUSE_HEADER = 0x1
ETHTOOL_A_PAUSE_AUTONEG = 0x2

4
vendor/modules.txt vendored
View file

@ -161,7 +161,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
# github.com/jesseduffield/gocui v0.3.1-0.20211031223253-24baf341da75
# github.com/jesseduffield/gocui v0.3.1-0.20211102081536-e4eee64f4d13
## explicit
github.com/jesseduffield/gocui
# github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@ -253,7 +253,7 @@ golang.org/x/crypto/ssh/knownhosts
golang.org/x/net/context
golang.org/x/net/internal/socks
golang.org/x/net/proxy
# golang.org/x/sys v0.0.0-20211031064116-611d5d643895
# golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c
## explicit
golang.org/x/sys/cpu
golang.org/x/sys/internal/unsafeheader