diff --git a/.gitignore b/.gitignore index a1a4f660d..5210291e4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ commands.log extra/lgit.rb notes/go.notes TODO.notes +TODO.md diff --git a/branches_panel.go b/branches_panel.go index 4060b670a..0a927e1b9 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -1,110 +1,115 @@ package main import ( - "fmt" + "fmt" - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/gocui" ) func handleBranchPress(g *gocui.Gui, v *gocui.View) error { - branch := getSelectedBranch(v) - if output, err := gitCheckout(branch.Name, false); err != nil { - createErrorPanel(g, output) - } - return refreshSidePanels(g) + index := getItemPosition(v) + if index == 0 { + return createErrorPanel(g, "You have already checked out this branch") + } + branch := getSelectedBranch(v) + if output, err := gitCheckout(branch.Name, false); err != nil { + createErrorPanel(g, output) + } + return refreshSidePanels(g) } func handleForceCheckout(g *gocui.Gui, v *gocui.View) error { - branch := getSelectedBranch(v) - return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes (y/n)", func(g *gocui.Gui, v *gocui.View) error { - if output, err := gitCheckout(branch.Name, true); err != nil { - createErrorPanel(g, output) - } - return refreshSidePanels(g) - }, nil) + branch := getSelectedBranch(v) + return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes (y/n)", func(g *gocui.Gui, v *gocui.View) error { + if output, err := gitCheckout(branch.Name, true); err != nil { + createErrorPanel(g, output) + } + return refreshSidePanels(g) + }, nil) } func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { - createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error { - if output, err := gitCheckout(trimmedContent(v), false); err != nil { - return createErrorPanel(g, output) - } - return refreshSidePanels(g) - }) - return nil + createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error { + if output, err := gitCheckout(trimmedContent(v), false); err != nil { + return createErrorPanel(g, output) + } + return refreshSidePanels(g) + }) + return nil } func handleNewBranch(g *gocui.Gui, v *gocui.View) error { - branch := state.Branches[0] - createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error { - if output, err := gitNewBranch(trimmedContent(v)); err != nil { - return createErrorPanel(g, output) - } - refreshSidePanels(g) - return handleBranchSelect(g, v) - }) - return nil + branch := state.Branches[0] + createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error { + if output, err := gitNewBranch(trimmedContent(v)); err != nil { + return createErrorPanel(g, output) + } + refreshSidePanels(g) + return handleBranchSelect(g, v) + }) + return nil } func handleMerge(g *gocui.Gui, v *gocui.View) error { - checkedOutBranch := state.Branches[0] - selectedBranch := getSelectedBranch(v) - defer refreshSidePanels(g) - if checkedOutBranch.Name == selectedBranch.Name { - return createErrorPanel(g, "You cannot merge a branch into itself") - } - if output, err := gitMerge(selectedBranch.Name); err != nil { - return createErrorPanel(g, output) - } - return nil + checkedOutBranch := state.Branches[0] + selectedBranch := getSelectedBranch(v) + defer refreshSidePanels(g) + if checkedOutBranch.Name == selectedBranch.Name { + return createErrorPanel(g, "You cannot merge a branch into itself") + } + if output, err := gitMerge(selectedBranch.Name); err != nil { + return createErrorPanel(g, output) + } + return nil } func getSelectedBranch(v *gocui.View) Branch { - lineNumber := getItemPosition(v) - return state.Branches[lineNumber] + lineNumber := getItemPosition(v) + return state.Branches[lineNumber] } func renderBranchesOptions(g *gocui.Gui) error { - return renderOptionsMap(g, map[string]string{ - "space": "checkout", - "f": "force checkout", - "m": "merge", - "c": "checkout by name", - }) + return renderOptionsMap(g, map[string]string{ + "space": "checkout", + "f": "force checkout", + "m": "merge", + "c": "checkout by name", + "n": "new branch", + }) } // may want to standardise how these select methods work func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { - if err := renderBranchesOptions(g); err != nil { - return err - } - // This really shouldn't happen: there should always be a master branch - if len(state.Branches) == 0 { - return renderString(g, "main", "No branches for this repo") - } - go func() { - branch := getSelectedBranch(v) - diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) - renderString(g, "main", diff) - }() - return nil + if err := renderBranchesOptions(g); err != nil { + return err + } + // This really shouldn't happen: there should always be a master branch + if len(state.Branches) == 0 { + return renderString(g, "main", "No branches for this repo") + } + go func() { + branch := getSelectedBranch(v) + diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) + renderString(g, "main", diff) + }() + return nil } // refreshStatus is called at the end of this because that's when we can // be sure there is a state.Branches array to pick the current branch from func refreshBranches(g *gocui.Gui) error { - g.Update(func(g *gocui.Gui) error { - v, err := g.View("branches") - if err != nil { - panic(err) - } - state.Branches = getGitBranches() - v.Clear() - for _, branch := range state.Branches { - fmt.Fprintln(v, branch.DisplayString) - } - resetOrigin(v) - return refreshStatus(g) - }) - return nil + g.Update(func(g *gocui.Gui) error { + v, err := g.View("branches") + if err != nil { + panic(err) + } + state.Branches = getGitBranches() + v.Clear() + for _, branch := range state.Branches { + fmt.Fprintln(v, branch.DisplayString) + } + resetOrigin(v) + return refreshStatus(g) + }) + return nil } diff --git a/files_panel.go b/files_panel.go index 8e718da7f..0c8b5ec3d 100644 --- a/files_panel.go +++ b/files_panel.go @@ -2,299 +2,303 @@ package main import ( - // "io" - // "io/ioutil" + // "io" + // "io/ioutil" - // "strings" + // "strings" - "errors" - "strings" + "errors" + "strings" - "github.com/fatih/color" - "github.com/jesseduffield/gocui" + "github.com/fatih/color" + "github.com/jesseduffield/gocui" ) var ( - // ErrNoFiles : when there are no modified files in the repo - ErrNoFiles = errors.New("No changed files") + // ErrNoFiles : when there are no modified files in the repo + ErrNoFiles = errors.New("No changed files") ) func stagedFiles(files []GitFile) []GitFile { - result := make([]GitFile, 0) - for _, file := range files { - if file.HasStagedChanges { - result = append(result, file) - } - } - return result + result := make([]GitFile, 0) + for _, file := range files { + if file.HasStagedChanges { + result = append(result, file) + } + } + return result } func handleFilePress(g *gocui.Gui, v *gocui.View) error { - file, err := getSelectedFile(g) - if err != nil { - return err - } + file, err := getSelectedFile(g) + if err != nil { + return err + } - if file.HasUnstagedChanges { - stageFile(file.Name) - } else { - unStageFile(file.Name) - } + if file.HasUnstagedChanges { + stageFile(file.Name) + } else { + unStageFile(file.Name, file.Tracked) + } - if err := refreshFiles(g); err != nil { - return err - } - if err := handleFileSelect(g, v); err != nil { - return err - } + if err := refreshFiles(g); err != nil { + return err + } + if err := handleFileSelect(g, v); err != nil { + return err + } - return nil + return nil } func getSelectedFile(g *gocui.Gui) (GitFile, error) { - if len(state.GitFiles) == 0 { - return GitFile{}, ErrNoFiles - } - filesView, err := g.View("files") - if err != nil { - panic(err) - } - lineNumber := getItemPosition(filesView) - return state.GitFiles[lineNumber], nil + if len(state.GitFiles) == 0 { + return GitFile{}, ErrNoFiles + } + filesView, err := g.View("files") + if err != nil { + panic(err) + } + lineNumber := getItemPosition(filesView) + return state.GitFiles[lineNumber], nil } func handleFileRemove(g *gocui.Gui, v *gocui.View) error { - file, err := getSelectedFile(g) - if err != nil { - return err - } - var deleteVerb string - if file.Tracked { - deleteVerb = "checkout" - } else { - deleteVerb = "delete" - } - return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)? (y/n)", func(g *gocui.Gui, v *gocui.View) error { - if err := removeFile(file); err != nil { - panic(err) - } - return refreshFiles(g) - }, nil) + file, err := getSelectedFile(g) + if err != nil { + return err + } + var deleteVerb string + if file.Tracked { + deleteVerb = "checkout" + } else { + deleteVerb = "delete" + } + return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)? (y/n)", func(g *gocui.Gui, v *gocui.View) error { + if err := removeFile(file); err != nil { + panic(err) + } + return refreshFiles(g) + }, nil) } func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { - file, err := getSelectedFile(g) - if err != nil { - return err - } - if file.Tracked { - return createErrorPanel(g, "Cannot ignore tracked files") - } - gitIgnore(file.Name) - return refreshFiles(g) + file, err := getSelectedFile(g) + if err != nil { + return err + } + if file.Tracked { + return createErrorPanel(g, "Cannot ignore tracked files") + } + gitIgnore(file.Name) + return refreshFiles(g) } func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error { - optionsMap := map[string]string{ - "tab": "next panel", - "S": "stash files", - "c": "commit changes", - "o": "open", - "s": "open in sublime", - "i": "ignore", - "d": "delete", - "space": "toggle staged", - } - if state.HasMergeConflicts { - optionsMap["a"] = "abort merge" - optionsMap["m"] = "resolve merge conflicts" - } - if gitFile == nil { - return renderOptionsMap(g, optionsMap) - } - if gitFile.Tracked { - optionsMap["d"] = "checkout" - } - return renderOptionsMap(g, optionsMap) + optionsMap := map[string]string{ + "tab": "next panel", + "S": "stash files", + "c": "commit changes", + "o": "open", + "s": "open in sublime", + "v": "open in vscode", + "i": "ignore", + "d": "delete", + "space": "toggle staged", + } + if state.HasMergeConflicts { + optionsMap["a"] = "abort merge" + optionsMap["m"] = "resolve merge conflicts" + } + if gitFile == nil { + return renderOptionsMap(g, optionsMap) + } + if gitFile.Tracked { + optionsMap["d"] = "checkout" + } + return renderOptionsMap(g, optionsMap) } func handleFileSelect(g *gocui.Gui, v *gocui.View) error { - gitFile, err := getSelectedFile(g) - if err != nil { - if err != ErrNoFiles { - return err - } - renderString(g, "main", "No changed files") - colorLog(color.FgRed, "error") - return renderfilesOptions(g, nil) - } - renderfilesOptions(g, &gitFile) - var content string - if gitFile.HasMergeConflicts { - return refreshMergePanel(g) - } + gitFile, err := getSelectedFile(g) + if err != nil { + if err != ErrNoFiles { + return err + } + renderString(g, "main", "No changed files") + return renderfilesOptions(g, nil) + } + renderfilesOptions(g, &gitFile) + var content string + if gitFile.HasMergeConflicts { + return refreshMergePanel(g) + } - content = getDiff(gitFile) - return renderString(g, "main", content) + content = getDiff(gitFile) + return renderString(g, "main", content) } func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { - if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts { - return createErrorPanel(g, "There are no staged files to commit") - } - createPromptPanel(g, filesView, "Commit message", func(g *gocui.Gui, v *gocui.View) error { - message := trimmedContent(v) - if message == "" { - return createErrorPanel(g, "You cannot commit without a commit message") - } - if err := gitCommit(message); err != nil { - panic(err) - } - refreshFiles(g) - return refreshCommits(g) - }) - return nil + if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts { + return createErrorPanel(g, "There are no staged files to commit") + } + createPromptPanel(g, filesView, "Commit message", func(g *gocui.Gui, v *gocui.View) error { + message := trimmedContent(v) + if message == "" { + return createErrorPanel(g, "You cannot commit without a commit message") + } + if err := gitCommit(message); err != nil { + panic(err) + } + refreshFiles(g) + return refreshCommits(g) + }) + return nil } func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (string, error)) error { - file, err := getSelectedFile(g) - if err != nil { - return err - } - _, err = open(file.Name) - return err + file, err := getSelectedFile(g) + if err != nil { + return err + } + _, err = open(file.Name) + return err } func handleFileOpen(g *gocui.Gui, v *gocui.View) error { - return genericFileOpen(g, v, openFile) + return genericFileOpen(g, v, openFile) } func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { - return genericFileOpen(g, v, sublimeOpenFile) + return genericFileOpen(g, v, sublimeOpenFile) +} +func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error { + return genericFileOpen(g, v, vsCodeOpenFile) } func refreshStateGitFiles() { - // get files to stage - gitFiles := getGitStatusFiles() - state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles) - updateHasMergeConflictStatus() + // get files to stage + gitFiles := getGitStatusFiles() + state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles) + updateHasMergeConflictStatus() } func updateHasMergeConflictStatus() error { - merging, err := isInMergeState() - if err != nil { - return err - } - state.HasMergeConflicts = merging - return nil + merging, err := isInMergeState() + if err != nil { + return err + } + state.HasMergeConflicts = merging + return nil } func renderGitFile(gitFile GitFile, filesView *gocui.View) { - // potentially inefficient to be instantiating these color - // objects with each render - red := color.New(color.FgRed) - green := color.New(color.FgGreen) - if !gitFile.Tracked { - red.Fprintln(filesView, gitFile.DisplayString) - return - } - green.Fprint(filesView, gitFile.DisplayString[0:1]) - red.Fprint(filesView, gitFile.DisplayString[1:3]) - if gitFile.HasUnstagedChanges { - red.Fprintln(filesView, gitFile.Name) - } else { - green.Fprintln(filesView, gitFile.Name) - } + // potentially inefficient to be instantiating these color + // objects with each render + red := color.New(color.FgRed) + green := color.New(color.FgGreen) + if !gitFile.Tracked && !gitFile.HasStagedChanges { + red.Fprintln(filesView, gitFile.DisplayString) + return + } + green.Fprint(filesView, gitFile.DisplayString[0:1]) + red.Fprint(filesView, gitFile.DisplayString[1:3]) + if gitFile.HasUnstagedChanges { + red.Fprintln(filesView, gitFile.Name) + } else { + green.Fprintln(filesView, gitFile.Name) + } } func catSelectedFile(g *gocui.Gui) (string, error) { - item, err := getSelectedFile(g) - if err != nil { - if err != ErrNoFiles { - return "", err - } - return "", renderString(g, "main", "No file to display") - } - cat, err := catFile(item.Name) - if err != nil { - panic(err) - } - return cat, nil + item, err := getSelectedFile(g) + if err != nil { + if err != ErrNoFiles { + return "", err + } + return "", renderString(g, "main", "No file to display") + } + cat, err := catFile(item.Name) + if err != nil { + panic(err) + } + return cat, nil } func refreshFiles(g *gocui.Gui) error { - filesView, err := g.View("files") - if err != nil { - return err - } - refreshStateGitFiles() - filesView.Clear() - for _, gitFile := range state.GitFiles { - renderGitFile(gitFile, filesView) - } - correctCursor(filesView) - if filesView == g.CurrentView() { - handleFileSelect(g, filesView) - } - return nil + filesView, err := g.View("files") + if err != nil { + return err + } + refreshStateGitFiles() + filesView.Clear() + for _, gitFile := range state.GitFiles { + renderGitFile(gitFile, filesView) + } + correctCursor(filesView) + if filesView == g.CurrentView() { + handleFileSelect(g, filesView) + } + return nil } func pullFiles(g *gocui.Gui, v *gocui.View) error { - devLog("pulling...") - createMessagePanel(g, v, "", "Pulling...") - go func() { - if output, err := gitPull(); err != nil { - createErrorPanel(g, output) - } else { - closeConfirmationPrompt(g) - refreshCommits(g) - refreshFiles(g) - refreshStatus(g) - devLog("pulled.") - } - }() - return nil + devLog("pulling...") + createMessagePanel(g, v, "", "Pulling...") + go func() { + if output, err := gitPull(); err != nil { + createErrorPanel(g, output) + } else { + closeConfirmationPrompt(g) + refreshCommits(g) + refreshFiles(g) + refreshStatus(g) + devLog("pulled.") + } + }() + return nil } func pushFiles(g *gocui.Gui, v *gocui.View) error { - devLog("pushing...") - createMessagePanel(g, v, "", "Pushing...") - go func() { - if output, err := gitPush(); err != nil { - createErrorPanel(g, output) - } else { - closeConfirmationPrompt(g) - refreshCommits(g) - refreshStatus(g) - devLog("pushed.") - } - }() - return nil + devLog("pushing...") + createMessagePanel(g, v, "", "Pushing...") + go func() { + if output, err := gitPush(); err != nil { + createErrorPanel(g, output) + } else { + closeConfirmationPrompt(g) + refreshCommits(g) + refreshStatus(g) + devLog("pushed.") + } + }() + return nil } func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error { - mergeView, err := g.View("main") - if err != nil { - return err - } - file, err := getSelectedFile(g) - if err != nil { - if err != ErrNoFiles { - return err - } - return nil - } - if !file.HasMergeConflicts { - return nil - } - switchFocus(g, v, mergeView) - return refreshMergePanel(g) + mergeView, err := g.View("main") + if err != nil { + return err + } + file, err := getSelectedFile(g) + if err != nil { + if err != ErrNoFiles { + return err + } + return nil + } + if !file.HasMergeConflicts { + return nil + } + switchFocus(g, v, mergeView) + return refreshMergePanel(g) } func handleAbortMerge(g *gocui.Gui, v *gocui.View) error { - output, err := gitAbortMerge() - if err != nil { - return createErrorPanel(g, output) - } - createMessagePanel(g, v, "", "Merge aborted") - return refreshFiles(g) + output, err := gitAbortMerge() + if err != nil { + return createErrorPanel(g, output) + } + createMessagePanel(g, v, "", "Merge aborted") + refreshStatus(g) + return refreshFiles(g) } diff --git a/gitcommands.go b/gitcommands.go index 091a638f8..aa80a0d1b 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -2,454 +2,510 @@ package main import ( - // "log" - "errors" - "fmt" - "os" - "os/exec" - "strings" - "time" + // "log" + "errors" + "fmt" + "os" + "os/exec" + "strings" + "time" - "github.com/fatih/color" + "github.com/fatih/color" ) // GitFile : A staged/unstaged file // TODO: decide whether to give all of these the Git prefix type GitFile struct { - Name string - HasStagedChanges bool - HasUnstagedChanges bool - Tracked bool - Deleted bool - HasMergeConflicts bool - DisplayString string + Name string + HasStagedChanges bool + HasUnstagedChanges bool + Tracked bool + Deleted bool + HasMergeConflicts bool + DisplayString string } // Branch : A git branch type Branch struct { - Name string - Type string - BaseBranch string - DisplayString string + Name string + Type string + BaseBranch string + DisplayString string } // Commit : A git commit type Commit struct { - Sha string - Name string - Pushed bool - DisplayString string + Sha string + Name string + Pushed bool + DisplayString string } // StashEntry : A git stash entry type StashEntry struct { - Index int - Name string - DisplayString string + Index int + Name string + DisplayString string } // Map (from https://gobyexample.com/collection-functions) func Map(vs []string, f func(string) string) []string { - vsm := make([]string, len(vs)) - for i, v := range vs { - vsm[i] = f(v) - } - return vsm + vsm := make([]string, len(vs)) + for i, v := range vs { + vsm[i] = f(v) + } + return vsm } func includesString(list []string, a string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false + for _, b := range list { + if b == a { + return true + } + } + return false } // not sure how to genericise this because []interface{} doesn't accept e.g. // []int arguments func includesInt(list []int, a int) bool { - for _, b := range list { - if b == a { - return true - } - } - return false + for _, b := range list { + if b == a { + return true + } + } + return false } func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { - if len(oldGitFiles) == 0 { - return newGitFiles - } + if len(oldGitFiles) == 0 { + return newGitFiles + } - appendedIndexes := make([]int, 0) + appendedIndexes := make([]int, 0) - // retain position of files we already could see - result := make([]GitFile, 0) - for _, oldGitFile := range oldGitFiles { - for newIndex, newGitFile := range newGitFiles { - if oldGitFile.Name == newGitFile.Name { - result = append(result, newGitFile) - appendedIndexes = append(appendedIndexes, newIndex) - break - } - } - } + // retain position of files we already could see + result := make([]GitFile, 0) + for _, oldGitFile := range oldGitFiles { + for newIndex, newGitFile := range newGitFiles { + if oldGitFile.Name == newGitFile.Name { + result = append(result, newGitFile) + appendedIndexes = append(appendedIndexes, newIndex) + break + } + } + } - // append any new files to the end - for index, newGitFile := range newGitFiles { - if !includesInt(appendedIndexes, index) { - result = append(result, newGitFile) - } - } + // append any new files to the end + for index, newGitFile := range newGitFiles { + if !includesInt(appendedIndexes, index) { + result = append(result, newGitFile) + } + } - return result + return result } func runDirectCommand(command string) (string, error) { - timeStart := time.Now() + timeStart := time.Now() - commandLog(command) - cmdOut, err := exec.Command("bash", "-c", command).CombinedOutput() - devLog("run direct command time for command: ", command, time.Now().Sub(timeStart)) + commandLog(command) + cmdOut, err := exec. + Command("bash", "-c", command). + CombinedOutput() + devLog("run direct command time for command: ", command, time.Now().Sub(timeStart)) - return string(cmdOut), err + return string(cmdOut), err } func branchStringParts(branchString string) (string, string) { - splitBranchName := strings.Split(branchString, "\t") - return splitBranchName[0], splitBranchName[1] + // expect string to be something like '4w master` + splitBranchName := strings.Split(branchString, "\t") + // if we have no \t then we have no recency, so just output that as blank + if len(splitBranchName) == 1 { + return "", branchString + } + return splitBranchName[0], splitBranchName[1] } // branchPropertiesFromName : returns branch type, base, and color func branchPropertiesFromName(name string) (string, string, color.Attribute) { - if strings.Contains(name, "feature/") { - return "feature", "develop", color.FgGreen - } else if strings.Contains(name, "bugfix/") { - return "bugfix", "develop", color.FgYellow - } else if strings.Contains(name, "hotfix/") { - return "hotfix", "master", color.FgRed - } - return "other", name, color.FgWhite + if strings.Contains(name, "feature/") { + return "feature", "develop", color.FgGreen + } else if strings.Contains(name, "bugfix/") { + return "bugfix", "develop", color.FgYellow + } else if strings.Contains(name, "hotfix/") { + return "hotfix", "master", color.FgRed + } + return "other", name, color.FgWhite } func coloredString(str string, colour *color.Color) string { - return colour.SprintFunc()(fmt.Sprint(str)) + return colour.SprintFunc()(fmt.Sprint(str)) } func withPadding(str string, padding int) string { - return str + strings.Repeat(" ", padding-len(str)) + if padding-len(str) < 0 { + return str + } + return str + strings.Repeat(" ", padding-len(str)) } func branchFromLine(line string, index int) Branch { - recency, name := branchStringParts(line) - branchType, branchBase, colourAttr := branchPropertiesFromName(name) - if index == 0 { - recency = " *" - } - colour := color.New(colourAttr) - displayString := withPadding(recency, 4) + coloredString(name, colour) - return Branch{ - Name: name, - Type: branchType, - BaseBranch: branchBase, - DisplayString: displayString, - } + recency, name := branchStringParts(line) + branchType, branchBase, colourAttr := branchPropertiesFromName(name) + if index == 0 { + recency = " *" + } + colour := color.New(colourAttr) + displayString := withPadding(recency, 4) + coloredString(name, colour) + return Branch{ + Name: name, + Type: branchType, + BaseBranch: branchBase, + DisplayString: displayString, + } } func getGitBranches() []Branch { - branches := make([]Branch, 0) - // check if there are any branches - branchCheck, _ := runDirectCommand("git branch") - if branchCheck == "" { - return branches - } - rawString, _ := runDirectCommand(getBranchesCommand) - branchLines := splitLines(rawString) - if len(branchLines) == 0 { - // sometimes the getBranchesCommand command returns nothing, in which case - // we assume you've just init'd or cloned the repo and you've got master - // checked out - branches = append(branches, branchFromLine(" *\tmaster", 0)) - } - for i, line := range branchLines { - branches = append(branches, branchFromLine(line, i)) - } - return branches + branches := make([]Branch, 0) + // check if there are any branches + branchCheck, _ := runDirectCommand("git branch") + if branchCheck == "" { + return append(branches, branchFromLine("master", 0)) + } + rawString, _ := runDirectCommand(getBranchesCommand) + branchLines := splitLines(rawString) + for i, line := range branchLines { + branches = append(branches, branchFromLine(line, i)) + } + branches = getAndMergeFetchedBranches(branches) + return branches +} + +func branchAlreadyStored(branchLine string, branches []Branch) bool { + for _, branch := range branches { + if branch.Name == branchLine { + return true + } + } + return false +} + +// here branches contains all the branches that we've checked out, along with +// the recency. In this function we append the branches that are in our heads +// directory i.e. things we've fetched but haven't necessarily checked out. +// Worth mentioning this has nothing to do with the 'git merge' operation +func getAndMergeFetchedBranches(branches []Branch) []Branch { + rawString, _ := runDirectCommand(getHeadsCommand) + branchLines := splitLines(rawString) + for _, line := range branchLines { + if branchAlreadyStored(line, branches) { + continue + } + branches = append(branches, branchFromLine(line, len(branches))) + } + return branches } // TODO: DRY up this function and getGitBranches func getGitStashEntries() []StashEntry { - stashEntries := make([]StashEntry, 0) - rawString, _ := runDirectCommand("git stash list --pretty='%gs'") - for i, line := range splitLines(rawString) { - stashEntries = append(stashEntries, stashEntryFromLine(line, i)) - } - return stashEntries + stashEntries := make([]StashEntry, 0) + rawString, _ := runDirectCommand("git stash list --pretty='%gs'") + for i, line := range splitLines(rawString) { + stashEntries = append(stashEntries, stashEntryFromLine(line, i)) + } + return stashEntries } func stashEntryFromLine(line string, index int) StashEntry { - return StashEntry{ - Name: line, - Index: index, - DisplayString: line, - } + return StashEntry{ + Name: line, + Index: index, + DisplayString: line, + } } func getStashEntryDiff(index int) (string, error) { - return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}") + return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}") +} + +func includes(array []string, str string) bool { + for _, arrayStr := range array { + if arrayStr == str { + return true + } + } + return false } func getGitStatusFiles() []GitFile { - statusOutput, _ := getGitStatus() - statusStrings := splitLines(statusOutput) - gitFiles := make([]GitFile, 0) + statusOutput, _ := getGitStatus() + statusStrings := splitLines(statusOutput) + gitFiles := make([]GitFile, 0) - for _, statusString := range statusStrings { - stagedChange := statusString[0:1] - unstagedChange := statusString[1:2] - filename := statusString[3:] - tracked := statusString[0:2] != "??" - gitFile := GitFile{ - Name: filename, - DisplayString: statusString, - HasStagedChanges: tracked && stagedChange != " " && stagedChange != "U", - HasUnstagedChanges: !tracked || unstagedChange != " ", - Tracked: tracked, - Deleted: unstagedChange == "D" || stagedChange == "D", - HasMergeConflicts: statusString[0:2] == "UU", - } - gitFiles = append(gitFiles, gitFile) - } - return gitFiles + for _, statusString := range statusStrings { + change := statusString[0:2] + stagedChange := change[0:1] + unstagedChange := statusString[1:2] + filename := statusString[3:] + tracked := !includes([]string{"??", "A "}, change) + gitFile := GitFile{ + Name: filename, + DisplayString: statusString, + HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange), + HasUnstagedChanges: unstagedChange != " ", + Tracked: tracked, + Deleted: unstagedChange == "D" || stagedChange == "D", + HasMergeConflicts: change == "UU", + } + devLog("tracked", gitFile.Tracked) + devLog("hasUnstagedChanges", gitFile.HasUnstagedChanges) + devLog("HasStagedChanges", gitFile.HasStagedChanges) + devLog("DisplayString", gitFile.DisplayString) + gitFiles = append(gitFiles, gitFile) + } + devLog(gitFiles) + return gitFiles } func gitStashDo(index int, method string) (string, error) { - return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}") + return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}") } func gitStashSave(message string) (string, error) { - output, err := runCommand("git stash save \"" + message + "\"") - if err != nil { - return output, err - } - // if there are no local changes to save, the exit code is 0, but we want - // to raise an error - if output == "No local changes to save\n" { - return output, errors.New(output) - } - return output, nil + output, err := runCommand("git stash save \"" + message + "\"") + if err != nil { + return output, err + } + // if there are no local changes to save, the exit code is 0, but we want + // to raise an error + if output == "No local changes to save\n" { + return output, errors.New(output) + } + return output, nil } func gitCheckout(branch string, force bool) (string, error) { - forceArg := "" - if force { - forceArg = "--force " - } - return runCommand("git checkout " + forceArg + branch) + forceArg := "" + if force { + forceArg = "--force " + } + return runCommand("git checkout " + forceArg + branch) } func runCommand(command string) (string, error) { - commandStartTime := time.Now() - commandLog(command) - splitCmd := strings.Split(command, " ") - cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() - devLog("run command time: ", time.Now().Sub(commandStartTime)) - return string(cmdOut), err + commandStartTime := time.Now() + commandLog(command) + splitCmd := strings.Split(command, " ") + cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() + devLog("run command time: ", time.Now().Sub(commandStartTime)) + return string(cmdOut), err } func openFile(filename string) (string, error) { - return runCommand("open " + filename) + return runCommand("open " + filename) +} + +func vsCodeOpenFile(filename string) (string, error) { + return runCommand("code -r " + filename) } func sublimeOpenFile(filename string) (string, error) { - return runCommand("subl " + filename) + return runCommand("subl " + filename) } func getBranchDiff(branch string, baseBranch string) (string, error) { - return runCommand("git diff --color " + baseBranch + "..." + branch) + return runCommand("git diff --color " + baseBranch + "..." + branch) } func verifyInGitRepo() { - if output, err := runCommand("git status"); err != nil { - fmt.Println(output) - os.Exit(1) - } + if output, err := runCommand("git status"); err != nil { + fmt.Println(output) + os.Exit(1) + } } func getCommits() []Commit { - pushables := gitCommitsToPush() - log := getLog() - commits := make([]Commit, 0) - // now we can split it up and turn it into commits - lines := splitLines(log) - for _, line := range lines { - splitLine := strings.Split(line, " ") - sha := splitLine[0] - pushed := includesString(pushables, sha) - commits = append(commits, Commit{ - Sha: sha, - Name: strings.Join(splitLine[1:], " "), - Pushed: pushed, - DisplayString: strings.Join(splitLine, " "), - }) - } - return commits + pushables := gitCommitsToPush() + log := getLog() + commits := make([]Commit, 0) + // now we can split it up and turn it into commits + lines := splitLines(log) + for _, line := range lines { + splitLine := strings.Split(line, " ") + sha := splitLine[0] + pushed := includesString(pushables, sha) + commits = append(commits, Commit{ + Sha: sha, + Name: strings.Join(splitLine[1:], " "), + Pushed: pushed, + DisplayString: strings.Join(splitLine, " "), + }) + } + return commits } func getLog() string { - // currently limiting to 30 for performance reasons - // TODO: add lazyloading when you scroll down - result, err := runDirectCommand("git log --oneline -30") - if err != nil { - // assume if there is an error there are no commits yet for this branch - return "" - } - return result + // currently limiting to 30 for performance reasons + // TODO: add lazyloading when you scroll down + result, err := runDirectCommand("git log --oneline -30") + if err != nil { + // assume if there is an error there are no commits yet for this branch + return "" + } + return result } func gitIgnore(filename string) { - if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil { - panic(err) - } + if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil { + panic(err) + } } func gitShow(sha string) string { - result, err := runDirectCommand("git show --color " + sha) - if err != nil { - panic(err) - } - return result + result, err := runDirectCommand("git show --color " + sha) + if err != nil { + panic(err) + } + return result } func getDiff(file GitFile) string { - cachedArg := "" - if file.HasStagedChanges { - cachedArg = "--cached " - } - deletedArg := "" - if file.Deleted { - deletedArg = "-- " - } - trackedArg := "" - if !file.Tracked { - trackedArg = "--no-index /dev/null " - } - command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name - // for now we assume an error means the file was deleted - s, _ := runCommand(command) - return s + cachedArg := "" + if file.HasStagedChanges && !file.HasUnstagedChanges { + cachedArg = "--cached " + } + deletedArg := "" + if file.Deleted { + deletedArg = "-- " + } + trackedArg := "" + if !file.Tracked && !file.HasStagedChanges { + trackedArg = "--no-index /dev/null " + } + command := "git diff -b --color " + cachedArg + deletedArg + trackedArg + file.Name + // for now we assume an error means the file was deleted + s, _ := runCommand(command) + return s } func catFile(file string) (string, error) { - return runDirectCommand("cat " + file) + return runDirectCommand("cat " + file) } func stageFile(file string) error { - _, err := runCommand("git add " + file) - return err + _, err := runCommand("git add " + file) + return err } -func unStageFile(file string) error { - _, err := runCommand("git reset HEAD " + file) - return err +func unStageFile(file string, tracked bool) error { + var command string + if tracked { + command = "git reset HEAD " + } else { + command = "git rm --cached " + } + devLog(command) + _, err := runCommand(command + file) + return err } func getGitStatus() (string, error) { - return runCommand("git status --untracked-files=all --short") + return runCommand("git status --untracked-files=all --short") } func isInMergeState() (bool, error) { - output, err := runCommand("git status --untracked-files=all") - if err != nil { - return false, err - } - return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil + output, err := runCommand("git status --untracked-files=all") + if err != nil { + return false, err + } + return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil } func removeFile(file GitFile) error { - // if the file isn't tracked, we assume you want to delete it - if !file.Tracked { - _, err := runCommand("rm -rf ./" + file.Name) - return err - } - // if the file is tracked, we assume you want to just check it out - _, err := runCommand("git checkout " + file.Name) - return err + // if the file isn't tracked, we assume you want to delete it + if !file.Tracked { + _, err := runCommand("rm -rf ./" + file.Name) + return err + } + // if the file is tracked, we assume you want to just check it out + _, err := runCommand("git checkout " + file.Name) + return err } func gitCommit(message string) error { - _, err := runDirectCommand("git commit -m \"" + message + "\"") - return err + _, err := runDirectCommand("git commit -m \"" + message + "\"") + return err } func gitPull() (string, error) { - return runDirectCommand("git pull --no-edit") + return runDirectCommand("git pull --no-edit") } func gitPush() (string, error) { - return runDirectCommand("git push -u") + return runDirectCommand("git push -u") } func gitSquashPreviousTwoCommits(message string) (string, error) { - return runDirectCommand("git reset --soft head^ && git commit --amend -m \"" + message + "\"") + return runDirectCommand("git reset --soft head^ && git commit --amend -m \"" + message + "\"") } func gitRenameCommit(message string) (string, error) { - return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"") + return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"") } func gitFetch() (string, error) { - return runDirectCommand("git fetch") + return runDirectCommand("git fetch") } func gitResetToCommit(sha string) (string, error) { - return runDirectCommand("git reset " + sha) + return runDirectCommand("git reset " + sha) } func gitNewBranch(name string) (string, error) { - return runDirectCommand("git checkout -b " + name) + return runDirectCommand("git checkout -b " + name) } func gitListStash() (string, error) { - return runDirectCommand("git stash list") + return runDirectCommand("git stash list") } func gitMerge(branchName string) (string, error) { - return runDirectCommand("git merge --no-edit " + branchName) + return runDirectCommand("git merge --no-edit " + branchName) } func gitAbortMerge() (string, error) { - return runDirectCommand("git merge --abort") + return runDirectCommand("git merge --abort") } func gitUpstreamDifferenceCount() (string, string) { - pushableCount, err := runDirectCommand("git rev-list @{u}..head --count") - if err != nil { - return "?", "?" - } - pullableCount, err := runDirectCommand("git rev-list head..@{u} --count") - if err != nil { - return "?", "?" - } - return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) + pushableCount, err := runDirectCommand("git rev-list @{u}..head --count") + if err != nil { + return "?", "?" + } + pullableCount, err := runDirectCommand("git rev-list head..@{u} --count") + if err != nil { + return "?", "?" + } + return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) } func gitCommitsToPush() []string { - pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit") - if err != nil { - return make([]string, 0) - } - return splitLines(pushables) + pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit") + if err != nil { + return make([]string, 0) + } + return splitLines(pushables) } func gitCurrentBranchName() string { - branchName, err := runDirectCommand("git rev-parse --abbrev-ref HEAD") - // if there is an error, assume there are no branches yet - if err != nil { - return "" - } - return branchName + branchName, err := runDirectCommand("git symbolic-ref --short HEAD") + // if there is an error, assume there are no branches yet + if err != nil { + return "" + } + return branchName } const getBranchesCommand = `set -e @@ -465,6 +521,26 @@ git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD | { printf "%s\t%s\n" "$date" "$branch" fi fi - done | sed 's/ days /d /g' | sed 's/ weeks /w /g' | sed 's/ hours /h /g' | sed 's/ minutes /m /g' | sed 's/ seconds /m /g' | sed 's/ago//g' | tr -d ' ' + done \ + | sed 's/ days /d /g' \ + | sed 's/ day /d /g' \ + | sed 's/ weeks /w /g' \ + | sed 's/ week /w /g' \ + | sed 's/ hours /h /g' \ + | sed 's/ hour /h /g' \ + | sed 's/ minutes /m /g' \ + | sed 's/ minute /m /g' \ + | sed 's/ seconds /s /g' \ + | sed 's/ second /s /g' \ + | sed 's/ago//g' \ + | tr -d ' ' } ` + +const getHeadsCommand = `git show-ref \ +| grep 'refs/heads/\|refs/remotes/origin/' \ +| sed 's/.*refs\/heads\///g' \ +| sed 's/.*refs\/remotes\/origin\///g' \ +| grep -v '^HEAD$' \ +| sort \ +| uniq` diff --git a/gui.go b/gui.go index 01ae7640f..6a343ce21 100644 --- a/gui.go +++ b/gui.go @@ -2,300 +2,304 @@ package main import ( - // "io" - // "io/ioutil" + // "io" + // "io/ioutil" - "log" - "time" - // "strings" - "github.com/golang-collections/collections/stack" - "github.com/jesseduffield/gocui" + "log" + "time" + + // "strings" + "github.com/golang-collections/collections/stack" + "github.com/jesseduffield/gocui" ) type stateType struct { - GitFiles []GitFile - Branches []Branch - Commits []Commit - StashEntries []StashEntry - PreviousView string - HasMergeConflicts bool - ConflictIndex int - ConflictTop bool - Conflicts []conflict - EditHistory *stack.Stack + GitFiles []GitFile + Branches []Branch + Commits []Commit + StashEntries []StashEntry + PreviousView string + HasMergeConflicts bool + ConflictIndex int + ConflictTop bool + Conflicts []conflict + EditHistory *stack.Stack } type conflict struct { - start int - middle int - end int + start int + middle int + end int } var state = stateType{ - GitFiles: make([]GitFile, 0), - PreviousView: "files", - Commits: make([]Commit, 0), - StashEntries: make([]StashEntry, 0), - ConflictIndex: 0, - ConflictTop: true, - Conflicts: make([]conflict, 0), - EditHistory: stack.New(), + GitFiles: make([]GitFile, 0), + PreviousView: "files", + Commits: make([]Commit, 0), + StashEntries: make([]StashEntry, 0), + ConflictIndex: 0, + ConflictTop: true, + Conflicts: make([]conflict, 0), + EditHistory: stack.New(), } func scrollUpMain(g *gocui.Gui, v *gocui.View) error { - mainView, _ := g.View("main") - ox, oy := mainView.Origin() - if oy >= 1 { - return mainView.SetOrigin(ox, oy-1) - } - return nil + mainView, _ := g.View("main") + ox, oy := mainView.Origin() + if oy >= 1 { + return mainView.SetOrigin(ox, oy-1) + } + return nil } func scrollDownMain(g *gocui.Gui, v *gocui.View) error { - mainView, _ := g.View("main") - ox, oy := mainView.Origin() - if oy < len(mainView.BufferLines()) { - return mainView.SetOrigin(ox, oy+1) - } - return nil + mainView, _ := g.View("main") + ox, oy := mainView.Origin() + if oy < len(mainView.BufferLines()) { + return mainView.SetOrigin(ox, oy+1) + } + return nil } func handleRefresh(g *gocui.Gui, v *gocui.View) error { - return refreshSidePanels(g) + return refreshSidePanels(g) } func keybindings(g *gocui.Gui) error { - if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil { - return err - } - if err := g.SetKeybinding("", 'q', gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, scrollUpMain); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil { - return err - } - if err := g.SetKeybinding("", 'P', gocui.ModNone, pushFiles); err != nil { - return err - } - if err := g.SetKeybinding("", 'p', gocui.ModNone, pullFiles); err != nil { - return err - } - if err := g.SetKeybinding("", 'R', gocui.ModNone, handleRefresh); err != nil { - return err - } - if err := g.SetKeybinding("files", 'c', gocui.ModNone, handleCommitPress); err != nil { - return err - } - if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil { - return err - } - if err := g.SetKeybinding("files", 'd', gocui.ModNone, handleFileRemove); err != nil { - return err - } - if err := g.SetKeybinding("files", 'm', gocui.ModNone, handleSwitchToMerge); err != nil { - return err - } - if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil { - return err - } - if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil { - return err - } - if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil { - return err - } - if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil { - return err - } - if err := g.SetKeybinding("files", 'a', gocui.ModNone, handleAbortMerge); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowUp, gocui.ModNone, handleSelectTop); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyEsc, gocui.ModNone, handleEscapeMerge); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowDown, gocui.ModNone, handleSelectBottom); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeySpace, gocui.ModNone, handlePickConflict); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowLeft, gocui.ModNone, handleSelectPrevConflict); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowRight, gocui.ModNone, handleSelectNextConflict); err != nil { - return err - } - if err := g.SetKeybinding("main", 'z', gocui.ModNone, handlePopFileSnapshot); err != nil { - return err - } - if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'c', gocui.ModNone, handleCheckoutByName); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'F', gocui.ModNone, handleForceCheckout); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'n', gocui.ModNone, handleNewBranch); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'm', gocui.ModNone, handleMerge); err != nil { - return err - } - if err := g.SetKeybinding("commits", 's', gocui.ModNone, handleCommitSquashDown); err != nil { - return err - } - if err := g.SetKeybinding("commits", 'r', gocui.ModNone, handleRenameCommit); err != nil { - return err - } - if err := g.SetKeybinding("commits", 'g', gocui.ModNone, handleResetToCommit); err != nil { - return err - } - if err := g.SetKeybinding("stash", gocui.KeySpace, gocui.ModNone, handleStashApply); err != nil { - return err - } - // TODO: come up with a better keybinding (p/P used for pushing/pulling which - // I'd like to be global. Perhaps all global keybindings should use a modifier - // like command? But then there's gonna be hotkey conflicts with the terminal - if err := g.SetKeybinding("stash", 'k', gocui.ModNone, handleStashPop); err != nil { - return err - } - if err := g.SetKeybinding("stash", 'd', gocui.ModNone, handleStashDrop); err != nil { - return err - } - return nil + if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil { + return err + } + if err := g.SetKeybinding("", 'q', gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, scrollUpMain); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil { + return err + } + if err := g.SetKeybinding("", 'P', gocui.ModNone, pushFiles); err != nil { + return err + } + if err := g.SetKeybinding("", 'p', gocui.ModNone, pullFiles); err != nil { + return err + } + if err := g.SetKeybinding("", 'R', gocui.ModNone, handleRefresh); err != nil { + return err + } + if err := g.SetKeybinding("files", 'c', gocui.ModNone, handleCommitPress); err != nil { + return err + } + if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil { + return err + } + if err := g.SetKeybinding("files", 'd', gocui.ModNone, handleFileRemove); err != nil { + return err + } + if err := g.SetKeybinding("files", 'm', gocui.ModNone, handleSwitchToMerge); err != nil { + return err + } + if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil { + return err + } + if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil { + return err + } + if err := g.SetKeybinding("files", 'v', gocui.ModNone, handleVsCodeFileOpen); err != nil { + return err + } + if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil { + return err + } + if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil { + return err + } + if err := g.SetKeybinding("files", 'a', gocui.ModNone, handleAbortMerge); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowUp, gocui.ModNone, handleSelectTop); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyEsc, gocui.ModNone, handleEscapeMerge); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowDown, gocui.ModNone, handleSelectBottom); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeySpace, gocui.ModNone, handlePickConflict); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowLeft, gocui.ModNone, handleSelectPrevConflict); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowRight, gocui.ModNone, handleSelectNextConflict); err != nil { + return err + } + if err := g.SetKeybinding("main", 'z', gocui.ModNone, handlePopFileSnapshot); err != nil { + return err + } + if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { + return err + } + if err := g.SetKeybinding("branches", 'c', gocui.ModNone, handleCheckoutByName); err != nil { + return err + } + if err := g.SetKeybinding("branches", 'F', gocui.ModNone, handleForceCheckout); err != nil { + return err + } + if err := g.SetKeybinding("branches", 'n', gocui.ModNone, handleNewBranch); err != nil { + return err + } + if err := g.SetKeybinding("branches", 'm', gocui.ModNone, handleMerge); err != nil { + return err + } + if err := g.SetKeybinding("commits", 's', gocui.ModNone, handleCommitSquashDown); err != nil { + return err + } + if err := g.SetKeybinding("commits", 'r', gocui.ModNone, handleRenameCommit); err != nil { + return err + } + if err := g.SetKeybinding("commits", 'g', gocui.ModNone, handleResetToCommit); err != nil { + return err + } + if err := g.SetKeybinding("stash", gocui.KeySpace, gocui.ModNone, handleStashApply); err != nil { + return err + } + // TODO: come up with a better keybinding (p/P used for pushing/pulling which + // I'd like to be global. Perhaps all global keybindings should use a modifier + // like command? But then there's gonna be hotkey conflicts with the terminal + if err := g.SetKeybinding("stash", 'k', gocui.ModNone, handleStashPop); err != nil { + return err + } + if err := g.SetKeybinding("stash", 'd', gocui.ModNone, handleStashDrop); err != nil { + return err + } + return nil } func layout(g *gocui.Gui) error { - g.Highlight = true - g.SelFgColor = gocui.AttrBold - width, height := g.Size() - leftSideWidth := width / 3 - statusFilesBoundary := 2 - filesBranchesBoundary := 2 * height / 5 // height - 20 - commitsBranchesBoundary := 3 * height / 5 // height - 10 - commitsStashBoundary := height - 5 // height - 5 + g.Highlight = true + g.SelFgColor = gocui.AttrBold + width, height := g.Size() + leftSideWidth := width / 3 + statusFilesBoundary := 2 + filesBranchesBoundary := 2 * height / 5 // height - 20 + commitsBranchesBoundary := 3 * height / 5 // height - 10 + commitsStashBoundary := height - 5 // height - 5 - optionsTop := height - 2 - // hiding options if there's not enough space - if height < 30 { - optionsTop = height - 1 - } + optionsTop := height - 2 + // hiding options if there's not enough space + if height < 30 { + optionsTop = height - 1 + } - filesView, err := g.SetView("files", 0, statusFilesBoundary+1, leftSideWidth, filesBranchesBoundary-1) - if err != nil { - if err != gocui.ErrUnknownView { - return err - } - filesView.Highlight = true - filesView.Title = "Files" - } + filesView, err := g.SetView("files", 0, statusFilesBoundary+1, leftSideWidth, filesBranchesBoundary-1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + filesView.Highlight = true + filesView.Title = "Files" + } - if v, err := g.SetView("status", 0, statusFilesBoundary-2, leftSideWidth, statusFilesBoundary); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Status" - } + if v, err := g.SetView("status", 0, statusFilesBoundary-2, leftSideWidth, statusFilesBoundary); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Status" + } - mainView, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop) - if err != nil { - if err != gocui.ErrUnknownView { - return err - } - mainView.Title = "Diff" - mainView.Wrap = true - } + mainView, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + mainView.Title = "Diff" + mainView.Wrap = true + } - if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, commitsBranchesBoundary-1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Branches" + if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, commitsBranchesBoundary-1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Branches" - } + } - if v, err := g.SetView("commits", 0, commitsBranchesBoundary, leftSideWidth, commitsStashBoundary-1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Commits" + if v, err := g.SetView("commits", 0, commitsBranchesBoundary, leftSideWidth, commitsStashBoundary-1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Commits" - } + } - if v, err := g.SetView("stash", 0, commitsStashBoundary, leftSideWidth, optionsTop); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Stash" - } + if v, err := g.SetView("stash", 0, commitsStashBoundary, leftSideWidth, optionsTop); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Stash" + } - if v, err := g.SetView("options", -1, optionsTop, width, optionsTop+2); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.BgColor = gocui.ColorBlue - v.Frame = false - v.Title = "Options" + if v, err := g.SetView("options", -1, optionsTop, width, optionsTop+2); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.BgColor = gocui.ColorBlue + v.Frame = false + v.Title = "Options" - // these are only called once - handleFileSelect(g, filesView) - refreshFiles(g) - refreshBranches(g) - refreshCommits(g) - refreshStashEntries(g) - nextView(g, nil) - } + // these are only called once + handleFileSelect(g, filesView) + refreshFiles(g) + refreshBranches(g) + refreshCommits(g) + refreshStashEntries(g) + nextView(g, nil) + } - return nil + return nil } func fetch(g *gocui.Gui) { - gitFetch() - refreshStatus(g) + gitFetch() + refreshStatus(g) } func run() { - g, err := gocui.NewGui(gocui.OutputNormal) - if err != nil { - log.Panicln(err) - } - defer g.Close() + g, err := gocui.NewGui(gocui.OutputNormal) + if err != nil { + log.Panicln(err) + } + defer g.Close() - // periodically fetching to check for upstream differences - go func() { - for range time.Tick(time.Second * 60) { - fetch(g) - } - }() + // periodically fetching to check for upstream differences + go func() { + for range time.Tick(time.Second * 60) { + fetch(g) + } + }() - g.SetManagerFunc(layout) + g.SetManagerFunc(layout) - if err := keybindings(g); err != nil { - log.Panicln(err) - } + if err := keybindings(g); err != nil { + log.Panicln(err) + } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - log.Panicln(err) - } + if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } } func quit(g *gocui.Gui, v *gocui.View) error { - return gocui.ErrQuit + return gocui.ErrQuit } diff --git a/main.go b/main.go index 9e7b52683..b3586b04e 100644 --- a/main.go +++ b/main.go @@ -1,59 +1,69 @@ package main import ( - "flag" - "fmt" - "log" - "os" - "os/user" - "time" + "flag" + "fmt" + "log" + "os" + "os/user" + "time" - "github.com/fatih/color" + "github.com/fatih/color" ) var ( - startTime time.Time - debugging bool + startTime time.Time + debugging bool ) func homeDirectory() string { - usr, err := user.Current() - if err != nil { - log.Fatal(err) - } - return usr.HomeDir + usr, err := user.Current() + if err != nil { + log.Fatal(err) + } + return usr.HomeDir } func devLog(objects ...interface{}) { - localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...) + localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...) } func colorLog(colour color.Attribute, objects ...interface{}) { - localLog(colour, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...) + localLog(colour, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...) } func commandLog(objects ...interface{}) { - localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/commands.log", objects...) + localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/commands.log", objects...) } func localLog(colour color.Attribute, path string, objects ...interface{}) { - if !debugging { - return - } - f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) - defer f.Close() - for _, object := range objects { - colorFunction := color.New(colour).SprintFunc() - f.WriteString(colorFunction(fmt.Sprint(object)) + "\n") - } + if !debugging { + return + } + f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) + defer f.Close() + for _, object := range objects { + colorFunction := color.New(colour).SprintFunc() + f.WriteString(colorFunction(fmt.Sprint(object)) + "\n") + } +} + +func navigateToRepoRootDirectory() { + _, err := os.Stat(".git") + for os.IsNotExist(err) { + devLog("going up a directory to find the root") + os.Chdir("..") + _, err = os.Stat(".git") + } } func main() { - debuggingPointer := flag.Bool("debug", false, "a boolean") - flag.Parse() - debugging = *debuggingPointer - devLog("\n\n\n\n\n\n\n\n\n\n") - startTime = time.Now() - verifyInGitRepo() - run() + debuggingPointer := flag.Bool("debug", false, "a boolean") + flag.Parse() + debugging = *debuggingPointer + devLog("\n\n\n\n\n\n\n\n\n\n") + startTime = time.Now() + verifyInGitRepo() + navigateToRepoRootDirectory() + run() } diff --git a/newFile b/newFile new file mode 100644 index 000000000..3bbb0e0c8 --- /dev/null +++ b/newFile @@ -0,0 +1 @@ +newFile diff --git a/newFile2 b/newFile2 new file mode 100644 index 000000000..3bbb0e0c8 --- /dev/null +++ b/newFile2 @@ -0,0 +1 @@ +newFile diff --git a/stash_panel.go b/stash_panel.go index 5b39793c2..c50756bee 100644 --- a/stash_panel.go +++ b/stash_panel.go @@ -1,93 +1,92 @@ package main import ( - "fmt" + "fmt" - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/gocui" ) func refreshStashEntries(g *gocui.Gui) error { - g.Update(func(g *gocui.Gui) error { - v, err := g.View("stash") - if err != nil { - panic(err) - } - state.StashEntries = getGitStashEntries() - v.Clear() - for _, stashEntry := range state.StashEntries { - fmt.Fprintln(v, stashEntry.DisplayString) - } - return resetOrigin(v) - }) - return nil + g.Update(func(g *gocui.Gui) error { + v, err := g.View("stash") + if err != nil { + panic(err) + } + state.StashEntries = getGitStashEntries() + v.Clear() + for _, stashEntry := range state.StashEntries { + fmt.Fprintln(v, stashEntry.DisplayString) + } + return resetOrigin(v) + }) + return nil } func getSelectedStashEntry(v *gocui.View) *StashEntry { - if len(state.StashEntries) == 0 { - return nil - } - lineNumber := getItemPosition(v) - return &state.StashEntries[lineNumber] + if len(state.StashEntries) == 0 { + return nil + } + lineNumber := getItemPosition(v) + return &state.StashEntries[lineNumber] } func renderStashOptions(g *gocui.Gui) error { - return renderOptionsMap(g, map[string]string{ - "space": "apply", - "k": "pop", - "d": "drop", - }) + return renderOptionsMap(g, map[string]string{ + "space": "apply", + "k": "pop", + "d": "drop", + }) } func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { - if err := renderStashOptions(g); err != nil { - return err - } - go func() { - stashEntry := getSelectedStashEntry(v) - if stashEntry == nil { - renderString(g, "main", "No stash entries") - return - } - diff, _ := getStashEntryDiff(stashEntry.Index) - renderString(g, "main", diff) - }() - return nil + if err := renderStashOptions(g); err != nil { + return err + } + go func() { + stashEntry := getSelectedStashEntry(v) + if stashEntry == nil { + renderString(g, "main", "No stash entries") + return + } + diff, _ := getStashEntryDiff(stashEntry.Index) + renderString(g, "main", diff) + }() + return nil } func handleStashApply(g *gocui.Gui, v *gocui.View) error { - return stashDo(g, v, "apply") + return stashDo(g, v, "apply") } func handleStashPop(g *gocui.Gui, v *gocui.View) error { - return stashDo(g, v, "pop") + return stashDo(g, v, "pop") } func handleStashDrop(g *gocui.Gui, v *gocui.View) error { - return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry? (y/n)", func(g *gocui.Gui, v *gocui.View) error { - return stashDo(g, v, "drop") - }, nil) - return nil + return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry? (y/n)", func(g *gocui.Gui, v *gocui.View) error { + return stashDo(g, v, "drop") + }, nil) } func stashDo(g *gocui.Gui, v *gocui.View, method string) error { - stashEntry := getSelectedStashEntry(v) - if stashEntry == nil { - return createErrorPanel(g, "No stash to "+method) - } - if output, err := gitStashDo(stashEntry.Index, method); err != nil { - createErrorPanel(g, output) - } - refreshStashEntries(g) - return refreshFiles(g) + stashEntry := getSelectedStashEntry(v) + if stashEntry == nil { + return createErrorPanel(g, "No stash to "+method) + } + if output, err := gitStashDo(stashEntry.Index, method); err != nil { + createErrorPanel(g, output) + } + refreshStashEntries(g) + return refreshFiles(g) } func handleStashSave(g *gocui.Gui, filesView *gocui.View) error { - createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { - if output, err := gitStashSave(trimmedContent(v)); err != nil { - createErrorPanel(g, output) - } - refreshStashEntries(g) - return refreshFiles(g) - }) - return nil + createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { + if output, err := gitStashSave(trimmedContent(v)); err != nil { + createErrorPanel(g, output) + } + refreshStashEntries(g) + return refreshFiles(g) + }) + return nil } diff --git a/view_helpers.go b/view_helpers.go index 294ab93ca..dd43aeade 100644 --- a/view_helpers.go +++ b/view_helpers.go @@ -1,199 +1,198 @@ package main import ( - "fmt" - "sort" - "strings" + "fmt" + "sort" + "strings" - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/gocui" ) var cyclableViews = []string{"files", "branches", "commits", "stash"} func refreshSidePanels(g *gocui.Gui) error { - refreshBranches(g) - refreshFiles(g) - refreshCommits(g) - return nil + refreshBranches(g) + refreshFiles(g) + refreshCommits(g) + return nil } func nextView(g *gocui.Gui, v *gocui.View) error { - var focusedViewName string - if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { - focusedViewName = cyclableViews[0] - } else { - for i := range cyclableViews { - if v.Name() == cyclableViews[i] { - focusedViewName = cyclableViews[i+1] - break - } - if i == len(cyclableViews)-1 { - devLog(v.Name() + " is not in the list of views") - return nil - } - } - } - focusedView, err := g.View(focusedViewName) - if err != nil { - panic(err) - return err - } - return switchFocus(g, v, focusedView) + var focusedViewName string + if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { + focusedViewName = cyclableViews[0] + } else { + for i := range cyclableViews { + if v.Name() == cyclableViews[i] { + focusedViewName = cyclableViews[i+1] + break + } + if i == len(cyclableViews)-1 { + devLog(v.Name() + " is not in the list of views") + return nil + } + } + } + focusedView, err := g.View(focusedViewName) + if err != nil { + panic(err) + } + return switchFocus(g, v, focusedView) } func newLineFocused(g *gocui.Gui, v *gocui.View) error { - mainView, _ := g.View("main") - mainView.SetOrigin(0, 0) + mainView, _ := g.View("main") + mainView.SetOrigin(0, 0) - switch v.Name() { - case "files": - return handleFileSelect(g, v) - case "branches": - return handleBranchSelect(g, v) - case "confirmation": - return nil - case "main": - // TODO: pull this out into a 'view focused' function - refreshMergePanel(g) - v.Highlight = false - return nil - case "commits": - return handleCommitSelect(g, v) - case "stash": - return handleStashEntrySelect(g, v) - default: - panic("No view matching newLineFocused switch statement") - } + switch v.Name() { + case "files": + return handleFileSelect(g, v) + case "branches": + return handleBranchSelect(g, v) + case "confirmation": + return nil + case "main": + // TODO: pull this out into a 'view focused' function + refreshMergePanel(g) + v.Highlight = false + return nil + case "commits": + return handleCommitSelect(g, v) + case "stash": + return handleStashEntrySelect(g, v) + default: + panic("No view matching newLineFocused switch statement") + } } func returnFocus(g *gocui.Gui, v *gocui.View) error { - previousView, err := g.View(state.PreviousView) - if err != nil { - panic(err) - } - return switchFocus(g, v, previousView) + previousView, err := g.View(state.PreviousView) + if err != nil { + panic(err) + } + return switchFocus(g, v, previousView) } // pass in oldView = nil if you don't want to be able to return to your old view func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { - // we assume we'll never want to return focus to a confirmation panel i.e. - // we should never stack confirmation panels - if oldView != nil && oldView.Name() != "confirmation" { - oldView.Highlight = false - devLog("setting previous view to:", oldView.Name()) - state.PreviousView = oldView.Name() - } - newView.Highlight = true - devLog(newView.Name()) - if _, err := g.SetCurrentView(newView.Name()); err != nil { - return err - } - g.Cursor = newView.Editable - return newLineFocused(g, newView) + // we assume we'll never want to return focus to a confirmation panel i.e. + // we should never stack confirmation panels + if oldView != nil && oldView.Name() != "confirmation" { + oldView.Highlight = false + devLog("setting previous view to:", oldView.Name()) + state.PreviousView = oldView.Name() + } + newView.Highlight = true + devLog(newView.Name()) + if _, err := g.SetCurrentView(newView.Name()); err != nil { + return err + } + g.Cursor = newView.Editable + return newLineFocused(g, newView) } func getItemPosition(v *gocui.View) int { - _, cy := v.Cursor() - _, oy := v.Origin() - return oy + cy + _, cy := v.Cursor() + _, oy := v.Origin() + return oy + cy } func trimmedContent(v *gocui.View) string { - return strings.TrimSpace(v.Buffer()) + return strings.TrimSpace(v.Buffer()) } func cursorUp(g *gocui.Gui, v *gocui.View) error { - // swallowing cursor movements in main - // TODO: pull this out - if v == nil || v.Name() == "main" { - return nil - } + // swallowing cursor movements in main + // TODO: pull this out + if v == nil || v.Name() == "main" { + return nil + } - ox, oy := v.Origin() - cx, cy := v.Cursor() - if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { - if err := v.SetOrigin(ox, oy-1); err != nil { - return err - } - } + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } - newLineFocused(g, v) - return nil + newLineFocused(g, v) + return nil } func cursorDown(g *gocui.Gui, v *gocui.View) error { - // swallowing cursor movements in main - // TODO: pull this out - if v == nil || v.Name() == "main" { - return nil - } - cx, cy := v.Cursor() - ox, oy := v.Origin() - if cy+oy >= len(v.BufferLines())-2 { - return nil - } - if err := v.SetCursor(cx, cy+1); err != nil { - if err := v.SetOrigin(ox, oy+1); err != nil { - return err - } - } + // swallowing cursor movements in main + // TODO: pull this out + if v == nil || v.Name() == "main" { + return nil + } + cx, cy := v.Cursor() + ox, oy := v.Origin() + if cy+oy >= len(v.BufferLines())-2 { + return nil + } + if err := v.SetCursor(cx, cy+1); err != nil { + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } - newLineFocused(g, v) - return nil + newLineFocused(g, v) + return nil } func resetOrigin(v *gocui.View) error { - if err := v.SetCursor(0, 0); err != nil { - return err - } - return v.SetOrigin(0, 0) + if err := v.SetCursor(0, 0); err != nil { + return err + } + return v.SetOrigin(0, 0) } // if the cursor down past the last item, move it up one func correctCursor(v *gocui.View) error { - cx, cy := v.Cursor() - _, oy := v.Origin() - lineCount := len(v.BufferLines()) - 2 - if cy >= lineCount-oy { - return v.SetCursor(cx, lineCount-oy) - } - return nil + cx, cy := v.Cursor() + _, oy := v.Origin() + lineCount := len(v.BufferLines()) - 2 + if cy >= lineCount-oy { + return v.SetCursor(cx, lineCount-oy) + } + return nil } func renderString(g *gocui.Gui, viewName, s string) error { - g.Update(func(*gocui.Gui) error { - v, err := g.View(viewName) - if err != nil { - panic(err) - } - v.Clear() - fmt.Fprint(v, s) - v.Wrap = true - return nil - }) - return nil + g.Update(func(*gocui.Gui) error { + v, err := g.View(viewName) + if err != nil { + panic(err) + } + v.Clear() + fmt.Fprint(v, s) + v.Wrap = true + return nil + }) + return nil } func splitLines(multilineString string) []string { - if multilineString == "" || multilineString == "\n" { - return make([]string, 0) - } - lines := strings.Split(multilineString, "\n") - if lines[len(lines)-1] == "" { - return lines[:len(lines)-1] - } - return lines + if multilineString == "" || multilineString == "\n" { + return make([]string, 0) + } + lines := strings.Split(multilineString, "\n") + if lines[len(lines)-1] == "" { + return lines[:len(lines)-1] + } + return lines } func optionsMapToString(optionsMap map[string]string) string { - optionsArray := make([]string, 0) - for key, description := range optionsMap { - optionsArray = append(optionsArray, key+": "+description) - } - sort.Strings(optionsArray) - return strings.Join(optionsArray, ", ") + optionsArray := make([]string, 0) + for key, description := range optionsMap { + optionsArray = append(optionsArray, key+": "+description) + } + sort.Strings(optionsArray) + return strings.Join(optionsArray, ", ") } func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error { - return renderString(g, "options", optionsMapToString(optionsMap)) + return renderString(g, "options", optionsMapToString(optionsMap)) }