diff --git a/.gitignore b/.gitignore index f759e8a06..e87f09a89 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,14 @@ # Logs *.log -# Extras -extra/lgit.rb +# Hidden +.* + +# TODO +TODO.* # Notes -notes/go.notes -TODO.notes -TODO.md +*.notes # Tests test/repos/repo @@ -17,3 +18,9 @@ coverage.txt # Binaries lazygit + +# Exceptions +!.gitignore +!.goreleaser.yml +!.circleci/ +!.github/ \ No newline at end of file diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index df4dcff78..26c3c5618 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -10,11 +10,11 @@ import ( ) func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { - index := gui.getItemPosition(v) + index := gui.getItemPosition(gui.getBranchesView(g)) if index == 0 { return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch")) } - branch := gui.getSelectedBranch(v) + branch := gui.getSelectedBranch(gui.getBranchesView(g)) if err := gui.GitCommand.Checkout(branch.Name, false); err != nil { gui.createErrorPanel(g, err.Error()) } @@ -115,16 +115,7 @@ func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch { } func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error { - return gui.renderOptionsMap(g, map[string]string{ - "space": gui.Tr.SLocalize("checkout"), - "f": gui.Tr.SLocalize("forceCheckout"), - "m": gui.Tr.SLocalize("merge"), - "c": gui.Tr.SLocalize("checkoutByName"), - "n": gui.Tr.SLocalize("newBranch"), - "d": gui.Tr.SLocalize("deleteBranch"), - "D": gui.Tr.SLocalize("forceDeleteBranch"), - "← → ↑ ↓": gui.Tr.SLocalize("navigate"), - }) + return gui.renderGlobalOptions(g) } // may want to standardise how these select methods work diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 0969692b6..1c1662475 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -59,13 +59,7 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error } func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error { - return gui.renderOptionsMap(g, map[string]string{ - "s": gui.Tr.SLocalize("squashDown"), - "r": gui.Tr.SLocalize("rename"), - "g": gui.Tr.SLocalize("resetToThisCommit"), - "f": gui.Tr.SLocalize("fixupCommit"), - "← → ↑ ↓": gui.Tr.SLocalize("navigate"), - }) + return gui.renderGlobalOptions(g) } func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { @@ -140,10 +134,10 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { - if gui.getItemPosition(v) != 0 { + if gui.getItemPosition(gui.getCommitsView(g)) != 0 { return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) } - gui.createPromptPanel(g, v, gui.Tr.SLocalize("RenameCommit"), func(g *gocui.Gui, v *gocui.View) error { + return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil { return gui.createErrorPanel(g, err.Error()) } @@ -156,7 +150,7 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { - if gui.getItemPosition(v) != 0 { + if gui.getItemPosition(gui.getCommitsView(g)) != 0 { return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 1971b3453..bfcc938bb 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -172,31 +172,7 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error { - optionsMap := map[string]string{ - "← → ↑ ↓": gui.Tr.SLocalize("navigate"), - "S": gui.Tr.SLocalize("stashFiles"), - "c": gui.Tr.SLocalize("CommitChanges"), - "o": gui.Tr.SLocalize("open"), - "i": gui.Tr.SLocalize("ignore"), - "d": gui.Tr.SLocalize("delete"), - "space": gui.Tr.SLocalize("toggleStaged"), - "R": gui.Tr.SLocalize("refresh"), - "t": gui.Tr.SLocalize("addPatch"), - "e": gui.Tr.SLocalize("edit"), - "a": gui.Tr.SLocalize("toggleStagedAll"), - "PgUp/PgDn": gui.Tr.SLocalize("scroll"), - } - if gui.State.HasMergeConflicts { - optionsMap["a"] = gui.Tr.SLocalize("abortMerge") - optionsMap["m"] = gui.Tr.SLocalize("resolveMergeConflicts") - } - if file == nil { - return gui.renderOptionsMap(g, optionsMap) - } - if file.Tracked { - optionsMap["d"] = gui.Tr.SLocalize("checkout") - } - return gui.renderOptionsMap(g, optionsMap) + return gui.renderGlobalOptions(g) } func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 413e48202..e3cc61529 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -83,6 +83,7 @@ type guiState struct { EditHistory *stack.Stack Platform commands.Platform Updating bool + Keys []Binding } // NewGui builds a new gui handler @@ -346,6 +347,15 @@ func (gui *Gui) renderAppStatus(g *gocui.Gui) error { return nil } +func (gui *Gui) renderGlobalOptions(g *gocui.Gui) error { + return gui.renderOptionsMap(g, map[string]string{ + "PgUp/PgDn": gui.Tr.SLocalize("scroll"), + "← → ↑ ↓": gui.Tr.SLocalize("navigate"), + "esc/q": gui.Tr.SLocalize("close"), + "x": gui.Tr.SLocalize("menu"), + }) +} + func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) { go func() { for range time.Tick(interval) { diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 494381749..c8ed7d3c8 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -6,75 +6,360 @@ import "github.com/jesseduffield/gocui" // is only handled if the given view has focus, or handled globally if the view // is "" type Binding struct { - ViewName string - Handler func(*gocui.Gui, *gocui.View) error - Key interface{} // FIXME: find out how to get `gocui.Key | rune` - Modifier gocui.Modifier + ViewName string + Handler func(*gocui.Gui, *gocui.View) error + Key interface{} // FIXME: find out how to get `gocui.Key | rune` + Modifier gocui.Modifier + KeyReadable string + Description string } -func (gui *Gui) keybindings(g *gocui.Gui) error { +func (gui *Gui) GetKeybindings() []Binding { bindings := []Binding{ - {ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit}, - {ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit}, - {ViewName: "", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.quit}, - {ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, - {ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain}, - {ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, - {ViewName: "", Key: gocui.KeyCtrlD, Modifier: gocui.ModNone, Handler: gui.scrollDownMain}, - {ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles}, - {ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles}, - {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh}, - {ViewName: "status", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleEditConfig}, - {ViewName: "status", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleOpenConfig}, - {ViewName: "status", Key: 'u', Modifier: gocui.ModNone, Handler: gui.handleCheckForUpdate}, - {ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress}, - {ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress}, - {ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress}, - {ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove}, - {ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge}, - {ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit}, - {ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen}, - {ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile}, - {ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles}, - {ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave}, - {ViewName: "files", Key: 'A', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge}, - {ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleStageAll}, - {ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch}, - {ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard}, - {ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge}, - {ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handlePickHunk}, - {ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: gui.handlePickBothHunks}, - {ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict}, - {ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict}, - {ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleSelectTop}, - {ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleSelectBottom}, - {ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict}, - {ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict}, - {ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleSelectTop}, - {ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectBottom}, - {ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: gui.handlePopFileSnapshot}, - {ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress}, - {ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName}, - {ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout}, - {ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch}, - {ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch}, - {ViewName: "branches", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleForceDeleteBranch}, - {ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge}, - {ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown}, - {ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit}, - {ViewName: "commits", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRenameCommitEditor}, - {ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit}, - {ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup}, - {ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply}, - {ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop}, - {ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop}, - {ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm}, - {ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose}, + { + ViewName: "", + Key: 'q', + Modifier: gocui.ModNone, + Handler: gui.quit, + }, { + ViewName: "", + Key: gocui.KeyCtrlC, + Modifier: gocui.ModNone, + Handler: gui.quit, + }, { + ViewName: "", + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.quit, + }, { + ViewName: "", + Key: gocui.KeyPgup, + Modifier: gocui.ModNone, + Handler: gui.scrollUpMain, + }, { + ViewName: "", + Key: gocui.KeyPgdn, + Modifier: gocui.ModNone, + Handler: gui.scrollDownMain, + }, { + ViewName: "", + Key: gocui.KeyCtrlU, + Modifier: gocui.ModNone, + Handler: gui.scrollUpMain, + }, { + ViewName: "", + Key: gocui.KeyCtrlD, + Modifier: gocui.ModNone, + Handler: gui.scrollDownMain, + }, { + ViewName: "", + Key: 'P', + Modifier: gocui.ModNone, + Handler: gui.pushFiles, + Description: gui.Tr.SLocalize("push"), + }, { + ViewName: "", + Key: 'p', + Modifier: gocui.ModNone, + Handler: gui.pullFiles, + Description: gui.Tr.SLocalize("pull"), + }, { + ViewName: "", + Key: 'R', + Modifier: gocui.ModNone, + Handler: gui.handleRefresh, + Description: gui.Tr.SLocalize("refresh"), + }, { + ViewName: "", + Key: 'x', + Modifier: gocui.ModNone, + Handler: gui.handleMenu, + }, { + ViewName: "status", + Key: 'e', + Modifier: gocui.ModNone, + Handler: gui.handleEditConfig, + Description: gui.Tr.SLocalize("EditConfig"), + }, { + ViewName: "status", + Key: 'o', + Modifier: gocui.ModNone, + Handler: gui.handleOpenConfig, + Description: gui.Tr.SLocalize("OpenConfig"), + }, { + ViewName: "status", + Key: 'u', + Modifier: gocui.ModNone, + Handler: gui.handleCheckForUpdate, + Description: gui.Tr.SLocalize("checkForUpdate"), + }, { + ViewName: "files", + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.handleCommitPress, + Description: gui.Tr.SLocalize("CommitChanges"), + }, { + ViewName: "files", + Key: 'C', + Modifier: gocui.ModNone, + Handler: gui.handleCommitEditorPress, + Description: gui.Tr.SLocalize("CommitChangesWithEditor"), + }, { + ViewName: "files", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleFilePress, + KeyReadable: "space", + Description: gui.Tr.SLocalize("toggleStaged"), + }, { + ViewName: "files", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleFileRemove, + Description: gui.Tr.SLocalize("removeFile"), + }, { + ViewName: "files", + Key: 'm', + Modifier: gocui.ModNone, + Handler: gui.handleSwitchToMerge, + Description: gui.Tr.SLocalize("resolveMergeConflicts"), + }, { + ViewName: "files", + Key: 'e', + Modifier: gocui.ModNone, + Handler: gui.handleFileEdit, + Description: gui.Tr.SLocalize("editFile"), + }, { + ViewName: "files", + Key: 'o', + Modifier: gocui.ModNone, + Handler: gui.handleFileOpen, + Description: gui.Tr.SLocalize("openFile"), + }, { + ViewName: "files", + Key: 'i', + Modifier: gocui.ModNone, + Handler: gui.handleIgnoreFile, + Description: gui.Tr.SLocalize("ignoreFile"), + }, { + ViewName: "files", + Key: 'r', + Modifier: gocui.ModNone, + Handler: gui.handleRefreshFiles, + Description: gui.Tr.SLocalize("refreshFiles"), + }, { + ViewName: "files", + Key: 'S', + Modifier: gocui.ModNone, + Handler: gui.handleStashSave, + Description: gui.Tr.SLocalize("stashFiles"), + }, { + ViewName: "files", + Key: 'A', + Modifier: gocui.ModNone, + Handler: gui.handleAbortMerge, + Description: gui.Tr.SLocalize("abortMerge"), + }, { + ViewName: "files", + Key: 'a', + Modifier: gocui.ModNone, + Handler: gui.handleStageAll, + Description: gui.Tr.SLocalize("toggleStagedAll"), + }, { + ViewName: "files", + Key: 't', + Modifier: gocui.ModNone, + Handler: gui.handleAddPatch, + Description: gui.Tr.SLocalize("addPatch"), + }, { + ViewName: "files", + Key: 'D', + Modifier: gocui.ModNone, + Handler: gui.handleResetHard, + Description: gui.Tr.SLocalize("resetHard"), + }, { + ViewName: "main", + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.handleEscapeMerge, + }, { + ViewName: "main", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handlePickHunk, + }, { + ViewName: "main", + Key: 'b', + Modifier: gocui.ModNone, + Handler: gui.handlePickBothHunks, + }, { + ViewName: "main", + Key: gocui.KeyArrowLeft, + Modifier: gocui.ModNone, + Handler: gui.handleSelectPrevConflict, + }, { + ViewName: "main", + Key: gocui.KeyArrowRight, + Modifier: gocui.ModNone, + Handler: gui.handleSelectNextConflict, + }, { + ViewName: "main", + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + Handler: gui.handleSelectTop, + }, { + ViewName: "main", + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + Handler: gui.handleSelectBottom, + }, { + ViewName: "main", + Key: 'h', + Modifier: gocui.ModNone, + Handler: gui.handleSelectPrevConflict, + }, { + ViewName: "main", + Key: 'l', + Modifier: gocui.ModNone, + Handler: gui.handleSelectNextConflict, + }, { + ViewName: "main", + Key: 'k', + Modifier: gocui.ModNone, + Handler: gui.handleSelectTop, + }, { + ViewName: "main", + Key: 'j', + Modifier: gocui.ModNone, + Handler: gui.handleSelectBottom, + }, { + ViewName: "main", + Key: 'z', + Modifier: gocui.ModNone, + Handler: gui.handlePopFileSnapshot, + }, { + ViewName: "branches", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleBranchPress, + KeyReadable: "space", + Description: gui.Tr.SLocalize("checkout"), + }, { + ViewName: "branches", + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.handleCheckoutByName, + Description: gui.Tr.SLocalize("checkoutByName"), + }, { + ViewName: "branches", + Key: 'F', + Modifier: gocui.ModNone, + Handler: gui.handleForceCheckout, + Description: gui.Tr.SLocalize("forceCheckout"), + }, { + ViewName: "branches", + Key: 'n', + Modifier: gocui.ModNone, + Handler: gui.handleNewBranch, + Description: gui.Tr.SLocalize("newBranch"), + }, { + ViewName: "branches", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleDeleteBranch, + Description: gui.Tr.SLocalize("deleteBranch"), + }, { + ViewName: "branches", + Key: 'D', + Modifier: gocui.ModNone, + Handler: gui.handleForceDeleteBranch, + Description: gui.Tr.SLocalize("forceDeleteBranch"), + }, { + ViewName: "branches", + Key: 'm', + Modifier: gocui.ModNone, + Handler: gui.handleMerge, + Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"), + }, { + ViewName: "commits", + Key: 's', + Modifier: gocui.ModNone, + Handler: gui.handleCommitSquashDown, + Description: gui.Tr.SLocalize("squashDown"), + }, { + ViewName: "commits", + Key: 'r', + Modifier: gocui.ModNone, + Handler: gui.handleRenameCommit, + Description: gui.Tr.SLocalize("renameCommit"), + }, { + ViewName: "commits", + Key: 'R', + Modifier: gocui.ModNone, + Handler: gui.handleRenameCommitEditor, + Description: gui.Tr.SLocalize("renameCommitEditor"), + }, { + ViewName: "commits", + Key: 'g', + Modifier: gocui.ModNone, + Handler: gui.handleResetToCommit, + Description: gui.Tr.SLocalize("resetToThisCommit"), + }, { + ViewName: "commits", + Key: 'f', + Modifier: gocui.ModNone, + Handler: gui.handleCommitFixup, + Description: gui.Tr.SLocalize("fixupCommit"), + }, { + ViewName: "stash", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleStashApply, + KeyReadable: "space", + Description: gui.Tr.SLocalize("apply"), + }, { + ViewName: "stash", + Key: 'g', + Modifier: gocui.ModNone, + Handler: gui.handleStashPop, + Description: gui.Tr.SLocalize("pop"), + }, { + ViewName: "stash", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleStashDrop, + Description: gui.Tr.SLocalize("drop"), + }, { + ViewName: "commitMessage", + Key: gocui.KeyEnter, + Modifier: gocui.ModNone, + Handler: gui.handleCommitConfirm, + }, { + ViewName: "commitMessage", + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.handleCommitClose, + }, { + ViewName: "menu", + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.handleMenuClose, + }, { + ViewName: "menu", + Key: 'q', + Modifier: gocui.ModNone, + Handler: gui.handleMenuClose, + }, { + ViewName: "menu", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleMenuPress, + }, } // Would make these keybindings global but that interferes with editing // input in the confirmation panel - for _, viewName := range []string{"status", "files", "branches", "commits", "stash"} { + for _, viewName := range []string{"status", "files", "branches", "commits", "stash", "menu"} { bindings = append(bindings, []Binding{ {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, @@ -88,6 +373,12 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { }...) } + return bindings +} + +func (gui *Gui) keybindings(g *gocui.Gui) error { + bindings := gui.GetKeybindings() + for _, binding := range bindings { if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil { return err diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go new file mode 100644 index 000000000..68041390d --- /dev/null +++ b/pkg/gui/menu_panel.go @@ -0,0 +1,131 @@ +package gui + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +func (gui *Gui) handleMenuPress(g *gocui.Gui, v *gocui.View) error { + lineNumber := gui.getItemPosition(v) + if gui.State.Keys[lineNumber].Key == nil { + return nil + } + if len(gui.State.Keys) > lineNumber { + err := gui.handleMenuClose(g, v) + if err != nil { + return err + } + return gui.State.Keys[lineNumber].Handler(g, v) + } + return nil +} + +func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error { + // doing nothing for now + // but it is needed for switch in newLineFocused + return nil +} + +func (gui *Gui) renderMenuOptions(g *gocui.Gui) error { + optionsMap := map[string]string{ + "esc/q": gui.Tr.SLocalize("close"), + "↑ ↓": gui.Tr.SLocalize("navigate"), + "space": gui.Tr.SLocalize("execute"), + } + return gui.renderOptionsMap(g, optionsMap) +} + +func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error { + // better to delete because for example after closing update confirmation panel, + // the focus isn't set back to any of panels and one is unable to even quit + //_, err := g.SetViewOnBottom(v.Name()) + err := g.DeleteView("menu") + if err != nil { + return err + } + return gui.returnFocus(g, v) +} + +func (gui *Gui) GetKey(binding Binding) string { + r, ok := binding.Key.(rune) + key := "" + + if ok { + key = string(r) + } else if binding.KeyReadable != "" { + key = binding.KeyReadable + } + + return key +} + +func (gui *Gui) GetMaxKeyLength(bindings []Binding) int { + max := 0 + for _, binding := range bindings { + keyLength := len(gui.GetKey(binding)) + if keyLength > max { + max = keyLength + } + } + return max +} + +func (gui *Gui) handleMenu(g *gocui.Gui, v *gocui.View) error { + var ( + contentGlobal, contentPanel []string + bindingsGlobal, bindingsPanel []Binding + ) + // clear keys slice, so we don't have ghost elements + gui.State.Keys = gui.State.Keys[:0] + bindings := gui.GetKeybindings() + padWidth := gui.GetMaxKeyLength(bindings) + + for _, binding := range bindings { + key := gui.GetKey(binding) + if key != "" && binding.Description != "" { + content := fmt.Sprintf("%s %s", utils.WithPadding(key, padWidth), binding.Description) + switch binding.ViewName { + case "": + contentGlobal = append(contentGlobal, content) + bindingsGlobal = append(bindingsGlobal, binding) + case v.Name(): + contentPanel = append(contentPanel, content) + bindingsPanel = append(bindingsPanel, binding) + } + } + } + + // append dummy element to have a separator between + // panel and global keybindings + contentPanel = append(contentPanel, "") + bindingsPanel = append(bindingsPanel, Binding{}) + + content := append(contentPanel, contentGlobal...) + gui.State.Keys = append(bindingsPanel, bindingsGlobal...) + // append newline at the end so the last line would be selectable + contentJoined := strings.Join(content, "\n") + "\n" + + // y1-1 so there will not be an extra space at the end of panel + x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, contentJoined) + menuView, _ := g.SetView("menu", x0, y0, x1, y1-1, 0) + menuView.Title = strings.Title(gui.Tr.SLocalize("menu")) + menuView.FgColor = gocui.ColorWhite + + if err := gui.renderMenuOptions(g); err != nil { + return err + } + + fmt.Fprint(menuView, contentJoined) + + g.Update(func(g *gocui.Gui) error { + _, err := g.SetViewOnTop("menu") + if err != nil { + return err + } + return gui.switchFocus(g, v, menuView) + }) + return nil +} diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 0c323e254..9ca07717b 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -27,17 +27,13 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry { if len(gui.State.StashEntries) == 0 { return nil } - lineNumber := gui.getItemPosition(v) + stashView, _ := gui.g.View("stash") + lineNumber := gui.getItemPosition(stashView) return &gui.State.StashEntries[lineNumber] } func (gui *Gui) renderStashOptions(g *gocui.Gui) error { - return gui.renderOptionsMap(g, map[string]string{ - "space": gui.Tr.SLocalize("apply"), - "g": gui.Tr.SLocalize("pop"), - "d": gui.Tr.SLocalize("drop"), - "← → ↑ ↓": gui.Tr.SLocalize("navigate"), - }) + return gui.renderGlobalOptions(g) } func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 583d7805a..6948e9678 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -42,11 +42,7 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error { } func (gui *Gui) renderStatusOptions(g *gocui.Gui) error { - return gui.renderOptionsMap(g, map[string]string{ - "o": gui.Tr.SLocalize("OpenConfig"), - "e": gui.Tr.SLocalize("EditConfig"), - "u": gui.Tr.SLocalize("CheckForUpdate"), - }) + return gui.renderGlobalOptions(g) } func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 3a59945a5..7a5bd8874 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -82,6 +82,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { mainView.SetOrigin(0, 0) switch v.Name() { + case "menu": + return gui.handleMenuSelect(g, v) case "status": return gui.handleStatusSelect(g, v) case "files": @@ -145,6 +147,7 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { return err } g.Cursor = newView.Editable + return gui.newLineFocused(g, newView) } @@ -245,6 +248,7 @@ func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) err } // TODO: refactor properly +// i'm so sorry but had to add this getBranchesView func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View { v, _ := g.View("files") return v @@ -260,6 +264,11 @@ func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View { return v } +func (gui *Gui) getBranchesView(g *gocui.Gui) *gocui.View { + v, _ := g.View("branches") + return v +} + func (gui *Gui) trimmedContent(v *gocui.View) string { return strings.TrimSpace(v.Buffer()) } diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 0ce156cd5..21330e55c 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -34,12 +34,24 @@ func addDutch(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CommitChanges", Other: "Commit Veranderingen", + }, &i18n.Message{ + ID: "CommitChangesWithEditor", + Other: "commit changes using git editor", }, &i18n.Message{ ID: "StatusTitle", Other: "Status", + }, &i18n.Message{ + ID: "GlobalTitle", + Other: "Global", }, &i18n.Message{ ID: "navigate", Other: "navigeer", + }, &i18n.Message{ + ID: "menu", + Other: "menu", + }, &i18n.Message{ + ID: "execute", + Other: "execute", }, &i18n.Message{ ID: "stashFiles", Other: "stash-bestanden", @@ -172,6 +184,9 @@ func addDutch(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CloseConfirm", Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen", + }, &i18n.Message{ + ID: "close", + Other: "close", }, &i18n.Message{ ID: "SureResetThisCommit", Other: "Weet je het zeker dat je wil resetten naar deze commit?", @@ -212,8 +227,11 @@ func addDutch(i18nObject *i18n.Bundle) error { ID: "OnlyRenameTopCommit", Other: "Je kan alleen de bovenste commit hernoemen", }, &i18n.Message{ - ID: "RenameCommit", - Other: "Hernoem Commit", + ID: "renameCommit", + Other: "hernoem commit", + }, &i18n.Message{ + ID: "renameCommitEditor", + Other: "rename commit with editor", }, &i18n.Message{ ID: "PotentialErrInGetselectedCommit", Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)", @@ -295,6 +313,60 @@ func addDutch(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "MergeAborted", Other: "Merge afgebroken", + }, &i18n.Message{ + ID: "OpenConfig", + Other: "open config file", + }, &i18n.Message{ + ID: "EditConfig", + Other: "edit config file", + }, &i18n.Message{ + ID: "ForcePush", + Other: "Force push", + }, &i18n.Message{ + ID: "ForcePushPrompt", + Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.", + }, &i18n.Message{ + ID: "checkForUpdate", + Other: "check for update", + }, &i18n.Message{ + ID: "CheckingForUpdates", + Other: "Checking for updates...", + }, &i18n.Message{ + ID: "OnLatestVersionErr", + Other: "You already have the latest version", + }, &i18n.Message{ + ID: "MajorVersionErr", + Other: "New version ({{.newVersion}}) has non-backwards compatible changes compared to the current version ({{.currentVersion}})", + }, &i18n.Message{ + ID: "CouldNotFindBinaryErr", + Other: "Could not find any binary at {{.url}}", + }, &i18n.Message{ + ID: "AnonymousReportingTitle", + Other: "Help make lazygit better", + }, &i18n.Message{ + ID: "AnonymousReportingPrompt", + Other: "Would you like to enable anonymous reporting data to help improve lazygit? (enter/esc)", + }, &i18n.Message{ + ID: "removeFile", + Other: `delete if untracked / checkout if tracked (aka go away)`, + }, &i18n.Message{ + ID: "editFile", + Other: `edit file`, + }, &i18n.Message{ + ID: "openFile", + Other: `open file`, + }, &i18n.Message{ + ID: "ignoreFile", + Other: `add to .gitignore`, + }, &i18n.Message{ + ID: "refreshFiles", + Other: `refresh files`, + }, &i18n.Message{ + ID: "resetHard", + Other: `reset hard`, + }, &i18n.Message{ + ID: "mergeIntoCurrentBranch", + Other: `merge into currently checked out branch`, }, &i18n.Message{ ID: "ConfirmQuit", Other: `Are you sure you want to quit?`, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index f9707ced6..4f009c075 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -42,12 +42,24 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CommitChanges", Other: "commit changes", + }, &i18n.Message{ + ID: "CommitChangesWithEditor", + Other: "commit changes using git editor", }, &i18n.Message{ ID: "StatusTitle", Other: "Status", + }, &i18n.Message{ + ID: "GlobalTitle", + Other: "Global", }, &i18n.Message{ ID: "navigate", Other: "navigate", + }, &i18n.Message{ + ID: "menu", + Other: "menu", + }, &i18n.Message{ + ID: "execute", + Other: "execute", }, &i18n.Message{ ID: "stashFiles", Other: "stash files", @@ -69,6 +81,12 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "refresh", Other: "refresh", + }, &i18n.Message{ + ID: "push", + Other: "push", + }, &i18n.Message{ + ID: "pull", + Other: "pull", }, &i18n.Message{ ID: "addPatch", Other: "add patch", @@ -183,6 +201,9 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CloseConfirm", Other: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm", + }, &i18n.Message{ + ID: "close", + Other: "close", }, &i18n.Message{ ID: "SureResetThisCommit", Other: "Are you sure you want to reset to this commit?", @@ -223,8 +244,11 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "OnlyRenameTopCommit", Other: "Can only rename topmost commit", }, &i18n.Message{ - ID: "RenameCommit", - Other: "Rename Commit", + ID: "renameCommit", + Other: "rename commit", + }, &i18n.Message{ + ID: "renameCommitEditor", + Other: "rename commit with editor", }, &i18n.Message{ ID: "PotentialErrInGetselectedCommit", Other: "potential error in getSelected Commit (mismatched ui and state)", @@ -319,8 +343,8 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "ForcePushPrompt", Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.", }, &i18n.Message{ - ID: "CheckForUpdate", - Other: "Check for update", + ID: "checkForUpdate", + Other: "check for update", }, &i18n.Message{ ID: "CheckingForUpdates", Other: "Checking for updates...", @@ -342,6 +366,27 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "GitconfigParseErr", Other: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`, + }, &i18n.Message{ + ID: "removeFile", + Other: `delete if untracked / checkout if tracked`, + }, &i18n.Message{ + ID: "editFile", + Other: `edit file`, + }, &i18n.Message{ + ID: "openFile", + Other: `open file`, + }, &i18n.Message{ + ID: "ignoreFile", + Other: `add to .gitignore`, + }, &i18n.Message{ + ID: "refreshFiles", + Other: `refresh files`, + }, &i18n.Message{ + ID: "resetHard", + Other: `reset hard`, + }, &i18n.Message{ + ID: "mergeIntoCurrentBranch", + Other: `merge into currently checked out branch`, }, &i18n.Message{ ID: "ConfirmQuit", Other: `Are you sure you want to quit?`, diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 1de545274..c64ab87b8 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -32,12 +32,24 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CommitChanges", Other: "commituj zmiany", + }, &i18n.Message{ + ID: "CommitChangesWithEditor", + Other: "commituj zmiany używając edytora z gita", }, &i18n.Message{ ID: "StatusTitle", Other: "Status", + }, &i18n.Message{ + ID: "GlobalTitle", + Other: "Globalne", }, &i18n.Message{ ID: "navigate", Other: "nawiguj", + }, &i18n.Message{ + ID: "menu", + Other: "menu", + }, &i18n.Message{ + ID: "execute", + Other: "wykonaj", }, &i18n.Message{ ID: "stashFiles", Other: "przechowaj pliki", @@ -134,6 +146,9 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "DeleteBranchMessage", Other: "Jesteś pewien, że chcesz usunąć gałąź {{.selectedBranchName}} ?", + }, &i18n.Message{ + ID: "ForceDeleteBranchMessage", + Other: "Na pewno wymusić usunięcie gałęzi {{.selectedBranchName}}?", }, &i18n.Message{ ID: "CantMergeBranchIntoItself", Other: "Nie możesz scalić gałęzi do samej siebie", @@ -152,6 +167,9 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "deleteBranch", Other: "usuń gałąź", + }, &i18n.Message{ + ID: "forceDeleteBranch", + Other: "usuń gałąź (wymuś)", }, &i18n.Message{ ID: "NoBranchesThisRepo", Other: "Brak gałęzi dla tego repozytorium", @@ -164,6 +182,9 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CloseConfirm", Other: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź", + }, &i18n.Message{ + ID: "close", + Other: "zamknij", }, &i18n.Message{ ID: "SureResetThisCommit", Other: "Jesteś pewny, że chcesz zresetować ten commit?", @@ -204,8 +225,11 @@ func addPolish(i18nObject *i18n.Bundle) error { ID: "OnlyRenameTopCommit", Other: "Można przmianować tylko najwyższy commit", }, &i18n.Message{ - ID: "RenameCommit", - Other: "Przemianuj commit", + ID: "renameCommit", + Other: "przemianuj commit", + }, &i18n.Message{ + ID: "renameCommitEditor", + Other: "przemianuj commit w edytorze", }, &i18n.Message{ ID: "PotentialErrInGetselectedCommit", Other: "potencjalny błąd w getSelected Commit (niedopasowane ui i stan)", @@ -287,6 +311,60 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "MergeAborted", Other: "Scalanie anulowane", + }, &i18n.Message{ + ID: "OpenConfig", + Other: "otwórz plik konfiguracyjny", + }, &i18n.Message{ + ID: "EditConfig", + Other: "edytuj plik konfiguracyjny", + }, &i18n.Message{ + ID: "ForcePush", + Other: "Wymuś wypchnięcie", + }, &i18n.Message{ + ID: "ForcePushPrompt", + Other: "Twoja gałąź rozeszła się z gałęzią zdalną. Wciśnij 'esc' aby anulować lub 'enter' aby wymusić wypchnięcie.", + }, &i18n.Message{ + ID: "checkForUpdate", + Other: "sprawdź aktualizacje", + }, &i18n.Message{ + ID: "CheckingForUpdates", + Other: "Sprawdzanie aktualizacji...", + }, &i18n.Message{ + ID: "OnLatestVersionErr", + Other: "Już posiadasz najnowszą wersję", + }, &i18n.Message{ + ID: "MajorVersionErr", + Other: "Nowa wersja ({{.newVersion}}) posiada niekompatybilne zmiany w porównaniu do obecnej wersji ({{.currentVersion}})", + }, &i18n.Message{ + ID: "CouldNotFindBinaryErr", + Other: "Nie można znaleźć pliku binarnego w {{.url}}", + }, &i18n.Message{ + ID: "AnonymousReportingTitle", + Other: "Help make lazygit better", + }, &i18n.Message{ + ID: "AnonymousReportingPrompt", + Other: "Włączyć anonimowe raportowanie błędów w celu pomocy w usprawnianiu lazygita (enter/esc)?", + }, &i18n.Message{ + ID: "removeFile", + Other: `usuń jeśli nie śledzony / przełącz jeśli śledzony`, + }, &i18n.Message{ + ID: "editFile", + Other: `edytuj plik`, + }, &i18n.Message{ + ID: "openFile", + Other: `otwórz plik`, + }, &i18n.Message{ + ID: "ignoreFile", + Other: `dodaj do .gitignore`, + }, &i18n.Message{ + ID: "refreshFiles", + Other: `odśwież pliki`, + }, &i18n.Message{ + ID: "resetHard", + Other: `zresetuj twardo`, + }, &i18n.Message{ + ID: "mergeIntoCurrentBranch", + Other: `scal do obecnej gałęzi`, }, &i18n.Message{ ID: "ConfirmQuit", Other: `Na pewno chcesz wyjść z programu?`, diff --git a/scripts/generate_cheatsheet.go b/scripts/generate_cheatsheet.go new file mode 100644 index 000000000..cfa6d92ad --- /dev/null +++ b/scripts/generate_cheatsheet.go @@ -0,0 +1,54 @@ +// run: +// LANG=en go run generate_cheatsheet.go +// to generate Keybindings_en.md file in current directory +// change LANG to generate cheatsheet in different language (if supported) + +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/jesseduffield/lazygit/pkg/app" + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +func main() { + appConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool)) + a, _ := app.NewApp(appConfig) + lang := a.Tr.GetLanguage() + name := "Keybindings_" + lang + ".md" + bindings := a.Gui.GetKeybindings() + padWidth := a.Gui.GetMaxKeyLength(bindings) + file, _ := os.Create(name) + current := "v" + content := "" + title := "" + + file.WriteString("# Lazygit " + a.Tr.SLocalize("menu")) + + for _, binding := range bindings { + if key := a.Gui.GetKey(binding); key != "" && (binding.Description != "" || key == "x") { + if binding.ViewName != current { + current = binding.ViewName + if current == "" { + title = a.Tr.SLocalize("GlobalTitle") + } else { + title = a.Tr.SLocalize(strings.Title(current) + "Title") + } + content = fmt.Sprintf("\n\n## %s\n
\n", title) + file.WriteString(content) + } + // workaround to include menu keybinding in cheatsheet + // could not add this Description field directly to keybindings.go, + // because then menu key would be displayed in menu itself and that is undesirable + if key == "x" { + binding.Description = a.Tr.SLocalize("menu") + } + content = fmt.Sprintf("\t%s%s %s\n", key, strings.TrimPrefix(utils.WithPadding(key, padWidth), key), binding.Description) + file.WriteString(content) + } + } +}